diff --git a/Dockerfile b/Dockerfile
index 6d5bdf8..df603e7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,16 +9,16 @@ COPY ./files/plugin-EnvironmentVariables-5.0.3/ /var/www/html/plugins/Environmen
COPY ./files/plugin-CustomVariables-5.0.4/ /var/www/html/plugins/CustomVariables
# Add the HeatmapSessionRecording plugin
-COPY ./files/plugin-HeatmapSessionRecording-5.2.3/ /var/www/html/plugins/HeatmapSessionRecording
+COPY ./files/plugin-HeatmapSessionRecording-5.2.4/ /var/www/html/plugins/HeatmapSessionRecording
# Add the UsersFlow plugin
COPY ./files/plugin-UsersFlow-5.0.5/ /var/www/html/plugins/UsersFlow
-# Our custom configuration settings. We put it in /usr/src because the
-# entrypoint.sh builds the /var/www/html folder from the /usr/src/matomo
-# folder. This ensures that the config file from our updated container is the
-# one that is pushed to the persistent EFS storage.
-COPY ./files/config.ini.php /usr/src/matomo/config/config.ini.php
+# Add the SearchEngineKeywordsPerformance plugin
+COPY ./files/plugin-SearchEngineKeywordsPerformance-5.0.22/ /var/www/html/plugins/SearchEngineKeywordsPerformance
+
+# Our custom configuration settings.
+COPY ./files/config.ini.php /var/www/html/config/config.ini.php
# The HeatmapSessionRecording and UsersFlow update the matomo.js and piwik.js
# files when they are activated. Those updates have been captured and we
diff --git a/docs/HowTos/HOWTO-miscellaneous.md b/docs/HowTos/HOWTO-miscellaneous.md
index 2985213..272bfd6 100644
--- a/docs/HowTos/HOWTO-miscellaneous.md
+++ b/docs/HowTos/HOWTO-miscellaneous.md
@@ -34,8 +34,13 @@ To retrieve the **task number** value for the command:
OR
```bash
-aws ecs list-clusters --output text | grep matomo | cut -d'/' -f2
-aws ecs list-tasks --cluster $(aws ecs list-clusters --output text | grep matomo | cut -d'/' -f2) --query "taskArns[*]" --output text | cut -d'/' -f3
+aws ecs execute-command --region us-east-1 --cluster $(aws ecs list-clusters --output text | grep matomo | cut -d'/' -f2) --task $(aws ecs list-tasks --cluster $(aws ecs list-clusters --output text | grep matomo | cut -d'/' -f2) --query "taskArns[*]" --output text | cut -d'/' -f3) --command "/bin/bash" --interactive
+```
+
+If you need to force a redeployment of the task for the service, this one-liner will work:
+
+```bash
+aws ecs update-service --cluster $(aws ecs list-clusters --output text | grep matomo | cut -d'/' -f2) --service $(aws ecs list-services --cluster $(aws ecs list-clusters --output text | grep matomo | cut -d'/' -f2) --output text | grep matomo | cut -d'/' -f3) --force-new-deployment
```
## Reset 2-Factor auth
diff --git a/docs/HowTos/HOWTO-premium-plugins.md b/docs/HowTos/HOWTO-premium-plugins.md
index aaaff53..5be77ac 100644
--- a/docs/HowTos/HOWTO-premium-plugins.md
+++ b/docs/HowTos/HOWTO-premium-plugins.md
@@ -8,7 +8,7 @@ After some initial testing in Dev1, it's not as simple as just dumping the new p
1. Some plugins require changes to the database tables or just new tables. This requires that the plugin installation process is triggered to kick off the script that updates the tables.
1. The *Marketplace* plugin must be active for license keys to work.
-## The config.ini.php file
+## A note about the config.ini.php file
The `config.ini.php` file has two lists of plugins under two different headings.
@@ -22,8 +22,8 @@ In the end, the premium plugin installation is a two-pass process.
### High level overview
-1. Install license key (via UI or CLI) so that it is in the database.
-2. Go through a dev -> stage -> prod deployment cycle of the container to install the plugin folder(s) into the container.
+1. Install license key (via UI or CLI) so that it is in the database (this apparently only needs to be done once as all future premium plugins get linked to the same license key).
+2. Go through a dev -> stage -> prod deployment cycle of the container to install the plugin folder(s) into the container
3. Activate the new plugin(s) (via UI or CLI) so that any database changes are properly executed.
4. Go through a dev -> stage -> prod deployment cycle of the container to match the updated `config.ini.php` file on the server.
@@ -31,7 +31,7 @@ In the end, the premium plugin installation is a two-pass process.
#### 1. Install the license key
-Before installing the license key, the *Marketplace* plugin must be activated. This is a one-time update to the `config.ini.php` file to add the *Marketplace* pluging to the `[Plugins]` section.
+Before installing the license key, the *Marketplace* plugin must be activated. This is a one-time update to the `config.ini.php` file to add the *Marketplace* pluging to the `[Plugins]` section - all new premium plugin purchases are linked to the same license key.
According to the support team at Matomo, the premium license key can be installed in two instances of Matomo, "stage" and "prod." So, we can do some initial validation of a license key in Dev1, but the key cannot remain installed in the Dev1 instance. The license key installation can either be done by a user with "superuser" privileges in the Matomo web UI or it can be done by a member of InfraEng who has ssh access to the running container task/service. The CLI command is
@@ -39,11 +39,13 @@ According to the support team at Matomo, the premium license key can be installe
./console marketplace:set-license-key --license-key=LICENSE-KEY ""
```
-This needs to be done on each of the stage & prod instances of Matomo.
+This needs to be done once on each of the stage & prod instances of Matomo.
#### 2. Install the plugin files
-In this phase, the files are installed in the container *but no changes are made to the `config.ini.php` file. This will **not** activate the plugins, it will just make them visible in the UI.
+In this phase, the files are installed in the container **but** no changes are made to the `config.ini.php` file. This will **not** activate the plugins, it will just make them visible in the UI.
+
+**Note**: It is possible to do this with the `/var/www/html/console` utility when logged in to the cli of the running conatiner. However, that method introduces potential file permission errors since the command is run as `root` and the content in the `/var/www/html` folder needs to be owned by `www-data`.
#### 3. Activate the plugin
@@ -53,7 +55,9 @@ Once the plugin files are installed in the container, it's time to activate the
./console plugin:activate [...]
```
-This will change the `config.ini.php` file on the container. It is **very** important to capture these changes and put them back in the `config.ini.php` in the container (see step 4).
+This will change the `config/config.ini.php` file -- which is actually persisted on the EFS filesystem linked to the container. It is important to capture any changes that happen in this file so that we can back-fill this repository in case we need to redeploy in a DR scenario.
+
+It's also important to note that this `plugin:activate` command very likely makes changes to the database (adding/removing tables/columns or other changes).
#### 4. Backfill this repo
diff --git a/files/backup-data.sh b/files/backup-data.sh
index 5567a35..7dfe4b8 100755
--- a/files/backup-data.sh
+++ b/files/backup-data.sh
@@ -1,15 +1,31 @@
#!/bin/bash
-target_dir="/mnt/efs"
+# Define source directories
+source_dirs=(
+ "/var/www/html/config"
+ "/var/www/html/misc"
+ "/var/www/html/js"
+)
-mkdir -p "$target_dir/config"
-tar -cf - -C "/var/www/html/config" . | tar -xf - -C "$target_dir/config"
+# Define target directory
+target_dir="/mnt/efs/backups"
-mkdir -p "$target_dir/misc"
-tar -cf - -C "/var/www/html/misc" . | tar -xf - -C "$target_dir/misc"
+# Loop through each source directory and duplicate it to the target directory
+for src in "${source_dirs[@]}"; do
+ # Extract the directory name from the source path
+ dir_name=$(basename "$src")
+
+ # Create the target directory if it doesn't exist
+ mkdir -p "$target_dir/$dir_name"
+
+ # Use tar to duplicate the directory
+ tar -cf - -C "$src" . | tar -xf - -C "$target_dir/$dir_name"
+done
-mkdir -p "$target_dir/js"
-tar -cf - -C "/var/www/html/js" . | tar -xf - -C "$target_dir/js"
+echo "Directories have been successfully duplicated to $target_dir."
cp -a "/var/www/html/matomo.js" "$target_dir/matomo.js"
cp -a "/var/www/html/piwik.js" "$target_dir/piwik.js"
+
+# finally, make sure everything is www-data:www-data
+chown -R www-data:www-data "$target_dir"
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/API.php b/files/plugin-HeatmapSessionRecording-5.2.4/API.php
new file mode 100644
index 0000000..8dd9ecc
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/API.php
@@ -0,0 +1,999 @@
+validator = $validator;
+ $this->aggregator = $aggregator;
+ $this->siteHsr = $siteHsr;
+ $this->logHsr = $logHsr;
+ $this->logEvent = $logEvent;
+ $this->logHsrSite = $logHsrSite;
+ $this->systemSettings = $settings;
+ $this->configuration = $configuration;
+
+ $dir = Plugin\Manager::getPluginDirectory('UserCountry');
+ require_once $dir . '/functions.php';
+ }
+
+ /**
+ * Adds a new heatmap.
+ *
+ * Once added, the system will start recording activities for this heatmap.
+ *
+ * @param int $idSite
+ * @param string $name The name of heatmap which will be visible in the reporting UI.
+ * @param array $matchPageRules Eg. array(array('attribute' => 'url', 'type' => 'equals_simple', 'inverted' => 0, 'value' => 'http://example.com/directory'))
+ * For a list of available attribute and type values call {@link getAvailableTargetPageRules()}.
+ * "inverted" should be "0" or "1".
+ * @param int $sampleLimit The number of page views you want to record. Once the sample limit has been reached, the heatmap will be ended automatically.
+ * @param float $sampleRate Needs to be between 0 and 100 where 100 means => 100%, 10 => 10%, 0.1 => 0.1%.
+ * Defines how often a visitor will be actually recorded when they match the page rules, also known as "traffic". Currently max one decimal is supported.
+ * @param string $excludedElements Optional, a comma separated list of CSS selectors to exclude elements from being shown in the heatmap. For example to disable popups etc.
+ * @param string $screenshotUrl Optional, a URL to define on which page a screenshot should be taken.
+ * @param int $breakpointMobile If the device type cannot be detected, we will put any device having a lower width than this value into the mobile category. Useful if your website is responsive.
+ * @param int $breakpointTablet If the device type cannot be detected, we will put any device having a lower width than this value into the tablet category. Useful if your website is responsive.
+ * @return int
+ */
+ public function addHeatmap($idSite, $name, $matchPageRules, $sampleLimit = 1000, $sampleRate = 5, $excludedElements = false, $screenshotUrl = false, $breakpointMobile = false, $breakpointTablet = false, $captureDomManually = false)
+ {
+ $this->validator->checkHeatmapReportWritePermission($idSite);
+
+ if ($breakpointMobile === false || $breakpointMobile === null) {
+ $breakpointMobile = $this->systemSettings->breakpointMobile->getValue();
+ }
+
+ if ($breakpointTablet === false || $breakpointTablet === null) {
+ $breakpointTablet = $this->systemSettings->breakpointTablet->getValue();
+ }
+
+ $createdDate = Date::now()->getDatetime();
+
+ $matchPageRules = $this->unsanitizePageRules($matchPageRules);
+ $screenshotUrl = $this->unsanitizeScreenshotUrl($screenshotUrl);
+
+ return $this->siteHsr->addHeatmap($idSite, $name, $matchPageRules, $sampleLimit, $sampleRate, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet, $captureDomManually, $createdDate);
+ }
+
+ private function unsanitizeScreenshotUrl($screenshotUrl)
+ {
+ if (!empty($screenshotUrl) && is_string($screenshotUrl)) {
+ $screenshotUrl = Common::unsanitizeInputValue($screenshotUrl);
+ }
+
+ return $screenshotUrl;
+ }
+
+ private function unsanitizePageRules($matchPageRules)
+ {
+ if (!empty($matchPageRules) && is_array($matchPageRules)) {
+ foreach ($matchPageRules as $index => $matchPageRule) {
+ if (is_array($matchPageRule) && !empty($matchPageRule['value'])) {
+ $matchPageRules[$index]['value'] = Common::unsanitizeInputValue($matchPageRule['value']);
+ }
+ }
+ }
+ return $matchPageRules;
+ }
+
+ /**
+ * Updates an existing heatmap.
+ *
+ * All fields need to be set in order to update a heatmap. Easiest way is to get all values for a heatmap via
+ * "HeatmapSessionRecording.getHeatmap", make the needed changes on the heatmap, and send all values back to
+ * "HeatmapSessionRecording.updateHeatmap".
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap you want to update.
+ * @param string $name The name of heatmap which will be visible in the reporting UI.
+ * @param array $matchPageRules Eg. array(array('attribute' => 'url', 'type' => 'equals_simple', 'inverted' => 0, 'value' => 'http://example.com/directory'))
+ * For a list of available attribute and type values call {@link getAvailableTargetPageRules()}.
+ * "inverted" should be "0" or "1".
+ * @param int $sampleLimit The number of page views you want to record. Once the sample limit has been reached, the heatmap will be ended automatically.
+ * @param float $sampleRate Needs to be between 0 and 100 where 100 means => 100%, 10 => 10%, 0.1 => 0.1%.
+ * Defines how often a visitor will be actually recorded when they match the page rules, also known as "traffic". Currently max one decimal is supported.
+ * @param string $excludedElements Optional, a comma separated list of CSS selectors to exclude elements from being shown in the heatmap. For example to disable popups etc.
+ * @param string $screenshotUrl Optional, a URL to define on which page a screenshot should be taken.
+ * @param int $breakpointMobile If the device type cannot be detected, we will put any device having a lower width than this value into the mobile category. Useful if your website is responsive.
+ * @param int $breakpointTablet If the device type cannot be detected, we will put any device having a lower width than this value into the tablet category. Useful if your website is responsive.
+ */
+ public function updateHeatmap($idSite, $idSiteHsr, $name, $matchPageRules, $sampleLimit = 1000, $sampleRate = 5, $excludedElements = false, $screenshotUrl = false, $breakpointMobile = false, $breakpointTablet = false, $captureDomManually = false)
+ {
+ $this->validator->checkHeatmapReportWritePermission($idSite);
+ $this->siteHsr->checkHeatmapExists($idSite, $idSiteHsr);
+
+ if ($breakpointMobile === false || $breakpointMobile === null) {
+ $breakpointMobile = $this->systemSettings->breakpointMobile->getValue();
+ }
+
+ if ($breakpointTablet === false || $breakpointTablet === null) {
+ $breakpointTablet = $this->systemSettings->breakpointTablet->getValue();
+ }
+
+ $updatedDate = Date::now()->getDatetime();
+
+ $matchPageRules = $this->unsanitizePageRules($matchPageRules);
+ $screenshotUrl = $this->unsanitizeScreenshotUrl($screenshotUrl);
+
+ $this->siteHsr->updateHeatmap($idSite, $idSiteHsr, $name, $matchPageRules, $sampleLimit, $sampleRate, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet, $captureDomManually, $updatedDate);
+ }
+
+ /**
+ * Deletes / removes the screenshot from a heatmap
+ * @param int $idSite
+ * @param int $idSiteHsr
+ * @return bool
+ * @throws Exception
+ */
+ public function deleteHeatmapScreenshot($idSite, $idSiteHsr)
+ {
+ $this->validator->checkHeatmapReportWritePermission($idSite);
+ $this->siteHsr->checkHeatmapExists($idSite, $idSiteHsr);
+
+ $heatmap = $this->siteHsr->getHeatmap($idSite, $idSiteHsr);
+ if (!empty($heatmap['status']) && $heatmap['status'] === SiteHsrDao::STATUS_ACTIVE) {
+ $this->siteHsr->setPageTreeMirror($idSite, $idSiteHsr, null, null);
+
+ if (!empty($heatmap['page_treemirror'])) {
+ // only needed when a screenshot existed before that
+ Cache::deleteCacheWebsiteAttributes($idSite);
+ }
+ return true;
+ } elseif (!empty($heatmap['status'])) {
+ throw new Exception('The screenshot can be only removed from active heatmaps');
+ }
+ }
+
+ /**
+ * Adds a new session recording.
+ *
+ * Once added, the system will start recording sessions.
+ *
+ * @param int $idSite
+ * @param string $name The name of session recording which will be visible in the reporting UI.
+ * @param array $matchPageRules Eg. array(array('attribute' => 'url', 'type' => 'equals_simple', 'inverted' => 0, 'value' => 'http://example.com/directory'))
+ * For a list of available attribute and type values call {@link getAvailableTargetPageRules()}.
+ * "inverted" should be "0" or "1". Leave it empty to record any page.
+ * If page rules are set, a session will be only recorded as soon as a visitor has reached a page that matches these rules.
+ * @param int $sampleLimit The number of sessions you want to record. Once the sample limit has been reached, the session recording will be ended automatically.
+ * @param float $sampleRate Needs to be between 0 and 100 where 100 means => 100%, 10 => 10%, 0.1 => 0.1%.
+ * Defines how often a visitor will be actually recorded when they match the page rules, also known as "traffic". Currently max one decimal is supported.
+ * @param int $minSessionTime If defined, will only record sessions when the visitor has spent more than this many seconds on the current page.
+ * @param int $requiresActivity If enabled (default), the session will be only recorded if the visitor has at least scrolled and clicked once.
+ * @param int $captureKeystrokes If enabled (default), any text that a user enters into text form elements will be recorded.
+ * Password fields will be automatically masked and you can mask other elements with sensitive data using a data-matomo-mask attribute.
+ * @return int
+ */
+ public function addSessionRecording($idSite, $name, $matchPageRules = array(), $sampleLimit = 1000, $sampleRate = 10, $minSessionTime = 0, $requiresActivity = true, $captureKeystrokes = true)
+ {
+ $this->validator->checkSessionReportWritePermission($idSite);
+
+ $createdDate = Date::now()->getDatetime();
+
+ $matchPageRules = $this->unsanitizePageRules($matchPageRules);
+
+ return $this->siteHsr->addSessionRecording($idSite, $name, $matchPageRules, $sampleLimit, $sampleRate, $minSessionTime, $requiresActivity, $captureKeystrokes, $createdDate);
+ }
+
+ /**
+ * Updates an existing session recording.
+ *
+ * All fields need to be set in order to update a session recording. Easiest way is to get all values for a
+ * session recording via "HeatmapSessionRecording.getSessionRecording", make the needed changes on the recording,
+ * and send all values back to "HeatmapSessionRecording.updateSessionRecording".
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the session recording you want to update.
+ * @param string $name The name of session recording which will be visible in the reporting UI.
+ * @param array $matchPageRules Eg. array(array('attribute' => 'url', 'type' => 'equals_simple', 'inverted' => 0, 'value' => 'http://example.com/directory'))
+ * For a list of available attribute and type values call {@link getAvailableTargetPageRules()}.
+ * "inverted" should be "0" or "1". Leave it empty to record any page.
+ * If page rules are set, a session will be only recorded as soon as a visitor has reached a page that matches these rules.
+ * @param int $sampleLimit The number of sessions you want to record. Once the sample limit has been reached, the session recording will be ended automatically.
+ * @param float $sampleRate Needs to be between 0 and 100 where 100 means => 100%, 10 => 10%, 0.1 => 0.1%.
+ * Defines how often a visitor will be actually recorded when they match the page rules, also known as "traffic". Currently max one decimal is supported.
+ * @param int $minSessionTime If defined, will only record sessions when the visitor has spent more than this many seconds on the current page.
+ * @param int $requiresActivity If enabled (default), the session will be only recorded if the visitor has at least scrolled and clicked once.
+ * @param int $captureKeystrokes If enabled (default), any text that a user enters into text form elements will be recorded.
+ * Password fields will be automatically masked and you can mask other elements with sensitive data using a data-matomo-mask attribute.
+ */
+ public function updateSessionRecording($idSite, $idSiteHsr, $name, $matchPageRules = array(), $sampleLimit = 1000, $sampleRate = 10, $minSessionTime = 0, $requiresActivity = true, $captureKeystrokes = true)
+ {
+ $this->validator->checkSessionReportWritePermission($idSite);
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ $updatedDate = Date::now()->getDatetime();
+
+ $matchPageRules = $this->unsanitizePageRules($matchPageRules);
+
+ $this->siteHsr->updateSessionRecording($idSite, $idSiteHsr, $name, $matchPageRules, $sampleLimit, $sampleRate, $minSessionTime, $requiresActivity, $captureKeystrokes, $updatedDate);
+ }
+
+ /**
+ * Get a specific heatmap by its ID.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap.
+ * @return array|false
+ */
+ public function getHeatmap($idSite, $idSiteHsr)
+ {
+ $this->validator->checkHeatmapReportViewPermission($idSite);
+ $this->siteHsr->checkHeatmapExists($idSite, $idSiteHsr);
+
+ $heatmap = $this->siteHsr->getHeatmap($idSite, $idSiteHsr);
+
+ return $heatmap;
+ }
+
+ /**
+ * Get a specific session recording by its ID.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap.
+ * @return array|false
+ */
+ public function getSessionRecording($idSite, $idSiteHsr)
+ {
+ $this->validator->checkSessionReportViewPermission($idSite);
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ return $this->siteHsr->getSessionRecording($idSite, $idSiteHsr);
+ }
+
+ /**
+ * Pauses the given heatmap.
+ *
+ * When a heatmap is paused, all the tracking will be paused until its resumed again.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap
+ */
+ public function pauseHeatmap($idSite, $idSiteHsr)
+ {
+ $this->validator->checkHeatmapReportWritePermission($idSite);
+ $this->siteHsr->checkHeatmapExists($idSite, $idSiteHsr);
+
+ $this->siteHsr->pauseHeatmap($idSite, $idSiteHsr);
+
+ Cache::deleteCacheWebsiteAttributes($idSite);
+ }
+
+ /**
+ * Resumes the given heatmap.
+ *
+ * When a heatmap is resumed, all the tracking will be enabled.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap
+ */
+ public function resumeHeatmap($idSite, $idSiteHsr)
+ {
+ $this->validator->checkHeatmapReportWritePermission($idSite);
+ $this->siteHsr->checkHeatmapExists($idSite, $idSiteHsr);
+
+ $this->siteHsr->resumeHeatmap($idSite, $idSiteHsr);
+
+ Cache::deleteCacheWebsiteAttributes($idSite);
+ }
+
+ /**
+ * Deletes the given heatmap.
+ *
+ * When a heatmap is deleted, the report will be no longer available in the API and tracked data for this
+ * heatmap might be removed.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap
+ */
+ public function deleteHeatmap($idSite, $idSiteHsr)
+ {
+ $this->validator->checkHeatmapReportWritePermission($idSite);
+
+ $this->siteHsr->deactivateHeatmap($idSite, $idSiteHsr);
+ }
+
+ /**
+ * Ends / finishes the given heatmap.
+ *
+ * When you end a heatmap, the heatmap reports will be still available via API and UI but no new heatmap activity
+ * will be recorded for this heatmap.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap.
+ */
+ public function endHeatmap($idSite, $idSiteHsr)
+ {
+ $this->validator->checkHeatmapReportWritePermission($idSite);
+ $this->siteHsr->checkHeatmapExists($idSite, $idSiteHsr);
+
+ $this->siteHsr->endHeatmap($idSite, $idSiteHsr);
+ }
+
+ /**
+ * Pauses the given session recording.
+ *
+ * When a session recording is paused, all the tracking will be paused until its resumed again.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap
+ */
+ public function pauseSessionRecording($idSite, $idSiteHsr)
+ {
+ $this->validator->checkSessionReportWritePermission($idSite);
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ $this->siteHsr->pauseSessionRecording($idSite, $idSiteHsr);
+
+ Cache::deleteCacheWebsiteAttributes($idSite);
+ }
+
+ /**
+ * Resumes the given session recording.
+ *
+ * When a session recording is resumed, all the tracking will be enabled.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the heatmap
+ */
+ public function resumeSessionRecording($idSite, $idSiteHsr)
+ {
+ $this->validator->checkSessionReportWritePermission($idSite);
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ $this->siteHsr->resumeSessionRecording($idSite, $idSiteHsr);
+
+ Cache::deleteCacheWebsiteAttributes($idSite);
+ }
+
+ /**
+ * Deletes the given session recording.
+ *
+ * When a session recording is deleted, any related recordings be no longer available in the API and tracked data
+ * for this session recording might be removed.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the session recording.
+ */
+ public function deleteSessionRecording($idSite, $idSiteHsr)
+ {
+
+ $this->validator->checkSessionReportWritePermission($idSite);
+
+ $this->siteHsr->deactivateSessionRecording($idSite, $idSiteHsr);
+ }
+
+ /**
+ * Ends / finishes the given session recording.
+ *
+ * When you end a session recording, the session recording reports will be still available via API and UI but no new
+ * session will be recorded anymore.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the session recording.
+ */
+ public function endSessionRecording($idSite, $idSiteHsr)
+ {
+ $this->validator->checkSessionReportWritePermission($idSite);
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ $this->siteHsr->endSessionRecording($idSite, $idSiteHsr);
+ }
+
+ /**
+ * Get all available heatmaps for a specific website or app.
+ *
+ * It will return active as well as ended heatmaps but not any deleted heatmaps.
+ *
+ * @param int $idSite
+ * @param bool|int $includePageTreeMirror set to 0 if you don't need the page tree mirror for heatmaps (improves performance)
+ * @return array
+ */
+ public function getHeatmaps($idSite, $includePageTreeMirror = true)
+ {
+ $this->validator->checkHeatmapReportViewPermission($idSite);
+
+ return $this->siteHsr->getHeatmaps($idSite, !empty($includePageTreeMirror));
+ }
+
+ /**
+ * Get all available session recordings for a specific website or app.
+ *
+ * It will return active as well as ended session recordings but not any deleted session recordings.
+ *
+ * @param int $idSite
+ * @return array
+ */
+ public function getSessionRecordings($idSite)
+ {
+ $this->validator->checkSessionReportViewPermission($idSite);
+
+ return $this->siteHsr->getSessionRecordings($idSite);
+ }
+
+ /**
+ * Returns all page views that were recorded during a particular session / visit. We do not apply segments as it is
+ * used for video player when replaying sessions etc.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of a session recording
+ * @param int $idVisit The visit / session id
+ * @return array
+ */
+ private function getRecordedPageViewsInSession($idSite, $idSiteHsr, $idVisit, $period, $date)
+ {
+ $timezone = Site::getTimezoneFor($idSite);
+
+ // ideally we would also check if idSiteHsr is actually linked to idLogHsr but not really needed for security reasons
+ $pageviews = $this->aggregator->getRecordedPageViewsInSession($idSite, $idSiteHsr, $idVisit, $period, $date, $segment = false);
+
+ $isAnonymous = Piwik::isUserIsAnonymous();
+
+ foreach ($pageviews as &$pageview) {
+ $pageview['server_time_pretty'] = Date::factory($pageview['server_time'], $timezone)->getLocalized(DateTimeFormatProvider::DATETIME_FORMAT_SHORT);
+
+ if ($isAnonymous) {
+ unset($pageview['idvisitor']);
+ } else {
+ $pageview['idvisitor'] = bin2hex($pageview['idvisitor']);
+ }
+
+ $formatter = new Formatter();
+ $pageview['time_on_page_pretty'] = $formatter->getPrettyTimeFromSeconds(intval($pageview['time_on_page'] / 1000), $asSentence = true);
+ }
+
+ return $pageviews;
+ }
+
+ /**
+ * Returns all recorded sessions for a specific session recording.
+ *
+ * To get the actual recorded data for any of the recorded sessions, call {@link getRecordedSession()}.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idSiteHsr The id of the session recording you want to retrieve all the recorded sessions for.
+ * @param bool $segment
+ * @param int $idSubtable Optional visit id if you want to get all recorded pageviews of a specific visitor
+ * @return DataTable
+ */
+ public function getRecordedSessions($idSite, $period, $date, $idSiteHsr, $segment = false, $idSubtable = false)
+ {
+ $this->validator->checkSessionReportViewPermission($idSite);
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ $idVisit = $idSubtable;
+
+ try {
+ PeriodFactory::checkPeriodIsEnabled($period);
+ } catch (\Exception $e) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_PeriodDisabledErrorMessage', $period));
+ }
+
+ if (!empty($idVisit)) {
+ $recordings = $this->aggregator->getRecordedPageViewsInSession($idSite, $idSiteHsr, $idVisit, $period, $date, $segment);
+ } else {
+ $recordings = $this->aggregator->getRecordedSessions($idSite, $idSiteHsr, $period, $date, $segment);
+ }
+
+ $table = new DataTable();
+ $table->disableFilter('AddColumnsProcessedMetrics');
+ $table->setMetadata('idSiteHsr', $idSiteHsr);
+
+ if (!empty($recordings)) {
+ $table->addRowsFromSimpleArray($recordings);
+ }
+
+ if (empty($idVisit)) {
+ $table->queueFilter(function (DataTable $table) {
+ foreach ($table->getRowsWithoutSummaryRow() as $row) {
+ if ($idVisit = $row->getColumn('idvisit')) {
+ $row->setNonLoadedSubtableId($idVisit);
+ }
+ }
+ });
+ } else {
+ $table->disableFilter('Sort');
+ }
+
+ if (!method_exists(SettingsServer::class, 'isMatomoForWordPress') || !SettingsServer::isMatomoForWordPress()) {
+ $table->queueFilter(function (DataTable $table) use ($idSite, $idSiteHsr, $period, $date) {
+ foreach ($table->getRowsWithoutSummaryRow() as $row) {
+ $idLogHsr = $row->getColumn('idloghsr');
+ $row->setMetadata('sessionReplayUrl', SiteHsrModel::completeWidgetUrl('replayRecording', 'idSiteHsr=' . (int) $idSiteHsr . '&idLogHsr=' . (int) $idLogHsr, $idSite, $period, $date));
+ }
+ });
+ }
+
+ $table->filter('Piwik\Plugins\HeatmapSessionRecording\DataTable\Filter\EnrichRecordedSessions');
+
+ return $table;
+ }
+
+ /**
+ * Get all activities of a specific recorded session.
+ *
+ * This includes events such as clicks, mouse moves, scrolls, resizes, page / HTML DOM changes, form changed.
+ * It is recommended to call this API method with filter_limit = -1 to retrieve all results. It also returns
+ * metadata like the viewport size the user had when it was recorded, the browser, operating system, and more.
+ *
+ * To see what each event type in the events property means, call {@link getEventTypes()}.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the session recording you want to retrieve the data for.
+ * @param int $idLogHsr The id of the recorded session you want to retrieve the data for.
+ * @return array
+ * @throws Exception
+ */
+ public function getRecordedSession($idSite, $idSiteHsr, $idLogHsr)
+ {
+ $this->validator->checkSessionReportViewPermission($idSite);
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ // ideally we would also check if idSiteHsr is actually linked to idLogHsr but not really needed for security reasons
+ $session = $this->aggregator->getRecordedSession($idLogHsr);
+
+ if (empty($session['idsite']) || empty($idSite)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorSessionRecordingDoesNotExist'));
+ }
+
+ if ($session['idsite'] != $idSite) {
+ // important otherwise can fetch any log entry!
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorSessionRecordingDoesNotExist'));
+ }
+
+ $session['idvisitor'] = !empty($session['idvisitor']) ? bin2hex($session['idvisitor']) : '';
+
+ if (Piwik::isUserIsAnonymous()) {
+ foreach (EnrichRecordedSessions::getBlockedFields() as $blockedField) {
+ if (isset($session[$blockedField])) {
+ $session[$blockedField] = null;
+ }
+ }
+ }
+
+
+ $configBrowserName = !empty($session['config_browser_name']) ? $session['config_browser_name'] : '';
+ $session['browser_name'] = \Piwik\Plugins\DevicesDetection\getBrowserName($configBrowserName);
+ $session['browser_logo'] = \Piwik\Plugins\DevicesDetection\getBrowserLogo($configBrowserName);
+ $configOs = !empty($session['config_os']) ? $session['config_os'] : '';
+ $session['os_name'] = \Piwik\Plugins\DevicesDetection\getOsFullName($configOs);
+ $session['os_logo'] = \Piwik\Plugins\DevicesDetection\getOsLogo($configOs);
+ $session['device_name'] = \Piwik\Plugins\DevicesDetection\getDeviceTypeLabel($session['config_device_type']);
+ $session['device_logo'] = \Piwik\Plugins\DevicesDetection\getDeviceTypeLogo($session['config_device_type']);
+
+ if (!empty($session['config_device_model'])) {
+ $session['device_name'] .= ', ' . $session['config_device_model'];
+ }
+
+ $session['location_name'] = '';
+ $session['location_logo'] = '';
+
+ if (!empty($session['location_country'])) {
+ $session['location_name'] = \Piwik\Plugins\UserCountry\countryTranslate($session['location_country']);
+ $session['location_logo'] = \Piwik\Plugins\UserCountry\getFlagFromCode($session['location_country']);
+
+ if (!empty($session['location_region']) && $session['location_region'] != Visit::UNKNOWN_CODE) {
+ $session['location_name'] .= ', ' . \Piwik\Plugins\UserCountry\getRegionNameFromCodes($session['location_country'], $session['location_region']);
+ }
+
+ if (!empty($session['location_city'])) {
+ $session['location_name'] .= ', ' . $session['location_city'];
+ }
+ }
+
+ $timezone = Site::getTimezoneFor($idSite);
+ $session['server_time_pretty'] = Date::factory($session['server_time'], $timezone)->getLocalized(DateTimeFormatProvider::DATETIME_FORMAT_SHORT);
+
+ $formatter = new Formatter();
+ $session['time_on_page_pretty'] = $formatter->getPrettyTimeFromSeconds(intval($session['time_on_page'] / 1000), $asSentence = true);
+
+ // we make sure to get all recorded pageviews in this session
+ $serverTime = Date::factory($session['server_time']);
+ $from = $serverTime->subDay(1)->toString();
+ $to = $serverTime->addDay(1)->toString();
+
+ $period = 'range';
+ $dateRange = $from . ',' . $to;
+
+ $session['events'] = $this->logEvent->getEventsForPageview($idLogHsr);
+ $session['pageviews'] = $this->getRecordedPageViewsInSession($idSite, $idSiteHsr, $session['idvisit'], $period, $dateRange);
+ $session['numPageviews'] = count($session['pageviews']);
+
+ return $session;
+ }
+
+ /**
+ * Deletes all recorded page views within a recorded session.
+ *
+ * Once a recorded session has been deleted, the replay video will no longer be available in the UI and no data
+ * can be retrieved anymore via the API.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the session recording you want to delete the data.
+ * @param int $idVisit The visitId of the recorded session you want to delete.
+ */
+ public function deleteRecordedSession($idSite, $idSiteHsr, $idVisit)
+ {
+ $this->validator->checkSessionReportWritePermission($idSite);
+ // make sure the recording actually belongs to that site, otherwise could delete any recording for any other site
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ // we also need to make sure the visit actually belongs to that site
+ $idLogHsrs = $this->logHsr->findLogHsrIdsInVisit($idSite, $idVisit);
+
+ foreach ($idLogHsrs as $idLogHsr) {
+ $this->logHsrSite->unlinkRecord($idLogHsr, $idSiteHsr);
+ }
+ }
+
+ /**
+ * Deletes an individual page view within a recorded session.
+ *
+ * It only deletes one recorded session of one page view, not all recorded sessions.
+ * Once a recorded page view has been deleted, the replay video will no longer be available in the UI and no data
+ * can be retrieved anymore via the API for this page view.
+ *
+ * @param int $idSite
+ * @param int $idSiteHsr The id of the session recording you want to delete the data.
+ * @param int $idLogHsr The id of the recorded session you want to delete.
+ */
+ public function deleteRecordedPageview($idSite, $idSiteHsr, $idLogHsr)
+ {
+ $this->validator->checkWritePermission($idSite);
+ // make sure the recording actually belongs to that site, otherwise could delete any recording for any other site
+ $this->siteHsr->checkSessionRecordingExists($idSite, $idSiteHsr);
+
+ $this->logHsrSite->unlinkRecord($idLogHsr, $idSiteHsr);
+ }
+
+ /**
+ * Get metadata for a specific heatmap like the number of samples / pageviews that were recorded or the
+ * average above the fold per device type.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idSiteHsr The id of the heatmap you want to retrieve the meta data for.
+ * @param bool|string $segment
+ * @return array
+ */
+ public function getRecordedHeatmapMetadata($idSite, $period, $date, $idSiteHsr, $segment = false)
+ {
+ $this->validator->checkHeatmapReportViewPermission($idSite);
+ $this->siteHsr->checkHeatmapExists($idSite, $idSiteHsr);
+
+ $samples = $this->aggregator->getRecordedHeatmapMetadata($idSiteHsr, $idSite, $period, $date, $segment);
+
+ $result = array('nb_samples_device_all' => 0);
+
+ foreach ($samples as $sample) {
+ $result['nb_samples_device_' . $sample['device_type']] = $sample['value'];
+ $result['avg_fold_device_' . $sample['device_type']] = round(($sample['avg_fold'] / LogHsr::SCROLL_ACCURACY) * 100, 1);
+ $result['nb_samples_device_all'] += $sample['value'];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get all activities of a heatmap.
+ *
+ * For example retrieve all mouse movements made by desktop visitors, or all clicks made my tablet visitors, or
+ * all scrolls by mobile users. It is recommended to call this method with filter_limit = -1 to retrieve all
+ * results. As there can be many results, you may want to call this method several times using filter_limit and
+ * filter_offset.
+ *
+ * @param int $idSite
+ * @param string $period
+ * @param string $date
+ * @param int $idSiteHsr The id of the heatmap you want to retrieve the data for.
+ * @param int $heatmapType To see which heatmap types can be used, call {@link getAvailableHeatmapTypes()}
+ * @param int $deviceType To see which device types can be used, call {@link getAvailableDeviceTypes()}
+ * @param bool|string $segment
+ * @return array
+ */
+ public function getRecordedHeatmap($idSite, $period, $date, $idSiteHsr, $heatmapType, $deviceType, $segment = false)
+ {
+ $this->validator->checkHeatmapReportViewPermission($idSite);
+ $this->siteHsr->checkHeatmapExists($idSite, $idSiteHsr);
+
+ if ($heatmapType == RequestProcessor::EVENT_TYPE_SCROLL) {
+ $heatmap = $this->aggregator->aggregateScrollHeatmap($idSiteHsr, $deviceType, $idSite, $period, $date, $segment);
+ } else {
+ $heatmap = $this->aggregator->aggregateHeatmap($idSiteHsr, $heatmapType, $deviceType, $idSite, $period, $date, $segment);
+ }
+
+ // we do not return dataTable here as it doubles the time it takes to call this method (eg 4s vs 7s when heaps of data)
+ // datatable is not really needed here as we don't want to sort it or so
+ return $heatmap;
+ }
+
+ /**
+ * @param $idSite
+ * @param $idSiteHsr
+ * @param $idLogHsr
+ * @return array
+ * @hide
+ */
+ public function getEmbedSessionInfo($idSite, $idSiteHsr, $idLogHsr)
+ {
+ $this->validator->checkSessionReportViewPermission($idSite);
+
+ $aggregator = new Aggregator();
+ return $aggregator->getEmbedSessionInfo($idSite, $idSiteHsr, $idLogHsr);
+ }
+
+ /**
+ * Tests, checks whether the given URL matches the given page rules.
+ *
+ * This can be used before configuring a heatmap or session recording to make sure the configured target page(s)
+ * will match a specific URL.
+ *
+ * @param string $url
+ * @param array $matchPageRules
+ * @return array
+ * @throws Exception
+ */
+ public function testUrlMatchPages($url, $matchPageRules = array())
+ {
+ $this->validator->checkHasSomeWritePermission();
+
+ if ($url === '' || $url === false || $url === null) {
+ return array('url' => '', 'matches' => false);
+ }
+
+ if (!empty($matchPageRules) && !is_array($matchPageRules)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorNotAnArray', 'matchPageRules'));
+ }
+
+ $url = Common::unsanitizeInputValue($url);
+
+ if (!empty($matchPageRules)) {
+ $pageRules = new PageRules($matchPageRules, '', $needsOneEntry = false);
+ $pageRules->check();
+ }
+
+ $matchPageRules = $this->unsanitizePageRules($matchPageRules);
+
+ $allMatch = HsrMatcher::matchesAllPageRules($matchPageRules, $url);
+
+ return array('url' => $url, 'matches' => $allMatch);
+ }
+
+ /**
+ * Get a list of valid heatmap and session recording statuses (eg "active", "ended")
+ *
+ * @return array
+ */
+ public function getAvailableStatuses()
+ {
+ $this->validator->checkHasSomeWritePermission();
+
+ return array(
+ array('value' => SiteHsrDao::STATUS_ACTIVE, 'name' => Piwik::translate('HeatmapSessionRecording_StatusActive')),
+ array('value' => SiteHsrDao::STATUS_ENDED, 'name' => Piwik::translate('HeatmapSessionRecording_StatusEnded')),
+ );
+ }
+
+ /**
+ * Get a list of all available target attributes and target types for "pageTargets" / "page rules".
+ *
+ * For example URL, URL Parameter, Path, simple comparison, contains, starts with, and more.
+ *
+ * @return array
+ */
+ public function getAvailableTargetPageRules()
+ {
+ $this->validator->checkHasSomeWritePermission();
+
+ return PageRuleMatcher::getAvailableTargetTypes();
+ }
+
+ /**
+ * Get a list of available device types that can be used when fetching a heatmap report.
+ *
+ * For example desktop, tablet, mobile.
+ *
+ * @return array
+ */
+ public function getAvailableDeviceTypes()
+ {
+ Piwik::checkUserHasSomeViewAccess();
+
+ return array(
+ array('name' => Piwik::translate('General_Desktop'),
+ 'key' => LogHsr::DEVICE_TYPE_DESKTOP,
+ 'logo' => 'plugins/Morpheus/icons/dist/devices/desktop.png'),
+ array('name' => Piwik::translate('DevicesDetection_Tablet'),
+ 'key' => LogHsr::DEVICE_TYPE_TABLET,
+ 'logo' => 'plugins/Morpheus/icons/dist/devices/tablet.png'),
+ array('name' => Piwik::translate('General_Mobile'),
+ 'key' => LogHsr::DEVICE_TYPE_MOBILE,
+ 'logo' => 'plugins/Morpheus/icons/dist/devices/smartphone.png'),
+ );
+ }
+
+ /**
+ * Get a list of available heatmap types that can be used when fetching a heatmap report.
+ *
+ * For example click, mouse move, scroll.
+ *
+ * @return array
+ */
+ public function getAvailableHeatmapTypes()
+ {
+ Piwik::checkUserHasSomeViewAccess();
+
+ return array(
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityClick'),
+ 'key' => RequestProcessor::EVENT_TYPE_CLICK),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityMove'),
+ 'key' => RequestProcessor::EVENT_TYPE_MOVEMENT),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityScroll'),
+ 'key' => RequestProcessor::EVENT_TYPE_SCROLL),
+ );
+ }
+
+ /**
+ * Get a list of available session recording sample limits.
+ *
+ * Note: This is only a suggested list of sample limits that should be shown in the UI when creating or editing a
+ * session recording. When you configure a session recording via the API directly, any limit can be used.
+ *
+ * For example 50, 100, 200, 500
+ *
+ * @return array
+ */
+ public function getAvailableSessionRecordingSampleLimits()
+ {
+ $this->validator->checkHasSomeWritePermission();
+ $this->validator->checkSessionRecordingEnabled();
+
+ return $this->configuration->getSessionRecordingSampleLimits();
+ }
+
+ /**
+ * Get a list of available event types that may be returned eg when fetching a recorded session.
+ *
+ * @return array
+ */
+ public function getEventTypes()
+ {
+ Piwik::checkUserHasSomeViewAccess();
+
+ return array(
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityMove'),
+ 'key' => RequestProcessor::EVENT_TYPE_MOVEMENT),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityClick'),
+ 'key' => RequestProcessor::EVENT_TYPE_CLICK),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityScroll'),
+ 'key' => RequestProcessor::EVENT_TYPE_SCROLL),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityResize'),
+ 'key' => RequestProcessor::EVENT_TYPE_RESIZE),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityInitialDom'),
+ 'key' => RequestProcessor::EVENT_TYPE_INITIAL_DOM),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityPageChange'),
+ 'key' => RequestProcessor::EVENT_TYPE_MUTATION),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityFormText'),
+ 'key' => RequestProcessor::EVENT_TYPE_FORM_TEXT),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityFormValue'),
+ 'key' => RequestProcessor::EVENT_TYPE_FORM_VALUE),
+ array(
+ 'name' => Piwik::translate('HeatmapSessionRecording_ActivityScrollElement'),
+ 'key' => RequestProcessor::EVENT_TYPE_SCROLL_ELEMENT),
+ );
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Actions/ActionHsr.php b/files/plugin-HeatmapSessionRecording-5.2.4/Actions/ActionHsr.php
new file mode 100644
index 0000000..8eb7998
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Actions/ActionHsr.php
@@ -0,0 +1,61 @@
+getParam('url');
+
+ $this->setActionUrl($url);
+ }
+
+ public static function shouldHandle(Request $request)
+ {
+ $params = $request->getParams();
+ $isHsrRequest = Common::getRequestVar(RequestProcessor::TRACKING_PARAM_HSR_ID_VIEW, '', 'string', $params);
+
+ return !empty($isHsrRequest);
+ }
+
+ protected function getActionsToLookup()
+ {
+ return array();
+ }
+
+ // Do not track this Event URL as Entry/Exit Page URL (leave the existing entry/exit)
+ public function getIdActionUrlForEntryAndExitIds()
+ {
+ return false;
+ }
+
+ // Do not track this Event Name as Entry/Exit Page Title (leave the existing entry/exit)
+ public function getIdActionNameForEntryAndExitIds()
+ {
+ return false;
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/BaseActivity.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/BaseActivity.php
new file mode 100644
index 0000000..daac776
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/BaseActivity.php
@@ -0,0 +1,114 @@
+ $this->getSiteData($idSite),
+ 'version' => 'v1',
+ 'hsr' => $this->getHsrData($idSiteHsr, $idSite),
+ );
+ }
+
+ private function getSiteData($idSite)
+ {
+ return array(
+ 'site_id' => $idSite,
+ 'site_name' => Site::getNameFor($idSite)
+ );
+ }
+
+ private function getHsrData($idSiteHsr, $idSite)
+ {
+ $dao = $this->getDao();
+ $hsr = $dao->getRecord($idSite, $idSiteHsr, SiteHsrDao::RECORD_TYPE_HEATMAP);
+ if (empty($hsr)) {
+ // maybe it is a session? we could make this faster by adding a new method to DAO that returns hsr independent of type
+ $hsr = $dao->getRecord($idSite, $idSiteHsr, SiteHsrDao::RECORD_TYPE_SESSION);
+ }
+
+ $hsrName = '';
+ if (!empty($hsr['name'])) {
+ // hsr name might not be set when we are handling deleteExperiment activity
+ $hsrName = $hsr['name'];
+ }
+
+ return array(
+ 'id' => $idSiteHsr,
+ 'name' => $hsrName
+ );
+ }
+
+ public function getPerformingUser($eventData = null)
+ {
+ $login = Piwik::getCurrentUserLogin();
+
+ if ($login === self::USER_ANONYMOUS || empty($login)) {
+ // anonymous cannot change an experiment, in this case the system changed it, eg during tracking it started
+ // an experiment
+ return self::USER_SYSTEM;
+ }
+
+ return $login;
+ }
+
+ private function getDao()
+ {
+ // we do not get it via DI as it would slow down creation of all activities on all requests. Instead only
+ // create instance when needed
+ return StaticContainer::get('Piwik\Plugins\HeatmapSessionRecording\Dao\SiteHsrDao');
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapAdded.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapAdded.php
new file mode 100644
index 0000000..ad04966
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapAdded.php
@@ -0,0 +1,41 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_HeatmapAddedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapDeleted.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapDeleted.php
new file mode 100644
index 0000000..fae6d25
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapDeleted.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_HeatmapDeletedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapEnded.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapEnded.php
new file mode 100644
index 0000000..28afe54
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapEnded.php
@@ -0,0 +1,44 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_HeatmapEndedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapPaused.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapPaused.php
new file mode 100644
index 0000000..ddacfb5
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapPaused.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_HeatmapPausedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapResumed.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapResumed.php
new file mode 100644
index 0000000..9062306
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapResumed.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_HeatmapResumedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapScreenshotDeleted.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapScreenshotDeleted.php
new file mode 100644
index 0000000..4275d16
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapScreenshotDeleted.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_HeatmapScreenshotDeletedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapUpdated.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapUpdated.php
new file mode 100644
index 0000000..16b0636
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapUpdated.php
@@ -0,0 +1,42 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_HeatmapUpdatedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/RecordedPageviewDeleted.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/RecordedPageviewDeleted.php
new file mode 100644
index 0000000..0c608b3
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/RecordedPageviewDeleted.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_RecordedPageviewDeletedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/RecordedSessionDeleted.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/RecordedSessionDeleted.php
new file mode 100644
index 0000000..0eb581a
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/RecordedSessionDeleted.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_RecordedSessionDeletedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingAdded.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingAdded.php
new file mode 100644
index 0000000..6178a0e
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingAdded.php
@@ -0,0 +1,41 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_SessionRecordingAddedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingDeleted.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingDeleted.php
new file mode 100644
index 0000000..2f35f7f
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingDeleted.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_SessionRecordingDeletedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingEnded.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingEnded.php
new file mode 100644
index 0000000..dfd0df0
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingEnded.php
@@ -0,0 +1,44 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_SessionRecordingEndedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingPaused.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingPaused.php
new file mode 100644
index 0000000..7b376fd
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingPaused.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_SessionRecordingPausedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingResumed.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingResumed.php
new file mode 100644
index 0000000..b89500d
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingResumed.php
@@ -0,0 +1,46 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_SessionRecordingResumedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingUpdated.php b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingUpdated.php
new file mode 100644
index 0000000..fd35fa1
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingUpdated.php
@@ -0,0 +1,42 @@
+formatActivityData($idSiteHsr, $idSite);
+ }
+
+ public function getTranslatedDescription($activityData, $performingUser)
+ {
+ $siteName = $this->getSiteNameFromActivityData($activityData);
+ $hsrName = $this->getHsrNameFromActivityData($activityData);
+
+ return Piwik::translate('HeatmapSessionRecording_SessionRecordingUpdatedActivity', [$hsrName, $siteName]);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Archiver/Aggregator.php b/files/plugin-HeatmapSessionRecording-5.2.4/Archiver/Aggregator.php
new file mode 100644
index 0000000..bc15cef
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Archiver/Aggregator.php
@@ -0,0 +1,476 @@
+forceSleepInQuery) {
+ $extraWhere = 'SLEEP(1) AND';
+ }
+
+ $query = sprintf(
+ 'SELECT /* HeatmapSessionRecording.findRecording */ hsrsite.idsitehsr,
+ min(hsr.idloghsr) as idloghsr
+ FROM %s hsr
+ LEFT JOIN %s hsrsite ON hsr.idloghsr = hsrsite.idloghsr
+ LEFT JOIN %s hsrevent ON hsrevent.idloghsr = hsr.idloghsr and hsrevent.event_type = %s
+ LEFT JOIN %s sitehsr ON hsrsite.idsitehsr = sitehsr.idsitehsr
+ WHERE %s hsr.idvisit = ? and sitehsr.record_type = ? and hsrevent.idhsrblob is not null and hsrsite.idsitehsr is not null
+ GROUP BY hsrsite.idsitehsr
+ LIMIT 1',
+ Common::prefixTable('log_hsr'),
+ Common::prefixTable('log_hsr_site'),
+ Common::prefixTable('log_hsr_event'),
+ RequestProcessor::EVENT_TYPE_INITIAL_DOM,
+ Common::prefixTable('site_hsr'),
+ $extraWhere
+ );
+
+ $readerDb = $this->getDbReader();
+ $query = DbHelper::addMaxExecutionTimeHintToQuery($query, $this->getLiveQueryMaxExecutionTime());
+
+ try {
+ return $readerDb->fetchRow($query, array($idVisit, SiteHsrDao::RECORD_TYPE_SESSION));
+ } catch (\Exception $e) {
+ Model::handleMaxExecutionTimeError($readerDb, $e, '', Date::now(), Date::now(), null, 0, ['sql' => $query]);
+ throw $e;
+ }
+ }
+
+ private function getDbReader()
+ {
+ if (method_exists(Db::class, 'getReader')) {
+ return Db::getReader();
+ } else {
+ return Db::get();
+ }
+ }
+
+ public function findRecordings($visitIds)
+ {
+ if (empty($visitIds)) {
+ return array();
+ }
+
+ $visitIds = array_map('intval', $visitIds);
+
+ $extraWhere = '';
+ if ($this->forceSleepInQuery) {
+ $extraWhere = 'SLEEP(1) AND';
+ }
+
+ $query = sprintf(
+ 'SELECT /* HeatmapSessionRecording.findRecordings */ hsrsite.idsitehsr,
+ min(hsr.idloghsr) as idloghsr,
+ hsr.idvisit
+ FROM %s hsr
+ LEFT JOIN %s hsrsite ON hsr.idloghsr = hsrsite.idloghsr
+ LEFT JOIN %s hsrevent ON hsrevent.idloghsr = hsr.idloghsr and hsrevent.event_type = %s
+ LEFT JOIN %s sitehsr ON hsrsite.idsitehsr = sitehsr.idsitehsr
+ WHERE %s hsr.idvisit IN ("%s") and sitehsr.record_type = ? and hsrevent.idhsrblob is not null and hsrsite.idsitehsr is not null
+ GROUP BY hsr.idvisit, hsrsite.idsitehsr',
+ Common::prefixTable('log_hsr'),
+ Common::prefixTable('log_hsr_site'),
+ Common::prefixTable('log_hsr_event'),
+ RequestProcessor::EVENT_TYPE_INITIAL_DOM,
+ Common::prefixTable('site_hsr'),
+ $extraWhere,
+ implode('","', $visitIds)
+ );
+
+ $readerDb = $this->getDbReader();
+ $query = DbHelper::addMaxExecutionTimeHintToQuery($query, $this->getLiveQueryMaxExecutionTime());
+
+ try {
+ return $readerDb->fetchAll($query, array(SiteHsrDao::RECORD_TYPE_SESSION));
+ } catch (\Exception $e) {
+ Model::handleMaxExecutionTimeError($readerDb, $e, '', Date::now(), Date::now(), null, 0, ['sql' => $query]);
+ throw $e;
+ }
+ }
+
+ private function getLiveQueryMaxExecutionTime()
+ {
+ return Config::getInstance()->General['live_query_max_execution_time'];
+ }
+
+ public function getEmbedSessionInfo($idSite, $idSiteHsr, $idLogHsr)
+ {
+ $logHsr = Common::prefixTable('log_hsr');
+ $logHsrSite = Common::prefixTable('log_hsr_site');
+ $logAction = Common::prefixTable('log_action');
+ $logEvent = Common::prefixTable('log_hsr_event');
+ $logBlob = Common::prefixTable('log_hsr_blob');
+
+ $query = sprintf(
+ 'SELECT laction.name as base_url,
+ laction.url_prefix, hsrblob.`value` as initial_mutation, hsrblob.compressed
+ FROM %s hsr
+ LEFT JOIN %s laction ON laction.idaction = hsr.idaction_url
+ LEFT JOIN %s hsr_site ON hsr_site.idloghsr = hsr.idloghsr
+ LEFT JOIN %s hsrevent ON hsrevent.idloghsr = hsr.idloghsr and hsrevent.event_type = %s
+ LEFT JOIN %s hsrblob ON hsrevent.idhsrblob = hsrblob.idhsrblob
+ WHERE hsr.idloghsr = ? and hsr.idsite = ? and hsr_site.idsitehsr = ?
+ and hsrevent.idhsrblob is not null and `hsrblob`.`value` is not null
+ LIMIT 1',
+ $logHsr,
+ $logAction,
+ $logHsrSite,
+ $logEvent,
+ RequestProcessor::EVENT_TYPE_INITIAL_DOM,
+ $logBlob
+ );
+
+ $row = $this->getDbReader()->fetchRow($query, array($idLogHsr, $idSite, $idSiteHsr));
+
+ if (!empty($row['compressed'])) {
+ $row['initial_mutation'] = gzuncompress($row['initial_mutation']);
+ }
+
+ return $row;
+ }
+
+ public function getRecordedSession($idLogHsr)
+ {
+ $select = 'log_action.name as url,
+ log_visit.idvisit,
+ log_visit.idvisitor,
+ log_hsr.idsite,
+ log_visit.location_country,
+ log_visit.location_region,
+ log_visit.location_city,
+ log_visit.config_os,
+ log_visit.config_device_type,
+ log_visit.config_device_model,
+ log_visit.config_browser_name,
+ log_hsr.time_on_page,
+ log_hsr.server_time,
+ log_hsr.viewport_w_px,
+ log_hsr.viewport_h_px,
+ log_hsr.scroll_y_max_relative,
+ log_hsr.fold_y_relative';
+
+ $logHsr = Common::prefixTable('log_hsr');
+ $logVisit = Common::prefixTable('log_visit');
+ $logAction = Common::prefixTable('log_action');
+
+ $query = sprintf('SELECT %s
+ FROM %s log_hsr
+ LEFT JOIN %s log_visit ON log_hsr.idvisit = log_visit.idvisit
+ LEFT JOIN %s log_action ON log_action.idaction = log_hsr.idaction_url
+ WHERE log_hsr.idloghsr = ?', $select, $logHsr, $logVisit, $logAction);
+
+ return $this->getDbReader()->fetchRow($query, array($idLogHsr));
+ }
+
+ public function getRecordedSessions($idSite, $idSiteHsr, $period, $date, $segment)
+ {
+ $period = Period\Factory::build($period, $date);
+ $segment = new Segment($segment, array($idSite));
+ $site = new Site($idSite);
+
+ $from = array(
+ 'log_hsr',
+ array(
+ 'table' => 'log_hsr_site',
+ 'joinOn' => 'log_hsr_site.idloghsr = log_hsr.idloghsr'
+ ),
+ array(
+ 'table' => 'log_visit',
+ 'joinOn' => 'log_visit.idvisit = log_hsr.idvisit'
+ ),
+ array(
+ 'table' => 'log_action',
+ 'joinOn' => 'log_action.idaction = log_hsr.idaction_url'
+ ),
+ array(
+ 'table' => 'log_hsr_event',
+ 'joinOn' => 'log_hsr_event.idloghsr = log_hsr.idloghsr and log_hsr_event.event_type = ' . RequestProcessor::EVENT_TYPE_INITIAL_DOM
+ )
+ );
+
+ // we need to make sure to show only sessions that have an initial mutation with time_since_load = 0, otherwise
+ // the recording won't work.
+ $logHsrEventTable = Common::prefixTable('log_hsr_event');
+
+ $actionQuery = sprintf('SELECT count(*) FROM %1$s as hsr_ev
+ WHERE hsr_ev.idloghsr = log_hsr_site.idloghsr and hsr_ev.event_type not in (%2$s, %3$s)', $logHsrEventTable, RequestProcessor::EVENT_TYPE_CSS, RequestProcessor::EVENT_TYPE_INITIAL_DOM);
+
+ $select = 'log_hsr.idvisit as label,
+ count(*) as nb_pageviews,
+ log_hsr.idvisit,
+ SUBSTRING_INDEX(GROUP_CONCAT(CAST(log_action.name AS CHAR) ORDER BY log_hsr.server_time ASC SEPARATOR \'##\'), \'##\', 1) as first_url,
+ SUBSTRING_INDEX(GROUP_CONCAT(CAST(log_action.name AS CHAR) ORDER BY log_hsr.server_time DESC SEPARATOR \'##\'), \'##\', 1) as last_url,
+ sum(log_hsr.time_on_page) as time_on_site,
+ (' . $actionQuery . ') as total_events,
+ min(log_hsr_site.idloghsr) as idloghsr,
+ log_visit.idvisitor,
+ log_visit.location_country,
+ log_visit.location_region,
+ log_visit.location_city,
+ log_visit.config_os,
+ log_visit.config_device_type,
+ log_visit.config_device_model,
+ log_visit.config_browser_name,
+ min(log_hsr.server_time) as server_time';
+
+ $params = new ArchiveProcessor\Parameters($site, $period, $segment);
+ $logAggregator = new LogAggregator($params);
+
+ $where = $logAggregator->getWhereStatement('log_hsr', 'server_time');
+ $where .= sprintf(" and log_hsr_site.idsitehsr = %d and log_hsr_event.idhsrblob is not null", (int) $idSiteHsr);
+ $groupBy = 'log_hsr.idvisit';
+ $orderBy = 'log_hsr.server_time DESC';
+
+ $revertSubselect = $this->applyForceSubselect($segment, 'log_hsr.idvisit,log_hsr_site.idloghsr');
+
+ $query = $logAggregator->generateQuery($select, $from, $where, $groupBy, $orderBy);
+
+ if (!empty($revertSubselect) && is_callable($revertSubselect)) {
+ call_user_func($revertSubselect);
+ }
+
+ $dbReader = $this->getDbReader();
+ $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $this->getLiveQueryMaxExecutionTime());
+
+ try {
+ return $dbReader->fetchAll($query['sql'], $query['bind']);
+ } catch (\Exception $e) {
+ Model::handleMaxExecutionTimeError($dbReader, $e, '', Date::now(), Date::now(), null, 0, $query);
+ throw $e;
+ }
+ }
+
+ private function applyForceSubselect($segment, $subselectForced)
+ {
+ // for performance reasons we use this and not `LogAggregator->allowUsageSegmentCache()`
+ // That's because this is a LIVE query and not archived... and HSR tables usually have few entries < 5000
+ // so segmentation should be fairly fast using this method compared to allowUsageSegmentCache
+ // which would query the entire log_visit over several days with the applied query and then create the temp table
+ // and only then apply the log_hsr query.
+ // it should be a lot faster this way
+ if (class_exists('Piwik\DataAccess\LogQueryBuilder') && !$segment->isEmpty()) {
+ $logQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder');
+ if (
+ method_exists($logQueryBuilder, 'getForcedInnerGroupBySubselect') &&
+ method_exists($logQueryBuilder, 'forceInnerGroupBySubselect')
+ ) {
+ $forceGroupByBackup = $logQueryBuilder->getForcedInnerGroupBySubselect();
+ $logQueryBuilder->forceInnerGroupBySubselect($subselectForced);
+
+ return function () use ($forceGroupByBackup, $logQueryBuilder) {
+ $logQueryBuilder->forceInnerGroupBySubselect($forceGroupByBackup);
+ };
+ }
+ }
+ }
+
+ public function getRecordedPageViewsInSession($idSite, $idSiteHsr, $idVisit, $period, $date, $segment)
+ {
+ $period = Period\Factory::build($period, $date);
+ $segment = new Segment($segment, array($idSite));
+ $site = new Site($idSite);
+
+ $from = array(
+ 'log_hsr',
+ array(
+ 'table' => 'log_hsr_site',
+ 'joinOn' => 'log_hsr_site.idloghsr = log_hsr.idloghsr'
+ ),
+ array(
+ 'table' => 'log_visit',
+ 'joinOn' => 'log_visit.idvisit = log_hsr.idvisit'
+ ),
+ array(
+ 'table' => 'log_action',
+ 'joinOn' => 'log_action.idaction = log_hsr.idaction_url'
+ ),
+ array(
+ 'table' => 'log_hsr_event',
+ 'joinOn' => 'log_hsr_event.idloghsr = log_hsr.idloghsr and log_hsr_event.event_type = ' . RequestProcessor::EVENT_TYPE_INITIAL_DOM
+ )
+ );
+
+ // we need to make sure to show only sessions that have an initial mutation with time_since_load = 0, otherwise
+ // the recording won't work. If this happens often, we might "end / finish" a configured session recording
+ // earlier since we have eg recorded 1000 sessions, but user sees only 950 which will be confusing but we can
+ // for now not take this into consideration during tracking when we get number of available samples only using
+ // log_hsr_site to detect if the number of configured sessions have been reached. ideally we would at some point
+ // also make sure to include this check there but will be slower.
+
+ $select = 'log_action.name as label,
+ log_visit.idvisitor,
+ log_hsr_site.idloghsr,
+ log_hsr.time_on_page as time_on_page,
+ CONCAT(log_hsr.viewport_w_px, "x", log_hsr.viewport_h_px) as resolution,
+ log_hsr.server_time,
+ log_hsr.scroll_y_max_relative,
+ log_hsr.fold_y_relative';
+
+ $params = new ArchiveProcessor\Parameters($site, $period, $segment);
+ $logAggregator = new LogAggregator($params);
+
+ $where = $logAggregator->getWhereStatement('log_hsr', 'server_time');
+ $where .= sprintf(" and log_hsr_site.idsitehsr = %d and log_hsr.idvisit = %d and log_hsr_event.idhsrblob is not null ", (int) $idSiteHsr, (int) $idVisit);
+ $groupBy = '';
+ $orderBy = 'log_hsr.server_time ASC';
+
+ $revertSubselect = $this->applyForceSubselect($segment, 'log_hsr.idvisit,log_hsr_site.idloghsr');
+
+ $query = $logAggregator->generateQuery($select, $from, $where, $groupBy, $orderBy);
+
+ if (!empty($revertSubselect) && is_callable($revertSubselect)) {
+ call_user_func($revertSubselect);
+ }
+
+ return $this->getDbReader()->fetchAll($query['sql'], $query['bind']);
+ }
+
+ public function aggregateHeatmap($idSiteHsr, $heatmapType, $deviceType, $idSite, $period, $date, $segment)
+ {
+ $heatmapTypeWhere = '';
+ if ($heatmapType == RequestProcessor::EVENT_TYPE_CLICK) {
+ $heatmapTypeWhere .= 'log_hsr_event.event_type = ' . (int) $heatmapType;
+ } elseif ($heatmapType == RequestProcessor::EVENT_TYPE_MOVEMENT) {
+ $heatmapTypeWhere .= 'log_hsr_event.event_type IN(' . (int) RequestProcessor::EVENT_TYPE_MOVEMENT . ',' . (int) RequestProcessor::EVENT_TYPE_CLICK . ')';
+ } else {
+ throw new \Exception('Heatmap type not supported');
+ }
+
+ $period = Period\Factory::build($period, $date);
+ $segment = new Segment($segment, array($idSite));
+ $site = new Site($idSite);
+
+ $from = array(
+ 'log_hsr',
+ array(
+ 'table' => 'log_hsr_site',
+ 'joinOn' => 'log_hsr_site.idloghsr = log_hsr.idloghsr'
+ ),
+ array(
+ 'table' => 'log_hsr_event',
+ 'joinOn' => 'log_hsr_site.idloghsr = log_hsr_event.idloghsr'
+ ),
+ array(
+ 'table' => 'log_action',
+ 'joinOn' => 'log_action.idaction = log_hsr_event.idselector'
+ )
+ );
+
+ $select = 'log_action.name as selector,
+ log_hsr_event.x as offset_x,
+ log_hsr_event.y as offset_y,
+ count(*) as value';
+
+ $params = new ArchiveProcessor\Parameters($site, $period, $segment);
+ $logAggregator = new LogAggregator($params);
+
+ $where = $logAggregator->getWhereStatement('log_hsr', 'server_time');
+ $where .= ' and log_hsr_site.idsitehsr = ' . (int) $idSiteHsr . ' and log_hsr_event.idselector is not null and ' . $heatmapTypeWhere;
+ $where .= ' and log_hsr.device_type = ' . (int) $deviceType;
+
+ $groupBy = 'log_hsr_event.idselector, log_hsr_event.x, log_hsr_event.y';
+ $orderBy = '';
+
+ $query = $logAggregator->generateQuery($select, $from, $where, $groupBy, $orderBy);
+
+ return $this->getDbReader()->fetchAll($query['sql'], $query['bind']);
+ }
+
+ public function getRecordedHeatmapMetadata($idSiteHsr, $idSite, $period, $date, $segment)
+ {
+ $period = Period\Factory::build($period, $date);
+ $segment = new Segment($segment, array($idSite));
+ $site = new Site($idSite);
+
+ $from = array(
+ 'log_hsr',
+ array(
+ 'table' => 'log_hsr_site',
+ 'joinOn' => 'log_hsr_site.idloghsr = log_hsr.idloghsr'
+ )
+ );
+
+ $select = 'log_hsr.device_type, count(*) as value, avg(log_hsr.fold_y_relative) as avg_fold';
+
+ $params = new ArchiveProcessor\Parameters($site, $period, $segment);
+ $logAggregator = new LogAggregator($params);
+
+ $where = $logAggregator->getWhereStatement('log_hsr', 'server_time');
+ $where .= ' and log_hsr_site.idsitehsr = ' . (int) $idSiteHsr;
+ $groupBy = 'log_hsr.device_type';
+ $orderBy = '';
+
+ $query = $logAggregator->generateQuery($select, $from, $where, $groupBy, $orderBy);
+
+ return $this->getDbReader()->fetchAll($query['sql'], $query['bind']);
+ }
+
+ public function aggregateScrollHeatmap($idSiteHsr, $deviceType, $idSite, $period, $date, $segment)
+ {
+ $period = Period\Factory::build($period, $date);
+ $segment = new Segment($segment, array($idSite));
+ $site = new Site($idSite);
+
+ $from = array('log_hsr',
+ array(
+ 'table' => 'log_hsr_site',
+ 'joinOn' => 'log_hsr_site.idloghsr = log_hsr.idloghsr'
+ ),
+ );
+
+ $select = 'log_hsr.scroll_y_max_relative as label,
+ count(*) as value';
+
+ $params = new ArchiveProcessor\Parameters($site, $period, $segment);
+ $logAggregator = new LogAggregator($params);
+ $where = $logAggregator->getWhereStatement('log_hsr', 'server_time');
+ $where .= ' and log_hsr_site.idsitehsr = ' . (int) $idSiteHsr;
+ $where .= ' and log_hsr.device_type = ' . (int) $deviceType;
+
+ $groupBy = 'log_hsr.scroll_y_max_relative';
+ $orderBy = 'label ASC'; // labels are no from 0-1000 i.e page from top to bottom, so top label should always come first 0..100..500..1000
+
+ $query = $logAggregator->generateQuery($select, $from, $where, $groupBy, $orderBy);
+
+ return $this->getDbReader()->fetchAll($query['sql'], $query['bind']);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/CHANGELOG.md b/files/plugin-HeatmapSessionRecording-5.2.4/CHANGELOG.md
new file mode 100644
index 0000000..a32f012
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/CHANGELOG.md
@@ -0,0 +1,452 @@
+## Changelog
+
+5.2.4 - 2025-06-09
+- Started showing the troubleshooting link even when no heatmap sample has been recorded
+- Do not crash when displaying a heatmap for a page with invalid HTML
+
+5.2.3 - 2025-01-20
+- Added an activity for pause and resume action
+- Added a troubleshooting FAQ link for heatmaps
+
+5.2.2 - 2024-12-16
+- Fixes PHP deprecation warnings
+
+5.2.1 - 2024-12-02
+- Added activities to track deleting recorded sessions and page views
+
+5.2.0 - 2024-11-04
+- Implemented a tooltip which displays click count and rate
+
+5.1.8 - 2024-10-17
+- Fixes excluded_elements not working for escaped values for a heatmap
+
+5.1.7 - 2024-10-11
+- Fixes classes with word script being removed due to xss filtering
+
+5.1.6 - 2024-08-26
+- Pricing updated
+
+5.1.5
+- Added cover image for marketplace
+
+5.1.4
+- Fixes captureInitialDom not working for single heatmap
+
+5.1.3
+- Added code to disable matomo.js file writable check code for Matomo Cloud
+
+5.1.2
+- Added code to alert if matomo.js is not writable
+
+5.1.1
+- Fixed applying segment returns error for SessionRecording
+
+5.1.0
+- Added an option to capture Heatmap DOM on demand
+
+5.0.10
+- Added total actions column in Session Recording listing page
+
+5.0.9
+- Added code to keep playing on resize event
+- Added code to update Translation keys via event
+
+5.0.8
+- Changes for README.md
+- Fixed an error that occurs when viewing posts that have heatmaps associated in WordPress.
+
+5.0.7
+- Fixed issue where form fields that were supposed to be unmasked weren't
+- Added code to pause/resume heatmap for Matomo Cloud
+
+5.0.6
+- Fixed input[type="button"] background being ignored
+- Added code to display AdBlocker banner when detected
+
+5.0.5
+- Fixed regression where good configs were disabled
+
+5.0.4
+- Fixed location provider not loading for cloud customers
+
+5.0.3
+- Fixed error when location provider is null
+
+5.0.2
+- Added option to fire heatmap/session recording only for certain geographies
+
+5.0.1
+- Compatibility with Matomo 5.0.0-b4
+
+5.0.0
+- Compatibility with Matomo 5
+
+4.5.10
+- Started skipping deletion of heatmap and session recordings for proxysite
+
+4.5.9
+- Started hiding period selector when viewing heatmaps
+
+4.5.8
+- Fixed scroll data not displaying correctly due to sort missing
+
+4.5.7
+- Fixed deprecation warnings for PHP 8.1
+
+4.5.6
+- Changed time_on_page column to BIGINT for new installation for log_hsr and log_hsr_event table
+
+4.5.5
+- Fixed session recording not masking image with `data-matomo-mask` attribute set on parent node
+
+4.5.4
+- Fixed unmasking issue for text-node elements
+- Fixed recording to not end on tabs switch
+
+4.5.3
+- Added support to pass media attribute if present for external stylesheets
+
+4.5.2
+- Made regex to work consistently, #PG-373
+- Added examples of possible xss from portswigger.net
+
+4.5.1
+- Fixed mutation id bug to load css from DB
+
+4.5.0
+- Starting migrating AngularJS to Vue.
+- Migrated view code to VueJs
+- Updated code to respect max execution time during Archiving
+
+4.4.3
+- Added code to remove attributes with possible XSS values
+
+4.4.2
+- Added support for lazy loaded images
+
+4.4.1
+- Fixed masking issue for dynamically added DOM elements
+
+4.4.0
+- Added option to disable heatmap independently
+- Stopped showing visitor profile icon in session recording when visitor profile is disabled
+
+4.3.1
+- Fixed recorded session link not working for segmented logs in visit action
+
+4.3.0
+- Started storing CSS content in DB
+- Fixed range error when range is disabled
+
+4.2.1
+- Fixed double encoded segments
+
+4.2.0
+- Fixed heatmap not triggering when tracker configured directly.
+- Added masking for images with height and width
+- Added masking for [input type="image"]
+- Fixed non-masking bug for child elements with data-matomo-unmask
+
+4.1.2
+- Fix to record inputs with data-matomo-unmask
+
+4.1.1
+- Removed masking for input type button, submit and reset
+
+4.1.0
+- Added option to disable session recording independently
+
+4.0.14
+- Support Matomo's new content security policy header
+
+4.0.13
+- Fix sharing a session might not work anymore with latest Matomo version
+
+4.0.12
+- Ensure configs.php is loaded correctly with multiple trackers
+- Translation updates
+
+4.0.11
+- Improve handling of attribute changes
+- Add translations for Czech, Dutch & Portuguese
+
+4.0.10
+- Further improvements for loading for iframes
+
+4.0.9
+- Improve loading for iframes
+
+4.0.8
+- Improve tracking react pages
+
+4.0.7
+- Add category help texts
+- Increase possible sample limit
+- jQuery 3 compatibility for WP
+
+4.0.6
+- Performance improvements
+
+4.0.4
+- Compatibility with Matomo 4.X
+
+4.0.3
+- Compatibility with Matomo 4.X
+
+4.0.2
+- Compatibility with Matomo 4.X
+
+4.0.1
+- Handle base URLs better
+
+4.0.0
+- Compatibility with Matomo 4.X
+
+3.2.39
+- Better handling for base URL
+
+3.2.38
+- Improve SPA tracking
+
+3.2.37
+- Improve sorting of server time
+
+3.2.36
+- Fix number of recorded pages may be wrong when a segment is applied
+
+3.2.35
+- Improve widgetize feature when embedded as iframe
+
+3.2.34
+- Further improvements for WordPress
+
+3.2.33
+- Improve compatibilty with WordPress
+
+3.2.32
+- Improve checking for number of previously recorded sessions
+
+3.2.31
+- Matomo for WordPress support
+
+3.2.30
+- Send less tracking requests by queueing more requests together
+
+3.2.29
+- Use DB reader in Aggregator for better compatibility with Matomo 3.12
+
+3.2.28
+- Improvements for Matomo 3.12 to support faster segment archiving
+- Better support for single page applications
+
+3.2.27
+ - Show search box for entities
+ - Support usage of a reader DB when configured
+
+3.2.26
+ - Tracker improvements
+
+3.2.25
+ - Tracker improvements
+
+3.2.24
+ - Generate correct session recording link when a visitor matches multiple recordings in the visitor log
+
+3.2.23
+ - Internal tracker performance improvements
+
+3.2.22
+ - Add more translations
+ - Tracker improvements
+ - Internal changes
+
+3.2.21
+ - title-text of JavaScript Tracking option help box shows HTML
+ - Add primary key to log_event table for new installs (existing users should receive the update with Matomo 4)
+
+3.2.20
+ - Fix tracker may under circumstances not enable tracking after disabling it manually
+
+3.2.19
+ - Add possibility to delete an already taken heatmap screenshot so it can be re-taken
+
+3.2.18
+ - Performance improvements for high traffic websites
+
+3.2.17
+ - Add possibility to define alternative CSS file through `data-matomo-href`
+ - Added new API method `HeatmapSessionRecording.deleteHeatmapScreenshot` to delete an already taken heatmap screenshot
+ - Add possibility to delete an already taken heatmap screenshot so it can be re-taken
+
+3.2.16
+ - Add useDateUrl=0 to default Heatmap export URL so it can be used easier
+
+3.2.15
+ - Support a URL parameter &useDateUrl=1 in exported heatmaps to fetch heatmaps only for a specific date range
+
+3.2.14
+ - Improve compatibility with tag manager
+ - Fix possible notice when matching url array parameters
+ - Add command to remove a stored heatmap
+
+3.2.13
+ - Fix some coordinate cannot be calculated for SVG elements
+ - Added more languages
+ - Use new brand colors
+ - If time on page is too high, abort the tracking request
+
+3.2.12
+ - Update tracker file
+
+3.2.11
+ - Add possibility to mask images
+
+3.2.10
+ - Make sure to replay scrolling in element correctly
+
+3.2.9
+ - Change min height of heatmaps to 400 pixels.
+
+3.2.8
+ - When widgetizing the session player it bursts out of the iframe
+ - Log more debug information in tracker
+ - Use API calls instead of model
+
+3.2.7
+ - Support new "Write" role
+
+3.2.6
+ - Improve compatibility with styled-components and similar projects
+ - Add possibility to not record mouse and touch movements.
+
+3.2.5
+ - Compatibility with SiteUrlTrackingID plugin
+ - Ensure selectors are generated correctly
+
+3.2.4
+ - Allow users to pass sample limit of zero for unlimited recordings
+ - Show which page view within a session is currently being replayed
+
+3.2.3
+ - In configs.php return a 403 if Matomo is not installed yet
+
+3.2.2
+ - Validate an entered regular expression when configuring a heatmap or session recording
+ - Improve heatmap rendering of sharepoint sites
+
+3.2.1
+ - Improve the rendering of heatmaps and session recordings
+
+3.2.0
+ - Optimize tracker cache file
+ - Prevent recording injected CSS resources that only work on a visitors' computer such as Kaspersky Antivirus CSS.
+ - For better GDPR compliance disable capture keystroke in sessions by default.
+ - Added logic to support Matomo GDPR features
+ - Only specifically whitelisted form fields can now be recorded in plain text
+ - Some form fields that could potentially include personal information such as an address will be always masked and anonymized
+ - Trim any whitespace when configuring target pages
+
+3.1.9
+ - Support new attribute `data-matomo-mask` which works similar to `data-piwik-mask` but additionally allows to mask content of elements.
+
+3.1.8
+ - Support new CSS rendering classes matomoHsr, matomoHeatmap and matomoSessionRecording
+ - For input text fields prefer a set value on the element directly
+ - Differentiate between scrolling of the window and scrolling within an element (part of the window)
+ - Replay in the recorded session when a user is scrolling within an element
+
+3.1.7
+ - Make sure validating URL works correctly with HTML entities
+ - Prevent possible fatal error when opening manage screen for all websites
+
+3.1.6
+ - Renamed Piwik to Matomo
+
+3.1.5
+ - Fix requested stylesheet URLs were requested lowercase when using a relative base href in the recorded page
+ - Show more accurate time on page and record pageviews for a longer period in case a user is not active right away.
+
+3.1.4
+ - Prevent target rules in heatmap or session recording to visually disappear under circumstances when not using the cancel or back button.
+ - Respect URL prefix (eg www.) when replaying a session recording, may fix some displaying issues if website does not work without www.
+ - Improved look of widgetized session recording
+
+3.1.3
+ - Make Heatmap & Session Recording compatible with canvas and webgl libraries like threejs and earcut
+ - Better detected of the embedded heatmap height
+ - Fix scroll heatmap did not paint the last scroll section correctly
+ - It is now possible to configure the sample limits in the config via `[HeatmapSessionRecording] session_recording_sample_limits = 50,100,...`
+
+3.1.2
+ - Added URL to view heatmap and to replay a session recording to the API response
+ - Fix widgetized URL for heatmaps and sessions redirected to another page when authenticated via token_auth
+
+3.1.1
+ - Better error code when a site does not exist
+ - Fix configs.php may fail if plugins directory is a symlink
+ - Available sessions are now also displayed in the visitor profile
+
+3.1.0
+ - Added autoplay feature for page views within a visit
+ - Added possibility to change replay speed
+ - Added possibility to skip long pauses in a session recording automatically
+ - Better base URL detection in case a relative base URL is used
+
+3.0.15
+ - Fix only max 100 heatmaps or session recordings were shown when managing them for a specific site.
+ - Mask closing body in embedded page so it won't be replaced by some server logic
+
+3.0.14
+ - Make sure to find all matches for a root folder when "equals simple" is used
+
+3.0.13
+ - Fix a custom set based URL was ignored.
+
+3.0.12
+ - Fix session recording stops when a user changes a file form field because form value is not allowed to be changed.
+
+3.0.11
+ - Improve the performance of a DB query of a daily task when cleaning up blob entries.
+
+3.0.10
+ - Improve the performance of a DB query of a daily task
+ - Respect the new config setting `enable_internet_features` in the system check
+
+3.0.9
+ - Make sure page rules work fine when using HTML entities
+
+3.0.8
+ - Fix possible notice when tracking
+ - Avoid some logs in chrome when viewing a heatmaps or session recordings
+ - Always prefer same protocol when replaying sessions as currently used
+
+3.0.7
+ - When using an "equals exactly" comparison, ignore a trailing slash when there is no path set
+ - Let users customize if the tracking code should be included only when active records are configured
+
+3.0.6
+ - Fix link to replay session in visitor log may not work under circumstances
+
+3.0.5
+ - More detailed "no data message" when nothing has been recorded yet
+ - Fix select fields were not recorded
+
+3.0.4
+ - Only add tracker code when heatmap or sessions are actually active in any site
+ - Added index on site_hsr table
+ - Add custom stylesheets for custom styling
+
+3.0.3
+ - Add system check for configs.php
+ - On install, if .htaccess was not created, create the file manually
+
+3.0.2
+ - Enrich system summary widget
+ - Show an arrow instead of a dash between entry and exit url
+ - Added some German translations
+
+3.0.1
+ - Updated translations
+
+3.0.0
+ - Heatmap & Session Recording for Piwik 3
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Categories/HeatmapCategory.php b/files/plugin-HeatmapSessionRecording-5.2.4/Categories/HeatmapCategory.php
new file mode 100644
index 0000000..946c2ea
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Categories/HeatmapCategory.php
@@ -0,0 +1,26 @@
+' . Piwik::translate('HeatmapSessionRecording_ManageHeatmapSubcategoryHelp') . '
';
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Categories/ManageSessionRecordingSubcategory.php b/files/plugin-HeatmapSessionRecording-5.2.4/Categories/ManageSessionRecordingSubcategory.php
new file mode 100644
index 0000000..fa5c615
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Categories/ManageSessionRecordingSubcategory.php
@@ -0,0 +1,32 @@
+' . Piwik::translate('HeatmapSessionRecording_ManageSessionRecordingSubcategoryHelp') . '';
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Categories/SessionRecordingsCategory.php b/files/plugin-HeatmapSessionRecording-5.2.4/Categories/SessionRecordingsCategory.php
new file mode 100644
index 0000000..f376af1
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Categories/SessionRecordingsCategory.php
@@ -0,0 +1,26 @@
+getMetric($row, 'config_browser_name');
+ }
+
+ public function getDependentMetrics()
+ {
+ return array(
+ 'config_browser_name',
+ );
+ }
+
+ public function showsHtml()
+ {
+ return true;
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ if (empty($value) || $value === 'UNK') {
+ return false;
+ }
+
+ $title = \Piwik\Plugins\DevicesDetection\getBrowserName($value);
+
+ return ' ';
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Device.php b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Device.php
new file mode 100644
index 0000000..c0c0c6b
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Device.php
@@ -0,0 +1,78 @@
+ $this->getMetric($row, 'config_device_type'),
+ 'model' => $this->getMetric($row, 'config_device_model')
+ );
+ }
+
+ public function getDependentMetrics()
+ {
+ return array(
+ 'config_device_type',
+ 'config_device_model',
+ );
+ }
+
+ public function showsHtml()
+ {
+ return true;
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ if (empty($value['type']) && $value['type'] !== 0 && $value['type'] !== '0') {
+ return false;
+ }
+
+ $title = \Piwik\Plugins\DevicesDetection\getDeviceTypeLabel($value['type']);
+
+ if (!empty($value['model'])) {
+ $title .= ', ' . SafeDecodeLabel::decodeLabelSafe($value['model']);
+ }
+
+ return ' ';
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Location.php b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Location.php
new file mode 100644
index 0000000..671a658
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Location.php
@@ -0,0 +1,86 @@
+ $this->getMetric($row, 'location_country'),
+ 'region' => $this->getMetric($row, 'location_region'),
+ 'city' => $this->getMetric($row, 'location_city'),
+ );
+ }
+
+ public function getDependentMetrics()
+ {
+ return array(
+ 'location_country',
+ 'location_region',
+ 'location_city'
+ );
+ }
+
+ public function showsHtml()
+ {
+ return true;
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ if (empty($value['country']) || $value['country'] === Visit::UNKNOWN_CODE) {
+ return false;
+ }
+
+ $title = \Piwik\Plugins\UserCountry\countryTranslate($value['country']);
+
+ if (!empty($value['region']) && $value['region'] !== Visit::UNKNOWN_CODE) {
+ $title .= ', ' . \Piwik\Plugins\UserCountry\getRegionNameFromCodes($value['country'], $value['region']);
+ }
+
+ if (!empty($value['city'])) {
+ $title .= ', ' . SafeDecodeLabel::decodeLabelSafe($value['city']);
+ }
+
+ return ' ';
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/OperatingSystem.php b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/OperatingSystem.php
new file mode 100644
index 0000000..d78691b
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/OperatingSystem.php
@@ -0,0 +1,67 @@
+getMetric($row, 'config_os');
+ }
+
+ public function getDependentMetrics()
+ {
+ return array(
+ 'config_os',
+ );
+ }
+
+ public function showsHtml()
+ {
+ return true;
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ if (empty($value) || $value === 'UNK') {
+ return false;
+ }
+
+ $title = \Piwik\Plugins\DevicesDetection\getOsFullName($value);
+
+ return ' ';
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/SessionTime.php b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/SessionTime.php
new file mode 100644
index 0000000..0de98a4
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/SessionTime.php
@@ -0,0 +1,97 @@
+dateFormat = $dateFormat;
+ }
+
+ public function getName()
+ {
+ return 'server_time';
+ }
+
+ public function getTranslatedName()
+ {
+ return Piwik::translate('HeatmapSessionRecording_ColumnTime');
+ }
+
+ public function getDocumentation()
+ {
+ return Piwik::translate('HeatmapSessionRecording_ColumnTimeDocumentation');
+ }
+
+ public function compute(Row $row)
+ {
+ return $this->getMetric($row, 'server_time');
+ }
+
+ public function getDependentMetrics()
+ {
+ return array($this->getName());
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ $date = Date::factory($value, $this->timezone);
+
+ $dateTimeFormatProvider = StaticContainer::get('Piwik\Intl\Data\Provider\DateTimeFormatProvider');
+
+ $template = $dateTimeFormatProvider->getFormatPattern($this->dateFormat);
+ $template = str_replace(array(' y ', '.y '), ' ', $template);
+
+ return $date->getLocalized($template);
+ }
+
+ public function beforeFormat($report, DataTable $table)
+ {
+ $this->idSite = DataTableFactory::getSiteIdFromMetadata($table);
+ if (empty($this->idSite)) {
+ $this->idSite = Common::getRequestVar('idSite', 0, 'int');
+ }
+ if (!empty($this->idSite)) {
+ $this->timezone = Site::getTimezoneFor($this->idSite);
+ return true;
+ }
+ return false; // skip formatting if there is no site to get currency info from
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TimeOnPage.php b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TimeOnPage.php
new file mode 100644
index 0000000..506472d
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TimeOnPage.php
@@ -0,0 +1,65 @@
+getMetric($row, $this->getName());
+ }
+
+ public function getDependentMetrics()
+ {
+ return array($this->getName());
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ if (!empty($value)) {
+ $value = round($value / 1000, 1); // convert ms to seconds
+ $value = (int) round($value);
+ }
+
+ $time = $formatter->getPrettyTimeFromSeconds($value, $asSentence = false);
+
+ if (strpos($time, '00:') === 0) {
+ $time = substr($time, 3);
+ }
+
+ return $time;
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TimeOnSite.php b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TimeOnSite.php
new file mode 100644
index 0000000..2ba27d2
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TimeOnSite.php
@@ -0,0 +1,37 @@
+getMetric($row, 'total_events');
+ }
+
+ public function getDependentMetrics()
+ {
+ return [];
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Commands/RemoveHeatmapScreenshot.php b/files/plugin-HeatmapSessionRecording-5.2.4/Commands/RemoveHeatmapScreenshot.php
new file mode 100644
index 0000000..86783de
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Commands/RemoveHeatmapScreenshot.php
@@ -0,0 +1,101 @@
+setName('heatmapsessionrecording:remove-heatmap-screenshot');
+ $this->setDescription('Removes a saved heatmap screenshot which can be useful if you want Matomo to re-take this screenshot. If the heatmap is currently ended, it will automatically restart it.');
+ $this->addRequiredValueOption('idsite', null, 'The ID of the site the heatmap belongs to');
+ $this->addRequiredValueOption('idheatmap', null, 'The ID of the heatamp');
+ }
+
+ /**
+ * @return int
+ */
+ protected function doExecute(): int
+ {
+ $this->checkAllRequiredOptionsAreNotEmpty();
+ $input = $this->getInput();
+ $output = $this->getOutput();
+ $idSite = $input->getOption('idsite');
+ $idHeatmap = $input->getOption('idheatmap');
+
+ $heatmap = Request::processRequest('HeatmapSessionRecording.getHeatmap', array(
+ 'idSite' => $idSite,
+ 'idSiteHsr' => $idHeatmap
+ ));
+
+ if ($heatmap['status'] === SiteHsrDao::STATUS_ENDED) {
+ $logHsrSite = new LogHsrSite();
+ $numSamplesTakenSoFar = $logHsrSite->getNumPageViews($idHeatmap);
+
+ $currentSampleLimit = $heatmap['sample_limit'];
+ $newSampleLimit = $numSamplesTakenSoFar + 50; // 50 heatmaps should be enough to collect at least once the dom.
+
+ $update = array('status' => SiteHsrDao::STATUS_ACTIVE);
+ if ($currentSampleLimit >= $newSampleLimit) {
+ $output->writeln('Sample limit remains unchanged at ' . $currentSampleLimit);
+ if ($currentSampleLimit - $numSamplesTakenSoFar > 75) {
+ $output->writeln('make sure to end the heatmap again as soon as a screenshot has been taken! ');
+ }
+ } else {
+ $output->writeln('Going to increase sample limit from ' . $currentSampleLimit . ' to ' . $newSampleLimit . ' so a screenshot can be retaken. The heatmap will be automatically ended after about 50 new recordings have been recorded.');
+ $output->writeln('Note: This means when you manage this heatmap the selected sample wont be shown correctly in the select field');
+ $update['sample_limit'] = $newSampleLimit;
+ }
+
+ $output->writeln('Going to change status of heatmap from ended to active');
+
+ $siteHsr = StaticContainer::get(SiteHsrDao::class);
+ $siteHsr->updateHsrColumns($idSite, $idHeatmap, array(
+ 'status' => SiteHsrDao::STATUS_ACTIVE,
+ 'sample_limit' => $newSampleLimit
+ ));
+ $output->writeln('Done');
+ }
+
+ $success = Request::processRequest('HeatmapSessionRecording.deleteHeatmapScreenshot', array(
+ 'idSite' => $idSite,
+ 'idSiteHsr' => $idHeatmap
+ ));
+
+ if ($success) {
+ Filesystem::deleteAllCacheOnUpdate();
+ /** @var HeatmapSessionRecording $hsr */
+ $hsr = Plugin\Manager::getInstance()->getLoadedPlugin('HeatmapSessionRecording');
+ $hsr->updatePiwikTracker();
+ $output->writeln('Screenhot removed ');
+
+ return self::SUCCESS;
+ }
+
+ $output->writeln('Heatmap not found ');
+ return self::FAILURE;
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Configuration.php b/files/plugin-HeatmapSessionRecording-5.2.4/Configuration.php
new file mode 100644
index 0000000..b8dd31a
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Configuration.php
@@ -0,0 +1,145 @@
+getConfig();
+ $config->HeatmapSessionRecording = array(
+ self::KEY_OPTIMIZE_TRACKING_CODE => self::DEFAULT_OPTIMIZE_TRACKING_CODE,
+ self::KEY_SESSION_RECORDING_SAMPLE_LIMITS => self::DEFAULT_SESSION_RECORDING_SAMPLE_LIMITS,
+ self::KEY_ENABLE_LOAD_CSS_FROM_DB => self::DEFAULT_ENABLE_LOAD_CSS_FROM_DB,
+ self::MAX_ALLOWED_TIME_ON_PAGE_COLUMN_LIMIT => pow(2, 63),
+ self::KEY_DEFAULT_HEATMAP_WIDTH => self::DEFAULT_HEATMAP_WIDTH
+
+ );
+ $config->forceSave();
+ }
+
+ public function uninstall()
+ {
+ $config = $this->getConfig();
+ $config->HeatmapSessionRecording = array();
+ $config->forceSave();
+ }
+
+ public function shouldOptimizeTrackingCode()
+ {
+ $value = $this->getConfigValue(self::KEY_OPTIMIZE_TRACKING_CODE, self::DEFAULT_OPTIMIZE_TRACKING_CODE);
+
+ return !empty($value);
+ }
+
+ public function isAnonymousSessionRecordingAccessEnabled($idSite)
+ {
+ $value = $this->getDiValue(self::KEY_ENABLE_ANONYMOUS_SESSION_RECORDING_ACCESS, self::DEFAULT_ENABLE_ANONYMOUS_SESSION_RECORDING_ACCESS);
+ $idSites = explode(',', $value);
+ $idSites = array_map('trim', $idSites);
+ $idSites = array_filter($idSites);
+ return in_array($idSite, $idSites);
+ }
+
+ public function getSessionRecordingSampleLimits()
+ {
+ $value = $this->getConfigValue(self::KEY_SESSION_RECORDING_SAMPLE_LIMITS, self::DEFAULT_SESSION_RECORDING_SAMPLE_LIMITS);
+
+ if (empty($value)) {
+ $value = self::DEFAULT_SESSION_RECORDING_SAMPLE_LIMITS;
+ }
+
+ $value = explode(',', $value);
+ $value = array_filter($value, function ($val) {
+ return !empty($val);
+ });
+ $value = array_map(function ($val) {
+ return intval(trim($val));
+ }, $value);
+ natsort($value);
+
+ if (empty($value)) {
+ // just a fallback in case config is completely misconfigured
+ $value = explode(',', self::DEFAULT_SESSION_RECORDING_SAMPLE_LIMITS);
+ }
+
+ return array_values($value);
+ }
+
+ public function isLoadCSSFromDBEnabled()
+ {
+ return $this->getConfigValue(self::KEY_ENABLE_LOAD_CSS_FROM_DB, self::DEFAULT_ENABLE_LOAD_CSS_FROM_DB);
+ }
+
+ public function getMaximumAllowedPageTime()
+ {
+ return $this->getConfigValue(self::MAX_ALLOWED_TIME_ON_PAGE_COLUMN_LIMIT, '');
+ }
+
+ public function getDefaultHeatmapWidth()
+ {
+ $width = $this->getConfigValue(self::KEY_DEFAULT_HEATMAP_WIDTH, 1280);
+ if (!in_array($width, self::HEATMAP_ALLOWED_WIDTHS)) {
+ $width = self::DEFAULT_HEATMAP_WIDTH;
+ }
+
+ return $width;
+ }
+
+ private function getConfig()
+ {
+ return Config::getInstance();
+ }
+
+ private function getConfigValue($name, $default)
+ {
+ $config = $this->getConfig();
+ $values = $config->HeatmapSessionRecording;
+ if (isset($values[$name])) {
+ return $values[$name];
+ }
+ return $default;
+ }
+
+ private function getDiValue($name, $default)
+ {
+ $value = $default;
+ try {
+ $value = StaticContainer::get('HeatmapSessionRecording.' . $name);
+ } catch (NotFoundException $ex) {
+ // ignore
+ }
+ return $value;
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Controller.php b/files/plugin-HeatmapSessionRecording-5.2.4/Controller.php
new file mode 100644
index 0000000..2fd7c93
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Controller.php
@@ -0,0 +1,463 @@
+validator = $validator;
+ $this->siteHsrModel = $model;
+ $this->systemSettings = $settings;
+ $this->mutationManipulator = $mutationManipulator;
+ $this->mutationManipulator->generateNonce();
+ $this->configuration = $configuration;
+ }
+
+ public function manageHeatmap()
+ {
+ $idSite = Common::getRequestVar('idSite');
+
+ if (strtolower($idSite) === 'all') {
+ // prevent fatal error... redirect to a specific site as it is not possible to manage for all sites
+ $this->validator->checkHasSomeWritePermission();
+ $this->redirectToIndex('HeatmapSessionRecording', 'manageHeatmap');
+ exit;
+ }
+
+ $this->checkSitePermission();
+ $this->validator->checkHeatmapReportWritePermission($this->idSite);
+
+ return $this->renderTemplate('manageHeatmap', array(
+ 'breakpointMobile' => (int) $this->systemSettings->breakpointMobile->getValue(),
+ 'breakpointTablet' => (int) $this->systemSettings->breakpointTablet->getValue(),
+ 'pauseReason' => Piwik::translate(HeatmapSessionRecording::getTranslationKey('pause'), [Piwik::translate('HeatmapSessionRecording_Heatmap')]),
+ 'isMatomoJsWritable' => HeatmapSessionRecording::isMatomoJsWritable()
+ ));
+ }
+
+ public function manageSessions()
+ {
+ $idSite = Common::getRequestVar('idSite');
+
+ if (strtolower($idSite) === 'all') {
+ // prevent fatal error... redirect to a specific site as it is not possible to manage for all sites
+ $this->validator->checkHasSomeWritePermission();
+ $this->redirectToIndex('HeatmapSessionRecording', 'manageSessions');
+ exit;
+ }
+
+ $this->checkSitePermission();
+ $this->validator->checkSessionReportWritePermission($this->idSite);
+
+ return $this->renderTemplate('manageSessions', array(
+ 'pauseReason' => Piwik::translate(HeatmapSessionRecording::getTranslationKey('pause'), [Piwik::translate('HeatmapSessionRecording_SessionRecording')]),
+ 'isMatomoJsWritable' => HeatmapSessionRecording::isMatomoJsWritable()
+ ));
+ }
+
+ private function checkNotInternetExplorerWhenUsingToken()
+ {
+ if (Common::getRequestVar('token_auth', '', 'string') && !empty($_SERVER['HTTP_USER_AGENT'])) {
+ // we want to detect device type only once for faster performance
+ $ddFactory = StaticContainer::get(\Piwik\DeviceDetector\DeviceDetectorFactory::class);
+ $deviceDetector = $ddFactory->makeInstance($_SERVER['HTTP_USER_AGENT']);
+ $client = $deviceDetector->getClient();
+
+ if (
+ (!empty($client['short_name']) && $client['short_name'] === 'IE')
+ || (!empty($client['name']) && $client['name'] === 'Internet Explorer')
+ || (!empty($client['name']) && $client['name'] === 'Opera Mini')
+ ) {
+ // see https://caniuse.com/?search=noreferrer
+ // and https://caniuse.com/?search=referrerpolicy
+ throw new \Exception('For security reasons this feature doesn\'t work in this browser when using authentication using token_auth. Please try a different browser or log in to view this.');
+ }
+ }
+ }
+
+ public function replayRecording()
+ {
+ $this->validator->checkSessionReportViewPermission($this->idSite);
+ $this->checkNotInternetExplorerWhenUsingToken();
+
+ $idLogHsr = Common::getRequestVar('idLogHsr', null, 'int');
+ $idSiteHsr = Common::getRequestVar('idSiteHsr', null, 'int');
+
+ $_GET['period'] = 'year'; // setting it randomly to not having to pass it in the URL
+ $_GET['date'] = 'today'; // date is ignored anyway
+
+ $recording = Request::processRequest('HeatmapSessionRecording.getRecordedSession', array(
+ 'idSite' => $this->idSite,
+ 'idLogHsr' => $idLogHsr,
+ 'idSiteHsr' => $idSiteHsr,
+ 'filter_limit' => '-1'
+ ), $default = []);
+
+ $currentPage = null;
+ if (!empty($recording['pageviews']) && is_array($recording['pageviews'])) {
+ $allPageviews = array_values($recording['pageviews']);
+ foreach ($allPageviews as $index => $pageview) {
+ if (!empty($pageview['idloghsr']) && $idLogHsr == $pageview['idloghsr']) {
+ $currentPage = $index + 1;
+ break;
+ }
+ }
+ }
+
+ $settings = $this->getPluginSettings();
+ $settings = $settings->load();
+ $skipPauses = !empty($settings['skip_pauses']);
+ $autoPlayEnabled = !empty($settings['autoplay_pageviews']);
+ $replaySpeed = !empty($settings['replay_speed']) ? (int) $settings['replay_speed'] : 1;
+ $isVisitorProfileEnabled = Manager::getInstance()->isPluginActivated('Live') && Live::isVisitorProfileEnabled();
+
+ if (!empty($recording['events'])) {
+ foreach ($recording['events'] as $recordingEventIndex => $recordingEventValue) {
+ if (
+ !empty($recordingEventValue['event_type']) &&
+ (
+ $recordingEventValue['event_type'] == RequestProcessor::EVENT_TYPE_INITIAL_DOM ||
+ $recordingEventValue['event_type'] == RequestProcessor::EVENT_TYPE_MUTATION
+ ) &&
+ !empty(
+ $recordingEventValue['text']
+ )
+ ) {
+ $recording['events'][$recordingEventIndex]['text'] = $this->mutationManipulator->manipulate($recordingEventValue['text'], $idSiteHsr, $idLogHsr);
+ break;
+ }
+ }
+ }
+
+ return $this->renderTemplate('replayRecording', array(
+ 'idLogHsr' => $idLogHsr,
+ 'idSiteHsr' => $idSiteHsr,
+ 'recording' => $recording,
+ 'scrollAccuracy' => LogHsr::SCROLL_ACCURACY,
+ 'offsetAccuracy' => LogHsrEvent::OFFSET_ACCURACY,
+ 'autoPlayEnabled' => $autoPlayEnabled,
+ 'visitorProfileEnabled' => $isVisitorProfileEnabled,
+ 'skipPausesEnabled' => $skipPauses,
+ 'replaySpeed' => $replaySpeed,
+ 'currentPage' => $currentPage
+ ));
+ }
+
+ protected function setBasicVariablesView($view)
+ {
+ parent::setBasicVariablesView($view);
+
+ if (
+ Common::getRequestVar('module', '', 'string') === 'Widgetize'
+ && Common::getRequestVar('action', '', 'string') === 'iframe'
+ && Common::getRequestVar('moduleToWidgetize', '', 'string') === 'HeatmapSessionRecording'
+ ) {
+ $action = Common::getRequestVar('actionToWidgetize', '', 'string');
+ if (in_array($action, array('replayRecording', 'showHeatmap'), true)) {
+ $view->enableFrames = true;
+ }
+ }
+ }
+
+ private function getPluginSettings()
+ {
+ $login = Piwik::getCurrentUserLogin();
+
+ $settings = new PluginSettingsTable('HeatmapSessionRecording', $login);
+ return $settings;
+ }
+
+ public function saveSessionRecordingSettings()
+ {
+ Piwik::checkUserHasSomeViewAccess();
+ $this->validator->checkSessionRecordingEnabled();
+ // there is no nonce for this action but that should also not be needed here. as it is just replay settings
+
+ $autoPlay = Common::getRequestVar('autoplay', '0', 'int');
+ $replaySpeed = Common::getRequestVar('replayspeed', '1', 'int');
+ $skipPauses = Common::getRequestVar('skippauses', '0', 'int');
+
+ $settings = $this->getPluginSettings();
+ $settings->save(array('autoplay_pageviews' => $autoPlay, 'replay_speed' => $replaySpeed, 'skip_pauses' => $skipPauses));
+ }
+
+ private function initHeatmapAuth()
+ {
+ // todo remove in Matomo 5 when we hopefully no longer support IE 11.
+ // This is mostly there to prevent forwarding tokens through referrer to third parties
+ // most browsers support this except IE11
+ // we said we're technically OK with IE11 forwarding a view token in worst case but we still have this here for now
+ $token_auth = Common::getRequestVar('token_auth', '', 'string');
+
+ if (!empty($token_auth)) {
+ $auth = StaticContainer::get('Piwik\Auth');
+ $auth->setTokenAuth($token_auth);
+ $auth->setPassword(null);
+ $auth->setPasswordHash(null);
+ $auth->setLogin(null);
+
+ Session::start();
+ $sessionInitializer = new SessionInitializer();
+ $sessionInitializer->initSession($auth);
+
+ $url = preg_replace('/&token_auth=[^&]{20,38}|$/i', '', Url::getCurrentUrl());
+ if ($url) {
+ Url::redirectToUrl($url);
+ return;
+ }
+ }
+
+ // if no token_auth, we just rely on an existing session auth check
+ }
+
+ protected function setBasicVariablesNoneAdminView($view)
+ {
+ parent::setBasicVariablesNoneAdminView($view);
+ if (Piwik::getAction() === 'embedPage' && Piwik::getModule() === 'HeatmapSessionRecording') {
+ $view->setXFrameOptions('allow');
+ }
+ }
+
+ public function embedPage()
+ {
+ $this->checkNotInternetExplorerWhenUsingToken();
+ $this->initHeatmapAuth();
+ $nonceRandom = '';
+
+ if (
+ property_exists($this, 'securityPolicy') &&
+ method_exists($this->securityPolicy, 'allowEmbedPage')
+ ) {
+ $toSearch = array("'unsafe-inline' ", "'unsafe-eval' ", "'unsafe-inline'", "'unsafe-eval'");
+ $nonceRandom = $this->mutationManipulator->getNonce();
+ $this->securityPolicy->overridePolicy('default-src', $this->securityPolicy::RULE_EMBEDDED_FRAME);
+ $this->securityPolicy->overridePolicy('img-src', $this->securityPolicy::RULE_EMBEDDED_FRAME);
+ $this->securityPolicy->addPolicy('script-src', str_replace($toSearch, '', $this->securityPolicy::RULE_DEFAULT) . "'nonce-$nonceRandom'");
+ }
+
+ $pathPrefix = HeatmapSessionRecording::getPathPrefix();
+ $jQueryPath = 'node_modules/jquery/dist/jquery.min.js';
+ if (HeatmapSessionRecording::isMatomoForWordPress()) {
+ $jQueryPath = includes_url('js/jquery/jquery.js');
+ }
+
+ $idLogHsr = Common::getRequestVar('idLogHsr', 0, 'int');
+ $idSiteHsr = Common::getRequestVar('idSiteHsr', null, 'int');
+
+ $_GET['period'] = 'year'; // setting it randomly to not having to pass it in the URL
+ $_GET['date'] = 'today'; // date is ignored anyway
+
+ if (empty($idLogHsr)) {
+ $this->validator->checkHeatmapReportViewPermission($this->idSite);
+
+ $heatmap = $this->getHeatmap($this->idSite, $idSiteHsr);
+
+ if (isset($heatmap[0])) {
+ $heatmap = $heatmap[0];
+ }
+
+ $baseUrl = $heatmap['screenshot_url'];
+ $initialMutation = $heatmap['page_treemirror'];
+ } else {
+ $this->validator->checkSessionReportViewPermission($this->idSite);
+ $this->checkSessionRecordingExists($this->idSite, $idSiteHsr);
+
+ $recording = Request::processRequest('HeatmapSessionRecording.getEmbedSessionInfo', [
+ 'idSite' => $this->idSite,
+ 'idSiteHsr' => $idSiteHsr,
+ 'idLogHsr' => $idLogHsr,
+ ], $default = []);
+
+ if (empty($recording)) {
+ throw new \Exception(Piwik::translate('HeatmapSessionRecording_ErrorSessionRecordingDoesNotExist'));
+ }
+
+ $baseUrl = $recording['base_url'];
+ $map = array_flip(PageUrl::$urlPrefixMap);
+
+ if (isset($recording['url_prefix']) !== null && isset($map[$recording['url_prefix']])) {
+ $baseUrl = $map[$recording['url_prefix']] . $baseUrl;
+ }
+
+ if (!empty($recording['initial_mutation'])) {
+ $initialMutation = $recording['initial_mutation'];
+ } else {
+ $initialMutation = '';
+ }
+ }
+
+ $initialMutation = $this->mutationManipulator->manipulate($initialMutation, $idSiteHsr, $idLogHsr);
+
+ return $this->renderTemplate('embedPage', array(
+ 'idLogHsr' => $idLogHsr,
+ 'idSiteHsr' => $idSiteHsr,
+ 'initialMutation' => $initialMutation,
+ 'baseUrl' => $baseUrl,
+ 'pathPrefix' => $pathPrefix,
+ 'jQueryPath' => $jQueryPath,
+ 'nonceRandom' => $nonceRandom
+ ));
+ }
+
+ public function showHeatmap()
+ {
+ $this->validator->checkHeatmapReportViewPermission($this->idSite);
+ $this->checkNotInternetExplorerWhenUsingToken();
+
+ $idSiteHsr = Common::getRequestVar('idSiteHsr', null, 'int');
+ $heatmapType = Common::getRequestVar('heatmapType', RequestProcessor::EVENT_TYPE_CLICK, 'int');
+ $deviceType = Common::getRequestVar('deviceType', LogHsr::DEVICE_TYPE_DESKTOP, 'int');
+
+ $heatmap = Request::processRequest('HeatmapSessionRecording.getHeatmap', array(
+ 'idSite' => $this->idSite,
+ 'idSiteHsr' => $idSiteHsr
+ ), $default = []);
+
+ if (isset($heatmap[0])) {
+ $heatmap = $heatmap[0];
+ }
+
+ $requestDate = $this->siteHsrModel->getPiwikRequestDate($heatmap);
+ $period = $requestDate['period'];
+ $dateRange = $requestDate['date'];
+
+ if (
+ !PeriodFactory::isPeriodEnabledForAPI($period) ||
+ Common::getRequestVar('useDateUrl', 0, 'int')
+ ) {
+ $period = Common::getRequestVar('period', null, 'string');
+ $dateRange = Common::getRequestVar('date', null, 'string');
+ }
+
+ try {
+ PeriodFactory::checkPeriodIsEnabled($period);
+ } catch (\Exception $e) {
+ $periodEscaped = Common::sanitizeInputValue(Piwik::translate('HeatmapSessionRecording_PeriodDisabledErrorMessage', $period));
+ return '' . $periodEscaped . '
';
+ }
+
+ $metadata = Request::processRequest('HeatmapSessionRecording.getRecordedHeatmapMetadata', array(
+ 'idSite' => $this->idSite,
+ 'idSiteHsr' => $idSiteHsr,
+ 'period' => $period,
+ 'date' => $dateRange
+ ), $default = []);
+
+ if (isset($metadata[0])) {
+ $metadata = $metadata[0];
+ }
+
+ $editUrl = 'index.php' . Url::getCurrentQueryStringWithParametersModified(array(
+ 'module' => 'HeatmapSessionRecording',
+ 'action' => 'manageHeatmap'
+ )) . '#?idSiteHsr=' . (int)$idSiteHsr;
+
+ $reportDocumentation = '';
+ if ($heatmap['status'] == SiteHsrDao::STATUS_ACTIVE) {
+ $reportDocumentation = Piwik::translate('HeatmapSessionRecording_RecordedHeatmapDocStatusActive', array($heatmap['sample_limit'], $heatmap['sample_rate'] . '%'));
+ } elseif ($heatmap['status'] == SiteHsrDao::STATUS_ENDED) {
+ $reportDocumentation = Piwik::translate('HeatmapSessionRecording_RecordedHeatmapDocStatusEnded');
+ }
+
+ $includedCountries = $this->systemSettings->getIncludedCountries();
+
+ return $this->renderTemplate('showHeatmap', array(
+ 'idSiteHsr' => $idSiteHsr,
+ 'editUrl' => $editUrl,
+ 'heatmapType' => $heatmapType,
+ 'deviceType' => $deviceType,
+ 'heatmapPeriod' => $period,
+ 'heatmapDate' => $dateRange,
+ 'heatmap' => $heatmap,
+ 'isActive' => $heatmap['status'] == SiteHsrDao::STATUS_ACTIVE,
+ 'heatmapMetadata' => $metadata,
+ 'reportDocumentation' => $reportDocumentation,
+ 'isScroll' => $heatmapType == RequestProcessor::EVENT_TYPE_SCROLL,
+ 'offsetAccuracy' => LogHsrEvent::OFFSET_ACCURACY,
+ 'heatmapTypes' => API::getInstance()->getAvailableHeatmapTypes(),
+ 'deviceTypes' => API::getInstance()->getAvailableDeviceTypes(),
+ 'includedCountries' => !empty($includedCountries) ? implode(', ', $includedCountries) : '',
+ 'desktopPreviewSize' => $this->configuration->getDefaultHeatmapWidth(),
+ 'allowedWidth' => Configuration::HEATMAP_ALLOWED_WIDTHS,
+ 'noDataMessageKey' => HeatmapSessionRecording::getTranslationKey('noDataHeatmap'),
+ 'isMatomoJsWritable' => HeatmapSessionRecording::isMatomoJsWritable(),
+ ));
+ }
+
+ private function getHeatmap($idSite, $idSiteHsr)
+ {
+ $heatmap = Request::processRequest('HeatmapSessionRecording.getHeatmap', [
+ 'idSite' => $idSite,
+ 'idSiteHsr' => $idSiteHsr,
+ ], $default = []);
+ if (empty($heatmap)) {
+ throw new \Exception(Piwik::translate('HeatmapSessionRecording_ErrorHeatmapDoesNotExist'));
+ }
+ return $heatmap;
+ }
+
+ private function checkSessionRecordingExists($idSite, $idSiteHsr)
+ {
+ $sessionRecording = Request::processRequest('HeatmapSessionRecording.getSessionRecording', [
+ 'idSite' => $idSite,
+ 'idSiteHsr' => $idSiteHsr,
+ ], $default = []);
+ if (empty($sessionRecording)) {
+ throw new \Exception(Piwik::translate('HeatmapSessionRecording_ErrorSessionRecordingDoesNotExist'));
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsr.php b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsr.php
new file mode 100644
index 0000000..8851782
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsr.php
@@ -0,0 +1,375 @@
+tablePrefixed = Common::prefixTable($this->table);
+ $this->logHsrSite = $logHsrSite;
+ }
+
+ private function getDb()
+ {
+ if (!isset($this->db)) {
+ $this->db = Db::get();
+ }
+ return $this->db;
+ }
+
+ public function install()
+ {
+ DbHelper::createTable($this->table, "
+ `idloghsr` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `idsite` INT UNSIGNED NOT NULL,
+ `idvisit` BIGINT UNSIGNED NOT NULL,
+ `idhsrview` CHAR(6) NOT NULL,
+ `idpageview` CHAR(6) NULL,
+ `idaction_url` INT(10) UNSIGNED NOT NULL DEFAULT 0,
+ `device_type` TINYINT(1) NOT NULL DEFAULT 1,
+ `server_time` DATETIME NOT NULL,
+ `time_on_page` BIGINT(8) UNSIGNED NOT NULL,
+ `viewport_w_px` SMALLINT(5) UNSIGNED DEFAULT 0,
+ `viewport_h_px` SMALLINT(5) UNSIGNED DEFAULT 0,
+ `scroll_y_max_relative` SMALLINT(5) UNSIGNED DEFAULT 0,
+ `fold_y_relative` SMALLINT(5) UNSIGNED DEFAULT 0,
+ PRIMARY KEY(`idloghsr`),
+ UNIQUE KEY idvisit_idhsrview (`idvisit`,`idhsrview`),
+ KEY idsite_servertime (`idsite`,`server_time`)");
+
+ // idpageview is only there so we can add it to visitor log later. Please note that idpageview is only set on
+ // the first tracking request. As the user may track a new pageview during the recording, the pageview may
+ // change over time. This is why we need the idhsrview.
+
+ // we need the idhsrview as there can be many recordings during one visit and this way we can control when
+ // to trigger a new recording / heatmap in the tracker by changing this id
+ }
+
+ public function uninstall()
+ {
+ Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed));
+ }
+
+ protected function getDeviceWidth($resolution)
+ {
+ if (!empty($resolution)) {
+ $parts = explode('x', $resolution);
+ if (count($parts) === 2 && $parts[0] > 1 && $parts[1] > 1) {
+ $width = $parts[0];
+ return (int) $width;
+ }
+ }
+
+ return 1280; // default desktop
+ }
+
+ protected function getDeviceType($hsrSiteIds, $idSite, $userAgent, $deviceWidth)
+ {
+ $deviceType = null;
+
+ // we want to detect device type only once for faster performance
+ $ddFactory = StaticContainer::get(\Piwik\DeviceDetector\DeviceDetectorFactory::class);
+ $deviceDetector = $ddFactory->makeInstance($userAgent);
+ $device = $deviceDetector->getDevice();
+
+ $checkWidth = false;
+ if (
+ in_array(
+ $device,
+ array(
+ AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE,
+ AbstractDeviceParser::DEVICE_TYPE_PHABLET,
+ AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE,
+ AbstractDeviceParser::DEVICE_TYPE_CAMERA,
+ AbstractDeviceParser::DEVICE_TYPE_CAR_BROWSER
+ ),
+ $strict = true
+ )
+ ) {
+ $deviceType = self::DEVICE_TYPE_MOBILE;
+ } elseif (in_array($device, array(AbstractDeviceParser::DEVICE_TYPE_TABLET), $strict = true)) {
+ $deviceType = self::DEVICE_TYPE_TABLET;
+ } elseif ($deviceType === AbstractDeviceParser::DEVICE_TYPE_DESKTOP) {
+ $deviceType = LogHsr::DEVICE_TYPE_DESKTOP;
+ $checkWidth = true;
+ } else {
+ $checkWidth = true;
+ }
+
+ if ($checkWidth && !empty($deviceWidth)) {
+ $hsrs = $this->getCachedHsrs($idSite);
+
+ foreach ($hsrs as $hsr) {
+ // the device type is only relevant for heatmaps so we only look for breakpoints in heatmaps
+ if (
+ $hsr['record_type'] == SiteHsrDao::RECORD_TYPE_HEATMAP
+ && in_array($hsr['idsitehsr'], $hsrSiteIds)
+ ) {
+ if ($deviceWidth < $hsr['breakpoint_mobile']) {
+ // resolution has to be lower than this
+ $deviceType = self::DEVICE_TYPE_MOBILE;
+ } elseif ($deviceWidth < $hsr['breakpoint_tablet']) {
+ $deviceType = self::DEVICE_TYPE_TABLET;
+ } else {
+ $deviceType = self::DEVICE_TYPE_DESKTOP;
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (empty($deviceType)) {
+ $deviceType = LogHsr::DEVICE_TYPE_DESKTOP;
+ }
+
+ return $deviceType;
+ }
+
+ protected function getCachedHsrs($idSite)
+ {
+ $cache = Tracker\Cache::getCacheWebsiteAttributes($idSite);
+
+ if (!empty($cache['hsr'])) {
+ return $cache['hsr'];
+ }
+
+ return array();
+ }
+
+ public function findIdLogHsr($idVisit, $idHsrView)
+ {
+ $query = sprintf('SELECT idloghsr FROM %s WHERE idvisit = ? and idhsrview = ? LIMIT 1', $this->tablePrefixed);
+
+ return $this->getDb()->fetchOne($query, array($idVisit, $idHsrView));
+ }
+
+ public function hasRecordedIdVisit($idVisit, $idSiteHsr)
+ {
+ $siteTable = Common::prefixTable('log_hsr_site');
+ $query = sprintf('SELECT lhsr.idvisit
+ FROM %s lhsr
+ LEFT JOIN %s lhsrsite ON lhsr.idloghsr=lhsrsite.idloghsr
+ WHERE lhsr.idvisit = ? and lhsrsite.idsitehsr = ?
+ LIMIT 1', $this->tablePrefixed, $siteTable);
+ $id = $this->getDb()->fetchOne($query, array($idVisit, $idSiteHsr));
+ return !empty($id);
+ }
+
+ // $hsrSiteIds => one recording may be long to several actual recordings.
+ public function record($hsrSiteIds, $idSite, $idVisit, $idHsrView, $idPageview, $url, $serverTime, $userAgent, $resolution, $timeOnPage, $viewportW, $viewportH, $scrollYMaxRelative, $foldYRelative)
+ {
+ if ($foldYRelative > self::SCROLL_ACCURACY) {
+ $foldYRelative = self::SCROLL_ACCURACY;
+ }
+
+ if ($scrollYMaxRelative > self::SCROLL_ACCURACY) {
+ $scrollYMaxRelative = self::SCROLL_ACCURACY;
+ }
+
+ $idLogHsr = $this->findIdLogHsr($idVisit, $idHsrView);
+
+ if (empty($idLogHsr)) {
+ // to prevent race conditions we use atomic insert. It may lead to more gaps in auto increment but there is
+ // no way around it
+
+ Piwik::postEvent('HeatmapSessionRecording.trackNewHsrSiteIds', array(&$hsrSiteIds, array('idSite' => $idSite, 'serverTime' => $serverTime, 'idVisit' => $idVisit)));
+
+ if (empty($hsrSiteIds)) {
+ throw new \Exception('No hsrSiteIds');
+ }
+
+ $values = array(
+ 'idvisit' => $idVisit,
+ 'idsite' => $idSite,
+ 'idhsrview' => $idHsrView,
+ 'idpageview' => $idPageview,
+ 'server_time' => $serverTime,
+ 'time_on_page' => $timeOnPage,
+ 'viewport_w_px' => $viewportW,
+ 'viewport_h_px' => $viewportH,
+ 'scroll_y_max_relative' => (int)$scrollYMaxRelative,
+ 'fold_y_relative' => (int) $foldYRelative,
+ );
+
+ $columns = implode('`,`', array_keys($values));
+ $bind = array_values($values);
+ $sql = sprintf('INSERT INTO %s (`%s`) VALUES(?,?,?,?,?,?,?,?,?,?)', $this->tablePrefixed, $columns);
+
+ try {
+ $result = $this->getDb()->query($sql, $bind);
+ } catch (\Exception $e) {
+ if (Db::get()->isErrNo($e, \Piwik\Updater\Migration\Db::ERROR_CODE_DUPLICATE_ENTRY)) {
+ // race condition where two tried to insert at same time... we need to update instead
+
+ $idLogHsr = $this->findIdLogHsr($idVisit, $idHsrView);
+ $this->updateRecord($idLogHsr, $timeOnPage, $scrollYMaxRelative);
+ return $idLogHsr;
+ }
+ throw $e;
+ }
+
+ $all = $this->getDb()->rowCount($result);
+
+ $idLogHsr = $this->getDb()->lastInsertId();
+
+ if ($all === 1 || $all === '1') {
+ // was inserted, resolve idaction! would be 2 or 0 on update
+ // to be efficient we want to resolve idaction only once
+ $url = PageUrl::normalizeUrl($url);
+ $ids = TableLogAction::loadIdsAction(array('idaction_url' => array($url['url'], Action::TYPE_PAGE_URL, $url['prefixId'])));
+
+ if (!empty($viewportW)) {
+ $deviceWidth = (int) $viewportW;
+ } else {
+ $deviceWidth = $this->getDeviceWidth($resolution);
+ }
+ $deviceType = $this->getDeviceType($hsrSiteIds, $idSite, $userAgent, $deviceWidth);
+
+ $idaction = $ids['idaction_url'];
+ $this->getDb()->query(
+ sprintf('UPDATE %s set idaction_url = ?, device_type = ? where idloghsr = ?', $this->tablePrefixed),
+ array($idaction, $deviceType, $idLogHsr)
+ );
+
+ foreach ($hsrSiteIds as $hsrId) {
+ // for performance reasons we check the limit only on hsr start and we make this way sure to still
+ // accept all following requests to that hsr
+ $this->logHsrSite->linkRecord($idLogHsr, $hsrId);
+ }
+ }
+ } else {
+ $this->updateRecord($idLogHsr, $timeOnPage, $scrollYMaxRelative);
+ }
+
+ return $idLogHsr;
+ }
+
+ public function updateRecord($idLogHsr, $timeOnPage, $scrollYMaxRelative)
+ {
+ $sql = sprintf(
+ 'UPDATE %s SET
+ time_on_page = if(? > time_on_page, ?, time_on_page),
+ scroll_y_max_relative = if(? > scroll_y_max_relative, ?, scroll_y_max_relative)
+ WHERE idloghsr = ?',
+ $this->tablePrefixed
+ );
+
+ $bind = array();
+ $bind[] = $timeOnPage;
+ $bind[] = $timeOnPage;
+ $bind[] = $scrollYMaxRelative;
+ $bind[] = $scrollYMaxRelative;
+ $bind[] = $idLogHsr;
+
+ $this->getDb()->query($sql, $bind);
+ }
+
+ public function getAllRecords()
+ {
+ return $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed);
+ }
+
+ public function findLogHsrIdsInVisit($idSite, $idVisit)
+ {
+ $rows = Db::fetchAll(sprintf('SELECT idloghsr FROM %s WHERE idvisit = ? and idsite = ?', $this->tablePrefixed), array($idVisit, $idSite));
+
+ $idLogHsrs = array();
+ foreach ($rows as $row) {
+ $idLogHsrs[] = (int) $row['idloghsr'];
+ }
+
+ return $idLogHsrs;
+ }
+
+ public function findDeletedLogHsrIds()
+ {
+ // DELETE ALL LOG ENTRIES WHOSE IDSITEHSR DOES NO LONGER EXIST
+ $rows = Db::fetchAll(sprintf(
+ 'SELECT DISTINCT log_hsr.idloghsr FROM %s log_hsr LEFT OUTER JOIN %s log_hsr_site ON log_hsr.idloghsr = log_hsr_site.idloghsr WHERE log_hsr_site.idsitehsr IS NULL',
+ $this->tablePrefixed,
+ Common::prefixTable('log_hsr_site')
+ ));
+
+ $idLogHsrsToDelete = array();
+ foreach ($rows as $row) {
+ $idLogHsrsToDelete[] = (int) $row['idloghsr'];
+ }
+
+ return $idLogHsrsToDelete;
+ }
+
+ public function deleteIdLogHsrsFromAllTables($idLogHsrsToDelete)
+ {
+ if (!is_array($idLogHsrsToDelete)) {
+ throw new \Exception('idLogHsrsToDelete is not an array');
+ }
+
+ if (empty($idLogHsrsToDelete)) {
+ return;
+ }
+
+ // we delete them in chunks of 2500
+ $idLogHsrsToDelete = array_chunk($idLogHsrsToDelete, 2500);
+
+ $tablesToDelete = array(
+ Common::prefixTable('log_hsr_event'),
+ Common::prefixTable('log_hsr_site'),
+ Common::prefixTable('log_hsr'),
+ );
+ foreach ($idLogHsrsToDelete as $idsToDelete) {
+ $idsToDelete = array_map('intval', $idsToDelete);
+ $idsToDelete = implode(',', $idsToDelete);
+ foreach ($tablesToDelete as $tableToDelete) {
+ $sql = sprintf('DELETE FROM %s WHERE idloghsr IN(%s)', $tableToDelete, $idsToDelete);
+ Db::query($sql);
+ }
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrBlob.php b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrBlob.php
new file mode 100644
index 0000000..9945d50
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrBlob.php
@@ -0,0 +1,180 @@
+tablePrefixed = Common::prefixTable($this->table);
+ }
+
+ private function getDb()
+ {
+ if (!isset($this->db)) {
+ $this->db = Db::get();
+ }
+ return $this->db;
+ }
+
+ public function install()
+ {
+ DbHelper::createTable($this->table, "
+ `idhsrblob` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `hash` INT(10) UNSIGNED NOT NULL,
+ `compressed` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
+ `value` MEDIUMBLOB NULL DEFAULT NULL,
+ PRIMARY KEY (`idhsrblob`),
+ INDEX (`hash`)");
+
+ // we always build the hash on the raw text for simplicity
+ }
+
+ public function uninstall()
+ {
+ Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed));
+ }
+
+ public function findEntry($textHash, $text, $textCompressed)
+ {
+ $sql = sprintf('SELECT idhsrblob FROM %s WHERE `hash` = ? and (`value` = ? or `value` = ?) LIMIT 1', $this->tablePrefixed);
+ $id = $this->getDb()->fetchOne($sql, array($textHash, $text, $textCompressed));
+
+ return $id;
+ }
+
+ public function createEntry($textHash, $text, $isCompressed)
+ {
+ $sql = sprintf('INSERT INTO %s (`hash`, `compressed`, `value`) VALUES(?,?,?) ', $this->tablePrefixed);
+ $this->getDb()->query($sql, array($textHash, (int) $isCompressed, $text));
+
+ return $this->getDb()->lastInsertId();
+ }
+
+ public function record($text)
+ {
+ if ($text === null || $text === false) {
+ return null;
+ }
+
+ $textHash = abs(crc32($text));
+ $textCompressed = $this->compress($text);
+
+ $id = $this->findEntry($textHash, $text, $textCompressed);
+
+ if (!empty($id)) {
+ return $id;
+ }
+
+ $isCompressed = 0;
+ if ($text !== $textCompressed && strlen($textCompressed) < strlen($text)) {
+ // detect if it is more efficient to store compressed or raw text
+ $text = $textCompressed;
+ $isCompressed = 1;
+ }
+
+ return $this->createEntry($textHash, $text, $isCompressed);
+ }
+
+ public function deleteUnusedBlobEntries()
+ {
+ $eventTable = Common::prefixTable('log_hsr_event');
+ $blobTable = Common::prefixTable('log_hsr_blob');
+
+ $blobEntries = Db::fetchAll('SELECT distinct idhsrblob FROM ' . $eventTable . ' LIMIT 2');
+ $blobEntries = array_filter($blobEntries, function ($val) {
+ return $val['idhsrblob'] !== null;
+ }); // remove null values.
+
+ if (empty($blobEntries)) {
+ // no longer any blobs in use... delete all blobs
+ $sql = 'DELETE FROM ' . $blobTable;
+ Db::query($sql);
+ return $sql;
+ }
+
+ $indexes = Db::fetchAll('SHOW INDEX FROM ' . $eventTable);
+ $indexSql = '';
+ foreach ($indexes as $index) {
+ if (
+ (!empty($index['Column_name']) && !empty($index['Key_name']) && $index['Column_name'] === 'idhsrblob')
+ || (!empty($index['Key_name']) && $index['Key_name'] === 'idhsrblob')
+ || (!empty($index['Key_name']) && $index['Key_name'] === 'index_idhsrblob')
+ ) {
+ $indexSql = 'FORCE INDEX FOR JOIN (' . $index['Key_name'] . ')';
+ break;
+ }
+ }
+
+ $sql = sprintf('DELETE hsrblob
+ FROM %s hsrblob
+ LEFT JOIN %s hsrevent %s on hsrblob.idhsrblob = hsrevent.idhsrblob
+ WHERE hsrevent.idloghsr is null', $blobTable, $eventTable, $indexSql);
+
+ Db::query($sql);
+ return $sql;
+ }
+
+ public function getAllRecords()
+ {
+ $blobs = $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed);
+ return $this->enrichRecords($blobs);
+ }
+
+ private function enrichRecords($blobs)
+ {
+ if (!empty($blobs)) {
+ foreach ($blobs as $index => &$blob) {
+ if (!empty($blob['compressed'])) {
+ $blob['value'] = $this->uncompress($blob['value']);
+ }
+ }
+ }
+
+ return $blobs;
+ }
+
+ private function compress($data)
+ {
+ if (!empty($data)) {
+ return gzcompress($data);
+ }
+
+ return $data;
+ }
+
+ private function uncompress($data)
+ {
+ if (!empty($data)) {
+ return gzuncompress($data);
+ }
+
+ return $data;
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrEvent.php b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrEvent.php
new file mode 100644
index 0000000..6a64065
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrEvent.php
@@ -0,0 +1,166 @@
+tablePrefixed = Common::prefixTable($this->table);
+ $this->logBlobHsr = $logBlobHsr;
+ }
+
+ private function getDb()
+ {
+ if (!isset($this->db)) {
+ $this->db = Db::get();
+ }
+ return $this->db;
+ }
+
+ public function install()
+ {
+ DbHelper::createTable($this->table, "
+ `idhsrevent` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ `idloghsr` INT(10) UNSIGNED NOT NULL,
+ `time_since_load` BIGINT(8) UNSIGNED NOT NULL DEFAULT 0,
+ `event_type` TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ `idselector` INT(10) UNSIGNED NULL DEFAULT NULL,
+ `x` SMALLINT(5) NOT NULL DEFAULT 0,
+ `y` SMALLINT(5) NOT NULL DEFAULT 0,
+ `idhsrblob` INT(10) UNSIGNED DEFAULT NULL,
+ PRIMARY KEY(`idhsrevent`),
+ INDEX idloghsr (`idloghsr`),
+ INDEX idhsrblob (`idhsrblob`)");
+ // x and y is not unsigned on purpose as it may hold rarely a negative value
+ }
+
+ public function uninstall()
+ {
+ Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed));
+ }
+
+ public function record($idloghsr, $timeSinceLoad, $eventType, $idSelector, $x, $y, $text)
+ {
+ if ($x > self::MAX_SIZE) {
+ $x = self::MAX_SIZE;
+ }
+
+ if ($y > self::MAX_SIZE) {
+ $y = self::MAX_SIZE;
+ }
+
+ if ($x === null || $x === false) {
+ $x = 0;
+ }
+
+ if ($y === null || $y === false) {
+ $y = 0;
+ }
+
+ $idHsrBlob = $this->logBlobHsr->record($text);
+
+ $values = array(
+ 'idloghsr' => $idloghsr,
+ 'time_since_load' => $timeSinceLoad,
+ 'event_type' => $eventType,
+ 'idselector' => $idSelector,
+ 'x' => $x,
+ 'y' => $y,
+ 'idhsrblob' => $idHsrBlob,
+ );
+
+ $columns = implode('`,`', array_keys($values));
+
+ $sql = sprintf('INSERT INTO %s (`%s`) VALUES(?,?,?,?,?,?,?) ', $this->tablePrefixed, $columns);
+
+ $bind = array_values($values);
+
+ $this->getDb()->query($sql, $bind);
+ }
+
+ public function getEventsForPageview($idLogHsr)
+ {
+ $sql = sprintf('SELECT %1$s.time_since_load, %1$s.event_type, %1$s.x, %1$s.y, %2$s.name as selector, %3$s.value as text, %3$s.compressed
+ FROM %1$s
+ LEFT JOIN %2$s ON %1$s.idselector = %2$s.idaction
+ LEFT JOIN %3$s ON %1$s.idhsrblob = %3$s.idhsrblob
+ WHERE %1$s.idloghsr = ? and %1$s.event_type != ?
+ ORDER BY time_since_load ASC', $this->tablePrefixed, Common::prefixTable('log_action'), Common::prefixTable('log_hsr_blob'));
+
+ $rows = $this->getDb()->fetchAll($sql, array($idLogHsr, RequestProcessor::EVENT_TYPE_CSS));
+ foreach ($rows as $index => $row) {
+ if (!empty($row['compressed'])) {
+ $rows[$index]['text'] = gzuncompress($row['text']);
+ }
+ unset($rows[$index]['compressed']);
+ }
+ return $rows;
+ }
+
+ public function getCssEvents($idSiteHsr, $idLoghsr = '')
+ {
+ //idLogHsr will be empty in case of heatmaps, we cannot use it in where clause to resolve that, when its heatmap the where condition is `AND 1=1` and for session recording its `AND x.idloghsr=$idLoghsr`
+ $idLogHsrLhs = '1';
+ $idLogHsrRhs = '1';
+ if (!empty($idLoghsr)) {
+ $idLogHsrLhs = 'x.idloghsr';
+ $idLogHsrRhs = $idLoghsr;
+ }
+ $sql = sprintf('SELECT distinct z.idhsrblob,a.name as url, z.value as text, z.compressed
+ FROM %2$s x,%3$s y,%4$s z,%5$s a
+ WHERE x.idsitehsr=? AND %1$s=? and y.event_type=? and x.idloghsr=y.idloghsr and y.idhsrblob = z.idhsrblob and a.idaction=y.idselector
+ order by z.idhsrblob ASC', $idLogHsrLhs, Common::prefixTable('log_hsr_site'), Common::prefixTable('log_hsr_event'), Common::prefixTable('log_hsr_blob'), Common::prefixTable('log_action'));
+
+ $rows = $this->getDb()->fetchAll($sql, array($idSiteHsr, $idLogHsrRhs, RequestProcessor::EVENT_TYPE_CSS));
+ foreach ($rows as $index => $row) {
+ if (!empty($row['compressed'])) {
+ $rows[$index]['text'] = gzuncompress($row['text']);
+ }
+ unset($rows[$index]['compressed']);
+ }
+ return $rows;
+ }
+
+ public function getAllRecords()
+ {
+ return $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrSite.php b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrSite.php
new file mode 100644
index 0000000..708aba3
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrSite.php
@@ -0,0 +1,141 @@
+tablePrefixed = Common::prefixTable($this->table);
+ }
+
+ private function getDb()
+ {
+ if (!isset($this->db)) {
+ $this->db = Db::get();
+ }
+ return $this->db;
+ }
+
+ public function install()
+ {
+ // it actually also has the advantage that removing an entry will be fast because when a user clicks on
+ // "delete heatmap" we could only remove this entry, and then have a daily cronjob to delete all entries that are
+ // no longer linked. instead of having to directly delete all data. Also it is more efficient to track when eg
+ // a session and a heatmap is being recording at the same time or when several heatmaps are being recorded at once
+ DbHelper::createTable($this->table, "
+ `idsitehsr` INT(10) UNSIGNED NOT NULL,
+ `idloghsr` INT(10) UNSIGNED NOT NULL,
+ PRIMARY KEY(`idsitehsr`, `idloghsr`),
+ INDEX index_idloghsr (`idloghsr`)");
+ }
+
+ public function uninstall()
+ {
+ Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed));
+ }
+
+ public function linkRecord($idLogHsr, $idSiteHsr)
+ {
+ $bind = array($idLogHsr,$idSiteHsr);
+ $sql = sprintf('INSERT INTO %s (`idloghsr`, `idsitehsr`) VALUES(?,?)', $this->tablePrefixed);
+
+ try {
+ $this->getDb()->query($sql, $bind);
+ } catch (\Exception $e) {
+ if (Db::get()->isErrNo($e, \Piwik\Updater\Migration\Db::ERROR_CODE_DUPLICATE_ENTRY)) {
+ return;
+ }
+ throw $e;
+ }
+ }
+
+ // should be fast as covered index
+ public function getNumPageViews($idSiteHsr)
+ {
+ $sql = sprintf('SELECT count(*) as numsamples FROM %s WHERE idsitehsr = ?', $this->tablePrefixed);
+
+ return (int) $this->getDb()->fetchOne($sql, array($idSiteHsr));
+ }
+
+ // should be fast as covered index
+ public function getNumSessions($idSiteHsr)
+ {
+ $sql = sprintf(
+ 'SELECT count(distinct idvisit)
+ FROM %s loghsrsite
+ left join %s loghsr on loghsr.idloghsr = loghsrsite.idloghsr
+ left join %s loghsrevent on loghsr.idloghsr = loghsrevent.idloghsr and loghsrevent.event_type = %s
+ WHERE loghsrsite.idsitehsr = ? and loghsrevent.idhsrblob is not null',
+ $this->tablePrefixed,
+ Common::prefixTable('log_hsr'),
+ Common::prefixTable('log_hsr_event'),
+ RequestProcessor::EVENT_TYPE_INITIAL_DOM
+ );
+
+ return (int) $this->getDb()->fetchOne($sql, array($idSiteHsr));
+ }
+
+ public function unlinkRecord($idLogHsr, $idSiteHsr)
+ {
+ $sql = sprintf('DELETE FROM %s WHERE idsitehsr = ? and idloghsr = ?', $this->tablePrefixed);
+
+ return $this->getDb()->query($sql, array($idSiteHsr, $idLogHsr));
+ }
+
+ public function unlinkSiteRecords($idSiteHsr)
+ {
+ $sql = sprintf('DELETE FROM %s WHERE idsitehsr = ?', $this->tablePrefixed);
+
+ return $this->getDb()->query($sql, array($idSiteHsr));
+ }
+
+ public function getAllRecords()
+ {
+ return $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed);
+ }
+
+ public function deleteNoLongerNeededRecords()
+ {
+ // DELETE ALL linked LOG ENTRIES WHOSE idsite does no longer exist or was removed
+ // we delete links for removed site_hsr entries, and for site_hsr entries with status deleted
+ // this query should only delete entries when they were deleted manually in the database basically.
+ // otherwise the application takes already care of removing the needed links
+ $sql = sprintf(
+ 'DELETE FROM %1$s WHERE %1$s.idsitehsr NOT IN (select site_hsr.idsitehsr from %2$s site_hsr where site_hsr.status = "%3$s" or site_hsr.status = "%4$s")',
+ Common::prefixTable('log_hsr_site'),
+ Common::prefixTable('site_hsr'),
+ SiteHsrDao::STATUS_ACTIVE,
+ SiteHsrDao::STATUS_ENDED
+ );
+
+ Db::query($sql);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Dao/SiteHsrDao.php b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/SiteHsrDao.php
new file mode 100644
index 0000000..037d00c
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Dao/SiteHsrDao.php
@@ -0,0 +1,422 @@
+tablePrefixed = Common::prefixTable($this->table);
+ }
+
+ private function getDb()
+ {
+ if (!isset($this->db)) {
+ $this->db = Db::get();
+ }
+ return $this->db;
+ }
+
+ public function install()
+ {
+ DbHelper::createTable($this->table, "
+ `idsitehsr` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `idsite` INT(10) UNSIGNED NOT NULL,
+ `name` VARCHAR(" . Name::MAX_LENGTH . ") NOT NULL,
+ `sample_rate` DECIMAL(4,1) UNSIGNED NOT NULL DEFAULT " . SampleRate::MAX_RATE . ",
+ `sample_limit` MEDIUMINT(8) UNSIGNED NOT NULL DEFAULT 1000,
+ `match_page_rules` TEXT DEFAULT '',
+ `excluded_elements` TEXT DEFAULT '',
+ `record_type` TINYINT(1) UNSIGNED DEFAULT 0,
+ `page_treemirror` MEDIUMBLOB NULL DEFAULT NULL,
+ `screenshot_url` VARCHAR(300) NULL DEFAULT NULL,
+ `breakpoint_mobile` SMALLINT(5) UNSIGNED NOT NULL DEFAULT 0,
+ `breakpoint_tablet` SMALLINT(5) UNSIGNED NOT NULL DEFAULT 0,
+ `min_session_time` SMALLINT(5) UNSIGNED NOT NULL DEFAULT 0,
+ `requires_activity` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
+ `capture_keystrokes` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
+ `created_date` DATETIME NOT NULL,
+ `updated_date` DATETIME NOT NULL,
+ `status` VARCHAR(10) NOT NULL DEFAULT '" . self::STATUS_ACTIVE . "',
+ `capture_manually` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
+ PRIMARY KEY(`idsitehsr`),
+ INDEX index_status_idsite (`status`, `idsite`),
+ INDEX index_idsite_record_type (`idsite`, `record_type`)");
+ }
+
+ public function createHeatmapRecord($idSite, $name, $sampleLimit, $sampleRate, $matchPageRules, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet, $status, $captureDomManually, $createdDate)
+ {
+ $columns = array(
+ 'idsite' => $idSite,
+ 'name' => $name,
+ 'sample_limit' => $sampleLimit,
+ 'match_page_rules' => $matchPageRules,
+ 'sample_rate' => $sampleRate,
+ 'status' => $status,
+ 'record_type' => self::RECORD_TYPE_HEATMAP,
+ 'created_date' => $createdDate,
+ 'updated_date' => $createdDate,
+ 'capture_manually' => !empty($captureDomManually) ? 1 : 0,
+ );
+
+ if (!empty($excludedElements)) {
+ $columns['excluded_elements'] = $excludedElements;
+ }
+
+ if (!empty($screenshotUrl)) {
+ $columns['screenshot_url'] = $screenshotUrl;
+ }
+ if ($breakpointMobile !== false && $breakpointMobile !== null) {
+ $columns['breakpoint_mobile'] = $breakpointMobile;
+ }
+
+ if ($breakpointTablet !== false && $breakpointTablet !== null) {
+ $columns['breakpoint_tablet'] = $breakpointTablet;
+ }
+
+ return $this->insertColumns($columns);
+ }
+
+ public function createSessionRecord($idSite, $name, $sampleLimit, $sampleRate, $matchPageRules, $minSessionTime, $requiresActivity, $captureKeystrokes, $status, $createdDate)
+ {
+ $columns = array(
+ 'idsite' => $idSite,
+ 'name' => $name,
+ 'sample_limit' => $sampleLimit,
+ 'match_page_rules' => $matchPageRules,
+ 'sample_rate' => $sampleRate,
+ 'status' => $status,
+ 'record_type' => self::RECORD_TYPE_SESSION,
+ 'min_session_time' => !empty($minSessionTime) ? $minSessionTime : 0,
+ 'requires_activity' => !empty($requiresActivity) ? 1 : 0,
+ 'capture_keystrokes' => !empty($captureKeystrokes) ? 1 : 0,
+ 'created_date' => $createdDate,
+ 'updated_date' => $createdDate,
+ );
+
+ return $this->insertColumns($columns);
+ }
+
+ private function insertColumns($columns)
+ {
+ $columns = $this->encodeFieldsWhereNeeded($columns);
+
+ $bind = array_values($columns);
+ $placeholder = Common::getSqlStringFieldsArray($columns);
+
+ $sql = sprintf(
+ 'INSERT INTO %s (`%s`) VALUES(%s)',
+ $this->tablePrefixed,
+ implode('`,`', array_keys($columns)),
+ $placeholder
+ );
+
+ $this->getDb()->query($sql, $bind);
+
+ $idSiteHsr = $this->getDb()->lastInsertId();
+
+ return (int) $idSiteHsr;
+ }
+
+ protected function getCurrentTime()
+ {
+ return Date::now()->getDatetime();
+ }
+
+ public function updateHsrColumns($idSite, $idSiteHsr, $columns)
+ {
+ $columns = $this->encodeFieldsWhereNeeded($columns);
+
+ if (!empty($columns)) {
+ if (!isset($columns['updated_date'])) {
+ $columns['updated_date'] = $this->getCurrentTime();
+ }
+
+ if (!empty($columns['page_treemirror'])) {
+ $columns['capture_manually'] = 0;
+ } elseif (!empty($columns['capture_manually'])) {
+ $columns['page_treemirror'] = null;
+ }
+
+ $fields = array();
+ $bind = array();
+ foreach ($columns as $key => $value) {
+ $fields[] = ' ' . $key . ' = ?';
+ $bind[] = $value;
+ }
+ $fields = implode(',', $fields);
+
+ $query = sprintf('UPDATE %s SET %s WHERE idsitehsr = ? AND idsite = ?', $this->tablePrefixed, $fields);
+ $bind[] = (int) $idSiteHsr;
+ $bind[] = (int) $idSite;
+
+ // we do not use $db->update() here as this method is as well used in Tracker mode and the tracker DB does not
+ // support "->update()". Therefore we use the query method where we know it works with tracker and regular DB
+ $this->getDb()->query($query, $bind);
+ }
+ }
+
+ public function hasRecords($idSite, $recordType)
+ {
+ $sql = sprintf('SELECT idsite FROM %s WHERE record_type = ? and `status` IN(?,?) and idsite = ? LIMIT 1', $this->tablePrefixed);
+ $records = $this->getDb()->fetchRow($sql, array($recordType, self::STATUS_ENDED, self::STATUS_ACTIVE, $idSite));
+
+ return !empty($records);
+ }
+
+ public function deleteRecord($idSite, $idSiteHsr)
+ {
+ // now we delete the heatmap manually and it should notice all log entries for that heatmap are no longer needed
+ $sql = sprintf('DELETE FROM %s WHERE idsitehsr = ? and idsite = ?', $this->tablePrefixed);
+ Db::query($sql, array($idSiteHsr, $idSite));
+ }
+
+ private function getAllFieldNames($includePageTreeMirror)
+ {
+ $fields = '`idsitehsr`,`idsite`,`name`, `sample_rate`, `sample_limit`, `match_page_rules`, `excluded_elements`, `record_type`, ';
+ if (!empty($includePageTreeMirror)) {
+ $fields .= '`page_treemirror`,';
+ }
+ $fields .= '`screenshot_url`, `breakpoint_mobile`, `breakpoint_tablet`, `min_session_time` , `requires_activity`, `capture_keystrokes`, `created_date`, `updated_date`, `status`, `capture_manually`';
+ return $fields;
+ }
+
+ public function getRecords($idSite, $recordType, $includePageTreeMirror)
+ {
+ $fields = $this->getAllFieldNames($includePageTreeMirror);
+ $sql = sprintf('SELECT ' . $fields . ' FROM %s WHERE record_type = ? and `status` IN(?,?,?) and idsite = ? order by created_date desc', $this->tablePrefixed);
+ $records = $this->getDb()->fetchAll($sql, array($recordType, self::STATUS_ENDED, self::STATUS_ACTIVE, self::STATUS_PAUSED, $idSite));
+
+ return $this->enrichRecords($records);
+ }
+
+ public function getRecord($idSite, $idSiteHsr, $recordType)
+ {
+ $sql = sprintf('SELECT * FROM %s WHERE record_type = ? and `status` IN(?,?,?) and idsite = ? and idsitehsr = ? LIMIT 1', $this->tablePrefixed);
+ $record = $this->getDb()->fetchRow($sql, array($recordType, self::STATUS_ENDED, self::STATUS_ACTIVE, self::STATUS_PAUSED, $idSite, $idSiteHsr));
+
+ return $this->enrichRecord($record);
+ }
+
+ public function getNumRecordsTotal($recordType)
+ {
+ $sql = sprintf('SELECT count(*) as total FROM %s WHERE record_type = ? and `status` IN(?,?,?)', $this->tablePrefixed);
+ return $this->getDb()->fetchOne($sql, array($recordType, self::STATUS_ENDED, self::STATUS_ACTIVE, self::STATUS_PAUSED));
+ }
+
+ public function hasActiveRecordsAcrossSites()
+ {
+ $query = $this->getQueryActiveRequests();
+
+ $sql = sprintf("SELECT count(*) as numrecords FROM %s WHERE %s LIMIT 1", $this->tablePrefixed, $query['where']);
+ $numRecords = $this->getDb()->fetchOne($sql, $query['bind']);
+
+ return !empty($numRecords);
+ }
+
+ private function getQueryActiveRequests()
+ {
+ // for sessions we also need to return ended sessions to make sure to record all page views once a user takes part in
+ // a session recording. Otherwise as soon as the limit of sessions has reached, it would stop recording any further page views in already started session recordings
+
+ // we only fetch recorded sessions with status ended for the last 24 hours to not expose any potential config and for faster processing etc
+ $oneDayAgo = Date::now()->subDay(1)->getDatetime();
+
+ return array(
+ 'where' => '(status = ? or (record_type = ? and status = ? and updated_date > ?))',
+ 'bind' => array(self::STATUS_ACTIVE, self::RECORD_TYPE_SESSION, self::STATUS_ENDED, $oneDayAgo)
+ );
+ }
+
+ /**
+ * For performance reasons the page_treemirror will be read only partially!
+ * @param $idSite
+ * @return mixed
+ * @throws \Piwik\Tracker\Db\DbException
+ */
+ public function getActiveRecords($idSite)
+ {
+ $query = $this->getQueryActiveRequests();
+
+ $bind = $query['bind'];
+ $bind[] = $idSite;
+
+ $fields = $this->getAllFieldNames(false);
+ // we want to avoid needing to read all the entire treemirror every time the tracking cache will be updated
+ // as in worst case every treemirror can be 16MB or in rare cases even more. Most of the time it's only like 50KB or so
+ // but we want to avoid fetching heaps of unneeded data
+ $fields .= ', SUBSTRING(page_treemirror, 1, 10) as page_treemirror';
+
+ // NOTE: If you adjust this query, you might also
+ $sql = sprintf("SELECT " . $fields . " FROM %s WHERE %s and idsite = ? ORDER BY idsitehsr asc", $this->tablePrefixed, $query['where']);
+ $records = $this->getDb()->fetchAll($sql, $bind);
+
+ foreach ($records as $index => $record) {
+ if (!empty($record['page_treemirror'])) {
+ // avoids an error when it tries to uncompress
+ $records[$index]['page_treemirror'] = $this->compress($record['page_treemirror']);
+ }
+ }
+
+ return $this->enrichRecords($records);
+ }
+
+ private function enrichRecords($records)
+ {
+ if (empty($records)) {
+ return $records;
+ }
+
+ foreach ($records as $index => $record) {
+ $records[$index] = $this->enrichRecord($record);
+ }
+
+ return $records;
+ }
+
+ private function enrichRecord($record)
+ {
+ if (empty($record)) {
+ return $record;
+ }
+
+ $record['idsitehsr'] = (int) $record['idsitehsr'];
+ $record['idsite'] = (int) $record['idsite'];
+ $record['sample_rate'] = number_format($record['sample_rate'], 1, '.', '');
+ $record['record_type'] = (int) $record['record_type'];
+ $record['sample_limit'] = (int) $record['sample_limit'];
+ $record['min_session_time'] = (int) $record['min_session_time'];
+ $record['breakpoint_mobile'] = (int) $record['breakpoint_mobile'];
+ $record['breakpoint_tablet'] = (int) $record['breakpoint_tablet'];
+ $record['match_page_rules'] = $this->decodeField($record['match_page_rules']);
+ $record['requires_activity'] = !empty($record['requires_activity']);
+ $record['capture_keystrokes'] = !empty($record['capture_keystrokes']);
+ $record['capture_manually'] = !empty($record['capture_manually']) ? 1 : 0;
+
+ if (!empty($record['page_treemirror'])) {
+ $record['page_treemirror'] = $this->uncompress($record['page_treemirror']);
+ } else {
+ $record['page_treemirror'] = '';
+ }
+
+ return $record;
+ }
+
+ public function uninstall()
+ {
+ Db::query(sprintf('DROP TABLE IF EXISTS `%s`', $this->tablePrefixed));
+ }
+
+ public function getAllEntities()
+ {
+ $records = $this->getDb()->fetchAll('SELECT * FROM ' . $this->tablePrefixed);
+
+ return $this->enrichRecords($records);
+ }
+
+ private function encodeFieldsWhereNeeded($columns)
+ {
+ foreach ($columns as $column => $value) {
+ if ($column === 'match_page_rules') {
+ $columns[$column] = $this->encodeField($value);
+ } elseif ($column === 'page_treemirror') {
+ if (!empty($value)) {
+ $columns[$column] = $this->compress($value);
+ } else {
+ $columns[$column] = null;
+ }
+ } elseif (in_array($column, array('breakpoint_mobile', 'breakpoint_tablet', 'min_session_time', 'sample_rate'), $strict = true)) {
+ if ($value > self::MAX_SMALLINT) {
+ $columns[$column] = self::MAX_SMALLINT;
+ }
+ } elseif (in_array($column, array('requires_activity', 'capture_keystrokes'), $strict = true)) {
+ if (!empty($value)) {
+ $columns[$column] = 1;
+ } else {
+ $columns[$column] = 0;
+ }
+ }
+ }
+
+ return $columns;
+ }
+
+ private function compress($data)
+ {
+ if (!empty($data)) {
+ return gzcompress($data);
+ }
+
+ return $data;
+ }
+
+ private function uncompress($data)
+ {
+ if (!empty($data)) {
+ return gzuncompress($data);
+ }
+
+ return $data;
+ }
+
+ private function encodeField($field)
+ {
+ if (empty($field) || !is_array($field)) {
+ $field = array();
+ }
+
+ return json_encode($field);
+ }
+
+ private function decodeField($field)
+ {
+ if (!empty($field)) {
+ $field = @json_decode($field, true);
+ }
+
+ if (empty($field) || !is_array($field)) {
+ $field = array();
+ }
+
+ return $field;
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/DataTable/Filter/EnrichRecordedSessions.php b/files/plugin-HeatmapSessionRecording-5.2.4/DataTable/Filter/EnrichRecordedSessions.php
new file mode 100644
index 0000000..0a306e7
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/DataTable/Filter/EnrichRecordedSessions.php
@@ -0,0 +1,73 @@
+getRowsWithoutSummaryRow() as $row) {
+ if ($isAnonymous) {
+ foreach (self::getBlockedFields() as $blockedField) {
+ if ($row->getColumn($blockedField) !== false) {
+ $row->setColumn($blockedField, false);
+ }
+ }
+ } else {
+ $row->setColumn('idvisitor', bin2hex($row->getColumn('idvisitor')));
+ }
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Diagnostic/ConfigsPhpCheck.php b/files/plugin-HeatmapSessionRecording-5.2.4/Diagnostic/ConfigsPhpCheck.php
new file mode 100644
index 0000000..c436154
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Diagnostic/ConfigsPhpCheck.php
@@ -0,0 +1,112 @@
+translator = $translator;
+ }
+
+ public function execute()
+ {
+ $label = $this->translator->translate('Heatmap & Session Recording Tracking');
+
+ $site = new Model();
+ $idSites = $site->getSitesId();
+ $idSite = array_shift($idSites);
+
+ $baseUrl = SettingsPiwik::getPiwikUrl();
+ if (!Common::stringEndsWith($baseUrl, '/')) {
+ $baseUrl .= '/';
+ }
+
+ $baseUrl .= HeatmapSessionRecording::getPathPrefix() . '/';
+ $baseUrl .= 'HeatmapSessionRecording/configs.php';
+ $testUrl = $baseUrl . '?idsite=' . (int) $idSite . '&trackerid=5lX6EM&url=http%3A%2F%2Ftest.test%2F';
+
+ $error = null;
+ $response = null;
+
+ $errorResult = $this->translator->translate('HeatmapSessionRecording_ConfigsPhpErrorResult');
+ $manualCheck = $this->translator->translate('HeatmapSessionRecording_ConfigsPhpManualCheck');
+
+ if (method_exists('\Piwik\SettingsPiwik', 'isInternetEnabled')) {
+ $isInternetEnabled = SettingsPiwik::isInternetEnabled();
+ if (!$isInternetEnabled) {
+ $unknown = $this->translator->translate('HeatmapSessionRecording_ConfigsInternetDisabled', $testUrl) . ' ' . $manualCheck;
+ return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $unknown));
+ }
+ }
+
+ try {
+ $response = Http::sendHttpRequest($testUrl, $timeout = 2);
+ } catch (\Exception $e) {
+ $error = $e->getMessage();
+ }
+
+ if (!empty($response)) {
+ $response = Common::mb_strtolower($response);
+ if (strpos($response, 'piwik.heatmapsessionrecording') !== false) {
+ $message = $this->translator->translate('HeatmapSessionRecording_ConfigsPhpSuccess', $baseUrl);
+ return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK, $message));
+ } elseif (strpos($response, 'forbidden') !== false || strpos($response, ' forbidden') !== false || strpos($response, ' denied ') !== false || strpos($response, '403 ') !== false || strpos($response, '404 ') !== false) {
+ // Likely the server returned eg a 403 HTML
+ $message = $this->translator->translate('HeatmapSessionRecording_ConfigsPhpNotAccessible', array($testUrl)) . ' ' . $errorResult;
+ return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_ERROR, $message));
+ }
+ }
+
+ if (!empty($error)) {
+ $error = Common::mb_strtolower($error);
+
+ if (strpos($error, 'forbidden ') !== false || strpos($error, ' forbidden') !== false || strpos($error, 'denied ') !== false || strpos($error, '403 ') !== false || strpos($error, '404 ') !== false) {
+ $message = $this->translator->translate('HeatmapSessionRecording_ConfigsPhpNotAccessible', array($testUrl)) . ' ' . $errorResult;
+ return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_ERROR, $message));
+ }
+
+ if (strpos($error, 'ssl ') !== false || strpos($error, ' ssl') !== false || strpos($error, 'self signed') !== false || strpos($error, 'certificate ') !== false) {
+ $message = $this->translator->translate('HeatmapSessionRecording_ConfigsPhpSelfSignedError', array($testUrl)) . ' ' . $manualCheck;
+ return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $message));
+ }
+
+ $unknownError = $this->translator->translate('HeatmapSessionRecording_ConfigsPhpUnknownError', array($testUrl, $error)) . ' ' . $errorResult;
+ return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $unknownError));
+ }
+
+ $unknown = $this->translator->translate('HeatmapSessionRecording_ConfigsPhpUnknown', $testUrl) . $manualCheck;
+ return array(DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $unknown));
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/HeatmapSessionRecording.php b/files/plugin-HeatmapSessionRecording-5.2.4/HeatmapSessionRecording.php
new file mode 100644
index 0000000..f9f7059
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/HeatmapSessionRecording.php
@@ -0,0 +1,886 @@
+register_route('HeatmapSessionRecording', 'getHeatmap');
+ $api->register_route('HeatmapSessionRecording', 'getHeatmaps');
+ $api->register_route('HeatmapSessionRecording', 'getRecordedHeatmapMetadata');
+ $api->register_route('HeatmapSessionRecording', 'getRecordedHeatmap');
+
+ $api->register_route('HeatmapSessionRecording', 'getSessionRecording');
+ $api->register_route('HeatmapSessionRecording', 'getSessionRecordings');
+ $api->register_route('HeatmapSessionRecording', 'getRecordedSessions');
+ $api->register_route('HeatmapSessionRecording', 'getRecordedSession');
+ });
+
+ /**
+ * @param array $actions
+ * @param \WP_Post $post
+ *
+ * @return mixed
+ */
+ function add_new_heat_map_link($actions, $post)
+ {
+ if (
+ !$post
+ || !is_plugin_active('matomo/matomo.php')
+ || !current_user_can('write_matomo')
+ ) {
+ return $actions;
+ }
+
+ if ($post->post_status !== 'publish') {
+ // the permalink url wouldn't be correct yet for unpublished post
+ return $actions;
+ }
+
+ $postUrl = get_permalink($post);
+ $rules = array(array(
+ 'attribute' => 'url',
+ 'type' => 'equals_simple',
+ 'inverted' => 0,
+ 'value' => $postUrl
+ ));
+
+ $hsrParams = array(
+ 'idSite' => 1,
+ 'idSiteHsr' => 0,
+ 'name' => $post->post_title,
+ // Encoded to avoid pitfalls of decoding multi-dimensional array URL params in JavaScript
+ 'matchPageRules' => json_encode($rules)
+ );
+
+ $url = Menu::get_matomo_reporting_url(
+ 'HeatmapSessionRecording_Heatmaps',
+ 'HeatmapSessionRecording_ManageHeatmaps',
+ $hsrParams
+ );
+
+ $actions['create_heatmap'] = 'Create Heatmap ';
+ return $actions;
+ }
+
+ function get_matomo_heatmaps()
+ {
+ static $heatmaps_cached;
+
+ global $wpdb;
+
+ if (!isset($heatmaps_cached)) {
+ $site = new Site();
+ $idsite = $site->get_current_matomo_site_id();
+
+ if (!$idsite) {
+ $heatmaps_cached = array(); // prevent it not being executed again
+ } else {
+ $wpDbSettings = new \WpMatomo\Db\Settings();
+ $tableName = $wpDbSettings->prefix_table_name('site_hsr');
+ $idsite = (int) $idsite;// needed cause we don't bind parameters below
+
+ $heatmaps_cached = $wpdb->get_results(
+ "select * from $tableName WHERE record_type = 1 AND idsite = $idsite AND status != 'deleted'",
+ ARRAY_A
+ );
+ }
+ }
+ return $heatmaps_cached;
+ }
+
+ /**
+ * @param array $actions
+ * @param \WP_Post $post
+ *
+ * @return mixed
+ */
+ function add_view_heat_map_link($actions, $post)
+ {
+ if (
+ !$post
+ || !is_plugin_active('matomo/matomo.php')
+ || !current_user_can('write_matomo')
+ ) {
+ return $actions;
+ }
+
+ $heatmaps = get_matomo_heatmaps();
+
+ if (empty($heatmaps)) {
+ return $actions;
+ }
+
+ $postUrl = get_permalink($post);
+
+ if (!$postUrl) {
+ return $actions;
+ }
+
+ if (class_exists(Bootstrap::class)) {
+ Bootstrap::do_bootstrap();
+ }
+
+ require_once('Tracker/PageRuleMatcher.php');
+ require_once('Tracker/HsrMatcher.php');
+
+ $heatmaps = array_values(array_filter($heatmaps, function ($heatmap) use ($postUrl) {
+ $systemSettings = StaticContainer::get(SystemSettings::class);
+ $includedCountries = $systemSettings->getIncludedCountries();
+ return HsrMatcher::matchesAllPageRules(json_decode($heatmap['match_page_rules'], true), $postUrl) && HsrMatcher::isIncludedCountry($includedCountries);
+ }));
+
+ $numMatches = count($heatmaps);
+ foreach ($heatmaps as $i => $heatmap) {
+ $url = Menu::get_matomo_reporting_url(
+ 'HeatmapSessionRecording_Heatmaps',
+ $heatmap['idsitehsr'],
+ array()
+ );
+ $linkText = 'View Heatmap';
+ if ($numMatches > 1) {
+ $linkText .= ' #' . ($i + 1);
+ }
+ $actions['view_heatmap_' . $i] =
+ '' . esc_html($linkText) . ' ';
+ }
+
+ return $actions;
+ }
+}
+
+class HeatmapSessionRecording extends \Piwik\Plugin
+{
+ public const EMBED_SESSION_TIME = 43200; // half day in seconds
+ public const ULR_PARAM_FORCE_SAMPLE = 'pk_hsr_forcesample';
+ public const ULR_PARAM_FORCE_CAPTURE_SCREEN = 'pk_hsr_capturescreen';
+ public const EMBED_SESSION_NAME = 'HSR_EMBED_SESSID';
+
+ public const TRACKER_READY_HOOK_NAME = '/*!! hsrTrackerReadyHook */';
+ public const TRACKER_READY_HOOK_NAME_WHEN_MINIFIED = '/*!!! hsrTrackerReadyHook */';
+
+ public function registerEvents()
+ {
+ return array(
+ 'Db.getActionReferenceColumnsByTable' => 'addActionReferenceColumnsByTable',
+ 'Tracker.Cache.getSiteAttributes' => 'addSiteTrackerCache',
+ 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
+ 'AssetManager.getJavaScriptFiles' => 'getJsFiles',
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ 'Template.jsGlobalVariables' => 'addJsGlobalVariables',
+ 'Category.addSubcategories' => 'addSubcategories',
+ 'SitesManager.deleteSite.end' => 'onDeleteSite',
+ 'Tracker.PageUrl.getQueryParametersToExclude' => 'getQueryParametersToExclude',
+ 'Widget.addWidgetConfigs' => 'addWidgetConfigs',
+ 'System.addSystemSummaryItems' => 'addSystemSummaryItems',
+ 'API.HeatmapSessionRecording.addHeatmap.end' => 'updatePiwikTracker',
+ 'API.HeatmapSessionRecording.addSessionRecording.end' => 'updatePiwikTracker',
+ 'CustomJsTracker.shouldAddTrackerFile' => 'shouldAddTrackerFile',
+ 'Updater.componentUpdated' => 'installHtAccess',
+ 'Live.visitorLogViewBeforeActionsInfo' => 'visitorLogViewBeforeActionsInfo',
+ 'Widgetize.shouldEmbedIframeEmpty' => 'shouldEmbedIframeEmpty',
+ 'Session.beforeSessionStart' => 'changeSessionLengthIfEmbedPage',
+ 'TwoFactorAuth.requiresTwoFactorAuthentication' => 'requiresTwoFactorAuthentication',
+ 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor',
+ 'CustomJsTracker.manipulateJsTracker' => 'disableHeatmapsDefaultIfNeeded',
+ 'AssetManager.addStylesheets' => [
+ 'function' => 'addStylesheets',
+ 'after' => true,
+ ],
+ 'Db.getTablesInstalled' => 'getTablesInstalled'
+ );
+ }
+
+ public function disableHeatmapsDefaultIfNeeded(&$content)
+ {
+ $settings = StaticContainer::get(SystemSettings::class);
+ if ($settings->disableTrackingByDefault->getValue()) {
+ $replace = 'Matomo.HeatmapSessionRecording._setDisabled();';
+ } else {
+ $replace = '';
+ }
+
+ $content = str_replace(array(self::TRACKER_READY_HOOK_NAME_WHEN_MINIFIED, self::TRACKER_READY_HOOK_NAME), $replace, $content);
+ }
+
+ /**
+ * Register the new tables, so Matomo knows about them.
+ *
+ * @param array $allTablesInstalled
+ */
+ public function getTablesInstalled(&$allTablesInstalled)
+ {
+ $allTablesInstalled[] = Common::prefixTable('log_hsr');
+ $allTablesInstalled[] = Common::prefixTable('log_hsr_blob');
+ $allTablesInstalled[] = Common::prefixTable('log_hsr_event');
+ $allTablesInstalled[] = Common::prefixTable('log_hsr_site');
+ $allTablesInstalled[] = Common::prefixTable('site_hsr');
+ }
+
+ public static function getPathPrefix()
+ {
+ $webRootDirs = Manager::getInstance()->getWebRootDirectoriesForCustomPluginDirs();
+ if (!empty($webRootDirs['HeatmapSessionRecording'])) {
+ $baseUrl = trim($webRootDirs['HeatmapSessionRecording'], '/');
+ } else {
+ $baseUrl = 'plugins';
+ }
+ return $baseUrl;
+ }
+
+ public static function isMatomoForWordPress()
+ {
+ return defined('ABSPATH') && function_exists('add_action');
+ }
+
+ public function addStylesheets(&$mergedContent)
+ {
+ if (self::isMatomoForWordPress()) {
+ // we hide this icon since it uses the widgetize feature which is disabled in WordPress
+ $mergedContent .= '.manageHsr .action .icon-show { display: none; }';
+ }
+ }
+ public function getPagesComparisonsDisabledFor(&$pages)
+ {
+ $pages[] = 'HeatmapSessionRecording_Heatmaps.*';
+ $pages[] = 'HeatmapSessionRecording_SessionRecordings.*';
+ }
+
+ public function addJsGlobalVariables()
+ {
+ $idSite = Common::getRequestVar('idSite', 0, 'int');
+
+ if ($idSite > 0 && Piwik::isUserHasWriteAccess($idSite)) {
+ echo 'piwik.heatmapWriteAccess = true;';
+ } else {
+ echo 'piwik.heatmapWriteAccess = false;';
+ }
+ }
+
+ public function requiresTwoFactorAuthentication(&$requiresAuth, $module, $action, $parameters)
+ {
+ if ($module == 'HeatmapSessionRecording' && $action === 'embedPage') {
+ $requiresAuth = false;
+ }
+ }
+
+ public function shouldEmbedIframeEmpty(&$shouldEmbedEmpty, $controllerName, $actionName)
+ {
+ if ($controllerName == 'HeatmapSessionRecording' && ($actionName == 'replayRecording' || $actionName == 'embedPage')) {
+ $shouldEmbedEmpty = true;
+ }
+ }
+
+ /**
+ * Fallback to add play session link for Matomo < 3.1.0
+ *
+ * NOTE: TO BE REMOVED EG FROM FEBRUARY OR MARCH 2018
+ *
+ * @param string $out
+ * @param Row $visitor
+ */
+ public function visitorLogViewBeforeActionsInfo(&$out, $visitor)
+ {
+ if (class_exists('\\Piwik\\Plugins\\Live\\VisitorDetailsAbstract')) {
+ return;
+ }
+
+ $idVisit = $visitor->getColumn('idVisit');
+ $idSite = (int) $visitor->getColumn('idSite');
+
+ if (empty($idSite) || empty($idVisit) || !$this->getValidator()->canViewSessionReport($idSite)) {
+ return;
+ }
+
+ $aggregator = new Aggregator();
+ $recording = $aggregator->findRecording($idVisit);
+ if (!empty($recording['idsitehsr'])) {
+ $title = Piwik::translate('HeatmapSessionRecording_ReplayRecordedSession');
+ $out .= ' ' . $title . ' ';
+ }
+ }
+
+ public function shouldAddTrackerFile(&$shouldAdd, $pluginName)
+ {
+ if ($pluginName === 'HeatmapSessionRecording') {
+ $config = new Configuration();
+
+ $siteHsrDao = $this->getSiteHsrDao();
+ if ($config->shouldOptimizeTrackingCode() && !$siteHsrDao->hasActiveRecordsAcrossSites()) {
+ // saves requests to configs.php while no heatmap or session recording configured.
+ $shouldAdd = false;
+ }
+ }
+ }
+
+ public function updatePiwikTracker()
+ {
+ if (Plugin\Manager::getInstance()->isPluginActivated('CustomJsTracker')) {
+ $trackerUpdater = StaticContainer::get('Piwik\Plugins\CustomJsTracker\TrackerUpdater');
+ if (!empty($trackerUpdater)) {
+ $trackerUpdater->update();
+ }
+ }
+ }
+
+ public function addSystemSummaryItems(&$systemSummary)
+ {
+ $dao = $this->getSiteHsrDao();
+ $numHeatmaps = $dao->getNumRecordsTotal(SiteHsrDao::RECORD_TYPE_HEATMAP);
+ $numSessions = $dao->getNumRecordsTotal(SiteHsrDao::RECORD_TYPE_SESSION);
+
+ $systemSummary[] = new SystemSummary\Item(
+ $key = 'heatmaps',
+ Piwik::translate('HeatmapSessionRecording_NHeatmaps', $numHeatmaps),
+ $value = null,
+ array('module' => 'HeatmapSessionRecording', 'action' => 'manageHeatmap'),
+ $icon = 'icon-drop',
+ $order = 6
+ );
+ $systemSummary[] = new SystemSummary\Item(
+ $key = 'sessionrecordings',
+ Piwik::translate('HeatmapSessionRecording_NSessionRecordings', $numSessions),
+ $value = null,
+ array('module' => 'HeatmapSessionRecording', 'action' => 'manageSessions'),
+ $icon = 'icon-play',
+ $order = 7
+ );
+ }
+
+ public function getQueryParametersToExclude(&$parametersToExclude)
+ {
+ // these are used by the tracker
+ $parametersToExclude[] = self::ULR_PARAM_FORCE_CAPTURE_SCREEN;
+ $parametersToExclude[] = self::ULR_PARAM_FORCE_SAMPLE;
+ }
+
+ public function onDeleteSite($idSite)
+ {
+ $model = $this->getSiteHsrModel();
+ $model->deactivateRecordsForSite($idSite);
+ }
+
+ private function getSiteHsrModel()
+ {
+ return StaticContainer::get('Piwik\Plugins\HeatmapSessionRecording\Model\SiteHsrModel');
+ }
+
+ private function getValidator()
+ {
+ return StaticContainer::get('Piwik\Plugins\HeatmapSessionRecording\Input\Validator');
+ }
+
+ public function addWidgetConfigs(&$configs)
+ {
+ $idSite = Common::getRequestVar('idSite', 0, 'int');
+
+ if (!$this->getValidator()->canViewHeatmapReport($idSite)) {
+ return;
+ }
+
+ $heatmaps = $this->getHeatmaps($idSite);
+
+ foreach ($heatmaps as $heatmap) {
+ $widget = new WidgetConfig();
+ $widget->setCategoryId('HeatmapSessionRecording_Heatmaps');
+ $widget->setSubcategoryId($heatmap['idsitehsr']);
+ $widget->setModule('HeatmapSessionRecording');
+ $widget->setAction('showHeatmap');
+ $widget->setParameters(array('idSiteHsr' => $heatmap['idsitehsr']));
+ $widget->setIsNotWidgetizable();
+ $configs[] = $widget;
+ }
+ }
+
+ public function addSubcategories(&$subcategories)
+ {
+ $idSite = Common::getRequestVar('idSite', 0, 'int');
+
+ if (empty($idSite)) {
+ // fallback for eg API.getReportMetadata which uses idSites
+ $idSite = Common::getRequestVar('idSites', 0, 'int');
+
+ if (empty($idSite)) {
+ return;
+ }
+ }
+
+ if ($this->getValidator()->canViewHeatmapReport($idSite)) {
+ $heatmaps = $this->getHeatmaps($idSite);
+
+ // we list recently created heatmaps first
+ $order = 20;
+ foreach ($heatmaps as $heatmap) {
+ $subcategory = new Subcategory();
+ $subcategory->setName($heatmap['name']);
+ $subcategory->setCategoryId('HeatmapSessionRecording_Heatmaps');
+ $subcategory->setId($heatmap['idsitehsr']);
+ $subcategory->setOrder($order++);
+ $subcategories[] = $subcategory;
+ }
+ }
+
+ if ($this->getValidator()->canViewSessionReport($idSite)) {
+ $recordings = $this->getSessionRecordings($idSite);
+
+ // we list recently created recordings first
+ $order = 20;
+ foreach ($recordings as $recording) {
+ $subcategory = new Subcategory();
+ $subcategory->setName($recording['name']);
+ $subcategory->setCategoryId('HeatmapSessionRecording_SessionRecordings');
+ $subcategory->setId($recording['idsitehsr']);
+ $subcategory->setOrder($order++);
+ $subcategories[] = $subcategory;
+ }
+ }
+ }
+
+ public function getClientSideTranslationKeys(&$result)
+ {
+ $result[] = 'General_Save';
+ $result[] = 'General_Done';
+ $result[] = 'General_Actions';
+ $result[] = 'General_Yes';
+ $result[] = 'General_No';
+ $result[] = 'General_Add';
+ $result[] = 'General_Remove';
+ $result[] = 'General_Id';
+ $result[] = 'General_Ok';
+ $result[] = 'General_Cancel';
+ $result[] = 'General_Name';
+ $result[] = 'General_Loading';
+ $result[] = 'General_LoadingData';
+ $result[] = 'General_Mobile';
+ $result[] = 'General_All';
+ $result[] = 'General_Search';
+ $result[] = 'CorePluginsAdmin_Status';
+ $result[] = 'DevicesDetection_Tablet';
+ $result[] = 'CoreUpdater_UpdateTitle';
+ $result[] = 'DevicesDetection_Device';
+ $result[] = 'Installation_Legend';
+ $result[] = 'HeatmapSessionRecording_DeleteScreenshot';
+ $result[] = 'HeatmapSessionRecording_DeleteHeatmapScreenshotConfirm';
+ $result[] = 'HeatmapSessionRecording_enable';
+ $result[] = 'HeatmapSessionRecording_disable';
+ $result[] = 'HeatmapSessionRecording_ChangeReplaySpeed';
+ $result[] = 'HeatmapSessionRecording_ClickToSkipPauses';
+ $result[] = 'HeatmapSessionRecording_AutoPlayNextPageview';
+ $result[] = 'HeatmapSessionRecording_XSamples';
+ $result[] = 'HeatmapSessionRecording_StatusActive';
+ $result[] = 'HeatmapSessionRecording_StatusEnded';
+ $result[] = 'HeatmapSessionRecording_StatusPaused';
+ $result[] = 'HeatmapSessionRecording_RequiresActivity';
+ $result[] = 'HeatmapSessionRecording_RequiresActivityHelp';
+ $result[] = 'HeatmapSessionRecording_CaptureKeystrokes';
+ $result[] = 'HeatmapSessionRecording_CaptureKeystrokesHelp';
+ $result[] = 'HeatmapSessionRecording_SessionRecording';
+ $result[] = 'HeatmapSessionRecording_Heatmap';
+ $result[] = 'HeatmapSessionRecording_ActivityClick';
+ $result[] = 'HeatmapSessionRecording_ActivityMove';
+ $result[] = 'HeatmapSessionRecording_ActivityScroll';
+ $result[] = 'HeatmapSessionRecording_ActivityResize';
+ $result[] = 'HeatmapSessionRecording_ActivityFormChange';
+ $result[] = 'HeatmapSessionRecording_ActivityPageChange';
+ $result[] = 'HeatmapSessionRecording_HeatmapWidth';
+ $result[] = 'HeatmapSessionRecording_Width';
+ $result[] = 'HeatmapSessionRecording_Action';
+ $result[] = 'HeatmapSessionRecording_DeviceType';
+ $result[] = 'HeatmapSessionRecording_PlayerDurationXofY';
+ $result[] = 'HeatmapSessionRecording_PlayerPlay';
+ $result[] = 'HeatmapSessionRecording_PlayerPause';
+ $result[] = 'HeatmapSessionRecording_PlayerRewindFast';
+ $result[] = 'HeatmapSessionRecording_PlayerForwardFast';
+ $result[] = 'HeatmapSessionRecording_PlayerReplay';
+ $result[] = 'HeatmapSessionRecording_PlayerPageViewPrevious';
+ $result[] = 'HeatmapSessionRecording_PlayerPageViewNext';
+ $result[] = 'HeatmapSessionRecording_SessionRecordingsUsageBenefits';
+ $result[] = 'HeatmapSessionRecording_ManageSessionRecordings';
+ $result[] = 'HeatmapSessionRecording_ManageHeatmaps';
+ $result[] = 'HeatmapSessionRecording_NoSessionRecordingsFound';
+ $result[] = 'HeatmapSessionRecording_FieldIncludedTargetsHelpSessions';
+ $result[] = 'HeatmapSessionRecording_NoHeatmapsFound';
+ $result[] = 'HeatmapSessionRecording_AvgAboveFoldTitle';
+ $result[] = 'HeatmapSessionRecording_AvgAboveFoldDescription';
+ $result[] = 'HeatmapSessionRecording_TargetPage';
+ $result[] = 'HeatmapSessionRecording_TargetPages';
+ $result[] = 'HeatmapSessionRecording_ViewReport';
+ $result[] = 'HeatmapSessionRecording_SampleLimit';
+ $result[] = 'HeatmapSessionRecording_SessionNameHelp';
+ $result[] = 'HeatmapSessionRecording_HeatmapSampleLimit';
+ $result[] = 'HeatmapSessionRecording_SessionSampleLimit';
+ $result[] = 'HeatmapSessionRecording_HeatmapSampleLimitHelp';
+ $result[] = 'HeatmapSessionRecording_SessionSampleLimitHelp';
+ $result[] = 'HeatmapSessionRecording_MinSessionTime';
+ $result[] = 'HeatmapSessionRecording_MinSessionTimeHelp';
+ $result[] = 'HeatmapSessionRecording_EditX';
+ $result[] = 'HeatmapSessionRecording_StopX';
+ $result[] = 'HeatmapSessionRecording_HeatmapUsageBenefits';
+ $result[] = 'HeatmapSessionRecording_AdvancedOptions';
+ $result[] = 'HeatmapSessionRecording_SampleRate';
+ $result[] = 'HeatmapSessionRecording_HeatmapSampleRateHelp';
+ $result[] = 'HeatmapSessionRecording_SessionSampleRateHelp';
+ $result[] = 'HeatmapSessionRecording_ExcludedElements';
+ $result[] = 'HeatmapSessionRecording_ExcludedElementsHelp';
+ $result[] = 'HeatmapSessionRecording_ScreenshotUrl';
+ $result[] = 'HeatmapSessionRecording_ScreenshotUrlHelp';
+ $result[] = 'HeatmapSessionRecording_BreakpointX';
+ $result[] = 'HeatmapSessionRecording_BreakpointGeneralHelp';
+ $result[] = 'HeatmapSessionRecording_Rule';
+ $result[] = 'HeatmapSessionRecording_UrlParameterValueToMatchPlaceholder';
+ $result[] = 'HeatmapSessionRecording_EditHeatmapX';
+ $result[] = 'HeatmapSessionRecording_TargetTypeIsAny';
+ $result[] = 'HeatmapSessionRecording_TargetTypeIsNot';
+ $result[] = 'HeatmapSessionRecording_PersonalInformationNote';
+ $result[] = 'HeatmapSessionRecording_UpdatingData';
+ $result[] = 'HeatmapSessionRecording_FieldIncludedTargetsHelp';
+ $result[] = 'HeatmapSessionRecording_DeleteX';
+ $result[] = 'HeatmapSessionRecording_DeleteHeatmapConfirm';
+ $result[] = 'HeatmapSessionRecording_BreakpointGeneralHelpManage';
+ $result[] = 'HeatmapSessionRecording_TargetPageTestTitle';
+ $result[] = 'HeatmapSessionRecording_TargetPageTestErrorInvalidUrl';
+ $result[] = 'HeatmapSessionRecording_TargetPageTestUrlMatches';
+ $result[] = 'HeatmapSessionRecording_TargetPageTestUrlNotMatches';
+ $result[] = 'HeatmapSessionRecording_TargetPageTestLabel';
+ $result[] = 'HeatmapSessionRecording_ErrorXNotProvided';
+ $result[] = 'HeatmapSessionRecording_ErrorPageRuleRequired';
+ $result[] = 'HeatmapSessionRecording_CreationDate';
+ $result[] = 'HeatmapSessionRecording_HeatmapCreated';
+ $result[] = 'HeatmapSessionRecording_HeatmapUpdated';
+ $result[] = 'HeatmapSessionRecording_FieldNamePlaceholder';
+ $result[] = 'HeatmapSessionRecording_HeatmapNameHelp';
+ $result[] = 'HeatmapSessionRecording_CreateNewHeatmap';
+ $result[] = 'HeatmapSessionRecording_CreateNewSessionRecording';
+ $result[] = 'HeatmapSessionRecording_EditSessionRecordingX';
+ $result[] = 'HeatmapSessionRecording_DeleteSessionRecordingConfirm';
+ $result[] = 'HeatmapSessionRecording_EndHeatmapConfirm';
+ $result[] = 'HeatmapSessionRecording_EndSessionRecordingConfirm';
+ $result[] = 'HeatmapSessionRecording_SessionRecordingCreated';
+ $result[] = 'HeatmapSessionRecording_SessionRecordingUpdated';
+ $result[] = 'HeatmapSessionRecording_Filter';
+ $result[] = 'HeatmapSessionRecording_PlayRecordedSession';
+ $result[] = 'HeatmapSessionRecording_DeleteRecordedSession';
+ $result[] = 'HeatmapSessionRecording_DeleteRecordedPageview';
+ $result[] = 'Live_ViewVisitorProfile';
+ $result[] = 'HeatmapSessionRecording_HeatmapXRecordedSamplesSince';
+ $result[] = 'HeatmapSessionRecording_PageviewsInVisit';
+ $result[] = 'HeatmapSessionRecording_ColumnTime';
+ $result[] = 'General_TimeOnPage';
+ $result[] = 'Goals_URL';
+ $result[] = 'General_Close';
+ $result[] = 'HeatmapSessionRecording_HeatmapX';
+ $result[] = 'HeatmapSessionRecording_NoHeatmapSamplesRecordedYet';
+ $result[] = 'HeatmapSessionRecording_NoHeatmapScreenshotRecordedYet';
+ $result[] = 'HeatmapSessionRecording_NoHeatmapSamplesRecordedYetWithoutSystemConfiguration';
+ $result[] = 'HeatmapSessionRecording_NoHeatmapScreenshotRecordedYetWithoutSystemConfiguration';
+ $result[] = 'HeatmapSessionRecording_HeatmapInfoTrackVisitsFromCountries';
+ $result[] = 'HeatmapSessionRecording_SessionRecordingInfoTrackVisitsFromCountries';
+ $result[] = 'HeatmapSessionRecording_AdBlockerDetected';
+ $result[] = 'HeatmapSessionRecording_CaptureDomTitle';
+ $result[] = 'HeatmapSessionRecording_CaptureDomInlineHelp';
+ $result[] = 'HeatmapSessionRecording_MatomoJSNotWritableErrorMessage';
+ $result[] = 'HeatmapSessionRecording_SessionRecordings';
+ $result[] = 'HeatmapSessionRecording_Heatmaps';
+ $result[] = 'HeatmapSessionRecording_Clicks';
+ $result[] = 'HeatmapSessionRecording_ClickRate';
+ $result[] = 'HeatmapSessionRecording_Moves';
+ $result[] = 'HeatmapSessionRecording_MoveRate';
+ $result[] = 'HeatmapSessionRecording_HeatmapTroubleshoot';
+ }
+
+ public function getJsFiles(&$jsFiles)
+ {
+ $jsFiles[] = "plugins/HeatmapSessionRecording/javascripts/rowaction.js";
+ }
+
+ public function getStylesheetFiles(&$stylesheets)
+ {
+ $stylesheets[] = "plugins/HeatmapSessionRecording/stylesheets/list-entities.less";
+ $stylesheets[] = "plugins/HeatmapSessionRecording/stylesheets/edit-entities.less";
+ $stylesheets[] = "plugins/HeatmapSessionRecording/vue/src/HsrTargetTest/HsrTargetTest.less";
+ $stylesheets[] = "plugins/HeatmapSessionRecording/vue/src/HsrUrlTarget/HsrUrlTarget.less";
+ $stylesheets[] = "plugins/HeatmapSessionRecording/stylesheets/recordings.less";
+ $stylesheets[] = "plugins/HeatmapSessionRecording/vue/src/SessionRecordingVis/SessionRecordingVis.less";
+ $stylesheets[] = "plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVis.less";
+ $stylesheets[] = "plugins/HeatmapSessionRecording/vue/src/Tooltip/Tooltip.less";
+ }
+
+ public function activate()
+ {
+ $this->installHtAccess();
+ }
+
+ public function install()
+ {
+ $siteHsr = new SiteHsrDao();
+ $siteHsr->install();
+
+ $hsrSite = new LogHsrSite();
+ $hsrSite->install();
+
+ $hsr = new LogHsr($hsrSite);
+ $hsr->install();
+
+ $blobHsr = new LogHsrBlob();
+ $blobHsr->install();
+
+ $event = new LogHsrEvent($blobHsr);
+ $event->install();
+
+ $this->installHtAccess();
+
+ $configuration = new Configuration();
+ $configuration->install();
+ }
+
+ public function installHtAccess()
+ {
+ $htaccess = new HtAccess();
+ $htaccess->install();
+ }
+
+ public function uninstall()
+ {
+ $siteHsr = new SiteHsrDao();
+ $siteHsr->uninstall();
+
+ $hsrSite = new LogHsrSite();
+ $hsrSite->uninstall();
+
+ $hsr = new LogHsr($hsrSite);
+ $hsr->uninstall();
+
+ $blobHsr = new LogHsrBlob();
+ $blobHsr->uninstall();
+
+ $event = new LogHsrEvent($blobHsr);
+ $event->uninstall();
+
+ $configuration = new Configuration();
+ $configuration->uninstall();
+ }
+
+ public function isTrackerPlugin()
+ {
+ return true;
+ }
+
+ private function getSiteHsrDao()
+ {
+ return StaticContainer::get('Piwik\Plugins\HeatmapSessionRecording\Dao\SiteHsrDao');
+ }
+
+ public function addSiteTrackerCache(&$content, $idSite)
+ {
+ $hsr = $this->getSiteHsrDao();
+ $hsrs = $hsr->getActiveRecords($idSite);
+
+ foreach ($hsrs as $index => $hsr) {
+ // we make sure to keep the cache file small as this is not needed in the cache
+ $hsrs[$index]['page_treemirror'] = !empty($hsr['page_treemirror']) ? '1' : null;
+ }
+
+ $content['hsr'] = $hsrs;
+ }
+
+ public function addActionReferenceColumnsByTable(&$result)
+ {
+ $result['log_hsr'] = array('idaction_url');
+ $result['log_hsr_event'] = array('idselector');
+ }
+
+ public function changeSessionLengthIfEmbedPage()
+ {
+ if (
+ SettingsServer::isTrackerApiRequest()
+ || Common::isPhpCliMode()
+ ) {
+ return;
+ }
+
+ // if there's no token_auth=... in the URL and there's no existing HSR session, then
+ // we don't change the session options and try to use the normal matomo session.
+ if (
+ Common::getRequestVar('token_auth', false) === false
+ && empty($_COOKIE[self::EMBED_SESSION_NAME])
+ ) {
+ return;
+ }
+
+ $module = Common::getRequestVar('module', '', 'string');
+ $action = Common::getRequestVar('action', '', 'string');
+ if (
+ $module == 'HeatmapSessionRecording'
+ && $action == 'embedPage'
+ ) {
+ Config::getInstance()->General['login_cookie_expire'] = self::EMBED_SESSION_TIME;
+
+ Session::$sessionName = self::EMBED_SESSION_NAME;
+ Session::rememberMe(Config::getInstance()->General['login_cookie_expire']);
+ }
+ }
+
+ public static function getTranslationKey($type)
+ {
+ $key = '';
+ switch ($type) {
+ case 'pause':
+ $key = 'HeatmapSessionRecording_PauseReason';
+ break;
+ case 'noDataSession':
+ $key = 'HeatmapSessionRecording_NoSessionRecordedYetWithoutSystemConfiguration';
+ break;
+ case 'noDataHeatmap':
+ $key = 'HeatmapSessionRecording_NoHeatmapSamplesRecordedYetWithoutSystemConfiguration';
+ break;
+ }
+
+ if (!$key) {
+ return null;
+ }
+
+ Piwik::postEvent('HeatmapSessionRecording.updateTranslationKey', [&$key]);
+ return $key;
+ }
+
+ public static function isMatomoJsWritable($checkSpecificFile = '')
+ {
+ if (Manager::getInstance()->isPluginActivated('Cloud')) {
+ return true;
+ }
+
+ $updater = StaticContainer::get('Piwik\Plugins\CustomJsTracker\TrackerUpdater');
+ $filePath = $updater->getToFile()->getPath();
+ $filesToCheck = array($filePath);
+ $jsCodeGenerator = new TrackerCodeGenerator();
+ if (SettingsPiwik::isMatomoInstalled() && $jsCodeGenerator->shouldPreferPiwikEndpoint()) {
+ // if matomo is not installed yet, we definitely prefer matomo.js... check for isMatomoInstalled is needed
+ // cause otherwise it would perform a db query before matomo DB is configured
+ $filesToCheck[] = str_replace('matomo.js', 'piwik.js', $filePath);
+ }
+
+ if (!empty($checkSpecificFile)) {
+ $filesToCheck = [$checkSpecificFile]; // mostly used for testing isMatomoJsWritable functionality
+ }
+
+ if (!Manager::getInstance()->isPluginActivated('CustomJsTracker')) {
+ return false;
+ }
+
+ foreach ($filesToCheck as $fileToCheck) {
+ $file = new File($fileToCheck);
+
+ if (!$file->hasWriteAccess()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private function getHeatmaps($idSite)
+ {
+ return Request::processRequest('HeatmapSessionRecording.getHeatmaps', [
+ 'idSite' => $idSite, 'filter_limit' => -1,
+ 'includePageTreeMirror' => 0 // IMPORTANT for performance and IO. If you need page tree mirror please add another method and don't remove this parameter
+ ], $default = []);
+ }
+
+ private function getSessionRecordings($idSite)
+ {
+ return Request::processRequest('HeatmapSessionRecording.getSessionRecordings', [
+ 'idSite' => $idSite, 'filter_limit' => -1
+ ], $default = []);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/Breakpoint.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/Breakpoint.php
new file mode 100644
index 0000000..bb0eb6a
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/Breakpoint.php
@@ -0,0 +1,66 @@
+breakpoint = $breakpoint;
+ $this->name = $name;
+ }
+
+ public function check()
+ {
+ $title = Piwik::translate('HeatmapSessionRecording_BreakpointX', array($this->name));
+
+ // zero is a valid value!
+ if ($this->breakpoint === false || $this->breakpoint === null || $this->breakpoint === '') {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotProvided', $title));
+ }
+
+ if (!is_numeric($this->breakpoint)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotANumber', array($title)));
+ }
+
+ if ($this->breakpoint < 0) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooLow', array($title, 0)));
+ }
+
+ if ($this->breakpoint > self::MAX_LIMIT) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooHigh', array($title, self::MAX_LIMIT)));
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/CaptureKeystrokes.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/CaptureKeystrokes.php
new file mode 100644
index 0000000..b6004f9
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/CaptureKeystrokes.php
@@ -0,0 +1,40 @@
+value = $value;
+ }
+
+ public function check()
+ {
+ $allowedValues = array('0', '1', 0, 1, true, false);
+
+ if (!in_array($this->value, $allowedValues, $strict = true)) {
+ $message = Piwik::translate('HeatmapSessionRecording_ErrorXNotWhitelisted', array('captureKeystrokes', '"1", "0"'));
+ throw new Exception($message);
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/ExcludedElements.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/ExcludedElements.php
new file mode 100644
index 0000000..2395013
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/ExcludedElements.php
@@ -0,0 +1,50 @@
+selector = $name;
+ }
+
+ public function check()
+ {
+ if ($this->selector === null || $this->selector === false || $this->selector === '') {
+ // selecto may not be set
+ return;
+ }
+
+ $title = Piwik::translate('HeatmapSessionRecording_ExcludedElements');
+
+ if (Common::mb_strlen($this->selector) > static::MAX_LENGTH) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooLong', array($title, static::MAX_LENGTH)));
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/MinSessionTime.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/MinSessionTime.php
new file mode 100644
index 0000000..22dd6d4
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/MinSessionTime.php
@@ -0,0 +1,57 @@
+minSessionTime = $minSessionTime;
+ }
+
+ public function check()
+ {
+ $title = 'HeatmapSessionRecording_MinSessionTime';
+
+ if ($this->minSessionTime === false || $this->minSessionTime === null || $this->minSessionTime === '') {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotProvided', $title));
+ }
+
+ if (!is_numeric($this->minSessionTime)) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotANumber', array($title)));
+ }
+
+ if ($this->minSessionTime < 0) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooLow', array($title, 0)));
+ }
+
+ if ($this->minSessionTime > self::MAX_LIMIT) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooHigh', array($title, self::MAX_LIMIT)));
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/Name.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/Name.php
new file mode 100644
index 0000000..f03692d
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/Name.php
@@ -0,0 +1,51 @@
+name = $name;
+ }
+
+ public function check()
+ {
+ $title = 'General_Name';
+
+ if (empty($this->name)) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotProvided', $title));
+ }
+
+ if (Common::mb_strlen($this->name) > static::MAX_LENGTH) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooLong', array($title, static::MAX_LENGTH)));
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/PageRule.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/PageRule.php
new file mode 100644
index 0000000..3c09d15
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/PageRule.php
@@ -0,0 +1,80 @@
+target = $targets;
+ $this->parameterName = $parameterName;
+ $this->index = $index;
+ }
+
+ public function check()
+ {
+ $titleSingular = 'HeatmapSessionRecording_PageRule';
+
+ if (!is_array($this->target)) {
+ $titleSingular = Piwik::translate($titleSingular);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorInnerIsNotAnArray', array($titleSingular, $this->parameterName)));
+ }
+
+ if (empty($this->target['attribute'])) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorArrayMissingKey', array('attribute', $this->parameterName, $this->index)));
+ }
+
+ if (empty($this->target['type'])) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorArrayMissingKey', array('type', $this->parameterName, $this->index)));
+ }
+
+ if (!array_key_exists('inverted', $this->target)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorArrayMissingKey', array('inverted', $this->parameterName, $this->index)));
+ }
+
+ if (empty($this->target['value']) && Tracker\PageRuleMatcher::doesTargetTypeRequireValue($this->target['type'])) {
+ // any is the only target type that may have an empty value
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorArrayMissingValue', array('value', $this->parameterName, $this->index)));
+ }
+
+ if ($this->target['type'] === Tracker\PageRuleMatcher::TYPE_REGEXP && isset($this->target['value'])) {
+ $pattern = Tracker\PageRuleMatcher::completeRegexpPattern($this->target['value']);
+ if (@preg_match($pattern, '') === false) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorInvalidRegExp', array($this->target['value'])));
+ }
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/PageRules.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/PageRules.php
new file mode 100644
index 0000000..562c871
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/PageRules.php
@@ -0,0 +1,61 @@
+targets = $targets;
+ $this->parameterName = $parameterName;
+ $this->needsAtLeastOneEntry = $needsAtLeastOneEntry;
+ }
+
+ public function check()
+ {
+ if ($this->needsAtLeastOneEntry && empty($this->targets)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotProvided', $this->parameterName));
+ }
+
+ if (!is_array($this->targets)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorNotAnArray', $this->parameterName));
+ }
+
+ foreach ($this->targets as $index => $target) {
+ $target = new PageRule($target, $this->parameterName, $index);
+ $target->check();
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/RequiresActivity.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/RequiresActivity.php
new file mode 100644
index 0000000..61ff6a6
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/RequiresActivity.php
@@ -0,0 +1,40 @@
+value = $value;
+ }
+
+ public function check()
+ {
+ $allowedValues = array('0', '1', 0, 1, true, false);
+
+ if (!in_array($this->value, $allowedValues, $strict = true)) {
+ $message = Piwik::translate('HeatmapSessionRecording_ErrorXNotWhitelisted', array('activated', '"1", "0"'));
+ throw new Exception($message);
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/SampleLimit.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/SampleLimit.php
new file mode 100644
index 0000000..daa8686
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/SampleLimit.php
@@ -0,0 +1,57 @@
+sampleLimit = $sampleLimit;
+ }
+
+ public function check()
+ {
+ $title = 'HeatmapSessionRecording_SampleLimit';
+
+ if ($this->sampleLimit === false || $this->sampleLimit === null || $this->sampleLimit === '') {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotProvided', $title));
+ }
+
+ if (!is_numeric($this->sampleLimit)) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotANumber', array($title)));
+ }
+
+ if ($this->sampleLimit < 0) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooLow', array($title, 0)));
+ }
+
+ if ($this->sampleLimit > self::MAX_LIMIT) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooHigh', array($title, self::MAX_LIMIT)));
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/SampleRate.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/SampleRate.php
new file mode 100644
index 0000000..b0f6261
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/SampleRate.php
@@ -0,0 +1,62 @@
+sampleRate = $sampleRate;
+ }
+
+ public function check()
+ {
+ $title = 'HeatmapSessionRecording_SampleRate';
+
+ if ($this->sampleRate === false || $this->sampleRate === null || $this->sampleRate === '') {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotProvided', $title));
+ }
+
+ if (!is_numeric($this->sampleRate)) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotANumber', array($title)));
+ }
+
+ if ($this->sampleRate < 0) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooLow', array($title, 0)));
+ }
+
+ if ($this->sampleRate > self::MAX_RATE) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooHigh', array($title, self::MAX_RATE)));
+ }
+
+ if (!preg_match('/^\d{1,3}\.?\d?$/', (string) $this->sampleRate)) {
+ $title = Piwik::translate($title);
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXNotANumber', array($title)));
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/ScreenshotUrl.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/ScreenshotUrl.php
new file mode 100644
index 0000000..2b40332
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/ScreenshotUrl.php
@@ -0,0 +1,58 @@
+url = $name;
+ }
+
+ public function check()
+ {
+ if ($this->url === null || $this->url === false || $this->url === '') {
+ // url may not be set
+ return;
+ }
+
+ $title = Piwik::translate('HeatmapSessionRecording_ScreenshotUrl');
+
+ if (preg_match('/\s/', $this->url)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXContainsWhitespace', $title));
+ }
+
+ if (strpos($this->url, '//') === false) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_UrlXDoesNotLookLikeUrl', array($title, static::MAX_LENGTH)));
+ }
+
+ if (Common::mb_strlen($this->url) > static::MAX_LENGTH) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorXTooLong', array($title, static::MAX_LENGTH)));
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Input/Validator.php b/files/plugin-HeatmapSessionRecording-5.2.4/Input/Validator.php
new file mode 100644
index 0000000..e1b99c3
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Input/Validator.php
@@ -0,0 +1,176 @@
+configuration = new Configuration();
+ $this->systemSettings = $systemSettings;
+ }
+
+ private function supportsMethod($method)
+ {
+ return method_exists('Piwik\Piwik', $method);
+ }
+
+ public function checkHasSomeWritePermission()
+ {
+ if ($this->supportsMethod('checkUserHasSomeWriteAccess')) {
+ // since Matomo 3.6.0
+ Piwik::checkUserHasSomeWriteAccess();
+ return;
+ }
+
+ Piwik::checkUserHasSomeAdminAccess();
+ }
+
+ public function checkWritePermission($idSite)
+ {
+ $this->checkSiteExists($idSite);
+ Piwik::checkUserIsNotAnonymous();
+
+ if ($this->supportsMethod('checkUserHasWriteAccess')) {
+ // since Matomo 3.6.0
+ Piwik::checkUserHasWriteAccess($idSite);
+ return;
+ }
+
+ Piwik::checkUserHasAdminAccess($idSite);
+ }
+
+ public function checkHeatmapReportViewPermission($idSite)
+ {
+ $this->checkSiteExists($idSite);
+ Piwik::checkUserHasViewAccess($idSite);
+ $this->checkHeatmapRecordingEnabled();
+ }
+
+ public function checkSessionReportViewPermission($idSite)
+ {
+ $this->checkSiteExists($idSite);
+ $this->checkUserIsNotAnonymousForView($idSite);
+ Piwik::checkUserHasViewAccess($idSite);
+ $this->checkSessionRecordingEnabled();
+ }
+
+ public function checkSessionReportWritePermission($idSite)
+ {
+ $this->checkWritePermission($idSite);
+ $this->checkSessionRecordingEnabled();
+ }
+
+ public function checkHeatmapReportWritePermission($idSite)
+ {
+ $this->checkWritePermission($idSite);
+ $this->checkHeatmapRecordingEnabled();
+ }
+
+ public function checkSessionRecordingEnabled()
+ {
+ if ($this->isSessionRecordingDisabled()) {
+ throw new \Exception(Piwik::translate('HeatmapSessionRecording_ErrorSessionRecordingDisabled'));
+ }
+ }
+
+ public function checkHeatmapRecordingEnabled()
+ {
+ if ($this->isHeatmapRecordingDisabled()) {
+ throw new \Exception(Piwik::translate('HeatmapSessionRecording_ErrorHeatmapRecordingDisabled'));
+ }
+ }
+
+ private function checkUserIsNotAnonymousForView($idSite)
+ {
+ if ($this->configuration->isAnonymousSessionRecordingAccessEnabled($idSite)) {
+ Piwik::checkUserHasViewAccess($idSite);
+ return;
+ }
+
+ Piwik::checkUserIsNotAnonymous();
+ }
+
+ private function checkSiteExists($idSite)
+ {
+ new Site($idSite);
+ }
+
+ public function canViewSessionReport($idSite)
+ {
+ if (empty($idSite) || $this->isSessionRecordingDisabled()) {
+ return false;
+ }
+
+ if (
+ !$this->configuration->isAnonymousSessionRecordingAccessEnabled($idSite)
+ && Piwik::isUserIsAnonymous()
+ ) {
+ return false;
+ }
+
+ return Piwik::isUserHasViewAccess($idSite);
+ }
+
+ public function canViewHeatmapReport($idSite)
+ {
+ if (empty($idSite) || $this->isHeatmapRecordingDisabled()) {
+ return false;
+ }
+
+ return Piwik::isUserHasViewAccess($idSite);
+ }
+
+ public function canWrite($idSite)
+ {
+ if (empty($idSite)) {
+ return false;
+ }
+
+ if ($this->supportsMethod('isUserHasWriteAccess')) {
+ // since Matomo 3.6.0
+ return Piwik::isUserHasWriteAccess($idSite);
+ }
+
+ return Piwik::isUserHasAdminAccess($idSite);
+ }
+
+ public function isSessionRecordingDisabled()
+ {
+ return $this->systemSettings->disableSessionRecording->getValue();
+ }
+
+ public function isHeatmapRecordingDisabled()
+ {
+ return $this->systemSettings->disableHeatmapRecording->getValue();
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Install/HtAccess.php b/files/plugin-HeatmapSessionRecording-5.2.4/Install/HtAccess.php
new file mode 100644
index 0000000..dc7017d
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Install/HtAccess.php
@@ -0,0 +1,66 @@
+getPluginDir() . '/.htaccess';
+ }
+
+ private function getSourcePath()
+ {
+ return $this->getPluginDir() . '/Install/htaccessTemplate';
+ }
+
+ private function exists()
+ {
+ $path = $this->getTargetPath();
+ return file_exists($path);
+ }
+
+ private function canCreate()
+ {
+ return is_writable($this->getPluginDir());
+ }
+
+ private function isContentDifferent()
+ {
+ $templateContent = trim(file_get_contents($this->getSourcePath()));
+ $fileContent = trim(file_get_contents($this->getTargetPath()));
+
+ return $templateContent !== $fileContent;
+ }
+
+ public function install()
+ {
+ if (
+ $this->canCreate() && (!$this->exists() || (is_readable($this->getTargetPath()) && $this->isContentDifferent()))
+ ) {
+ Filesystem::copy($this->getSourcePath(), $this->getTargetPath());
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Install/htaccessTemplate b/files/plugin-HeatmapSessionRecording-5.2.4/Install/htaccessTemplate
new file mode 100644
index 0000000..c302274
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Install/htaccessTemplate
@@ -0,0 +1,23 @@
+# This file is generated by InnoCraft - Piwik, do not edit directly
+# Please report any issue or improvement directly to the InnoCraft team.
+# Allow to serve configs.php which is safe
+
+
+
+ Order Allow,Deny
+ Allow from All
+
+ = 2.4>
+ Require all granted
+
+
+
+
+ Order Allow,Deny
+ Allow from All
+
+
+ Require all granted
+
+
+
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/LEGALNOTICE b/files/plugin-HeatmapSessionRecording-5.2.4/LEGALNOTICE
new file mode 100644
index 0000000..b8a6bb2
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/LEGALNOTICE
@@ -0,0 +1,46 @@
+COPYRIGHT
+
+ The software package is:
+
+ Copyright (C) 2017 InnoCraft Ltd (NZBN 6106769)
+
+
+SOFTWARE LICENSE
+
+ This software is licensed under the InnoCraft EULA and the license has been included in this
+ software package in the file LICENSE.
+
+
+THIRD-PARTY COMPONENTS AND LIBRARIES
+
+ The following components/libraries are redistributed in this package,
+ and subject to their respective licenses.
+
+ Name: heatmap.js
+ Link: https://www.patrick-wied.at/static/heatmapjs/
+ License: MIT
+ License File: ibs/heatmap.js/LICENSE
+
+ Name: mutation-summary
+ Link: https://github.com/rafaelw/mutation-summary/
+ License: Apache 2.0
+ License File: libs/mutation-summary/COPYING
+
+ Name: MutationObserver.js
+ Link: https://github.com/megawac/MutationObserver.js/tree/master
+ License: WTFPL, Version 2
+ License File: libs/MutationObserver.js/license
+
+ Name: svg.js
+ Link: http://tkyk.github.com/jquery-history-plugin/
+ License: http://svgjs.com/
+ License File: libs/svg.js/LICENSE.txt
+
+ Name: Get Element CSS Selector
+ Link: https://gist.github.com/asfaltboy/8aea7435b888164e8563
+ License: MIT
+ License File: included in tracker.min.js
+
+ Name: Material icons ("repeat", "looks_one", "looks_two", "looks_four", "looks_six") in angularjs/sessionvis/sessionvis.directive.html
+ Link: https://design.google.com/icons/
+ License: Apache License Version 2.0
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/LICENSE b/files/plugin-HeatmapSessionRecording-5.2.4/LICENSE
new file mode 100644
index 0000000..4686f35
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/LICENSE
@@ -0,0 +1,49 @@
+InnoCraft License
+
+This InnoCraft End User License Agreement (the "InnoCraft EULA") is between you and InnoCraft Ltd (NZBN 6106769) ("InnoCraft"). If you are agreeing to this Agreement not as an individual but on behalf of your company, then "Customer" or "you" means your company, and you are binding your company to this Agreement. InnoCraft may modify this Agreement from time to time, subject to the terms in Section (xii) below.
+
+By clicking on the "I’ve read and accept the terms & conditions (https://shop.matomo.org/terms-conditions/)" (or similar button) that is presented to you at the time of your Order, or by using or accessing InnoCraft products, you indicate your assent to be bound by this Agreement.
+
+
+InnoCraft EULA
+
+(i) InnoCraft is the licensor of the Plugin for Matomo Analytics (the "Software").
+
+(ii) Subject to the terms and conditions of this Agreement, InnoCraft grants you a limited, worldwide, non-exclusive, non-transferable and non-sublicensable license to install and use the Software only on hardware systems owned, leased or controlled by you, during the applicable License Term. The term of each Software license ("License Term") will be specified in your Order. Your License Term will end upon any breach of this Agreement.
+
+(iii) Unless otherwise specified in your Order, for each Software license that you purchase, you may install one production instance of the Software in a Matomo Analytics instance owned or operated by you, and accessible via one URL ("Matomo instance"). Additional licenses must be purchased in order to deploy the Software in multiple Matomo instances, including when these multiple Matomo instances are hosted on a single hardware system.
+
+(iv) Licenses granted by InnoCraft are granted subject to the condition that you must ensure the maximum number of Authorized Users and Authorized Sites that are able to access and use the Software is equal to the number of User and Site Licenses for which the necessary fees have been paid to InnoCraft for the Subscription period. You may upgrade your license at any time on payment of the appropriate fees to InnoCraft in order to increase the maximum number of authorized users or sites. The number of User and Site Licenses granted to you is dependent on the fees paid by you. “User License” means a license granted under this EULA to you to permit an Authorized User to use the Software. “Authorized User” means a person who has an account in the Matomo instance and for which the necessary fees (“Subscription fees”) have been paid to InnoCraft for the current license term. "Site License" means a license granted under this EULA to you to permit an Authorized Site to use the Matomo Marketplace Plugin. “Authorized Sites” means a website or a measurable within Matomo instance and for which the necessary fees (“Subscription fees”) have been paid to InnoCraft for the current license term. These restrictions also apply if you install the Matomo Analytics Platform as part of your WordPress.
+
+(v) Piwik Analytics was renamed to Matomo Analytics in January 2018. The same terms and conditions as well as any restrictions or grants apply if you are using any version of Piwik.
+
+(vi) The Software requires a license key in order to operate, which will be delivered to the email addresses specified in your Order when we have received payment of the applicable fees.
+
+(vii) Any information that InnoCraft may collect from you or your device will be subject to InnoCraft Privacy Policy (https://www.innocraft.com/privacy).
+
+(viii) You are bound by the Matomo Marketplace Terms and Conditions (https://shop.matomo.org/terms-conditions/).
+
+(ix) You may not reverse engineer or disassemble or re-distribute the Software in whole or in part, or create any derivative works from or sublicense any rights in the Software, unless otherwise expressly authorized in writing by InnoCraft.
+
+(x) The Software is protected by copyright and other intellectual property laws and treaties. InnoCraft own all title, copyright and other intellectual property rights in the Software, and the Software is licensed to you directly by InnoCraft, not sold.
+
+(xi) The Software is provided under an "as is" basis and without any support or maintenance. Nothing in this Agreement shall require InnoCraft to provide you with support or fixes to any bug, failure, mis-performance or other defect in The Software. InnoCraft may provide you, from time to time, according to his sole discretion, with updates of the Software. You hereby warrant to keep the Software up-to-date and install all relevant updates. InnoCraft shall provide any update free of charge.
+
+(xii) The Software is provided "as is", and InnoCraft hereby disclaim all warranties, including but not limited to any implied warranties of title, non-infringement, merchantability or fitness for a particular purpose. InnoCraft shall not be liable or responsible in any way for any losses or damage of any kind, including lost profits or other indirect or consequential damages, relating to your use of or reliance upon the Software.
+
+(xiii) We may update or modify this Agreement from time to time, including the referenced Privacy Policy and the Matomo Marketplace Terms and Conditions. If a revision meaningfully reduces your rights, we will use reasonable efforts to notify you (by, for example, sending an email to the billing or technical contact you designate in the applicable Order). If we modify the Agreement during your License Term or Subscription Term, the modified version will be effective upon your next renewal of a License Term.
+
+
+About InnoCraft Ltd
+
+At InnoCraft Ltd, we create innovating quality products to grow your business and to maximize your success.
+
+Our software products are built on top of Matomo Analytics: the leading open digital analytics platform used by more than one million websites worldwide. We are the creators and makers of the Matomo Analytics platform.
+
+
+Contact
+
+Email: contact@innocraft.com
+Contact form: https://www.innocraft.com/#contact
+Website: https://www.innocraft.com/
+Buy our products: Premium Features for Matomo Analytics https://plugins.matomo.org/premium
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Menu.php b/files/plugin-HeatmapSessionRecording-5.2.4/Menu.php
new file mode 100644
index 0000000..9ec1075
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Menu.php
@@ -0,0 +1,50 @@
+validator = $validator;
+ }
+
+ public function configureAdminMenu(MenuAdmin $menu)
+ {
+ $idSite = Common::getRequestVar('idSite', 0, 'int');
+
+ if (!empty($idSite) && !Piwik::isUserIsAnonymous() && $this->validator->canWrite($idSite)) {
+ if (!$this->validator->isHeatmapRecordingDisabled()) {
+ $menu->addMeasurableItem('HeatmapSessionRecording_Heatmaps', $this->urlForAction('manageHeatmap'), $orderId = 30);
+ }
+ if (!$this->validator->isSessionRecordingDisabled()) {
+ $menu->addMeasurableItem('HeatmapSessionRecording_SessionRecordings', $this->urlForAction('manageSessions'), $orderId = 30);
+ }
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/Model/SiteHsrModel.php b/files/plugin-HeatmapSessionRecording-5.2.4/Model/SiteHsrModel.php
new file mode 100644
index 0000000..f95767c
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/Model/SiteHsrModel.php
@@ -0,0 +1,489 @@
+dao = $dao;
+ $this->logHsrSite = $logHsrSite;
+ }
+
+ public function addHeatmap($idSite, $name, $matchPageRules, $sampleLimit, $sampleRate, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet, $captureDomManually, $createdDate)
+ {
+ $this->checkHeatmap($name, $matchPageRules, $sampleLimit, $sampleRate, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet);
+
+ $status = SiteHsrDao::STATUS_ACTIVE;
+
+ $idSiteHsr = $this->dao->createHeatmapRecord($idSite, $name, $sampleLimit, $sampleRate, $matchPageRules, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet, $status, $captureDomManually, $createdDate);
+ $this->clearTrackerCache($idSite);
+
+ return (int) $idSiteHsr;
+ }
+
+ public function updateHeatmap($idSite, $idSiteHsr, $name, $matchPageRules, $sampleLimit, $sampleRate, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet, $captureDomManually, $updatedDate)
+ {
+ $this->checkHeatmap($name, $matchPageRules, $sampleLimit, $sampleRate, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet);
+
+ $columns = array(
+ 'name' => $name,
+ 'sample_limit' => $sampleLimit,
+ 'match_page_rules' => $matchPageRules,
+ 'sample_rate' => $sampleRate,
+ 'excluded_elements' => $excludedElements,
+ 'screenshot_url' => $screenshotUrl,
+ 'breakpoint_mobile' => $breakpointMobile,
+ 'breakpoint_tablet' => $breakpointTablet,
+ 'updated_date' => $updatedDate,
+ );
+
+ if (!empty($captureDomManually)) {
+ $columns['capture_manually'] = 1;
+ $columns['page_treemirror'] = null;
+ } else {
+ $columns['capture_manually'] = 0;
+ }
+
+ $this->updateHsrColumns($idSite, $idSiteHsr, $columns);
+ $this->clearTrackerCache($idSite);
+ }
+
+ private function checkHeatmap($name, $matchPageRules, $sampleLimit, $sampleRate, $excludedElements, $screenshotUrl, $breakpointMobile, $breakpointTablet)
+ {
+ $name = new Name($name);
+ $name->check();
+
+ $pageRules = new PageRules($matchPageRules, 'matchPageRules', $needsOneEntry = true);
+ $pageRules->check();
+
+ $sampleLimit = new SampleLimit($sampleLimit);
+ $sampleLimit->check();
+
+ $sampleRate = new SampleRate($sampleRate);
+ $sampleRate->check();
+
+ $screenshotUrl = new ScreenshotUrl($screenshotUrl);
+ $screenshotUrl->check();
+
+ $excludedElements = new ExcludedElements($excludedElements);
+ $excludedElements->check();
+
+ $breakpointMobile = new Breakpoint($breakpointMobile, 'Mobile');
+ $breakpointMobile->check();
+
+ $breakpointTablet = new Breakpoint($breakpointTablet, 'Tablet');
+ $breakpointTablet->check();
+ }
+
+ public function addSessionRecording($idSite, $name, $matchPageRules, $sampleLimit, $sampleRate, $minSessionTime, $requiresActivity, $captureKeystrokes, $createdDate)
+ {
+ $this->checkSession($name, $matchPageRules, $sampleLimit, $sampleRate, $minSessionTime, $requiresActivity, $captureKeystrokes);
+ $status = SiteHsrDao::STATUS_ACTIVE;
+
+ $idSiteHsr = $this->dao->createSessionRecord($idSite, $name, $sampleLimit, $sampleRate, $matchPageRules, $minSessionTime, $requiresActivity, $captureKeystrokes, $status, $createdDate);
+
+ $this->clearTrackerCache($idSite);
+ return (int) $idSiteHsr;
+ }
+
+ public function updateSessionRecording($idSite, $idSiteHsr, $name, $matchPageRules, $sampleLimit, $sampleRate, $minSessionTime, $requiresActivity, $captureKeystrokes, $updatedDate)
+ {
+ $this->checkSession($name, $matchPageRules, $sampleLimit, $sampleRate, $minSessionTime, $requiresActivity, $captureKeystrokes);
+
+ $columns = array(
+ 'name' => $name,
+ 'sample_limit' => $sampleLimit,
+ 'match_page_rules' => $matchPageRules,
+ 'sample_rate' => $sampleRate,
+ 'min_session_time' => $minSessionTime,
+ 'requires_activity' => $requiresActivity,
+ 'capture_keystrokes' => $captureKeystrokes,
+ 'updated_date' => $updatedDate,
+ );
+
+ $this->updateHsrColumns($idSite, $idSiteHsr, $columns);
+ $this->clearTrackerCache($idSite);
+ }
+
+ private function checkSession($name, $matchPageRules, $sampleLimit, $sampleRate, $minSessionTime, $requiresActivity, $captureKeystrokes)
+ {
+ $name = new Name($name);
+ $name->check();
+
+ $pageRules = new PageRules($matchPageRules, 'matchPageRules', $needsOneEntry = false);
+ $pageRules->check();
+
+ $sampleLimit = new SampleLimit($sampleLimit);
+ $sampleLimit->check();
+
+ $sampleRate = new SampleRate($sampleRate);
+ $sampleRate->check();
+
+ $minSessionTime = new MinSessionTime($minSessionTime);
+ $minSessionTime->check();
+
+ $requiresActivity = new RequiresActivity($requiresActivity);
+ $requiresActivity->check();
+
+ $captureKeystrokes = new CaptureKeystrokes($captureKeystrokes);
+ $captureKeystrokes->check();
+ }
+
+ public function getHeatmap($idSite, $idSiteHsr)
+ {
+ $record = $this->dao->getRecord($idSite, $idSiteHsr, SiteHsrDao::RECORD_TYPE_HEATMAP);
+
+ return $this->enrichHeatmap($record);
+ }
+
+ public function getSessionRecording($idSite, $idSiteHsr)
+ {
+ $record = $this->dao->getRecord($idSite, $idSiteHsr, SiteHsrDao::RECORD_TYPE_SESSION);
+ return $this->enrichSessionRecording($record);
+ }
+
+ public function pauseHeatmap($idSite, $idSiteHsr)
+ {
+ $this->updateHsrColumns($idSite, $idSiteHsr, array('status' => SiteHsrDao::STATUS_PAUSED));
+ }
+
+ public function resumeHeatmap($idSite, $idSiteHsr)
+ {
+ $this->updateHsrColumns($idSite, $idSiteHsr, array('status' => SiteHsrDao::STATUS_ACTIVE));
+ }
+
+ public function deactivateHeatmap($idSite, $idSiteHsr)
+ {
+ $heatmap = $this->getHeatmap($idSite, $idSiteHsr);
+
+ if (!empty($heatmap)) {
+ $this->updateHsrColumns($idSite, $idSiteHsr, array('status' => SiteHsrDao::STATUS_DELETED));
+
+ // the actual recorded heatmap data will still exist but we remove the "links" which is quick. a task will later remove all entries
+ $this->logHsrSite->unlinkSiteRecords($idSiteHsr);
+ }
+ }
+
+ public function checkHeatmapExists($idSite, $idSiteHsr)
+ {
+ $hsr = $this->dao->getRecord($idSite, $idSiteHsr, SiteHsrDao::RECORD_TYPE_HEATMAP);
+
+ if (empty($hsr)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorHeatmapDoesNotExist'));
+ }
+ }
+
+ public function checkSessionRecordingExists($idSite, $idSiteHsr)
+ {
+ $hsr = $this->dao->getRecord($idSite, $idSiteHsr, SiteHsrDao::RECORD_TYPE_SESSION);
+
+ if (empty($hsr)) {
+ throw new Exception(Piwik::translate('HeatmapSessionRecording_ErrorSessionRecordingDoesNotExist'));
+ }
+ }
+
+ public function pauseSessionRecording($idSite, $idSiteHsr)
+ {
+ $this->updateHsrColumns($idSite, $idSiteHsr, array('status' => SiteHsrDao::STATUS_PAUSED));
+ }
+
+ public function resumeSessionRecording($idSite, $idSiteHsr)
+ {
+ $this->updateHsrColumns($idSite, $idSiteHsr, array('status' => SiteHsrDao::STATUS_ACTIVE));
+ }
+
+ public function deactivateSessionRecording($idSite, $idSiteHsr)
+ {
+ $session = $this->getSessionRecording($idSite, $idSiteHsr);
+
+ if (!empty($session)) {
+ $this->updateHsrColumns($idSite, $idSiteHsr, array('status' => SiteHsrDao::STATUS_DELETED));
+
+ // the actual recording will still exist but we remove the "links" which is quick. a task will later remove all entries
+ $this->logHsrSite->unlinkSiteRecords($idSiteHsr);
+ }
+ }
+
+ public function deactivateRecordsForSite($idSite)
+ {
+ foreach ($this->dao->getRecords($idSite, SiteHsrDao::RECORD_TYPE_HEATMAP, false) as $heatmap) {
+ $this->deactivateHeatmap($idSite, $heatmap['idsitehsr']);
+ }
+
+ foreach ($this->dao->getRecords($idSite, SiteHsrDao::RECORD_TYPE_SESSION, false) as $session) {
+ $this->deactivateSessionRecording($idSite, $session['idsitehsr']);
+ }
+ }
+
+ public function pauseRecordsForSite($idSite)
+ {
+ foreach ($this->dao->getRecords($idSite, SiteHsrDao::RECORD_TYPE_HEATMAP, false) as $heatmap) {
+ $this->pauseHeatmap($idSite, $heatmap['idsitehsr']);
+ }
+
+ foreach ($this->dao->getRecords($idSite, SiteHsrDao::RECORD_TYPE_SESSION, false) as $session) {
+ $this->pauseSessionRecording($idSite, $session['idsitehsr']);
+ }
+ }
+
+ public function resumeRecordsForSite($idSite)
+ {
+ foreach ($this->dao->getRecords($idSite, SiteHsrDao::RECORD_TYPE_HEATMAP, false) as $heatmap) {
+ $this->resumeHeatmap($idSite, $heatmap['idsitehsr']);
+ }
+
+ foreach ($this->dao->getRecords($idSite, SiteHsrDao::RECORD_TYPE_SESSION, false) as $session) {
+ $this->resumeSessionRecording($idSite, $session['idsitehsr']);
+ }
+ }
+
+ public function endHeatmap($idSite, $idSiteHsr)
+ {
+ $heatmap = $this->getHeatmap($idSite, $idSiteHsr);
+ if (!empty($heatmap)) {
+ $this->updateHsrColumns($idSite, $idSiteHsr, array('status' => SiteHsrDao::STATUS_ENDED));
+
+ Piwik::postEvent('HeatmapSessionRecording.endHeatmap', array($idSite, $idSiteHsr));
+ }
+ }
+
+ public function endSessionRecording($idSite, $idSiteHsr)
+ {
+ $session = $this->getSessionRecording($idSite, $idSiteHsr);
+ if (!empty($session)) {
+ $this->updateHsrColumns($idSite, $idSiteHsr, array('status' => SiteHsrDao::STATUS_ENDED));
+
+ Piwik::postEvent('HeatmapSessionRecording.endSessionRecording', array($idSite, $idSiteHsr));
+ }
+ }
+
+ /**
+ * @param $idSite
+ * @param bool $includePageTreeMirror performance and IO tweak has some heatmaps might have a 16MB or more treemirror and it would be loaded on every request causing a lot of IO etc.
+ * @return array
+ */
+ public function getHeatmaps($idSite, $includePageTreeMirror)
+ {
+ $heatmaps = $this->dao->getRecords($idSite, SiteHsrDao::RECORD_TYPE_HEATMAP, $includePageTreeMirror);
+
+ return $this->enrichHeatmaps($heatmaps);
+ }
+
+ public function getSessionRecordings($idSite)
+ {
+ $sessionRecordings = $this->dao->getRecords($idSite, SiteHsrDao::RECORD_TYPE_SESSION, $includePageTreeMirror = false);
+
+ return $this->enrichSessionRecordings($sessionRecordings);
+ }
+
+ public function hasSessionRecordings($idSite)
+ {
+ $hasSession = $this->dao->hasRecords($idSite, SiteHsrDao::RECORD_TYPE_SESSION);
+
+ return !empty($hasSession);
+ }
+
+ public function hasHeatmaps($idSite)
+ {
+ $hasHeatmap = $this->dao->hasRecords($idSite, SiteHsrDao::RECORD_TYPE_HEATMAP);
+
+ return !empty($hasHeatmap);
+ }
+
+ public function setPageTreeMirror($idSite, $idSiteHsr, $treeMirror, $screenshotUrl)
+ {
+ $heatmap = $this->getHeatmap($idSite, $idSiteHsr);
+ if (!empty($heatmap)) {
+ // only supported by heatmaps
+ $columns = array(
+ 'page_treemirror' => $treeMirror,
+ 'screenshot_url' => $screenshotUrl
+ );
+ if (!empty($heatmap['capture_manually']) && !empty($treeMirror)) {
+ $columns['capture_manually'] = 0;
+ }
+ $this->updateHsrColumns($idSite, $idSiteHsr, $columns);
+ }
+ }
+
+ public function getPiwikRequestDate($hsr)
+ {
+ // we sub one day to make sure to include them all
+ $from = Date::factory($hsr['created_date'])->subDay(1)->toString();
+ $to = Date::now()->addDay(1)->toString();
+
+ if ($from === $to) {
+ $dateRange = $from;
+ $period = 'year';
+ } else {
+ $period = 'range';
+ $dateRange = $from . ',' . $to;
+ }
+
+ return array('period' => $period, 'date' => $dateRange);
+ }
+
+ private function enrichHeatmaps($heatmaps)
+ {
+ if (empty($heatmaps)) {
+ return array();
+ }
+
+ foreach ($heatmaps as $index => $heatmap) {
+ $heatmaps[$index] = $this->enrichHeatmap($heatmap);
+ }
+
+ return $heatmaps;
+ }
+
+ private function enrichHeatmap($heatmap)
+ {
+ if (empty($heatmap)) {
+ return $heatmap;
+ }
+
+ unset($heatmap['record_type']);
+ unset($heatmap['min_session_time']);
+ unset($heatmap['requires_activity']);
+ unset($heatmap['capture_keystrokes']);
+ $heatmap['created_date_pretty'] = Date::factory($heatmap['created_date'])->getLocalized(DateTimeFormatProvider::DATE_FORMAT_SHORT);
+
+ if ((!method_exists(SettingsServer::class, 'isMatomoForWordPress') || !SettingsServer::isMatomoForWordPress()) && !SettingsServer::isTrackerApiRequest()) {
+ $heatmap['heatmapViewUrl'] = self::completeWidgetUrl('showHeatmap', 'idSiteHsr=' . (int) $heatmap['idsitehsr'] . '&useDateUrl=0', (int) $heatmap['idsite']);
+ }
+
+ return $heatmap;
+ }
+
+ public static function completeWidgetUrl($action, $params, $idSite, $period = null, $date = null)
+ {
+ if (!isset($date)) {
+ if (empty(self::$defaultDate)) {
+ $userPreferences = new UserPreferences();
+ self::$defaultDate = $userPreferences->getDefaultDate();
+ if (empty(self::$defaultDate)) {
+ self:: $defaultDate = 'today';
+ }
+ }
+ $date = self::$defaultDate;
+ }
+
+ if (!isset($period)) {
+ if (!isset(self::$defaultPeriod)) {
+ $userPreferences = new UserPreferences();
+ self::$defaultPeriod = $userPreferences->getDefaultPeriod(false);
+ if (empty(self::$defaultPeriod)) {
+ self::$defaultPeriod = 'day';
+ }
+ }
+ $period = self::$defaultPeriod;
+ }
+
+ $token = Access::getInstance()->getTokenAuth();
+
+ $url = 'index.php?module=Widgetize&action=iframe&moduleToWidgetize=HeatmapSessionRecording&actionToWidgetize=' . urlencode($action) . '&' . $params . '&idSite=' . (int) $idSite . '&period=' . urlencode($period) . '&date=' . urlencode($date);
+ if (!empty($token)) {
+ $url .= '&token_auth=' . urlencode($token);
+ }
+ return $url;
+ }
+
+ private function enrichSessionRecordings($sessionRecordings)
+ {
+ if (empty($sessionRecordings)) {
+ return array();
+ }
+
+ foreach ($sessionRecordings as $index => $sessionRecording) {
+ $sessionRecordings[$index] = $this->enrichSessionRecording($sessionRecording);
+ }
+
+ return $sessionRecordings;
+ }
+
+ private function enrichSessionRecording($session)
+ {
+ if (empty($session)) {
+ return $session;
+ }
+
+ unset($session['record_type']);
+ unset($session['screenshot_url']);
+ unset($session['page_treemirror']);
+ unset($session['excluded_elements']);
+ unset($session['breakpoint_mobile']);
+ unset($session['breakpoint_tablet']);
+ $session['created_date_pretty'] = Date::factory($session['created_date'])->getLocalized(DateTimeFormatProvider::DATE_FORMAT_SHORT);
+
+ return $session;
+ }
+
+ protected function getCurrentDateTime()
+ {
+ return Date::now()->getDatetime();
+ }
+
+ private function updateHsrColumns($idSite, $idSiteHsr, $columns)
+ {
+ if (!isset($columns['updated_date'])) {
+ $columns['updated_date'] = $this->getCurrentDateTime();
+ }
+
+ $this->dao->updateHsrColumns($idSite, $idSiteHsr, $columns);
+ $this->clearTrackerCache($idSite);
+ }
+
+ private function clearTrackerCache($idSite)
+ {
+ Tracker\Cache::deleteCacheWebsiteAttributes($idSite);
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/MutationManipulator.php b/files/plugin-HeatmapSessionRecording-5.2.4/MutationManipulator.php
new file mode 100644
index 0000000..5878e4f
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/MutationManipulator.php
@@ -0,0 +1,226 @@
+configuration = $configuration;
+ $this->generateNonce();
+ }
+
+ public function manipulate($initialMutation, $idSiteHsr, $idLogHsr)
+ {
+ $parseAndSanitizeCssLinks = $this->updateCssLinks($initialMutation, $idSiteHsr, $idLogHsr);
+
+ return $this->sanitizeNodeAttributes($parseAndSanitizeCssLinks);
+ }
+
+ public function updateCssLinks($initialMutation, $idSiteHsr, $idLogHsr)
+ {
+ if ($this->configuration->isLoadCSSFromDBEnabled()) {
+ $blob = new LogHsrBlob();
+ $dao = new LogHsrEvent($blob);
+ $cssEvents = $dao->getCssEvents($idSiteHsr, $idLogHsr);
+ if (!empty($cssEvents) && !empty($initialMutation)) {
+ $initialMutation = $this->updateInitialMutationWithInlineCss($initialMutation, $cssEvents);
+ }
+ }
+
+ return $initialMutation;
+ }
+
+ public function getNonce()
+ {
+ if (!$this->nonce) {
+ $this->generateNonce();
+ }
+
+
+ return $this->nonce;
+ }
+
+ public function generateNonce()
+ {
+ $this->nonce = $this->generateRandomString();
+ }
+
+ private function generateRandomString($length = 10)
+ {
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $charactersLength = strlen($characters);
+ $randomString = '';
+ for ($i = 0; $i < $length; $i++) {
+ $randomString .= $characters[rand(0, $charactersLength - 1)];
+ }
+ return $randomString;
+ }
+
+ public function sanitizeNodeAttributes($initialMutation)
+ {
+ $initialMutationArray = json_decode($initialMutation, true);
+ if (!empty($initialMutationArray['children'])) {
+ $this->parseMutationArrayRecursivelyToSanitizeNodes($initialMutationArray['children']);
+ $initialMutation = json_encode($initialMutationArray);
+ }
+
+ return $initialMutation;
+ }
+
+ public function updateInitialMutationWithInlineCss($initialMutation, $cssEvents)
+ {
+ $formattedCssEvents = $this->formatCssEvents($cssEvents);
+ $initialMutationArray = json_decode($initialMutation, true);
+ if (!empty($initialMutationArray['children']) && !empty($formattedCssEvents)) {
+ $this->parseMutationArrayRecursivelyForCssLinks($initialMutationArray['children'], $formattedCssEvents);
+
+ $initialMutation = json_encode($initialMutationArray);
+ }
+
+ return $initialMutation;
+ }
+
+ public function formatCssEvents($cssEvents)
+ {
+ $formatted = array();
+ foreach ($cssEvents as $cssEvent) {
+ if (!isset($formatted[md5(trim($cssEvent['url']))])) { //Only use the first one since the o/p is sorted by ID in ascending order
+ $formatted[md5(trim($cssEvent['url']))] = $cssEvent;
+ }
+ }
+
+ return $formatted;
+ }
+
+ private function parseMutationArrayRecursivelyForCssLinks(&$nodes, $cssEvents, &$id = 900000000)
+ {
+ foreach ($nodes as &$node) {
+ $parseChildNodes = true;
+ if (isset($node['tagName']) && $node['tagName'] == 'LINK' && !empty($node['attributes']['url']) && !empty($cssEvents) && !empty($cssEvents[md5(trim($node['attributes']['url']))]['text'])) {
+ $parseChildNodes = false;
+ $content = $cssEvents[md5(trim($node['attributes']['url']))]['text'];
+ if (!empty($content)) {
+ $node['tagName'] = 'STYLE';
+ $media = $node['attributes']['media'] ?? '';
+ if (isset($node['attributes'])) {
+ $node['attributes'] = [];
+ }
+ $node['attributes']['nonce'] = $this->getNonce();
+ if ($media) {
+ $node['attributes']['media'] = $media;
+ }
+ $node['childNodes'] = [
+ [
+ 'nodeType' => 3,
+ 'id' => $id++,
+ 'textContent' => $content
+ ]
+ ];
+ }
+ }
+
+ if ($parseChildNodes && !empty($node['childNodes'])) {
+ $this->parseMutationArrayRecursivelyForCssLinks($node['childNodes'], $cssEvents, $id);
+ }
+ }
+ }
+
+ private function parseMutationArrayRecursivelyToSanitizeNodes(&$nodes)
+ {
+ foreach ($nodes as &$node) {
+ if (!empty($node['attributes'])) {
+ // empty all the attributes with base64 and contains javascript/script/"("
+ // Eg: OR
+ foreach ($node['attributes'] as $nodeAttributeKey => &$nodeAttributeValue) {
+ // had to double encode `\x09` as `\\\\x09` in MutationManipulatorTest.php to make json_decode work, else it was giving "syntax error" via json_last_error_msg()
+ // Due to double encoding had to add entry for both "\\x09" and "\x09"
+ $nodeAttributeValue = str_replace(["\\x09", "\\x0a", "\\x0d", "\\0", "\x09", "\x0a", "\x0d", "\0"], "", $nodeAttributeValue);
+ $htmlDecodedAttributeValue = html_entity_decode($nodeAttributeValue, ENT_COMPAT, 'UTF-8');
+ if (
+ $htmlDecodedAttributeValue &&
+ (
+ stripos($htmlDecodedAttributeValue, 'ecmascript') !== false ||
+ stripos($htmlDecodedAttributeValue, 'javascript') !== false ||
+ stripos($htmlDecodedAttributeValue, 'script:') !== false ||
+ stripos($htmlDecodedAttributeValue, 'jscript') !== false ||
+ stripos($htmlDecodedAttributeValue, 'vbscript') !== false
+ )
+ ) {
+ $nodeAttributeValue = '';
+ } elseif (stripos($nodeAttributeValue, 'base64') !== false) {
+ $base64KeywordMadeLowerCase = str_ireplace('base64', 'base64', $nodeAttributeValue);
+ //For values like data:text/javascript;base64,YWxlcnQoMSk= we split the value into 2 parts
+ // part1: data:text/javascript;base64
+ // part2: ,YWxlcnQoMSk= we split the value into 2 parts
+ // we determine the position of first comma from second part and try to decode the base64 string and check fo possible XSS
+ // cannot assume the position of firstComma to be `0` since there can be string with spaces in beginning
+ $attributeExploded = explode('base64', $base64KeywordMadeLowerCase);
+ array_shift($attributeExploded);
+ if (!empty($attributeExploded)) {
+ foreach ($attributeExploded as $attributeExplodedValue) {
+ $htmlDecodedAttributeString = html_entity_decode($attributeExplodedValue, ENT_COMPAT, 'UTF-8');
+ $base64DecodedString = base64_decode($attributeExplodedValue);
+ $base64UrlDecodedString = base64_decode(urldecode($attributeExplodedValue));
+ if (
+ $this->isXssString($base64DecodedString) ||
+ $this->isXssString($base64UrlDecodedString) ||
+ $this->isXssString($htmlDecodedAttributeString) ||
+ $this->isXssString(urldecode($htmlDecodedAttributeString))
+ ) {
+ $nodeAttributeValue = '';
+ break;
+ }
+ }
+ }
+ } elseif ($nodeAttributeValue) {
+ $htmlDecodedString = html_entity_decode($nodeAttributeValue, ENT_COMPAT, 'UTF-8');
+ if (
+ $this->isXssString($htmlDecodedString) ||
+ $this->isXssString(urldecode($htmlDecodedString))
+ ) {
+ $nodeAttributeValue = '';
+ }
+ }
+ }
+ }
+
+ if (!empty($node['childNodes'])) {
+ $this->parseMutationArrayRecursivelyToSanitizeNodes($node['childNodes']);
+ }
+ }
+ }
+
+ private function isXssString($value)
+ {
+ if (
+ !empty($value) &&
+ (
+ stripos($value, 'script:') !== false ||
+ stripos($value, 'javascript') !== false ||
+ stripos($value, 'ecmascript') !== false ||
+ stripos($value, '
+```
+
+### Polyfill differences from standard interface
+
+#### MutationObserver
+
+* Implemented using a recursive `setTimeout` (every ~30 ms) rather than using a `setImmediate` polyfill; so calls will be made less frequently and likely with more data than the standard MutationObserver. In addition, it can miss changes that occur and then are lost in the interval window.
+* Setting an observed elements html using `innerHTML` will call `childList` observer listeners with several mutations with only 1 addedNode or removed node per mutation. With the standard you would have 1 call with multiple nodes in addedNodes and removedNodes node lists.
+* With `childList` and `subtree` changes in node order (eg first element gets swapped with last) should fire a `addedNode` and `removedNode` mutation but the correct node may not always be identified.
+
+#### MutationRecord
+
+* `addedNodes` and `removedNodes` are arrays instead of `NodeList`s
+* `oldValue` is always called with attribute changes
+* `nextSibling` and `previousSibling` correctfullness is questionable (hard to know if the order of appended items). I'd suggest not relying on them anyway (my tests are extremely permissive with these attributes)
+
+### Supported MutationObserverInit properties
+
+Currently supports the following [MutationObserverInit properties](https://developer.mozilla.org/en/docs/Web/API/MutationObserver#MutationObserverInit):
+
+* **childList**: Set to truthy if mutations to target's immediate children are to be observed.
+* **subtree**: Set to truthy to do deep scans on a target's children.
+* **attributes**: Set to truthy if mutations to target's children are to be observed. As explained in #4, the `style` attribute may not be matched in ie<8.
+* **attributeFilter**: Set to an array of attribute local names (without namespace) if not all attribute mutations need to be observed.
+* **attributeOldValue**: doesn't do anything attributes are always called with old value
+* **characterData**: currently follows Mozilla's implementation in that it will only watch `textNodes` values and not, like in webkit, where setting .innerHTML will add a characterData mutation.
+
+### Performance
+
+By default, the polyfill will check observed nodes about 25 times per second (30 ms interval) for mutations. Try running [these jsperf.com tests](http://jsperf.com/mutationobserver-shim) and the JSLitmus tests in the test suite for usage performance tests. It may be worthwile to adapt `MutationObserver._period` based on UA or heuristics (todo).
+
+From my tests observing any size element without `subtree` enabled is relatively cheap. Although I've optimized the subtree check to the best of my abilities it can be costly on large trees. You can draw your own conclusions based on the JSLitmus and jsperf tests noting that you can expect the `mo` to do its check 28+ times a second (by default).
+
+Although supported, I'd recommend against watching `attributes` on the `subtree` on large structures, as the check is complex and expensive on terrible hardware like my phone :(
+
+The included minified file has been tuned for performance.
+
+### Compatibility
+
+I've tested and verified compatibility in the following browsers + [these Sauce browsers](https://saucelabs.com/u/mutationobserver)
+
+* Internet Explorer 8 (emulated), 9, 10 in win7 and win8
+* Firefox 4, 21, 24, 26 in OSX, win7 and win8
+* Opera 11.8, 12.16 in win7
+* "Internet" on Android HTC One V
+* Blackberry 6.0.16
+
+Try [running the test suite](https://rawgithub.com/megawac/MutationObserver.js/master/test/index.html) and see some simple example usage:
+
+* http://jsbin.com/suqewogone listen to images being appended dynamically
+* http://jsbin.com/bapohopuwi autoscroll an element as new content is added
+
+See http://dev.opera.com/articles/view/mutation-observers-tutorial/ for some sample usage.
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/dist/README.md b/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/dist/README.md
new file mode 100644
index 0000000..a3e83b2
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/dist/README.md
@@ -0,0 +1,7 @@
+###Compiled files
+
+*Compiled by Google closure compiler in `ADVANCED_OPTIMIZATIONS`*
+
+- Original: 25 kB
+- Minified: 3.7 kB
+- Gzipped: 1.6 kB
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/dist/mutationobserver.min.js b/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/dist/mutationobserver.min.js
new file mode 100644
index 0000000..94e8949
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/dist/mutationobserver.min.js
@@ -0,0 +1,10 @@
+// mutationobserver-shim v0.3.2 (github.com/megawac/MutationObserver.js)
+// Authors: Graeme Yeates (github.com/megawac)
+window.MutationObserver=window.MutationObserver||function(w){function v(a){this.i=[];this.m=a}function I(a){(function c(){var d=a.takeRecords();d.length&&a.m(d,a);a.h=setTimeout(c,v._period)})()}function p(a){var b={type:null,target:null,addedNodes:[],removedNodes:[],previousSibling:null,nextSibling:null,attributeName:null,attributeNamespace:null,oldValue:null},c;for(c in a)b[c]!==w&&a[c]!==w&&(b[c]=a[c]);return b}function J(a,b){var c=C(a,b);return function(d){var f=d.length,n;b.a&&3===a.nodeType&&
+a.nodeValue!==c.a&&d.push(new p({type:"characterData",target:a,oldValue:c.a}));b.b&&c.b&&A(d,a,c.b,b.f);if(b.c||b.g)n=K(d,a,c,b);if(n||d.length!==f)c=C(a,b)}}function L(a,b){return b.value}function M(a,b){return"style"!==b.name?b.value:a.style.cssText}function A(a,b,c,d){for(var f={},n=b.attributes,k,g,x=n.length;x--;)k=n[x],g=k.name,d&&d[g]===w||(D(b,k)!==c[g]&&a.push(p({type:"attributes",target:b,attributeName:g,oldValue:c[g],attributeNamespace:k.namespaceURI})),f[g]=!0);for(g in c)f[g]||a.push(p({target:b,
+type:"attributes",attributeName:g,oldValue:c[g]}))}function K(a,b,c,d){function f(b,c,f,k,y){var g=b.length-1;y=-~((g-y)/2);for(var h,l,e;e=b.pop();)h=f[e.j],l=k[e.l],d.c&&y&&Math.abs(e.j-e.l)>=g&&(a.push(p({type:"childList",target:c,addedNodes:[h],removedNodes:[h],nextSibling:h.nextSibling,previousSibling:h.previousSibling})),y--),d.b&&l.b&&A(a,h,l.b,d.f),d.a&&3===h.nodeType&&h.nodeValue!==l.a&&a.push(p({type:"characterData",target:h,oldValue:l.a})),d.g&&n(h,l)}function n(b,c){for(var g=b.childNodes,
+q=c.c,x=g.length,v=q?q.length:0,h,l,e,m,t,z=0,u=0,r=0;u
+
+This work is free. You can redistribute it and/or modify it under the
+terms of the Do What The Fuck You Want To Public License, Version 2,
+as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
+
+This program is free software. It comes without any warranty, to
+the extent permitted by applicable law. You can redistribute it
+and/or modify it under the terms of the Do What The Fuck You Want
+To Public License, Version 2, as published by Sam Hocevar. See
+http://www.wtfpl.net/ for more details.
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/package.json b/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/package.json
new file mode 100644
index 0000000..f99c9bc
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "mutationobserver-shim",
+ "short name": "mutationobserver",
+ "description": "MutationObserver shim for ES3 environments",
+ "version": "0.3.2",
+ "keywords": [
+ "DOM",
+ "observer",
+ "mutation observer",
+ "MutationObserver"
+ ],
+ "authors": [
+ {
+ "name": "Graeme Yeates",
+ "email": "github.com/megawac"
+ }
+ ],
+ "repository": {
+ "type": "git",
+ "git": "git@github.com:megawac/MutationObserver.js.git",
+ "url": "github.com/megawac/MutationObserver.js"
+ },
+ "main": "dist/mutationobserver.min.js",
+ "scripts": {
+ "test": "grunt test --verbose"
+ },
+ "files": [
+ "MutationObserver.js",
+ "dist/mutationobserver.min.js"
+ ],
+ "license": {
+ "type": "WTFPL",
+ "version": "v2 2004",
+ "url": "http://www.wtfpl.net/"
+ },
+ "devDependencies": {
+ "grunt": "~0.4.2",
+ "grunt-bumpup": "~0.5.0",
+ "grunt-closurecompiler": "0.9",
+ "grunt-contrib-connect": "0.7",
+ "grunt-contrib-jshint": ">= 0.8",
+ "grunt-contrib-qunit": "~0.5.0",
+ "grunt-file-info": "~1.0.14",
+ "grunt-saucelabs": "~4.1.2",
+ "grunt-tagrelease": "~0.3.1",
+ "matchdep": "~0.3.0",
+ "phantomjs": "1.9.x"
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/COPYING b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/COPYING
new file mode 100644
index 0000000..65ee1c1
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/COPYING
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/README.md b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/README.md
new file mode 100644
index 0000000..5eac04f
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/README.md
@@ -0,0 +1,59 @@
+# What is this? #
+
+Mutation Summary is a JavaScript library that makes observing changes to the DOM fast, easy and safe.
+
+It's built on top of (and requires) a new browser API called [DOM Mutation Observers](http://dom.spec.whatwg.org/#mutation-observers).
+
+ * [Browsers which currently implement DOM Mutation Observers](DOMMutationObservers.md#browser-availability).
+ * [DOM Mutation Observers API and its relationship to this library and (the deprecated) DOM Mutation Events](DOMMutationObservers.md).
+
+
+
+# Why do I need it? #
+
+Mutation Summary does five main things for you:
+
+ * **It tells you how the document is different now from how it was.** As its name suggests, it summarizes what’s happened. It’s as if it takes a picture of the document when you first create it, and then again after each time it calls you back. When things have changed, it calls you with a concise description of exactly what’s different now from the last picture it took for you.
+ * **It handles any and all changes, no matter how complex.** All kinds of things can happen to the DOM: values can change and but put back to what they were, large parts can be pulled out, changed, rearranged, put back. Mutation Summary can take any crazy thing you throw at it. Go ahead, tear the document to shreds, Mutation Summary won’t even blink.
+ * **It lets you express what kinds of things you’re interested in.** It presents a query API that lets you tell it exactly what kinds of changes you’re interested in. This includes support for simple CSS-like selector descriptions of elements you care about.
+ * **It’s fast.** The time and memory it takes is dependant on number of changes that occurred (which typically involves only a few nodes) -- not the size of your document (which is commonly thousands of nodes).
+ * **It can automatically ignore changes you make during your callback.** Mutation Summary is going to call you back when changes have occurred. If you need to react to those changes by making more changes -- won’t you hear about those changes the next time it calls you back? Not unless you [ask for that](APIReference.md#configuration-options). By default, it stops watching the document immediately before it calls you back and resumes watching as soon as your callback finishes.
+
+# What is it useful for? #
+
+Lots of things, here are some examples:
+
+ * **Browser extensions.** Want to make a browser extension that creates a link to your mapping application whenever an address appears in a page? You’ll need to know when those addresses appear (and disappear).
+ * **Implement missing HTML capabilities.** Think building web apps is too darn hard and you know what’s missing from HTML that would make it a snap? Writing the code for the desired behavior is only half the battle--you’ll also need to know when those elements and attributes show up and what happens to them. In fact, there’s already two widely used classes of libraries which do exactly this, but don’t currently have a good way to observe changes to the DOM.
+ * **UI Widget** libraries, e.g. Dojo Widgets
+ * **Templating** and/or **Databinding** libraries, e.g. Angular or KnockoutJS
+ * **Text Editors.** HTML Text editors often want to observe what’s being input and “fix it up” so that they can maintain a consistent WYSWIG UI.
+
+# What is this _not_ useful for? #
+
+The intent here isn't to be all things to all use-cases. Mutation Summary is not meant to:
+
+ * **Use the DOM as some sort of state-transition machine.** It won't report transient states that the DOM moved through. It will only tell you what the difference is between the previous state and the present one.
+ * **Observing complex selectors.** It offers support for a simple [subset of CSS selectors](APIReference.md#supported-selector-syntax). Want to observe all elements that match `“div[foo] span.bar > p:first-child”`? Unfortunately, efficiently computing that is much harder and currently outside the scope of this library.
+
+Note that both of the above use cases are possible given the data that the underlying Mutation Observers API provides -- we simply judged them to be outside the "80% use case" that we targeted with this particular library.
+
+# Where can Mutation Summary be used? #
+
+The Mutation Summary library depends on the presence of the Mutation Observer DOM API. Mutation Observers are available in
+
+ * [Google Chrome](https://www.google.com/chrome)
+ * [Firefox](http://www.mozilla.org/en-US/firefox/new/)
+ * [Safari](http://www.apple.com/safari/)
+ * [Opera](http://www.opera.com/)
+ * [IE11](http://www.microsoft.com/ie)
+
+Mutation Observers is the work of the [W3C WebApps working group](http://www.w3.org/2008/webapps/). In the future it will be implemented in other browsers (we’ll keep the above list of supporting browsers as up-to-date as possible).
+
+# Great. I want to get started. What’s next? #
+
+ * Check out the [tutorial](Tutorial.md) and the [API reference](APIReference.md).
+
+# Google groups discussion list #
+
+ * [mutation-summary-discuss@googlegroups.com](https://groups.google.com/group/mutation-summary-discuss)
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/package.json b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/package.json
new file mode 100644
index 0000000..3029ee6
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "mutation-summary",
+ "version": "0.0.0",
+ "description": "Makes observing the DOM fast and easy",
+ "main": "src/mutation-summary.js",
+ "directories": {
+ "example": "examples",
+ "test": "tests"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://code.google.com/p/mutation-summary/"
+ },
+ "author": "",
+ "license": "Apache 2.0",
+ "devDependencies": {
+ "chai": "*",
+ "mocha": "*"
+ }
+}
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/src/mutation-summary.js b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/src/mutation-summary.js
new file mode 100644
index 0000000..feef885
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/src/mutation-summary.js
@@ -0,0 +1,1406 @@
+// Copyright 2011 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+var __extends = (this && this.__extends) || (function () {
+ var extendStatics = function (d, b) {
+ extendStatics = Object.setPrototypeOf ||
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
+ return extendStatics(d, b);
+ };
+ return function (d, b) {
+ if (typeof b !== "function" && b !== null)
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
+ extendStatics(d, b);
+ function __() { this.constructor = d; }
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+ };
+})();
+var MutationObserverCtor;
+if (typeof WebKitMutationObserver !== 'undefined')
+ MutationObserverCtor = WebKitMutationObserver;
+else
+ MutationObserverCtor = MutationObserver;
+if (MutationObserverCtor === undefined) {
+ console.error('DOM Mutation Observers are required.');
+ console.error('https://developer.mozilla.org/en-US/docs/DOM/MutationObserver');
+ throw Error('DOM Mutation Observers are required');
+}
+var NodeMap = /** @class */ (function () {
+ function NodeMap() {
+ this.nodes = [];
+ this.values = [];
+ }
+ NodeMap.prototype.isIndex = function (s) {
+ return +s === s >>> 0;
+ };
+ NodeMap.prototype.nodeId = function (node) {
+ var id = node[NodeMap.ID_PROP];
+ if (!id)
+ id = node[NodeMap.ID_PROP] = NodeMap.nextId_++;
+ return id;
+ };
+ NodeMap.prototype.set = function (node, value) {
+ var id = this.nodeId(node);
+ this.nodes[id] = node;
+ this.values[id] = value;
+ };
+ NodeMap.prototype.get = function (node) {
+ var id = this.nodeId(node);
+ return this.values[id];
+ };
+ NodeMap.prototype.has = function (node) {
+ return this.nodeId(node) in this.nodes;
+ };
+ NodeMap.prototype["delete"] = function (node) {
+ var id = this.nodeId(node);
+ delete this.nodes[id];
+ this.values[id] = undefined;
+ };
+ NodeMap.prototype.keys = function () {
+ var nodes = [];
+ for (var id in this.nodes) {
+ if (!this.isIndex(id))
+ continue;
+ nodes.push(this.nodes[id]);
+ }
+ return nodes;
+ };
+ NodeMap.ID_PROP = '__mutation_summary_node_map_id__';
+ NodeMap.nextId_ = 1;
+ return NodeMap;
+}());
+/**
+ * var reachableMatchableProduct = [
+ * // STAYED_OUT, ENTERED, STAYED_IN, EXITED
+ * [ STAYED_OUT, STAYED_OUT, STAYED_OUT, STAYED_OUT ], // STAYED_OUT
+ * [ STAYED_OUT, ENTERED, ENTERED, STAYED_OUT ], // ENTERED
+ * [ STAYED_OUT, ENTERED, STAYED_IN, EXITED ], // STAYED_IN
+ * [ STAYED_OUT, STAYED_OUT, EXITED, EXITED ] // EXITED
+ * ];
+ */
+var Movement;
+(function (Movement) {
+ Movement[Movement["STAYED_OUT"] = 0] = "STAYED_OUT";
+ Movement[Movement["ENTERED"] = 1] = "ENTERED";
+ Movement[Movement["STAYED_IN"] = 2] = "STAYED_IN";
+ Movement[Movement["REPARENTED"] = 3] = "REPARENTED";
+ Movement[Movement["REORDERED"] = 4] = "REORDERED";
+ Movement[Movement["EXITED"] = 5] = "EXITED";
+})(Movement || (Movement = {}));
+function enteredOrExited(changeType) {
+ return changeType === Movement.ENTERED || changeType === Movement.EXITED;
+}
+var NodeChange = /** @class */ (function () {
+ function NodeChange(node, childList, attributes, characterData, oldParentNode, added, attributeOldValues, characterDataOldValue) {
+ if (childList === void 0) { childList = false; }
+ if (attributes === void 0) { attributes = false; }
+ if (characterData === void 0) { characterData = false; }
+ if (oldParentNode === void 0) { oldParentNode = null; }
+ if (added === void 0) { added = false; }
+ if (attributeOldValues === void 0) { attributeOldValues = null; }
+ if (characterDataOldValue === void 0) { characterDataOldValue = null; }
+ this.node = node;
+ this.childList = childList;
+ this.attributes = attributes;
+ this.characterData = characterData;
+ this.oldParentNode = oldParentNode;
+ this.added = added;
+ this.attributeOldValues = attributeOldValues;
+ this.characterDataOldValue = characterDataOldValue;
+ this.isCaseInsensitive =
+ this.node.nodeType === Node.ELEMENT_NODE &&
+ this.node instanceof HTMLElement &&
+ this.node.ownerDocument instanceof HTMLDocument;
+ }
+ NodeChange.prototype.getAttributeOldValue = function (name) {
+ if (!this.attributeOldValues)
+ return undefined;
+ if (this.isCaseInsensitive)
+ name = name.toLowerCase();
+ return this.attributeOldValues[name];
+ };
+ NodeChange.prototype.getAttributeNamesMutated = function () {
+ var names = [];
+ if (!this.attributeOldValues)
+ return names;
+ for (var name in this.attributeOldValues) {
+ names.push(name);
+ }
+ return names;
+ };
+ NodeChange.prototype.attributeMutated = function (name, oldValue) {
+ this.attributes = true;
+ this.attributeOldValues = this.attributeOldValues || {};
+ if (name in this.attributeOldValues)
+ return;
+ this.attributeOldValues[name] = oldValue;
+ };
+ NodeChange.prototype.characterDataMutated = function (oldValue) {
+ if (this.characterData)
+ return;
+ this.characterData = true;
+ this.characterDataOldValue = oldValue;
+ };
+ // Note: is it possible to receive a removal followed by a removal. This
+ // can occur if the removed node is added to an non-observed node, that
+ // node is added to the observed area, and then the node removed from
+ // it.
+ NodeChange.prototype.removedFromParent = function (parent) {
+ this.childList = true;
+ if (this.added || this.oldParentNode)
+ this.added = false;
+ else
+ this.oldParentNode = parent;
+ };
+ NodeChange.prototype.insertedIntoParent = function () {
+ this.childList = true;
+ this.added = true;
+ };
+ // An node's oldParent is
+ // -its present parent, if its parentNode was not changed.
+ // -null if the first thing that happened to it was an add.
+ // -the node it was removed from if the first thing that happened to it
+ // was a remove.
+ NodeChange.prototype.getOldParent = function () {
+ if (this.childList) {
+ if (this.oldParentNode)
+ return this.oldParentNode;
+ if (this.added)
+ return null;
+ }
+ return this.node.parentNode;
+ };
+ return NodeChange;
+}());
+var ChildListChange = /** @class */ (function () {
+ function ChildListChange() {
+ this.added = new NodeMap();
+ this.removed = new NodeMap();
+ this.maybeMoved = new NodeMap();
+ this.oldPrevious = new NodeMap();
+ this.moved = undefined;
+ }
+ return ChildListChange;
+}());
+var TreeChanges = /** @class */ (function (_super) {
+ __extends(TreeChanges, _super);
+ function TreeChanges(rootNode, mutations) {
+ var _this = _super.call(this) || this;
+ _this.rootNode = rootNode;
+ _this.reachableCache = undefined;
+ _this.wasReachableCache = undefined;
+ _this.anyParentsChanged = false;
+ _this.anyAttributesChanged = false;
+ _this.anyCharacterDataChanged = false;
+ for (var m = 0; m < mutations.length; m++) {
+ var mutation = mutations[m];
+ switch (mutation.type) {
+ case 'childList':
+ _this.anyParentsChanged = true;
+ for (var i = 0; i < mutation.removedNodes.length; i++) {
+ var node = mutation.removedNodes[i];
+ _this.getChange(node).removedFromParent(mutation.target);
+ }
+ for (var i = 0; i < mutation.addedNodes.length; i++) {
+ var node = mutation.addedNodes[i];
+ _this.getChange(node).insertedIntoParent();
+ }
+ break;
+ case 'attributes':
+ _this.anyAttributesChanged = true;
+ var change = _this.getChange(mutation.target);
+ change.attributeMutated(mutation.attributeName, mutation.oldValue);
+ break;
+ case 'characterData':
+ _this.anyCharacterDataChanged = true;
+ var change = _this.getChange(mutation.target);
+ change.characterDataMutated(mutation.oldValue);
+ break;
+ }
+ }
+ return _this;
+ }
+ TreeChanges.prototype.getChange = function (node) {
+ var change = this.get(node);
+ if (!change) {
+ change = new NodeChange(node);
+ this.set(node, change);
+ }
+ return change;
+ };
+ TreeChanges.prototype.getOldParent = function (node) {
+ var change = this.get(node);
+ return change ? change.getOldParent() : node.parentNode;
+ };
+ TreeChanges.prototype.getIsReachable = function (node) {
+ if (node === this.rootNode)
+ return true;
+ if (!node)
+ return false;
+ this.reachableCache = this.reachableCache || new NodeMap();
+ var isReachable = this.reachableCache.get(node);
+ if (isReachable === undefined) {
+ isReachable = this.getIsReachable(node.parentNode);
+ this.reachableCache.set(node, isReachable);
+ }
+ return isReachable;
+ };
+ // A node wasReachable if its oldParent wasReachable.
+ TreeChanges.prototype.getWasReachable = function (node) {
+ if (node === this.rootNode)
+ return true;
+ if (!node)
+ return false;
+ this.wasReachableCache = this.wasReachableCache || new NodeMap();
+ var wasReachable = this.wasReachableCache.get(node);
+ if (wasReachable === undefined) {
+ wasReachable = this.getWasReachable(this.getOldParent(node));
+ this.wasReachableCache.set(node, wasReachable);
+ }
+ return wasReachable;
+ };
+ TreeChanges.prototype.reachabilityChange = function (node) {
+ if (this.getIsReachable(node)) {
+ return this.getWasReachable(node) ?
+ Movement.STAYED_IN : Movement.ENTERED;
+ }
+ return this.getWasReachable(node) ?
+ Movement.EXITED : Movement.STAYED_OUT;
+ };
+ return TreeChanges;
+}(NodeMap));
+var MutationProjection = /** @class */ (function () {
+ // TOOD(any)
+ function MutationProjection(rootNode, mutations, selectors, calcReordered, calcOldPreviousSibling) {
+ this.rootNode = rootNode;
+ this.mutations = mutations;
+ this.selectors = selectors;
+ this.calcReordered = calcReordered;
+ this.calcOldPreviousSibling = calcOldPreviousSibling;
+ this.treeChanges = new TreeChanges(rootNode, mutations);
+ this.entered = [];
+ this.exited = [];
+ this.stayedIn = new NodeMap();
+ this.visited = new NodeMap();
+ this.childListChangeMap = undefined;
+ this.characterDataOnly = undefined;
+ this.matchCache = undefined;
+ this.processMutations();
+ }
+ MutationProjection.prototype.processMutations = function () {
+ if (!this.treeChanges.anyParentsChanged &&
+ !this.treeChanges.anyAttributesChanged)
+ return;
+ var changedNodes = this.treeChanges.keys();
+ for (var i = 0; i < changedNodes.length; i++) {
+ this.visitNode(changedNodes[i], undefined);
+ }
+ };
+ MutationProjection.prototype.visitNode = function (node, parentReachable) {
+ if (this.visited.has(node))
+ return;
+ this.visited.set(node, true);
+ var change = this.treeChanges.get(node);
+ var reachable = parentReachable;
+ // node inherits its parent's reachability change unless
+ // its parentNode was mutated.
+ if ((change && change.childList) || reachable == undefined)
+ reachable = this.treeChanges.reachabilityChange(node);
+ if (reachable === Movement.STAYED_OUT)
+ return;
+ // Cache match results for sub-patterns.
+ this.matchabilityChange(node);
+ if (reachable === Movement.ENTERED) {
+ this.entered.push(node);
+ }
+ else if (reachable === Movement.EXITED) {
+ this.exited.push(node);
+ this.ensureHasOldPreviousSiblingIfNeeded(node);
+ }
+ else if (reachable === Movement.STAYED_IN) {
+ var movement = Movement.STAYED_IN;
+ if (change && change.childList) {
+ if (change.oldParentNode !== node.parentNode) {
+ movement = Movement.REPARENTED;
+ this.ensureHasOldPreviousSiblingIfNeeded(node);
+ }
+ else if (this.calcReordered && this.wasReordered(node)) {
+ movement = Movement.REORDERED;
+ }
+ }
+ this.stayedIn.set(node, movement);
+ }
+ if (reachable === Movement.STAYED_IN)
+ return;
+ // reachable === ENTERED || reachable === EXITED.
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ this.visitNode(child, reachable);
+ }
+ };
+ MutationProjection.prototype.ensureHasOldPreviousSiblingIfNeeded = function (node) {
+ if (!this.calcOldPreviousSibling)
+ return;
+ this.processChildlistChanges();
+ var parentNode = node.parentNode;
+ var nodeChange = this.treeChanges.get(node);
+ if (nodeChange && nodeChange.oldParentNode)
+ parentNode = nodeChange.oldParentNode;
+ var change = this.childListChangeMap.get(parentNode);
+ if (!change) {
+ change = new ChildListChange();
+ this.childListChangeMap.set(parentNode, change);
+ }
+ if (!change.oldPrevious.has(node)) {
+ change.oldPrevious.set(node, node.previousSibling);
+ }
+ };
+ MutationProjection.prototype.getChanged = function (summary, selectors, characterDataOnly) {
+ this.selectors = selectors;
+ this.characterDataOnly = characterDataOnly;
+ for (var i = 0; i < this.entered.length; i++) {
+ var node = this.entered[i];
+ var matchable = this.matchabilityChange(node);
+ if (matchable === Movement.ENTERED || matchable === Movement.STAYED_IN)
+ summary.added.push(node);
+ }
+ var stayedInNodes = this.stayedIn.keys();
+ for (var i = 0; i < stayedInNodes.length; i++) {
+ var node = stayedInNodes[i];
+ var matchable = this.matchabilityChange(node);
+ if (matchable === Movement.ENTERED) {
+ summary.added.push(node);
+ }
+ else if (matchable === Movement.EXITED) {
+ summary.removed.push(node);
+ }
+ else if (matchable === Movement.STAYED_IN && (summary.reparented || summary.reordered)) {
+ var movement = this.stayedIn.get(node);
+ if (summary.reparented && movement === Movement.REPARENTED)
+ summary.reparented.push(node);
+ else if (summary.reordered && movement === Movement.REORDERED)
+ summary.reordered.push(node);
+ }
+ }
+ for (var i = 0; i < this.exited.length; i++) {
+ var node = this.exited[i];
+ var matchable = this.matchabilityChange(node);
+ if (matchable === Movement.EXITED || matchable === Movement.STAYED_IN)
+ summary.removed.push(node);
+ }
+ };
+ MutationProjection.prototype.getOldParentNode = function (node) {
+ var change = this.treeChanges.get(node);
+ if (change && change.childList)
+ return change.oldParentNode ? change.oldParentNode : null;
+ var reachabilityChange = this.treeChanges.reachabilityChange(node);
+ if (reachabilityChange === Movement.STAYED_OUT || reachabilityChange === Movement.ENTERED)
+ throw Error('getOldParentNode requested on invalid node.');
+ return node.parentNode;
+ };
+ MutationProjection.prototype.getOldPreviousSibling = function (node) {
+ var parentNode = node.parentNode;
+ var nodeChange = this.treeChanges.get(node);
+ if (nodeChange && nodeChange.oldParentNode)
+ parentNode = nodeChange.oldParentNode;
+ var change = this.childListChangeMap.get(parentNode);
+ if (!change)
+ throw Error('getOldPreviousSibling requested on invalid node.');
+ return change.oldPrevious.get(node);
+ };
+ MutationProjection.prototype.getOldAttribute = function (element, attrName) {
+ var change = this.treeChanges.get(element);
+ if (!change || !change.attributes)
+ throw Error('getOldAttribute requested on invalid node.');
+ var value = change.getAttributeOldValue(attrName);
+ if (value === undefined)
+ throw Error('getOldAttribute requested for unchanged attribute name.');
+ return value;
+ };
+ MutationProjection.prototype.attributeChangedNodes = function (includeAttributes) {
+ if (!this.treeChanges.anyAttributesChanged)
+ return {}; // No attributes mutations occurred.
+ var attributeFilter;
+ var caseInsensitiveFilter;
+ if (includeAttributes) {
+ attributeFilter = {};
+ caseInsensitiveFilter = {};
+ for (var i = 0; i < includeAttributes.length; i++) {
+ var attrName = includeAttributes[i];
+ attributeFilter[attrName] = true;
+ caseInsensitiveFilter[attrName.toLowerCase()] = attrName;
+ }
+ }
+ var result = {};
+ var nodes = this.treeChanges.keys();
+ for (var i = 0; i < nodes.length; i++) {
+ var node = nodes[i];
+ var change = this.treeChanges.get(node);
+ if (!change.attributes)
+ continue;
+ if (Movement.STAYED_IN !== this.treeChanges.reachabilityChange(node) ||
+ Movement.STAYED_IN !== this.matchabilityChange(node)) {
+ continue;
+ }
+ var element = node;
+ var changedAttrNames = change.getAttributeNamesMutated();
+ for (var j = 0; j < changedAttrNames.length; j++) {
+ var attrName = changedAttrNames[j];
+ if (attributeFilter &&
+ !attributeFilter[attrName] &&
+ !(change.isCaseInsensitive && caseInsensitiveFilter[attrName])) {
+ continue;
+ }
+ var oldValue = change.getAttributeOldValue(attrName);
+ if (oldValue === element.getAttribute(attrName))
+ continue;
+ if (caseInsensitiveFilter && change.isCaseInsensitive)
+ attrName = caseInsensitiveFilter[attrName];
+ result[attrName] = result[attrName] || [];
+ result[attrName].push(element);
+ }
+ }
+ return result;
+ };
+ MutationProjection.prototype.getOldCharacterData = function (node) {
+ var change = this.treeChanges.get(node);
+ if (!change || !change.characterData)
+ throw Error('getOldCharacterData requested on invalid node.');
+ return change.characterDataOldValue;
+ };
+ MutationProjection.prototype.getCharacterDataChanged = function () {
+ if (!this.treeChanges.anyCharacterDataChanged)
+ return []; // No characterData mutations occurred.
+ var nodes = this.treeChanges.keys();
+ var result = [];
+ for (var i = 0; i < nodes.length; i++) {
+ var target = nodes[i];
+ if (Movement.STAYED_IN !== this.treeChanges.reachabilityChange(target))
+ continue;
+ var change = this.treeChanges.get(target);
+ if (!change.characterData ||
+ target.textContent == change.characterDataOldValue)
+ continue;
+ result.push(target);
+ }
+ return result;
+ };
+ MutationProjection.prototype.computeMatchabilityChange = function (selector, el) {
+ if (!this.matchCache)
+ this.matchCache = [];
+ if (!this.matchCache[selector.uid])
+ this.matchCache[selector.uid] = new NodeMap();
+ var cache = this.matchCache[selector.uid];
+ var result = cache.get(el);
+ if (result === undefined) {
+ result = selector.matchabilityChange(el, this.treeChanges.get(el));
+ cache.set(el, result);
+ }
+ return result;
+ };
+ MutationProjection.prototype.matchabilityChange = function (node) {
+ var _this = this;
+ // TODO(rafaelw): Include PI, CDATA?
+ // Only include text nodes.
+ if (this.characterDataOnly) {
+ switch (node.nodeType) {
+ case Node.COMMENT_NODE:
+ case Node.TEXT_NODE:
+ return Movement.STAYED_IN;
+ default:
+ return Movement.STAYED_OUT;
+ }
+ }
+ // No element filter. Include all nodes.
+ if (!this.selectors)
+ return Movement.STAYED_IN;
+ // Element filter. Exclude non-elements.
+ if (node.nodeType !== Node.ELEMENT_NODE)
+ return Movement.STAYED_OUT;
+ var el = node;
+ var matchChanges = this.selectors.map(function (selector) {
+ return _this.computeMatchabilityChange(selector, el);
+ });
+ var accum = Movement.STAYED_OUT;
+ var i = 0;
+ while (accum !== Movement.STAYED_IN && i < matchChanges.length) {
+ switch (matchChanges[i]) {
+ case Movement.STAYED_IN:
+ accum = Movement.STAYED_IN;
+ break;
+ case Movement.ENTERED:
+ if (accum === Movement.EXITED)
+ accum = Movement.STAYED_IN;
+ else
+ accum = Movement.ENTERED;
+ break;
+ case Movement.EXITED:
+ if (accum === Movement.ENTERED)
+ accum = Movement.STAYED_IN;
+ else
+ accum = Movement.EXITED;
+ break;
+ }
+ i++;
+ }
+ return accum;
+ };
+ MutationProjection.prototype.getChildlistChange = function (el) {
+ var change = this.childListChangeMap.get(el);
+ if (!change) {
+ change = new ChildListChange();
+ this.childListChangeMap.set(el, change);
+ }
+ return change;
+ };
+ MutationProjection.prototype.processChildlistChanges = function () {
+ if (this.childListChangeMap)
+ return;
+ this.childListChangeMap = new NodeMap();
+ for (var i = 0; i < this.mutations.length; i++) {
+ var mutation = this.mutations[i];
+ if (mutation.type != 'childList')
+ continue;
+ if (this.treeChanges.reachabilityChange(mutation.target) !== Movement.STAYED_IN &&
+ !this.calcOldPreviousSibling)
+ continue;
+ var change = this.getChildlistChange(mutation.target);
+ var oldPrevious = mutation.previousSibling;
+ function recordOldPrevious(node, previous) {
+ if (!node ||
+ change.oldPrevious.has(node) ||
+ change.added.has(node) ||
+ change.maybeMoved.has(node))
+ return;
+ if (previous &&
+ (change.added.has(previous) ||
+ change.maybeMoved.has(previous)))
+ return;
+ change.oldPrevious.set(node, previous);
+ }
+ for (var j = 0; j < mutation.removedNodes.length; j++) {
+ var node = mutation.removedNodes[j];
+ recordOldPrevious(node, oldPrevious);
+ if (change.added.has(node)) {
+ change.added["delete"](node);
+ }
+ else {
+ change.removed.set(node, true);
+ change.maybeMoved["delete"](node);
+ }
+ oldPrevious = node;
+ }
+ recordOldPrevious(mutation.nextSibling, oldPrevious);
+ for (var j = 0; j < mutation.addedNodes.length; j++) {
+ var node = mutation.addedNodes[j];
+ if (change.removed.has(node)) {
+ change.removed["delete"](node);
+ change.maybeMoved.set(node, true);
+ }
+ else {
+ change.added.set(node, true);
+ }
+ }
+ }
+ };
+ MutationProjection.prototype.wasReordered = function (node) {
+ if (!this.treeChanges.anyParentsChanged)
+ return false;
+ this.processChildlistChanges();
+ var parentNode = node.parentNode;
+ var nodeChange = this.treeChanges.get(node);
+ if (nodeChange && nodeChange.oldParentNode)
+ parentNode = nodeChange.oldParentNode;
+ var change = this.childListChangeMap.get(parentNode);
+ if (!change)
+ return false;
+ if (change.moved)
+ return change.moved.get(node);
+ change.moved = new NodeMap();
+ var pendingMoveDecision = new NodeMap();
+ function isMoved(node) {
+ if (!node)
+ return false;
+ if (!change.maybeMoved.has(node))
+ return false;
+ var didMove = change.moved.get(node);
+ if (didMove !== undefined)
+ return didMove;
+ if (pendingMoveDecision.has(node)) {
+ didMove = true;
+ }
+ else {
+ pendingMoveDecision.set(node, true);
+ didMove = getPrevious(node) !== getOldPrevious(node);
+ }
+ if (pendingMoveDecision.has(node)) {
+ pendingMoveDecision["delete"](node);
+ change.moved.set(node, didMove);
+ }
+ else {
+ didMove = change.moved.get(node);
+ }
+ return didMove;
+ }
+ var oldPreviousCache = new NodeMap();
+ function getOldPrevious(node) {
+ var oldPrevious = oldPreviousCache.get(node);
+ if (oldPrevious !== undefined)
+ return oldPrevious;
+ oldPrevious = change.oldPrevious.get(node);
+ while (oldPrevious &&
+ (change.removed.has(oldPrevious) || isMoved(oldPrevious))) {
+ oldPrevious = getOldPrevious(oldPrevious);
+ }
+ if (oldPrevious === undefined)
+ oldPrevious = node.previousSibling;
+ oldPreviousCache.set(node, oldPrevious);
+ return oldPrevious;
+ }
+ var previousCache = new NodeMap();
+ function getPrevious(node) {
+ if (previousCache.has(node))
+ return previousCache.get(node);
+ var previous = node.previousSibling;
+ while (previous && (change.added.has(previous) || isMoved(previous)))
+ previous = previous.previousSibling;
+ previousCache.set(node, previous);
+ return previous;
+ }
+ change.maybeMoved.keys().forEach(isMoved);
+ return change.moved.get(node);
+ };
+ return MutationProjection;
+}());
+var Summary = /** @class */ (function () {
+ function Summary(projection, query) {
+ var _this = this;
+ this.projection = projection;
+ this.added = [];
+ this.removed = [];
+ this.reparented = query.all || query.element || query.characterData ? [] : undefined;
+ this.reordered = query.all ? [] : undefined;
+ projection.getChanged(this, query.elementFilter, query.characterData);
+ if (query.all || query.attribute || query.attributeList) {
+ var filter = query.attribute ? [query.attribute] : query.attributeList;
+ var attributeChanged = projection.attributeChangedNodes(filter);
+ if (query.attribute) {
+ this.valueChanged = attributeChanged[query.attribute] || [];
+ }
+ else {
+ this.attributeChanged = attributeChanged;
+ if (query.attributeList) {
+ query.attributeList.forEach(function (attrName) {
+ if (!_this.attributeChanged.hasOwnProperty(attrName))
+ _this.attributeChanged[attrName] = [];
+ });
+ }
+ }
+ }
+ if (query.all || query.characterData) {
+ var characterDataChanged = projection.getCharacterDataChanged();
+ if (query.characterData)
+ this.valueChanged = characterDataChanged;
+ else
+ this.characterDataChanged = characterDataChanged;
+ }
+ if (this.reordered)
+ this.getOldPreviousSibling = projection.getOldPreviousSibling.bind(projection);
+ }
+ Summary.prototype.getOldParentNode = function (node) {
+ return this.projection.getOldParentNode(node);
+ };
+ Summary.prototype.getOldAttribute = function (node, name) {
+ return this.projection.getOldAttribute(node, name);
+ };
+ Summary.prototype.getOldCharacterData = function (node) {
+ return this.projection.getOldCharacterData(node);
+ };
+ Summary.prototype.getOldPreviousSibling = function (node) {
+ return this.projection.getOldPreviousSibling(node);
+ };
+ return Summary;
+}());
+// TODO(rafaelw): Allow ':' and '.' as valid name characters.
+var validNameInitialChar = /[a-zA-Z_]+/;
+var validNameNonInitialChar = /[a-zA-Z0-9_\-]+/;
+// TODO(rafaelw): Consider allowing backslash in the attrValue.
+// TODO(rafaelw): There's got a to be way to represent this state machine
+// more compactly???
+function escapeQuotes(value) {
+ return '"' + value.replace(/"/, '\\\"') + '"';
+}
+var Qualifier = /** @class */ (function () {
+ function Qualifier() {
+ }
+ Qualifier.prototype.matches = function (oldValue) {
+ if (oldValue === null)
+ return false;
+ if (this.attrValue === undefined)
+ return true;
+ if (!this.contains)
+ return this.attrValue == oldValue;
+ var tokens = oldValue.split(' ');
+ for (var i = 0; i < tokens.length; i++) {
+ if (this.attrValue === tokens[i])
+ return true;
+ }
+ return false;
+ };
+ Qualifier.prototype.toString = function () {
+ if (this.attrName === 'class' && this.contains)
+ return '.' + this.attrValue;
+ if (this.attrName === 'id' && !this.contains)
+ return '#' + this.attrValue;
+ if (this.contains)
+ return '[' + this.attrName + '~=' + escapeQuotes(this.attrValue) + ']';
+ if ('attrValue' in this)
+ return '[' + this.attrName + '=' + escapeQuotes(this.attrValue) + ']';
+ return '[' + this.attrName + ']';
+ };
+ return Qualifier;
+}());
+var Selector = /** @class */ (function () {
+ function Selector() {
+ this.uid = Selector.nextUid++;
+ this.qualifiers = [];
+ }
+ Object.defineProperty(Selector.prototype, "caseInsensitiveTagName", {
+ get: function () {
+ return this.tagName.toUpperCase();
+ },
+ enumerable: false,
+ configurable: true
+ });
+ Object.defineProperty(Selector.prototype, "selectorString", {
+ get: function () {
+ return this.tagName + this.qualifiers.join('');
+ },
+ enumerable: false,
+ configurable: true
+ });
+ Selector.prototype.isMatching = function (el) {
+ return el[Selector.matchesSelector](this.selectorString);
+ };
+ Selector.prototype.wasMatching = function (el, change, isMatching) {
+ if (!change || !change.attributes)
+ return isMatching;
+ var tagName = change.isCaseInsensitive ? this.caseInsensitiveTagName : this.tagName;
+ if (tagName !== '*' && tagName !== el.tagName)
+ return false;
+ var attributeOldValues = [];
+ var anyChanged = false;
+ for (var i = 0; i < this.qualifiers.length; i++) {
+ var qualifier = this.qualifiers[i];
+ var oldValue = change.getAttributeOldValue(qualifier.attrName);
+ attributeOldValues.push(oldValue);
+ anyChanged = anyChanged || (oldValue !== undefined);
+ }
+ if (!anyChanged)
+ return isMatching;
+ for (var i = 0; i < this.qualifiers.length; i++) {
+ var qualifier = this.qualifiers[i];
+ var oldValue = attributeOldValues[i];
+ if (oldValue === undefined)
+ oldValue = el.getAttribute(qualifier.attrName);
+ if (!qualifier.matches(oldValue))
+ return false;
+ }
+ return true;
+ };
+ Selector.prototype.matchabilityChange = function (el, change) {
+ var isMatching = this.isMatching(el);
+ if (isMatching)
+ return this.wasMatching(el, change, isMatching) ? Movement.STAYED_IN : Movement.ENTERED;
+ else
+ return this.wasMatching(el, change, isMatching) ? Movement.EXITED : Movement.STAYED_OUT;
+ };
+ Selector.parseSelectors = function (input) {
+ var selectors = [];
+ var currentSelector;
+ var currentQualifier;
+ function newSelector() {
+ if (currentSelector) {
+ if (currentQualifier) {
+ currentSelector.qualifiers.push(currentQualifier);
+ currentQualifier = undefined;
+ }
+ selectors.push(currentSelector);
+ }
+ currentSelector = new Selector();
+ }
+ function newQualifier() {
+ if (currentQualifier)
+ currentSelector.qualifiers.push(currentQualifier);
+ currentQualifier = new Qualifier();
+ }
+ var WHITESPACE = /\s/;
+ var valueQuoteChar;
+ var SYNTAX_ERROR = 'Invalid or unsupported selector syntax.';
+ var SELECTOR = 1;
+ var TAG_NAME = 2;
+ var QUALIFIER = 3;
+ var QUALIFIER_NAME_FIRST_CHAR = 4;
+ var QUALIFIER_NAME = 5;
+ var ATTR_NAME_FIRST_CHAR = 6;
+ var ATTR_NAME = 7;
+ var EQUIV_OR_ATTR_QUAL_END = 8;
+ var EQUAL = 9;
+ var ATTR_QUAL_END = 10;
+ var VALUE_FIRST_CHAR = 11;
+ var VALUE = 12;
+ var QUOTED_VALUE = 13;
+ var SELECTOR_SEPARATOR = 14;
+ var state = SELECTOR;
+ var i = 0;
+ while (i < input.length) {
+ var c = input[i++];
+ switch (state) {
+ case SELECTOR:
+ if (c.match(validNameInitialChar)) {
+ newSelector();
+ currentSelector.tagName = c;
+ state = TAG_NAME;
+ break;
+ }
+ if (c == '*') {
+ newSelector();
+ currentSelector.tagName = '*';
+ state = QUALIFIER;
+ break;
+ }
+ if (c == '.') {
+ newSelector();
+ newQualifier();
+ currentSelector.tagName = '*';
+ currentQualifier.attrName = 'class';
+ currentQualifier.contains = true;
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '#') {
+ newSelector();
+ newQualifier();
+ currentSelector.tagName = '*';
+ currentQualifier.attrName = 'id';
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '[') {
+ newSelector();
+ newQualifier();
+ currentSelector.tagName = '*';
+ currentQualifier.attrName = '';
+ state = ATTR_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c.match(WHITESPACE))
+ break;
+ throw Error(SYNTAX_ERROR);
+ case TAG_NAME:
+ if (c.match(validNameNonInitialChar)) {
+ currentSelector.tagName += c;
+ break;
+ }
+ if (c == '.') {
+ newQualifier();
+ currentQualifier.attrName = 'class';
+ currentQualifier.contains = true;
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '#') {
+ newQualifier();
+ currentQualifier.attrName = 'id';
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '[') {
+ newQualifier();
+ currentQualifier.attrName = '';
+ state = ATTR_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c.match(WHITESPACE)) {
+ state = SELECTOR_SEPARATOR;
+ break;
+ }
+ if (c == ',') {
+ state = SELECTOR;
+ break;
+ }
+ throw Error(SYNTAX_ERROR);
+ case QUALIFIER:
+ if (c == '.') {
+ newQualifier();
+ currentQualifier.attrName = 'class';
+ currentQualifier.contains = true;
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '#') {
+ newQualifier();
+ currentQualifier.attrName = 'id';
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '[') {
+ newQualifier();
+ currentQualifier.attrName = '';
+ state = ATTR_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c.match(WHITESPACE)) {
+ state = SELECTOR_SEPARATOR;
+ break;
+ }
+ if (c == ',') {
+ state = SELECTOR;
+ break;
+ }
+ throw Error(SYNTAX_ERROR);
+ case QUALIFIER_NAME_FIRST_CHAR:
+ if (c.match(validNameInitialChar)) {
+ currentQualifier.attrValue = c;
+ state = QUALIFIER_NAME;
+ break;
+ }
+ throw Error(SYNTAX_ERROR);
+ case QUALIFIER_NAME:
+ if (c.match(validNameNonInitialChar)) {
+ currentQualifier.attrValue += c;
+ break;
+ }
+ if (c == '.') {
+ newQualifier();
+ currentQualifier.attrName = 'class';
+ currentQualifier.contains = true;
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '#') {
+ newQualifier();
+ currentQualifier.attrName = 'id';
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '[') {
+ newQualifier();
+ state = ATTR_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c.match(WHITESPACE)) {
+ state = SELECTOR_SEPARATOR;
+ break;
+ }
+ if (c == ',') {
+ state = SELECTOR;
+ break;
+ }
+ throw Error(SYNTAX_ERROR);
+ case ATTR_NAME_FIRST_CHAR:
+ if (c.match(validNameInitialChar)) {
+ currentQualifier.attrName = c;
+ state = ATTR_NAME;
+ break;
+ }
+ if (c.match(WHITESPACE))
+ break;
+ throw Error(SYNTAX_ERROR);
+ case ATTR_NAME:
+ if (c.match(validNameNonInitialChar)) {
+ currentQualifier.attrName += c;
+ break;
+ }
+ if (c.match(WHITESPACE)) {
+ state = EQUIV_OR_ATTR_QUAL_END;
+ break;
+ }
+ if (c == '~') {
+ currentQualifier.contains = true;
+ state = EQUAL;
+ break;
+ }
+ if (c == '=') {
+ currentQualifier.attrValue = '';
+ state = VALUE_FIRST_CHAR;
+ break;
+ }
+ if (c == ']') {
+ state = QUALIFIER;
+ break;
+ }
+ throw Error(SYNTAX_ERROR);
+ case EQUIV_OR_ATTR_QUAL_END:
+ if (c == '~') {
+ currentQualifier.contains = true;
+ state = EQUAL;
+ break;
+ }
+ if (c == '=') {
+ currentQualifier.attrValue = '';
+ state = VALUE_FIRST_CHAR;
+ break;
+ }
+ if (c == ']') {
+ state = QUALIFIER;
+ break;
+ }
+ if (c.match(WHITESPACE))
+ break;
+ throw Error(SYNTAX_ERROR);
+ case EQUAL:
+ if (c == '=') {
+ currentQualifier.attrValue = '';
+ state = VALUE_FIRST_CHAR;
+ break;
+ }
+ throw Error(SYNTAX_ERROR);
+ case ATTR_QUAL_END:
+ if (c == ']') {
+ state = QUALIFIER;
+ break;
+ }
+ if (c.match(WHITESPACE))
+ break;
+ throw Error(SYNTAX_ERROR);
+ case VALUE_FIRST_CHAR:
+ if (c.match(WHITESPACE))
+ break;
+ if (c == '"' || c == "'") {
+ valueQuoteChar = c;
+ state = QUOTED_VALUE;
+ break;
+ }
+ currentQualifier.attrValue += c;
+ state = VALUE;
+ break;
+ case VALUE:
+ if (c.match(WHITESPACE)) {
+ state = ATTR_QUAL_END;
+ break;
+ }
+ if (c == ']') {
+ state = QUALIFIER;
+ break;
+ }
+ if (c == "'" || c == '"')
+ throw Error(SYNTAX_ERROR);
+ currentQualifier.attrValue += c;
+ break;
+ case QUOTED_VALUE:
+ if (c == valueQuoteChar) {
+ state = ATTR_QUAL_END;
+ break;
+ }
+ currentQualifier.attrValue += c;
+ break;
+ case SELECTOR_SEPARATOR:
+ if (c.match(WHITESPACE))
+ break;
+ if (c == ',') {
+ state = SELECTOR;
+ break;
+ }
+ throw Error(SYNTAX_ERROR);
+ }
+ }
+ switch (state) {
+ case SELECTOR:
+ case TAG_NAME:
+ case QUALIFIER:
+ case QUALIFIER_NAME:
+ case SELECTOR_SEPARATOR:
+ // Valid end states.
+ newSelector();
+ break;
+ default:
+ throw Error(SYNTAX_ERROR);
+ }
+ if (!selectors.length)
+ throw Error(SYNTAX_ERROR);
+ return selectors;
+ };
+ Selector.nextUid = 1;
+ Selector.matchesSelector = (function () {
+ var element = document.createElement('div');
+ if (typeof element['webkitMatchesSelector'] === 'function')
+ return 'webkitMatchesSelector';
+ if (typeof element['mozMatchesSelector'] === 'function')
+ return 'mozMatchesSelector';
+ if (typeof element['msMatchesSelector'] === 'function')
+ return 'msMatchesSelector';
+ return 'matchesSelector';
+ })();
+ return Selector;
+}());
+var attributeFilterPattern = /^([a-zA-Z:_]+[a-zA-Z0-9_\-:\.]*)$/;
+function validateAttribute(attribute) {
+ if (typeof attribute != 'string')
+ throw Error('Invalid request opion. attribute must be a non-zero length string.');
+ attribute = attribute.trim();
+ if (!attribute)
+ throw Error('Invalid request opion. attribute must be a non-zero length string.');
+ if (!attribute.match(attributeFilterPattern))
+ throw Error('Invalid request option. invalid attribute name: ' + attribute);
+ return attribute;
+}
+function validateElementAttributes(attribs) {
+ if (!attribs.trim().length)
+ throw Error('Invalid request option: elementAttributes must contain at least one attribute.');
+ var lowerAttributes = {};
+ var attributes = {};
+ var tokens = attribs.split(/\s+/);
+ for (var i = 0; i < tokens.length; i++) {
+ var name = tokens[i];
+ if (!name)
+ continue;
+ var name = validateAttribute(name);
+ var nameLower = name.toLowerCase();
+ if (lowerAttributes[nameLower])
+ throw Error('Invalid request option: observing multiple case variations of the same attribute is not supported.');
+ attributes[name] = true;
+ lowerAttributes[nameLower] = true;
+ }
+ return Object.keys(attributes);
+}
+function elementFilterAttributes(selectors) {
+ var attributes = {};
+ selectors.forEach(function (selector) {
+ selector.qualifiers.forEach(function (qualifier) {
+ attributes[qualifier.attrName] = true;
+ });
+ });
+ return Object.keys(attributes);
+}
+var MutationSummary = /** @class */ (function () {
+ function MutationSummary(opts) {
+ var _this = this;
+ this.connected = false;
+ this.options = MutationSummary.validateOptions(opts);
+ this.observerOptions = MutationSummary.createObserverOptions(this.options.queries);
+ this.root = this.options.rootNode;
+ this.callback = this.options.callback;
+ this.elementFilter = Array.prototype.concat.apply([], this.options.queries.map(function (query) {
+ return query.elementFilter ? query.elementFilter : [];
+ }));
+ if (!this.elementFilter.length)
+ this.elementFilter = undefined;
+ this.calcReordered = this.options.queries.some(function (query) {
+ return query.all;
+ });
+ this.queryValidators = []; // TODO(rafaelw): Shouldn't always define this.
+ if (MutationSummary.createQueryValidator) {
+ this.queryValidators = this.options.queries.map(function (query) {
+ return MutationSummary.createQueryValidator(_this.root, query);
+ });
+ }
+ this.observer = new MutationObserverCtor(function (mutations) {
+ _this.observerCallback(mutations);
+ });
+ this.reconnect();
+ }
+ MutationSummary.createObserverOptions = function (queries) {
+ var observerOptions = {
+ childList: true,
+ subtree: true
+ };
+ var attributeFilter;
+ function observeAttributes(attributes) {
+ if (observerOptions.attributes && !attributeFilter)
+ return; // already observing all.
+ observerOptions.attributes = true;
+ observerOptions.attributeOldValue = true;
+ if (!attributes) {
+ // observe all.
+ attributeFilter = undefined;
+ return;
+ }
+ // add to observed.
+ attributeFilter = attributeFilter || {};
+ attributes.forEach(function (attribute) {
+ attributeFilter[attribute] = true;
+ attributeFilter[attribute.toLowerCase()] = true;
+ });
+ }
+ queries.forEach(function (query) {
+ if (query.characterData) {
+ observerOptions.characterData = true;
+ observerOptions.characterDataOldValue = true;
+ return;
+ }
+ if (query.all) {
+ observeAttributes();
+ observerOptions.characterData = true;
+ observerOptions.characterDataOldValue = true;
+ return;
+ }
+ if (query.attribute) {
+ observeAttributes([query.attribute.trim()]);
+ return;
+ }
+ var attributes = elementFilterAttributes(query.elementFilter).concat(query.attributeList || []);
+ if (attributes.length)
+ observeAttributes(attributes);
+ });
+ if (attributeFilter)
+ observerOptions.attributeFilter = Object.keys(attributeFilter);
+ return observerOptions;
+ };
+ MutationSummary.validateOptions = function (options) {
+ for (var prop in options) {
+ if (!(prop in MutationSummary.optionKeys))
+ throw Error('Invalid option: ' + prop);
+ }
+ if (typeof options.callback !== 'function')
+ throw Error('Invalid options: callback is required and must be a function');
+ if (!options.queries || !options.queries.length)
+ throw Error('Invalid options: queries must contain at least one query request object.');
+ var opts = {
+ callback: options.callback,
+ rootNode: options.rootNode || document,
+ observeOwnChanges: !!options.observeOwnChanges,
+ oldPreviousSibling: !!options.oldPreviousSibling,
+ queries: []
+ };
+ for (var i = 0; i < options.queries.length; i++) {
+ var request = options.queries[i];
+ // all
+ if (request.all) {
+ if (Object.keys(request).length > 1)
+ throw Error('Invalid request option. all has no options.');
+ opts.queries.push({ all: true });
+ continue;
+ }
+ // attribute
+ if ('attribute' in request) {
+ var query = {
+ attribute: validateAttribute(request.attribute)
+ };
+ query.elementFilter = Selector.parseSelectors('*[' + query.attribute + ']');
+ if (Object.keys(request).length > 1)
+ throw Error('Invalid request option. attribute has no options.');
+ opts.queries.push(query);
+ continue;
+ }
+ // element
+ if ('element' in request) {
+ var requestOptionCount = Object.keys(request).length;
+ var query = {
+ element: request.element,
+ elementFilter: Selector.parseSelectors(request.element)
+ };
+ if (request.hasOwnProperty('elementAttributes')) {
+ query.attributeList = validateElementAttributes(request.elementAttributes);
+ requestOptionCount--;
+ }
+ if (requestOptionCount > 1)
+ throw Error('Invalid request option. element only allows elementAttributes option.');
+ opts.queries.push(query);
+ continue;
+ }
+ // characterData
+ if (request.characterData) {
+ if (Object.keys(request).length > 1)
+ throw Error('Invalid request option. characterData has no options.');
+ opts.queries.push({ characterData: true });
+ continue;
+ }
+ throw Error('Invalid request option. Unknown query request.');
+ }
+ return opts;
+ };
+ MutationSummary.prototype.createSummaries = function (mutations) {
+ if (!mutations || !mutations.length)
+ return [];
+ var projection = new MutationProjection(this.root, mutations, this.elementFilter, this.calcReordered, this.options.oldPreviousSibling);
+ var summaries = [];
+ for (var i = 0; i < this.options.queries.length; i++) {
+ summaries.push(new Summary(projection, this.options.queries[i]));
+ }
+ return summaries;
+ };
+ MutationSummary.prototype.checkpointQueryValidators = function () {
+ this.queryValidators.forEach(function (validator) {
+ if (validator)
+ validator.recordPreviousState();
+ });
+ };
+ MutationSummary.prototype.runQueryValidators = function (summaries) {
+ this.queryValidators.forEach(function (validator, index) {
+ if (validator)
+ validator.validate(summaries[index]);
+ });
+ };
+ MutationSummary.prototype.changesToReport = function (summaries) {
+ return summaries.some(function (summary) {
+ var summaryProps = ['added', 'removed', 'reordered', 'reparented',
+ 'valueChanged', 'characterDataChanged'];
+ if (summaryProps.some(function (prop) { return summary[prop] && summary[prop].length; }))
+ return true;
+ if (summary.attributeChanged) {
+ var attrNames = Object.keys(summary.attributeChanged);
+ var attrsChanged = attrNames.some(function (attrName) {
+ return !!summary.attributeChanged[attrName].length;
+ });
+ if (attrsChanged)
+ return true;
+ }
+ return false;
+ });
+ };
+ MutationSummary.prototype.observerCallback = function (mutations) {
+ if (!this.options.observeOwnChanges)
+ this.observer.disconnect();
+ var summaries = this.createSummaries(mutations);
+ this.runQueryValidators(summaries);
+ if (this.options.observeOwnChanges)
+ this.checkpointQueryValidators();
+ if (this.changesToReport(summaries))
+ this.callback(summaries);
+ // disconnect() may have been called during the callback.
+ if (!this.options.observeOwnChanges && this.connected) {
+ this.checkpointQueryValidators();
+ this.observer.observe(this.root, this.observerOptions);
+ }
+ };
+ MutationSummary.prototype.reconnect = function () {
+ if (this.connected)
+ throw Error('Already connected');
+ this.observer.observe(this.root, this.observerOptions);
+ this.connected = true;
+ this.checkpointQueryValidators();
+ };
+ MutationSummary.prototype.takeSummaries = function () {
+ if (!this.connected)
+ throw Error('Not connected');
+ var summaries = this.createSummaries(this.observer.takeRecords());
+ return this.changesToReport(summaries) ? summaries : undefined;
+ };
+ MutationSummary.prototype.disconnect = function () {
+ var summaries = this.takeSummaries();
+ this.observer.disconnect();
+ this.connected = false;
+ return summaries;
+ };
+ MutationSummary.NodeMap = NodeMap; // exposed for use in TreeMirror.
+ MutationSummary.parseElementFilter = Selector.parseSelectors; // exposed for testing.
+ MutationSummary.optionKeys = {
+ 'callback': true,
+ 'queries': true,
+ 'rootNode': true,
+ 'oldPreviousSibling': true,
+ 'observeOwnChanges': true
+ };
+ return MutationSummary;
+}());
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/src/mutation-summary.ts b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/src/mutation-summary.ts
new file mode 100644
index 0000000..ee3a268
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/src/mutation-summary.ts
@@ -0,0 +1,1750 @@
+// Copyright 2011 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+var MutationObserverCtor;
+if (typeof WebKitMutationObserver !== 'undefined')
+ MutationObserverCtor = WebKitMutationObserver;
+else
+ MutationObserverCtor = MutationObserver;
+
+if (MutationObserverCtor === undefined) {
+ console.error('DOM Mutation Observers are required.');
+ console.error('https://developer.mozilla.org/en-US/docs/DOM/MutationObserver');
+ throw Error('DOM Mutation Observers are required');
+}
+
+interface StringMap {
+ [key: string]: T;
+}
+
+interface NumberMap {
+ [key: number]: T;
+}
+
+class NodeMap {
+
+ private static ID_PROP = '__mutation_summary_node_map_id__';
+ private static nextId_:number = 1;
+
+ private nodes:Node[];
+ private values:T[];
+
+ constructor() {
+ this.nodes = [];
+ this.values = [];
+ }
+
+ private isIndex(s:string):boolean {
+ return +s === s >>> 0;
+ }
+
+ private nodeId(node:Node) {
+ var id = node[NodeMap.ID_PROP];
+ if (!id)
+ id = node[NodeMap.ID_PROP] = NodeMap.nextId_++;
+ return id;
+ }
+
+ set(node:Node, value:T) {
+ var id = this.nodeId(node);
+ this.nodes[id] = node;
+ this.values[id] = value;
+ }
+
+ get(node:Node):T {
+ var id = this.nodeId(node);
+ return this.values[id];
+ }
+
+ has(node:Node):boolean {
+ return this.nodeId(node) in this.nodes;
+ }
+
+ delete(node:Node) {
+ var id = this.nodeId(node);
+ delete this.nodes[id];
+ this.values[id] = undefined;
+ }
+
+ keys():Node[] {
+ var nodes:Node[] = [];
+ for (var id in this.nodes) {
+ if (!this.isIndex(id))
+ continue;
+ nodes.push(this.nodes[id]);
+ }
+
+ return nodes;
+ }
+}
+
+/**
+ * var reachableMatchableProduct = [
+ * // STAYED_OUT, ENTERED, STAYED_IN, EXITED
+ * [ STAYED_OUT, STAYED_OUT, STAYED_OUT, STAYED_OUT ], // STAYED_OUT
+ * [ STAYED_OUT, ENTERED, ENTERED, STAYED_OUT ], // ENTERED
+ * [ STAYED_OUT, ENTERED, STAYED_IN, EXITED ], // STAYED_IN
+ * [ STAYED_OUT, STAYED_OUT, EXITED, EXITED ] // EXITED
+ * ];
+ */
+
+enum Movement {
+ STAYED_OUT,
+ ENTERED,
+ STAYED_IN,
+ REPARENTED,
+ REORDERED,
+ EXITED
+}
+
+function enteredOrExited(changeType:Movement):boolean {
+ return changeType === Movement.ENTERED || changeType === Movement.EXITED;
+}
+
+class NodeChange {
+
+ public isCaseInsensitive:boolean;
+
+ constructor(public node:Node,
+ public childList:boolean = false,
+ public attributes:boolean = false,
+ public characterData:boolean = false,
+ public oldParentNode:Node = null,
+ public added:boolean = false,
+ private attributeOldValues:StringMap = null,
+ public characterDataOldValue:string = null) {
+ this.isCaseInsensitive =
+ this.node.nodeType === Node.ELEMENT_NODE &&
+ this.node instanceof HTMLElement &&
+ this.node.ownerDocument instanceof HTMLDocument;
+ }
+
+ getAttributeOldValue(name:string):string {
+ if (!this.attributeOldValues)
+ return undefined;
+ if (this.isCaseInsensitive)
+ name = name.toLowerCase();
+ return this.attributeOldValues[name];
+ }
+
+ getAttributeNamesMutated():string[] {
+ var names:string[] = [];
+ if (!this.attributeOldValues)
+ return names;
+ for (var name in this.attributeOldValues) {
+ names.push(name);
+ }
+ return names;
+ }
+
+ attributeMutated(name:string, oldValue:string) {
+ this.attributes = true;
+ this.attributeOldValues = this.attributeOldValues || {};
+
+ if (name in this.attributeOldValues)
+ return;
+
+ this.attributeOldValues[name] = oldValue;
+ }
+
+ characterDataMutated(oldValue:string) {
+ if (this.characterData)
+ return;
+ this.characterData = true;
+ this.characterDataOldValue = oldValue;
+ }
+
+ // Note: is it possible to receive a removal followed by a removal. This
+ // can occur if the removed node is added to an non-observed node, that
+ // node is added to the observed area, and then the node removed from
+ // it.
+ removedFromParent(parent:Node) {
+ this.childList = true;
+ if (this.added || this.oldParentNode)
+ this.added = false;
+ else
+ this.oldParentNode = parent;
+ }
+
+ insertedIntoParent() {
+ this.childList = true;
+ this.added = true;
+ }
+
+ // An node's oldParent is
+ // -its present parent, if its parentNode was not changed.
+ // -null if the first thing that happened to it was an add.
+ // -the node it was removed from if the first thing that happened to it
+ // was a remove.
+ getOldParent() {
+ if (this.childList) {
+ if (this.oldParentNode)
+ return this.oldParentNode;
+ if (this.added)
+ return null;
+ }
+
+ return this.node.parentNode;
+ }
+}
+
+class ChildListChange {
+
+ public added:NodeMap;
+ public removed:NodeMap;
+ public maybeMoved:NodeMap;
+ public oldPrevious:NodeMap;
+ public moved:NodeMap;
+
+ constructor() {
+ this.added = new NodeMap();
+ this.removed = new NodeMap();
+ this.maybeMoved = new NodeMap();
+ this.oldPrevious = new NodeMap();
+ this.moved = undefined;
+ }
+}
+
+class TreeChanges extends NodeMap {
+
+ public anyParentsChanged:boolean;
+ public anyAttributesChanged:boolean;
+ public anyCharacterDataChanged:boolean;
+
+ private reachableCache:NodeMap;
+ private wasReachableCache:NodeMap;
+
+ private rootNode:Node;
+
+ constructor(rootNode:Node, mutations:MutationRecord[]) {
+ super();
+
+ this.rootNode = rootNode;
+ this.reachableCache = undefined;
+ this.wasReachableCache = undefined;
+ this.anyParentsChanged = false;
+ this.anyAttributesChanged = false;
+ this.anyCharacterDataChanged = false;
+
+ for (var m = 0; m < mutations.length; m++) {
+ var mutation = mutations[m];
+ switch (mutation.type) {
+
+ case 'childList':
+ this.anyParentsChanged = true;
+ for (var i = 0; i < mutation.removedNodes.length; i++) {
+ var node = mutation.removedNodes[i];
+ this.getChange(node).removedFromParent(mutation.target);
+ }
+ for (var i = 0; i < mutation.addedNodes.length; i++) {
+ var node = mutation.addedNodes[i];
+ this.getChange(node).insertedIntoParent();
+ }
+ break;
+
+ case 'attributes':
+ this.anyAttributesChanged = true;
+ var change = this.getChange(mutation.target);
+ change.attributeMutated(mutation.attributeName, mutation.oldValue);
+ break;
+
+ case 'characterData':
+ this.anyCharacterDataChanged = true;
+ var change = this.getChange(mutation.target);
+ change.characterDataMutated(mutation.oldValue);
+ break;
+ }
+ }
+ }
+
+ getChange(node:Node):NodeChange {
+ var change = this.get(node);
+ if (!change) {
+ change = new NodeChange(node);
+ this.set(node, change);
+ }
+ return change;
+ }
+
+ getOldParent(node:Node):Node {
+ var change = this.get(node);
+ return change ? change.getOldParent() : node.parentNode;
+ }
+
+ getIsReachable(node:Node):boolean {
+ if (node === this.rootNode)
+ return true;
+ if (!node)
+ return false;
+
+ this.reachableCache = this.reachableCache || new NodeMap();
+ var isReachable = this.reachableCache.get(node);
+ if (isReachable === undefined) {
+ isReachable = this.getIsReachable(node.parentNode);
+ this.reachableCache.set(node, isReachable);
+ }
+ return isReachable;
+ }
+
+ // A node wasReachable if its oldParent wasReachable.
+ getWasReachable(node:Node):boolean {
+ if (node === this.rootNode)
+ return true;
+ if (!node)
+ return false;
+
+ this.wasReachableCache = this.wasReachableCache || new NodeMap();
+ var wasReachable:boolean = this.wasReachableCache.get(node);
+ if (wasReachable === undefined) {
+ wasReachable = this.getWasReachable(this.getOldParent(node));
+ this.wasReachableCache.set(node, wasReachable);
+ }
+ return wasReachable;
+ }
+
+ reachabilityChange(node:Node):Movement {
+ if (this.getIsReachable(node)) {
+ return this.getWasReachable(node) ?
+ Movement.STAYED_IN : Movement.ENTERED;
+ }
+
+ return this.getWasReachable(node) ?
+ Movement.EXITED : Movement.STAYED_OUT;
+ }
+}
+
+class MutationProjection {
+
+ private treeChanges:TreeChanges;
+ private entered:Node[];
+ private exited:Node[];
+ private stayedIn:NodeMap;
+ private visited:NodeMap;
+ private childListChangeMap:NodeMap;
+ private characterDataOnly:boolean;
+ private matchCache:NumberMap>;
+
+ // TOOD(any)
+ constructor(public rootNode:Node,
+ public mutations:MutationRecord[],
+ public selectors:Selector[],
+ public calcReordered:boolean,
+ public calcOldPreviousSibling:boolean) {
+
+ this.treeChanges = new TreeChanges(rootNode, mutations);
+ this.entered = [];
+ this.exited = [];
+ this.stayedIn = new NodeMap();
+ this.visited = new NodeMap();
+ this.childListChangeMap = undefined;
+ this.characterDataOnly = undefined;
+ this.matchCache = undefined;
+
+ this.processMutations();
+ }
+
+ processMutations() {
+ if (!this.treeChanges.anyParentsChanged &&
+ !this.treeChanges.anyAttributesChanged)
+ return;
+
+ var changedNodes:Node[] = this.treeChanges.keys();
+ for (var i = 0; i < changedNodes.length; i++) {
+ this.visitNode(changedNodes[i], undefined);
+ }
+ }
+
+ visitNode(node:Node, parentReachable:Movement) {
+ if (this.visited.has(node))
+ return;
+
+ this.visited.set(node, true);
+
+ var change = this.treeChanges.get(node);
+ var reachable = parentReachable;
+
+ // node inherits its parent's reachability change unless
+ // its parentNode was mutated.
+ if ((change && change.childList) || reachable == undefined)
+ reachable = this.treeChanges.reachabilityChange(node);
+
+ if (reachable === Movement.STAYED_OUT)
+ return;
+
+ // Cache match results for sub-patterns.
+ this.matchabilityChange(node);
+
+ if (reachable === Movement.ENTERED) {
+ this.entered.push(node);
+ } else if (reachable === Movement.EXITED) {
+ this.exited.push(node);
+ this.ensureHasOldPreviousSiblingIfNeeded(node);
+
+ } else if (reachable === Movement.STAYED_IN) {
+ var movement = Movement.STAYED_IN;
+
+ if (change && change.childList) {
+ if (change.oldParentNode !== node.parentNode) {
+ movement = Movement.REPARENTED;
+ this.ensureHasOldPreviousSiblingIfNeeded(node);
+ } else if (this.calcReordered && this.wasReordered(node)) {
+ movement = Movement.REORDERED;
+ }
+ }
+
+ this.stayedIn.set(node, movement);
+ }
+
+ if (reachable === Movement.STAYED_IN)
+ return;
+
+ // reachable === ENTERED || reachable === EXITED.
+ for (var child = node.firstChild; child; child = child.nextSibling) {
+ this.visitNode(child, reachable);
+ }
+ }
+
+ ensureHasOldPreviousSiblingIfNeeded(node:Node) {
+ if (!this.calcOldPreviousSibling)
+ return;
+
+ this.processChildlistChanges();
+
+ var parentNode = node.parentNode;
+ var nodeChange = this.treeChanges.get(node);
+ if (nodeChange && nodeChange.oldParentNode)
+ parentNode = nodeChange.oldParentNode;
+
+ var change = this.childListChangeMap.get(parentNode);
+ if (!change) {
+ change = new ChildListChange();
+ this.childListChangeMap.set(parentNode, change);
+ }
+
+ if (!change.oldPrevious.has(node)) {
+ change.oldPrevious.set(node, node.previousSibling);
+ }
+ }
+
+ getChanged(summary:Summary, selectors:Selector[], characterDataOnly:boolean) {
+ this.selectors = selectors;
+ this.characterDataOnly = characterDataOnly;
+
+ for (var i = 0; i < this.entered.length; i++) {
+ var node = this.entered[i];
+ var matchable = this.matchabilityChange(node);
+ if (matchable === Movement.ENTERED || matchable === Movement.STAYED_IN)
+ summary.added.push(node);
+ }
+
+ var stayedInNodes = this.stayedIn.keys();
+ for (var i = 0; i < stayedInNodes.length; i++) {
+ var node = stayedInNodes[i];
+ var matchable = this.matchabilityChange(node);
+
+ if (matchable === Movement.ENTERED) {
+ summary.added.push(node);
+ } else if (matchable === Movement.EXITED) {
+ summary.removed.push(node);
+ } else if (matchable === Movement.STAYED_IN && (summary.reparented || summary.reordered)) {
+ var movement:Movement = this.stayedIn.get(node);
+ if (summary.reparented && movement === Movement.REPARENTED)
+ summary.reparented.push(node);
+ else if (summary.reordered && movement === Movement.REORDERED)
+ summary.reordered.push(node);
+ }
+ }
+
+ for (var i = 0; i < this.exited.length; i++) {
+ var node = this.exited[i];
+ var matchable = this.matchabilityChange(node);
+ if (matchable === Movement.EXITED || matchable === Movement.STAYED_IN)
+ summary.removed.push(node);
+ }
+ }
+
+ getOldParentNode(node:Node):Node {
+ var change = this.treeChanges.get(node);
+ if (change && change.childList)
+ return change.oldParentNode ? change.oldParentNode : null;
+
+ var reachabilityChange = this.treeChanges.reachabilityChange(node);
+ if (reachabilityChange === Movement.STAYED_OUT || reachabilityChange === Movement.ENTERED)
+ throw Error('getOldParentNode requested on invalid node.');
+
+ return node.parentNode;
+ }
+
+ getOldPreviousSibling(node:Node):Node {
+ var parentNode = node.parentNode;
+ var nodeChange = this.treeChanges.get(node);
+ if (nodeChange && nodeChange.oldParentNode)
+ parentNode = nodeChange.oldParentNode;
+
+ var change = this.childListChangeMap.get(parentNode);
+ if (!change)
+ throw Error('getOldPreviousSibling requested on invalid node.');
+
+ return change.oldPrevious.get(node);
+ }
+
+ getOldAttribute(element:Node, attrName:string):string {
+ var change = this.treeChanges.get(element);
+ if (!change || !change.attributes)
+ throw Error('getOldAttribute requested on invalid node.');
+
+ var value = change.getAttributeOldValue(attrName);
+ if (value === undefined)
+ throw Error('getOldAttribute requested for unchanged attribute name.');
+
+ return value;
+ }
+
+ attributeChangedNodes(includeAttributes:string[]):StringMap {
+ if (!this.treeChanges.anyAttributesChanged)
+ return {}; // No attributes mutations occurred.
+
+ var attributeFilter:StringMap;
+ var caseInsensitiveFilter:StringMap;
+ if (includeAttributes) {
+ attributeFilter = {};
+ caseInsensitiveFilter = {};
+ for (var i = 0; i < includeAttributes.length; i++) {
+ var attrName:string = includeAttributes[i];
+ attributeFilter[attrName] = true;
+ caseInsensitiveFilter[attrName.toLowerCase()] = attrName;
+ }
+ }
+
+ var result:StringMap = {};
+ var nodes = this.treeChanges.keys();
+
+ for (var i = 0; i < nodes.length; i++) {
+ var node = nodes[i];
+
+ var change = this.treeChanges.get(node);
+ if (!change.attributes)
+ continue;
+
+ if (Movement.STAYED_IN !== this.treeChanges.reachabilityChange(node) ||
+ Movement.STAYED_IN !== this.matchabilityChange(node)) {
+ continue;
+ }
+
+ var element = node;
+ var changedAttrNames = change.getAttributeNamesMutated();
+ for (var j = 0; j < changedAttrNames.length; j++) {
+ var attrName = changedAttrNames[j];
+
+ if (attributeFilter &&
+ !attributeFilter[attrName] &&
+ !(change.isCaseInsensitive && caseInsensitiveFilter[attrName])) {
+ continue;
+ }
+
+ var oldValue = change.getAttributeOldValue(attrName);
+ if (oldValue === element.getAttribute(attrName))
+ continue;
+
+ if (caseInsensitiveFilter && change.isCaseInsensitive)
+ attrName = caseInsensitiveFilter[attrName];
+
+ result[attrName] = result[attrName] || [];
+ result[attrName].push(element);
+ }
+ }
+
+ return result;
+ }
+
+ getOldCharacterData(node:Node):string {
+ var change = this.treeChanges.get(node);
+ if (!change || !change.characterData)
+ throw Error('getOldCharacterData requested on invalid node.');
+
+ return change.characterDataOldValue;
+ }
+
+ getCharacterDataChanged():Node[] {
+ if (!this.treeChanges.anyCharacterDataChanged)
+ return []; // No characterData mutations occurred.
+
+ var nodes = this.treeChanges.keys();
+ var result:Node[] = [];
+ for (var i = 0; i < nodes.length; i++) {
+ var target = nodes[i];
+ if (Movement.STAYED_IN !== this.treeChanges.reachabilityChange(target))
+ continue;
+
+ var change = this.treeChanges.get(target);
+ if (!change.characterData ||
+ target.textContent == change.characterDataOldValue)
+ continue
+
+ result.push(target);
+ }
+
+ return result;
+ }
+
+ computeMatchabilityChange(selector:Selector, el:Element):Movement {
+ if (!this.matchCache)
+ this.matchCache = [];
+ if (!this.matchCache[selector.uid])
+ this.matchCache[selector.uid] = new NodeMap();
+
+ var cache = this.matchCache[selector.uid];
+ var result = cache.get(el);
+ if (result === undefined) {
+ result = selector.matchabilityChange(el, this.treeChanges.get(el));
+ cache.set(el, result);
+ }
+ return result;
+ }
+
+ matchabilityChange(node:Node) {
+ // TODO(rafaelw): Include PI, CDATA?
+ // Only include text nodes.
+ if (this.characterDataOnly) {
+ switch (node.nodeType) {
+ case Node.COMMENT_NODE:
+ case Node.TEXT_NODE:
+ return Movement.STAYED_IN;
+ default:
+ return Movement.STAYED_OUT;
+ }
+ }
+
+ // No element filter. Include all nodes.
+ if (!this.selectors)
+ return Movement.STAYED_IN;
+
+ // Element filter. Exclude non-elements.
+ if (node.nodeType !== Node.ELEMENT_NODE)
+ return Movement.STAYED_OUT;
+
+ var el = node;
+
+ var matchChanges = this.selectors.map((selector:Selector) => {
+ return this.computeMatchabilityChange(selector, el);
+ });
+
+ var accum:Movement = Movement.STAYED_OUT;
+ var i = 0;
+
+ while (accum !== Movement.STAYED_IN && i < matchChanges.length) {
+ switch(matchChanges[i]) {
+ case Movement.STAYED_IN:
+ accum = Movement.STAYED_IN;
+ break;
+ case Movement.ENTERED:
+ if (accum === Movement.EXITED)
+ accum = Movement.STAYED_IN;
+ else
+ accum = Movement.ENTERED;
+ break;
+ case Movement.EXITED:
+ if (accum === Movement.ENTERED)
+ accum = Movement.STAYED_IN;
+ else
+ accum = Movement.EXITED;
+ break;
+ }
+
+ i++;
+ }
+
+ return accum;
+ }
+
+ getChildlistChange(el:Element):ChildListChange {
+ var change = this.childListChangeMap.get(el);
+ if (!change) {
+ change = new ChildListChange();
+ this.childListChangeMap.set(el, change);
+ }
+
+ return change;
+ }
+
+ processChildlistChanges() {
+ if (this.childListChangeMap)
+ return;
+
+ this.childListChangeMap = new NodeMap();
+
+ for (var i = 0; i < this.mutations.length; i++) {
+ var mutation = this.mutations[i];
+ if (mutation.type != 'childList')
+ continue;
+
+ if (this.treeChanges.reachabilityChange(mutation.target) !== Movement.STAYED_IN &&
+ !this.calcOldPreviousSibling)
+ continue;
+
+ var change = this.getChildlistChange(mutation.target);
+
+ var oldPrevious = mutation.previousSibling;
+
+ function recordOldPrevious(node:Node, previous:Node) {
+ if (!node ||
+ change.oldPrevious.has(node) ||
+ change.added.has(node) ||
+ change.maybeMoved.has(node))
+ return;
+
+ if (previous &&
+ (change.added.has(previous) ||
+ change.maybeMoved.has(previous)))
+ return;
+
+ change.oldPrevious.set(node, previous);
+ }
+
+ for (var j = 0; j < mutation.removedNodes.length; j++) {
+ var node = mutation.removedNodes[j];
+ recordOldPrevious(node, oldPrevious);
+
+ if (change.added.has(node)) {
+ change.added.delete(node);
+ } else {
+ change.removed.set(node, true);
+ change.maybeMoved.delete(node);
+ }
+
+ oldPrevious = node;
+ }
+
+ recordOldPrevious(mutation.nextSibling, oldPrevious);
+
+ for (var j = 0; j < mutation.addedNodes.length; j++) {
+ var node = mutation.addedNodes[j];
+ if (change.removed.has(node)) {
+ change.removed.delete(node);
+ change.maybeMoved.set(node, true);
+ } else {
+ change.added.set(node, true);
+ }
+ }
+ }
+ }
+
+ wasReordered(node:Node) {
+ if (!this.treeChanges.anyParentsChanged)
+ return false;
+
+ this.processChildlistChanges();
+
+ var parentNode = node.parentNode;
+ var nodeChange = this.treeChanges.get(node);
+ if (nodeChange && nodeChange.oldParentNode)
+ parentNode = nodeChange.oldParentNode;
+
+ var change = this.childListChangeMap.get(parentNode);
+ if (!change)
+ return false;
+
+ if (change.moved)
+ return change.moved.get(node);
+
+ change.moved = new NodeMap();
+ var pendingMoveDecision = new NodeMap();
+
+ function isMoved(node:Node) {
+ if (!node)
+ return false;
+ if (!change.maybeMoved.has(node))
+ return false;
+
+ var didMove = change.moved.get(node);
+ if (didMove !== undefined)
+ return didMove;
+
+ if (pendingMoveDecision.has(node)) {
+ didMove = true;
+ } else {
+ pendingMoveDecision.set(node, true);
+ didMove = getPrevious(node) !== getOldPrevious(node);
+ }
+
+ if (pendingMoveDecision.has(node)) {
+ pendingMoveDecision.delete(node);
+ change.moved.set(node, didMove);
+ } else {
+ didMove = change.moved.get(node);
+ }
+
+ return didMove;
+ }
+
+ var oldPreviousCache = new NodeMap();
+ function getOldPrevious(node:Node):Node {
+ var oldPrevious = oldPreviousCache.get(node);
+ if (oldPrevious !== undefined)
+ return oldPrevious;
+
+ oldPrevious = change.oldPrevious.get(node);
+ while (oldPrevious &&
+ (change.removed.has(oldPrevious) || isMoved(oldPrevious))) {
+ oldPrevious = getOldPrevious(oldPrevious);
+ }
+
+ if (oldPrevious === undefined)
+ oldPrevious = node.previousSibling;
+ oldPreviousCache.set(node, oldPrevious);
+
+ return oldPrevious;
+ }
+
+ var previousCache = new NodeMap();
+ function getPrevious(node:Node):Node {
+ if (previousCache.has(node))
+ return previousCache.get(node);
+
+ var previous = node.previousSibling;
+ while (previous && (change.added.has(previous) || isMoved(previous)))
+ previous = previous.previousSibling;
+
+ previousCache.set(node, previous);
+ return previous;
+ }
+
+ change.maybeMoved.keys().forEach(isMoved);
+ return change.moved.get(node);
+ }
+}
+
+class Summary {
+ public added:Node[];
+ public removed:Node[];
+ public reparented:Node[];
+ public reordered:Node[];
+ public valueChanged:Node[];
+ public attributeChanged:StringMap;
+ public characterDataChanged:Node[];
+
+ constructor(private projection:MutationProjection, query:Query) {
+ this.added = [];
+ this.removed = [];
+ this.reparented = query.all || query.element || query.characterData ? [] : undefined;
+ this.reordered = query.all ? [] : undefined;
+
+ projection.getChanged(this, query.elementFilter, query.characterData);
+
+ if (query.all || query.attribute || query.attributeList) {
+ var filter = query.attribute ? [ query.attribute ] : query.attributeList;
+ var attributeChanged = projection.attributeChangedNodes(filter);
+
+ if (query.attribute) {
+ this.valueChanged = attributeChanged[query.attribute] || [];
+ } else {
+ this.attributeChanged = attributeChanged;
+ if (query.attributeList) {
+ query.attributeList.forEach((attrName) => {
+ if (!this.attributeChanged.hasOwnProperty(attrName))
+ this.attributeChanged[attrName] = [];
+ });
+ }
+ }
+ }
+
+ if (query.all || query.characterData) {
+ var characterDataChanged = projection.getCharacterDataChanged()
+
+ if (query.characterData)
+ this.valueChanged = characterDataChanged;
+ else
+ this.characterDataChanged = characterDataChanged;
+ }
+
+ if (this.reordered)
+ this.getOldPreviousSibling = projection.getOldPreviousSibling.bind(projection);
+ }
+
+ getOldParentNode(node:Node):Node {
+ return this.projection.getOldParentNode(node);
+ }
+
+ getOldAttribute(node:Node, name: string):string {
+ return this.projection.getOldAttribute(node, name);
+ }
+
+ getOldCharacterData(node:Node):string {
+ return this.projection.getOldCharacterData(node);
+ }
+
+ getOldPreviousSibling(node:Node):Node {
+ return this.projection.getOldPreviousSibling(node);
+ }
+}
+
+// TODO(rafaelw): Allow ':' and '.' as valid name characters.
+var validNameInitialChar = /[a-zA-Z_]+/;
+var validNameNonInitialChar = /[a-zA-Z0-9_\-]+/;
+
+// TODO(rafaelw): Consider allowing backslash in the attrValue.
+// TODO(rafaelw): There's got a to be way to represent this state machine
+// more compactly???
+
+function escapeQuotes(value:string):string {
+ return '"' + value.replace(/"/, '\\\"') + '"';
+}
+
+class Qualifier {
+ public attrName:string;
+ public attrValue:string;
+ public contains:boolean;
+
+ constructor() {}
+
+ public matches(oldValue:string):boolean {
+ if (oldValue === null)
+ return false;
+
+ if (this.attrValue === undefined)
+ return true;
+
+ if (!this.contains)
+ return this.attrValue == oldValue;
+
+ var tokens = oldValue.split(' ');
+ for (var i = 0; i < tokens.length; i++) {
+ if (this.attrValue === tokens[i])
+ return true;
+ }
+
+ return false;
+ }
+
+ public toString():string {
+ if (this.attrName === 'class' && this.contains)
+ return '.' + this.attrValue;
+
+ if (this.attrName === 'id' && !this.contains)
+ return '#' + this.attrValue;
+
+ if (this.contains)
+ return '[' + this.attrName + '~=' + escapeQuotes(this.attrValue) + ']';
+
+ if ('attrValue' in this)
+ return '[' + this.attrName + '=' + escapeQuotes(this.attrValue) + ']';
+
+ return '[' + this.attrName + ']';
+ }
+}
+
+class Selector {
+ private static nextUid:number = 1;
+ private static matchesSelector:string = (function(){
+ var element = document.createElement('div');
+ if (typeof element['webkitMatchesSelector'] === 'function')
+ return 'webkitMatchesSelector';
+ if (typeof element['mozMatchesSelector'] === 'function')
+ return 'mozMatchesSelector';
+ if (typeof element['msMatchesSelector'] === 'function')
+ return 'msMatchesSelector';
+
+ return 'matchesSelector';
+ })();
+
+ public tagName:string;
+ public qualifiers:Qualifier[];
+ public uid:number;
+
+ private get caseInsensitiveTagName():string {
+ return this.tagName.toUpperCase();
+ }
+
+ get selectorString() {
+ return this.tagName + this.qualifiers.join('');
+ }
+
+ constructor() {
+ this.uid = Selector.nextUid++;
+ this.qualifiers = [];
+ }
+
+ private isMatching(el:Element):boolean {
+ return el[Selector.matchesSelector](this.selectorString);
+ }
+
+ private wasMatching(el:Element, change:NodeChange, isMatching:boolean):boolean {
+ if (!change || !change.attributes)
+ return isMatching;
+
+ var tagName = change.isCaseInsensitive ? this.caseInsensitiveTagName : this.tagName;
+ if (tagName !== '*' && tagName !== el.tagName)
+ return false;
+
+ var attributeOldValues:string[] = [];
+ var anyChanged = false;
+ for (var i = 0; i < this.qualifiers.length; i++) {
+ var qualifier = this.qualifiers[i];
+ var oldValue = change.getAttributeOldValue(qualifier.attrName);
+ attributeOldValues.push(oldValue);
+ anyChanged = anyChanged || (oldValue !== undefined);
+ }
+
+ if (!anyChanged)
+ return isMatching;
+
+ for (var i = 0; i < this.qualifiers.length; i++) {
+ var qualifier = this.qualifiers[i];
+ var oldValue = attributeOldValues[i];
+ if (oldValue === undefined)
+ oldValue = el.getAttribute(qualifier.attrName);
+ if (!qualifier.matches(oldValue))
+ return false;
+ }
+
+ return true;
+ }
+
+ public matchabilityChange(el:Element, change:NodeChange):Movement {
+ var isMatching = this.isMatching(el);
+ if (isMatching)
+ return this.wasMatching(el, change, isMatching) ? Movement.STAYED_IN : Movement.ENTERED;
+ else
+ return this.wasMatching(el, change, isMatching) ? Movement.EXITED : Movement.STAYED_OUT;
+ }
+
+ public static parseSelectors(input:string):Selector[] {
+ var selectors:Selector[] = [];
+ var currentSelector:Selector;
+ var currentQualifier:Qualifier;
+
+ function newSelector() {
+ if (currentSelector) {
+ if (currentQualifier) {
+ currentSelector.qualifiers.push(currentQualifier);
+ currentQualifier = undefined;
+ }
+
+ selectors.push(currentSelector);
+ }
+ currentSelector = new Selector();
+ }
+
+ function newQualifier() {
+ if (currentQualifier)
+ currentSelector.qualifiers.push(currentQualifier);
+
+ currentQualifier = new Qualifier();
+ }
+
+ var WHITESPACE = /\s/;
+ var valueQuoteChar:string;
+ var SYNTAX_ERROR = 'Invalid or unsupported selector syntax.';
+
+ var SELECTOR = 1;
+ var TAG_NAME = 2;
+ var QUALIFIER = 3;
+ var QUALIFIER_NAME_FIRST_CHAR = 4;
+ var QUALIFIER_NAME = 5;
+ var ATTR_NAME_FIRST_CHAR = 6;
+ var ATTR_NAME = 7;
+ var EQUIV_OR_ATTR_QUAL_END = 8;
+ var EQUAL = 9;
+ var ATTR_QUAL_END = 10;
+ var VALUE_FIRST_CHAR = 11;
+ var VALUE = 12;
+ var QUOTED_VALUE = 13;
+ var SELECTOR_SEPARATOR = 14;
+
+ var state = SELECTOR;
+ var i = 0;
+ while (i < input.length) {
+ var c = input[i++];
+
+ switch (state) {
+ case SELECTOR:
+ if (c.match(validNameInitialChar)) {
+ newSelector();
+ currentSelector.tagName = c;
+ state = TAG_NAME;
+ break;
+ }
+
+ if (c == '*') {
+ newSelector();
+ currentSelector.tagName = '*';
+ state = QUALIFIER;
+ break;
+ }
+
+ if (c == '.') {
+ newSelector();
+ newQualifier();
+ currentSelector.tagName = '*';
+ currentQualifier.attrName = 'class';
+ currentQualifier.contains = true;
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '#') {
+ newSelector();
+ newQualifier();
+ currentSelector.tagName = '*';
+ currentQualifier.attrName = 'id';
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '[') {
+ newSelector();
+ newQualifier();
+ currentSelector.tagName = '*';
+ currentQualifier.attrName = '';
+ state = ATTR_NAME_FIRST_CHAR;
+ break;
+ }
+
+ if (c.match(WHITESPACE))
+ break;
+
+ throw Error(SYNTAX_ERROR);
+
+ case TAG_NAME:
+ if (c.match(validNameNonInitialChar)) {
+ currentSelector.tagName += c;
+ break;
+ }
+
+ if (c == '.') {
+ newQualifier();
+ currentQualifier.attrName = 'class';
+ currentQualifier.contains = true;
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '#') {
+ newQualifier();
+ currentQualifier.attrName = 'id';
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '[') {
+ newQualifier();
+ currentQualifier.attrName = '';
+ state = ATTR_NAME_FIRST_CHAR;
+ break;
+ }
+
+ if (c.match(WHITESPACE)) {
+ state = SELECTOR_SEPARATOR;
+ break;
+ }
+
+ if (c == ',') {
+ state = SELECTOR;
+ break;
+ }
+
+ throw Error(SYNTAX_ERROR);
+
+ case QUALIFIER:
+ if (c == '.') {
+ newQualifier();
+ currentQualifier.attrName = 'class';
+ currentQualifier.contains = true;
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '#') {
+ newQualifier();
+ currentQualifier.attrName = 'id';
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '[') {
+ newQualifier();
+ currentQualifier.attrName = '';
+ state = ATTR_NAME_FIRST_CHAR;
+ break;
+ }
+
+ if (c.match(WHITESPACE)) {
+ state = SELECTOR_SEPARATOR;
+ break;
+ }
+
+ if (c == ',') {
+ state = SELECTOR;
+ break;
+ }
+
+ throw Error(SYNTAX_ERROR);
+
+ case QUALIFIER_NAME_FIRST_CHAR:
+ if (c.match(validNameInitialChar)) {
+ currentQualifier.attrValue = c;
+ state = QUALIFIER_NAME;
+ break;
+ }
+
+ throw Error(SYNTAX_ERROR);
+
+ case QUALIFIER_NAME:
+ if (c.match(validNameNonInitialChar)) {
+ currentQualifier.attrValue += c;
+ break;
+ }
+
+ if (c == '.') {
+ newQualifier();
+ currentQualifier.attrName = 'class';
+ currentQualifier.contains = true;
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '#') {
+ newQualifier();
+ currentQualifier.attrName = 'id';
+ state = QUALIFIER_NAME_FIRST_CHAR;
+ break;
+ }
+ if (c == '[') {
+ newQualifier();
+ state = ATTR_NAME_FIRST_CHAR;
+ break;
+ }
+
+ if (c.match(WHITESPACE)) {
+ state = SELECTOR_SEPARATOR;
+ break;
+ }
+ if (c == ',') {
+ state = SELECTOR;
+ break
+ }
+
+ throw Error(SYNTAX_ERROR);
+
+ case ATTR_NAME_FIRST_CHAR:
+ if (c.match(validNameInitialChar)) {
+ currentQualifier.attrName = c;
+ state = ATTR_NAME;
+ break;
+ }
+
+ if (c.match(WHITESPACE))
+ break;
+
+ throw Error(SYNTAX_ERROR);
+
+ case ATTR_NAME:
+ if (c.match(validNameNonInitialChar)) {
+ currentQualifier.attrName += c;
+ break;
+ }
+
+ if (c.match(WHITESPACE)) {
+ state = EQUIV_OR_ATTR_QUAL_END;
+ break;
+ }
+
+ if (c == '~') {
+ currentQualifier.contains = true;
+ state = EQUAL;
+ break;
+ }
+
+ if (c == '=') {
+ currentQualifier.attrValue = '';
+ state = VALUE_FIRST_CHAR;
+ break;
+ }
+
+ if (c == ']') {
+ state = QUALIFIER;
+ break;
+ }
+
+ throw Error(SYNTAX_ERROR);
+
+ case EQUIV_OR_ATTR_QUAL_END:
+ if (c == '~') {
+ currentQualifier.contains = true;
+ state = EQUAL;
+ break;
+ }
+
+ if (c == '=') {
+ currentQualifier.attrValue = '';
+ state = VALUE_FIRST_CHAR;
+ break;
+ }
+
+ if (c == ']') {
+ state = QUALIFIER;
+ break;
+ }
+
+ if (c.match(WHITESPACE))
+ break;
+
+ throw Error(SYNTAX_ERROR);
+
+ case EQUAL:
+ if (c == '=') {
+ currentQualifier.attrValue = '';
+ state = VALUE_FIRST_CHAR
+ break;
+ }
+
+ throw Error(SYNTAX_ERROR);
+
+ case ATTR_QUAL_END:
+ if (c == ']') {
+ state = QUALIFIER;
+ break;
+ }
+
+ if (c.match(WHITESPACE))
+ break;
+
+ throw Error(SYNTAX_ERROR);
+
+ case VALUE_FIRST_CHAR:
+ if (c.match(WHITESPACE))
+ break;
+
+ if (c == '"' || c == "'") {
+ valueQuoteChar = c;
+ state = QUOTED_VALUE;
+ break;
+ }
+
+ currentQualifier.attrValue += c;
+ state = VALUE;
+ break;
+
+ case VALUE:
+ if (c.match(WHITESPACE)) {
+ state = ATTR_QUAL_END;
+ break;
+ }
+ if (c == ']') {
+ state = QUALIFIER;
+ break;
+ }
+ if (c == "'" || c == '"')
+ throw Error(SYNTAX_ERROR);
+
+ currentQualifier.attrValue += c;
+ break;
+
+ case QUOTED_VALUE:
+ if (c == valueQuoteChar) {
+ state = ATTR_QUAL_END;
+ break;
+ }
+
+ currentQualifier.attrValue += c;
+ break;
+
+ case SELECTOR_SEPARATOR:
+ if (c.match(WHITESPACE))
+ break;
+
+ if (c == ',') {
+ state = SELECTOR;
+ break
+ }
+
+ throw Error(SYNTAX_ERROR);
+ }
+ }
+
+ switch (state) {
+ case SELECTOR:
+ case TAG_NAME:
+ case QUALIFIER:
+ case QUALIFIER_NAME:
+ case SELECTOR_SEPARATOR:
+ // Valid end states.
+ newSelector();
+ break;
+ default:
+ throw Error(SYNTAX_ERROR);
+ }
+
+ if (!selectors.length)
+ throw Error(SYNTAX_ERROR);
+
+ return selectors;
+ }
+}
+
+var attributeFilterPattern = /^([a-zA-Z:_]+[a-zA-Z0-9_\-:\.]*)$/;
+
+function validateAttribute(attribute:string) {
+ if (typeof attribute != 'string')
+ throw Error('Invalid request opion. attribute must be a non-zero length string.');
+
+ attribute = attribute.trim();
+
+ if (!attribute)
+ throw Error('Invalid request opion. attribute must be a non-zero length string.');
+
+
+ if (!attribute.match(attributeFilterPattern))
+ throw Error('Invalid request option. invalid attribute name: ' + attribute);
+
+ return attribute;
+}
+
+function validateElementAttributes(attribs:string):string[] {
+ if (!attribs.trim().length)
+ throw Error('Invalid request option: elementAttributes must contain at least one attribute.');
+
+ var lowerAttributes = {};
+ var attributes = {};
+
+ var tokens = attribs.split(/\s+/);
+ for (var i = 0; i < tokens.length; i++) {
+ var name = tokens[i];
+ if (!name)
+ continue;
+
+ var name = validateAttribute(name);
+ var nameLower = name.toLowerCase();
+ if (lowerAttributes[nameLower])
+ throw Error('Invalid request option: observing multiple case variations of the same attribute is not supported.');
+
+ attributes[name] = true;
+ lowerAttributes[nameLower] = true;
+ }
+
+ return Object.keys(attributes);
+}
+
+
+
+function elementFilterAttributes(selectors:Selector[]):string[] {
+ var attributes:StringMap = {};
+
+ selectors.forEach((selector) => {
+ selector.qualifiers.forEach((qualifier) => {
+ attributes[qualifier.attrName] = true;
+ });
+ });
+
+ return Object.keys(attributes);
+}
+
+interface Query {
+ element?:string;
+ attribute?:string;
+ all?:boolean;
+ characterData?:boolean;
+ elementAttributes?:string;
+ attributeList?:string[];
+ elementFilter?:Selector[];
+}
+
+interface Options {
+ callback:(summaries:Summary[]) => any;
+ queries: Query[];
+ rootNode?:Node;
+ oldPreviousSibling?:boolean;
+ observeOwnChanges?:boolean;
+}
+
+class MutationSummary {
+
+ public static NodeMap = NodeMap; // exposed for use in TreeMirror.
+ public static parseElementFilter = Selector.parseSelectors; // exposed for testing.
+
+ public static createQueryValidator:(root:Node, query:Query)=>any;
+ private connected:boolean;
+ private options:Options;
+ private observer:MutationObserver;
+ private observerOptions:MutationObserverInit;
+ private root:Node;
+ private callback:(summaries:Summary[])=>any;
+ private elementFilter:Selector[];
+ private calcReordered:boolean;
+ private queryValidators:any[];
+
+ private static optionKeys:StringMap = {
+ 'callback': true, // required
+ 'queries': true, // required
+ 'rootNode': true,
+ 'oldPreviousSibling': true,
+ 'observeOwnChanges': true
+ };
+
+ private static createObserverOptions(queries:Query[]):MutationObserverInit {
+ var observerOptions:MutationObserverInit = {
+ childList: true,
+ subtree: true
+ };
+
+ var attributeFilter:StringMap;
+ function observeAttributes(attributes?:string[]) {
+ if (observerOptions.attributes && !attributeFilter)
+ return; // already observing all.
+
+ observerOptions.attributes = true;
+ observerOptions.attributeOldValue = true;
+
+ if (!attributes) {
+ // observe all.
+ attributeFilter = undefined;
+ return;
+ }
+
+ // add to observed.
+ attributeFilter = attributeFilter || {};
+ attributes.forEach((attribute) => {
+ attributeFilter[attribute] = true;
+ attributeFilter[attribute.toLowerCase()] = true;
+ });
+ }
+
+ queries.forEach((query) => {
+ if (query.characterData) {
+ observerOptions.characterData = true;
+ observerOptions.characterDataOldValue = true;
+ return;
+ }
+
+ if (query.all) {
+ observeAttributes();
+ observerOptions.characterData = true;
+ observerOptions.characterDataOldValue = true;
+ return;
+ }
+
+ if (query.attribute) {
+ observeAttributes([query.attribute.trim()]);
+ return;
+ }
+
+ var attributes = elementFilterAttributes(query.elementFilter).concat(query.attributeList || []);
+ if (attributes.length)
+ observeAttributes(attributes);
+ });
+
+ if (attributeFilter)
+ observerOptions.attributeFilter = Object.keys(attributeFilter);
+
+ return observerOptions;
+ }
+
+ private static validateOptions(options:Options):Options {
+ for (var prop in options) {
+ if (!(prop in MutationSummary.optionKeys))
+ throw Error('Invalid option: ' + prop);
+ }
+
+ if (typeof options.callback !== 'function')
+ throw Error('Invalid options: callback is required and must be a function');
+
+ if (!options.queries || !options.queries.length)
+ throw Error('Invalid options: queries must contain at least one query request object.');
+
+ var opts:Options = {
+ callback: options.callback,
+ rootNode: options.rootNode || document,
+ observeOwnChanges: !!options.observeOwnChanges,
+ oldPreviousSibling: !!options.oldPreviousSibling,
+ queries: []
+ };
+
+ for (var i = 0; i < options.queries.length; i++) {
+ var request = options.queries[i];
+
+ // all
+ if (request.all) {
+ if (Object.keys(request).length > 1)
+ throw Error('Invalid request option. all has no options.');
+
+ opts.queries.push({all: true});
+ continue;
+ }
+
+ // attribute
+ if ('attribute' in request) {
+ var query:Query = {
+ attribute: validateAttribute(request.attribute)
+ };
+
+ query.elementFilter = Selector.parseSelectors('*[' + query.attribute + ']');
+
+ if (Object.keys(request).length > 1)
+ throw Error('Invalid request option. attribute has no options.');
+
+ opts.queries.push(query);
+ continue;
+ }
+
+ // element
+ if ('element' in request) {
+ var requestOptionCount = Object.keys(request).length;
+ var query:Query = {
+ element: request.element,
+ elementFilter: Selector.parseSelectors(request.element)
+ };
+
+ if (request.hasOwnProperty('elementAttributes')) {
+ query.attributeList = validateElementAttributes(request.elementAttributes);
+ requestOptionCount--;
+ }
+
+ if (requestOptionCount > 1)
+ throw Error('Invalid request option. element only allows elementAttributes option.');
+
+ opts.queries.push(query);
+ continue;
+ }
+
+ // characterData
+ if (request.characterData) {
+ if (Object.keys(request).length > 1)
+ throw Error('Invalid request option. characterData has no options.');
+
+ opts.queries.push({ characterData: true });
+ continue;
+ }
+
+ throw Error('Invalid request option. Unknown query request.');
+ }
+
+ return opts;
+ }
+
+ private createSummaries(mutations:MutationRecord[]):Summary[] {
+ if (!mutations || !mutations.length)
+ return [];
+
+ var projection = new MutationProjection(this.root, mutations, this.elementFilter, this.calcReordered, this.options.oldPreviousSibling);
+
+ var summaries:Summary[] = [];
+ for (var i = 0; i < this.options.queries.length; i++) {
+ summaries.push(new Summary(projection, this.options.queries[i]));
+ }
+
+ return summaries;
+ }
+
+ private checkpointQueryValidators() {
+ this.queryValidators.forEach((validator) => {
+ if (validator)
+ validator.recordPreviousState();
+ });
+ }
+
+ private runQueryValidators(summaries:Summary[]) {
+ this.queryValidators.forEach((validator, index) => {
+ if (validator)
+ validator.validate(summaries[index]);
+ });
+ }
+
+ private changesToReport(summaries:Summary[]):boolean {
+ return summaries.some((summary) => {
+ var summaryProps = ['added', 'removed', 'reordered', 'reparented',
+ 'valueChanged', 'characterDataChanged'];
+ if (summaryProps.some(function(prop) { return summary[prop] && summary[prop].length; }))
+ return true;
+
+ if (summary.attributeChanged) {
+ var attrNames = Object.keys(summary.attributeChanged);
+ var attrsChanged = attrNames.some((attrName) => {
+ return !!summary.attributeChanged[attrName].length
+ });
+ if (attrsChanged)
+ return true;
+ }
+ return false;
+ });
+ }
+
+ constructor(opts:Options) {
+ this.connected = false;
+ this.options = MutationSummary.validateOptions(opts);
+ this.observerOptions = MutationSummary.createObserverOptions(this.options.queries);
+ this.root = this.options.rootNode;
+ this.callback = this.options.callback;
+
+ this.elementFilter = Array.prototype.concat.apply([], this.options.queries.map((query) => {
+ return query.elementFilter ? query.elementFilter : [];
+ }));
+ if (!this.elementFilter.length)
+ this.elementFilter = undefined;
+
+ this.calcReordered = this.options.queries.some((query) => {
+ return query.all;
+ });
+
+ this.queryValidators = []; // TODO(rafaelw): Shouldn't always define this.
+ if (MutationSummary.createQueryValidator) {
+ this.queryValidators = this.options.queries.map((query) => {
+ return MutationSummary.createQueryValidator(this.root, query);
+ });
+ }
+
+ this.observer = new MutationObserverCtor((mutations:MutationRecord[]) => {
+ this.observerCallback(mutations);
+ });
+
+ this.reconnect();
+ }
+
+ private observerCallback(mutations:MutationRecord[]) {
+ if (!this.options.observeOwnChanges)
+ this.observer.disconnect();
+
+ var summaries = this.createSummaries(mutations);
+ this.runQueryValidators(summaries);
+
+ if (this.options.observeOwnChanges)
+ this.checkpointQueryValidators();
+
+ if (this.changesToReport(summaries))
+ this.callback(summaries);
+
+ // disconnect() may have been called during the callback.
+ if (!this.options.observeOwnChanges && this.connected) {
+ this.checkpointQueryValidators();
+ this.observer.observe(this.root, this.observerOptions);
+ }
+ }
+
+ reconnect() {
+ if (this.connected)
+ throw Error('Already connected');
+
+ this.observer.observe(this.root, this.observerOptions);
+ this.connected = true;
+ this.checkpointQueryValidators();
+ }
+
+ takeSummaries():Summary[] {
+ if (!this.connected)
+ throw Error('Not connected');
+
+ var summaries = this.createSummaries(this.observer.takeRecords());
+ return this.changesToReport(summaries) ? summaries : undefined;
+ }
+
+ disconnect():Summary[] {
+ var summaries = this.takeSummaries();
+ this.observer.disconnect();
+ this.connected = false;
+ return summaries;
+ }
+}
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/util/tree-mirror.js b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/util/tree-mirror.js
new file mode 100644
index 0000000..fb63e09
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/util/tree-mirror.js
@@ -0,0 +1,268 @@
+///
+var TreeMirror = /** @class */ (function () {
+ function TreeMirror(root, delegate) {
+ this.root = root;
+ this.delegate = delegate;
+ this.idMap = {};
+ }
+ TreeMirror.prototype.initialize = function (rootId, children) {
+ this.idMap[rootId] = this.root;
+ for (var i = 0; i < children.length; i++)
+ this.deserializeNode(children[i], this.root);
+ };
+ TreeMirror.prototype.applyChanged = function (removed, addedOrMoved, attributes, text) {
+ var _this = this;
+ // NOTE: Applying the changes can result in an attempting to add a child
+ // to a parent which is presently an ancestor of the parent. This can occur
+ // based on random ordering of moves. The way we handle this is to first
+ // remove all changed nodes from their parents, then apply.
+ addedOrMoved.forEach(function (data) {
+ var node = _this.deserializeNode(data);
+ var parent = _this.deserializeNode(data.parentNode);
+ var previous = _this.deserializeNode(data.previousSibling);
+ if (node.parentNode)
+ node.parentNode.removeChild(node);
+ });
+ removed.forEach(function (data) {
+ var node = _this.deserializeNode(data);
+ if (node.parentNode)
+ node.parentNode.removeChild(node);
+ });
+ addedOrMoved.forEach(function (data) {
+ var node = _this.deserializeNode(data);
+ var parent = _this.deserializeNode(data.parentNode);
+ var previous = _this.deserializeNode(data.previousSibling);
+ parent.insertBefore(node, previous ? previous.nextSibling : parent.firstChild);
+ });
+ attributes.forEach(function (data) {
+ var node = _this.deserializeNode(data);
+ Object.keys(data.attributes).forEach(function (attrName) {
+ var newVal = data.attributes[attrName];
+ if (newVal === null) {
+ node.removeAttribute(attrName);
+ }
+ else {
+ if (!_this.delegate ||
+ !_this.delegate.setAttribute ||
+ !_this.delegate.setAttribute(node, attrName, newVal)) {
+ node.setAttribute(attrName, newVal);
+ }
+ }
+ });
+ });
+ text.forEach(function (data) {
+ var node = _this.deserializeNode(data);
+ node.textContent = data.textContent;
+ });
+ removed.forEach(function (node) {
+ delete _this.idMap[node.id];
+ });
+ };
+ TreeMirror.prototype.deserializeNode = function (nodeData, parent) {
+ var _this = this;
+ if (nodeData === null)
+ return null;
+ var node = this.idMap[nodeData.id];
+ if (node)
+ return node;
+ var doc = this.root.ownerDocument;
+ if (doc === null)
+ doc = this.root;
+ switch (nodeData.nodeType) {
+ case Node.COMMENT_NODE:
+ node = doc.createComment(nodeData.textContent);
+ break;
+ case Node.TEXT_NODE:
+ node = doc.createTextNode(nodeData.textContent);
+ break;
+ case Node.DOCUMENT_TYPE_NODE:
+ node = doc.implementation.createDocumentType(nodeData.name, nodeData.publicId, nodeData.systemId);
+ break;
+ case Node.ELEMENT_NODE:
+ if (this.delegate && this.delegate.createElement)
+ node = this.delegate.createElement(nodeData.tagName);
+ if (!node)
+ try {
+ node = doc.createElement(nodeData.tagName);
+ }
+ catch (e) {
+ console.log("Removing invalid node", nodeData);
+ return null;
+ }
+ Object.keys(nodeData.attributes).forEach(function (name) {
+ if (!_this.delegate ||
+ !_this.delegate.setAttribute ||
+ !_this.delegate.setAttribute(node, name, nodeData.attributes[name])) {
+ try {
+ node.setAttribute(name, nodeData.attributes[name]);
+ }
+ catch (e) {
+ console.log("Removing node due to invalid attribute", nodeData);
+ return null;
+ }
+ }
+ });
+ break;
+ }
+ if (!node)
+ throw "ouch";
+ this.idMap[nodeData.id] = node;
+ if (parent)
+ parent.appendChild(node);
+ if (nodeData.childNodes) {
+ for (var i = 0; i < nodeData.childNodes.length; i++)
+ this.deserializeNode(nodeData.childNodes[i], node);
+ }
+ return node;
+ };
+ return TreeMirror;
+}());
+var TreeMirrorClient = /** @class */ (function () {
+ function TreeMirrorClient(target, mirror, testingQueries) {
+ var _this = this;
+ this.target = target;
+ this.mirror = mirror;
+ this.nextId = 1;
+ this.knownNodes = new MutationSummary.NodeMap();
+ var rootId = this.serializeNode(target).id;
+ var children = [];
+ for (var child = target.firstChild; child; child = child.nextSibling)
+ children.push(this.serializeNode(child, true));
+ this.mirror.initialize(rootId, children);
+ var self = this;
+ var queries = [{ all: true }];
+ if (testingQueries)
+ queries = queries.concat(testingQueries);
+ this.mutationSummary = new MutationSummary({
+ rootNode: target,
+ callback: function (summaries) {
+ _this.applyChanged(summaries);
+ },
+ queries: queries
+ });
+ }
+ TreeMirrorClient.prototype.disconnect = function () {
+ if (this.mutationSummary) {
+ this.mutationSummary.disconnect();
+ this.mutationSummary = undefined;
+ }
+ };
+ TreeMirrorClient.prototype.rememberNode = function (node) {
+ var id = this.nextId++;
+ this.knownNodes.set(node, id);
+ return id;
+ };
+ TreeMirrorClient.prototype.forgetNode = function (node) {
+ this.knownNodes["delete"](node);
+ };
+ TreeMirrorClient.prototype.serializeNode = function (node, recursive) {
+ if (node === null)
+ return null;
+ var id = this.knownNodes.get(node);
+ if (id !== undefined) {
+ return { id: id };
+ }
+ var data = {
+ nodeType: node.nodeType,
+ id: this.rememberNode(node)
+ };
+ switch (data.nodeType) {
+ case Node.DOCUMENT_TYPE_NODE:
+ var docType = node;
+ data.name = docType.name;
+ data.publicId = docType.publicId;
+ data.systemId = docType.systemId;
+ break;
+ case Node.COMMENT_NODE:
+ case Node.TEXT_NODE:
+ data.textContent = node.textContent;
+ break;
+ case Node.ELEMENT_NODE:
+ var elm = node;
+ data.tagName = elm.tagName;
+ data.attributes = {};
+ for (var i = 0; i < elm.attributes.length; i++) {
+ var attr = elm.attributes[i];
+ data.attributes[attr.name] = attr.value;
+ }
+ if (recursive && elm.childNodes.length) {
+ data.childNodes = [];
+ for (var child = elm.firstChild; child; child = child.nextSibling)
+ data.childNodes.push(this.serializeNode(child, true));
+ }
+ break;
+ }
+ return data;
+ };
+ TreeMirrorClient.prototype.serializeAddedAndMoved = function (added, reparented, reordered) {
+ var _this = this;
+ var all = added.concat(reparented).concat(reordered);
+ var parentMap = new MutationSummary.NodeMap();
+ all.forEach(function (node) {
+ var parent = node.parentNode;
+ var children = parentMap.get(parent);
+ if (!children) {
+ children = new MutationSummary.NodeMap();
+ parentMap.set(parent, children);
+ }
+ children.set(node, true);
+ });
+ var moved = [];
+ parentMap.keys().forEach(function (parent) {
+ var children = parentMap.get(parent);
+ var keys = children.keys();
+ while (keys.length) {
+ var node = keys[0];
+ while (node.previousSibling && children.has(node.previousSibling))
+ node = node.previousSibling;
+ while (node && children.has(node)) {
+ var data = _this.serializeNode(node);
+ data.previousSibling = _this.serializeNode(node.previousSibling);
+ data.parentNode = _this.serializeNode(node.parentNode);
+ moved.push(data);
+ children["delete"](node);
+ node = node.nextSibling;
+ }
+ var keys = children.keys();
+ }
+ });
+ return moved;
+ };
+ TreeMirrorClient.prototype.serializeAttributeChanges = function (attributeChanged) {
+ var _this = this;
+ var map = new MutationSummary.NodeMap();
+ Object.keys(attributeChanged).forEach(function (attrName) {
+ attributeChanged[attrName].forEach(function (element) {
+ var record = map.get(element);
+ if (!record) {
+ record = _this.serializeNode(element);
+ record.attributes = {};
+ map.set(element, record);
+ }
+ record.attributes[attrName] = element.getAttribute(attrName);
+ });
+ });
+ return map.keys().map(function (node) {
+ return map.get(node);
+ });
+ };
+ TreeMirrorClient.prototype.applyChanged = function (summaries) {
+ var _this = this;
+ var summary = summaries[0];
+ var removed = summary.removed.map(function (node) {
+ return _this.serializeNode(node);
+ });
+ var moved = this.serializeAddedAndMoved(summary.added, summary.reparented, summary.reordered);
+ var attributes = this.serializeAttributeChanges(summary.attributeChanged);
+ var text = summary.characterDataChanged.map(function (node) {
+ var data = _this.serializeNode(node);
+ data.textContent = node.textContent;
+ return data;
+ });
+ this.mirror.applyChanged(removed, moved, attributes, text);
+ summary.removed.forEach(function (node) {
+ _this.forgetNode(node);
+ });
+ };
+ return TreeMirrorClient;
+}());
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/util/tree-mirror.ts b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/util/tree-mirror.ts
new file mode 100644
index 0000000..fd5f0d1
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/util/tree-mirror.ts
@@ -0,0 +1,375 @@
+///
+
+// Copyright 2013 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+interface NodeData {
+ id:number;
+ nodeType?:number;
+ name?:string;
+ publicId?:string;
+ systemId?:string;
+ textContent?:string;
+ tagName?:string;
+ attributes?:StringMap;
+ childNodes?:NodeData[];
+}
+
+interface PositionData extends NodeData {
+ previousSibling:NodeData;
+ parentNode:NodeData;
+}
+
+interface AttributeData extends NodeData {
+ attributes:StringMap;
+}
+
+interface TextData extends NodeData{
+ textContent:string;
+}
+
+class TreeMirror {
+
+ private idMap:NumberMap;
+
+ constructor(public root:Node, public delegate?:any) {
+ this.idMap = {};
+ }
+
+ initialize(rootId:number, children:NodeData[]) {
+ this.idMap[rootId] = this.root;
+
+ for (var i = 0; i < children.length; i++)
+ this.deserializeNode(children[i], this.root);
+ }
+
+ applyChanged(removed:NodeData[],
+ addedOrMoved:PositionData[],
+ attributes:AttributeData[],
+ text:TextData[]) {
+
+ // NOTE: Applying the changes can result in an attempting to add a child
+ // to a parent which is presently an ancestor of the parent. This can occur
+ // based on random ordering of moves. The way we handle this is to first
+ // remove all changed nodes from their parents, then apply.
+ addedOrMoved.forEach((data:PositionData) => {
+ var node = this.deserializeNode(data);
+ var parent = this.deserializeNode(data.parentNode);
+ var previous = this.deserializeNode(data.previousSibling);
+ if (node.parentNode)
+ node.parentNode.removeChild(node);
+ });
+
+ removed.forEach((data:NodeData) => {
+ var node = this.deserializeNode(data);
+ if (node.parentNode)
+ node.parentNode.removeChild(node);
+ });
+
+ addedOrMoved.forEach((data:PositionData) => {
+ var node = this.deserializeNode(data);
+ var parent = this.deserializeNode(data.parentNode);
+ var previous = this.deserializeNode(data.previousSibling);
+ parent.insertBefore(node,
+ previous ? previous.nextSibling : parent.firstChild);
+ });
+
+ attributes.forEach((data:AttributeData) => {
+ var node = this.deserializeNode(data);
+ Object.keys(data.attributes).forEach((attrName) => {
+ var newVal = data.attributes[attrName];
+ if (newVal === null) {
+ node.removeAttribute(attrName);
+ } else {
+ if (!this.delegate ||
+ !this.delegate.setAttribute ||
+ !this.delegate.setAttribute(node, attrName, newVal)) {
+ node.setAttribute(attrName, newVal);
+ }
+ }
+ });
+ });
+
+ text.forEach((data:TextData) => {
+ var node = this.deserializeNode(data);
+ node.textContent = data.textContent;
+ });
+
+ removed.forEach((node:NodeData) => {
+ delete this.idMap[node.id];
+ });
+ }
+
+ private deserializeNode(nodeData:NodeData, parent?:Element):Node {
+ if (nodeData === null)
+ return null;
+
+ var node:Node = this.idMap[nodeData.id];
+ if (node)
+ return node;
+
+ var doc = this.root.ownerDocument;
+ if (doc === null)
+ doc = this.root;
+
+ switch(nodeData.nodeType) {
+ case Node.COMMENT_NODE:
+ node = doc.createComment(nodeData.textContent);
+ break;
+
+ case Node.TEXT_NODE:
+ node = doc.createTextNode(nodeData.textContent);
+ break;
+
+ case Node.DOCUMENT_TYPE_NODE:
+ node = doc.implementation.createDocumentType(nodeData.name, nodeData.publicId, nodeData.systemId);
+ break;
+
+ case Node.ELEMENT_NODE:
+ if (this.delegate && this.delegate.createElement)
+ node = this.delegate.createElement(nodeData.tagName);
+ if (!node)
+ try {
+ node = doc.createElement(nodeData.tagName);
+ } catch (e) {
+ console.log("Removing invalid node", nodeData);
+ return null;
+ }
+
+ Object.keys(nodeData.attributes).forEach((name) => {
+ if (!this.delegate ||
+ !this.delegate.setAttribute ||
+ !this.delegate.setAttribute(node, name, nodeData.attributes[name])) {
+ try {
+ (node).setAttribute(name, nodeData.attributes[name]);
+ } catch (e) {
+ console.log("Removing node due to invalid attribute", nodeData);
+ return null;
+ }
+ }
+ });
+
+ break;
+ }
+
+ if (!node)
+ throw "ouch";
+
+ this.idMap[nodeData.id] = node;
+
+ if (parent)
+ parent.appendChild(node);
+
+ if (nodeData.childNodes) {
+ for (var i = 0; i < nodeData.childNodes.length; i++)
+ this.deserializeNode(nodeData.childNodes[i], node);
+ }
+
+ return node;
+ }
+}
+
+class TreeMirrorClient {
+ private nextId:number;
+
+ private mutationSummary:MutationSummary;
+ private knownNodes:NodeMap;
+
+ constructor(public target:Node, public mirror:any, testingQueries:Query[]) {
+ this.nextId = 1;
+ this.knownNodes = new MutationSummary.NodeMap();
+
+ var rootId = this.serializeNode(target).id;
+ var children:NodeData[] = [];
+ for (var child = target.firstChild; child; child = child.nextSibling)
+ children.push(this.serializeNode(child, true));
+
+ this.mirror.initialize(rootId, children);
+
+ var self = this;
+
+ var queries = [{ all: true }];
+
+ if (testingQueries)
+ queries = queries.concat(testingQueries);
+
+ this.mutationSummary = new MutationSummary({
+ rootNode: target,
+ callback: (summaries:Summary[]) => {
+ this.applyChanged(summaries);
+ },
+ queries: queries
+ });
+ }
+
+
+ disconnect() {
+ if (this.mutationSummary) {
+ this.mutationSummary.disconnect();
+ this.mutationSummary = undefined;
+ }
+ }
+
+ private rememberNode(node:Node):number {
+ var id = this.nextId++;
+ this.knownNodes.set(node, id);
+ return id;
+ }
+
+ private forgetNode(node:Node) {
+ this.knownNodes.delete(node);
+ }
+
+ private serializeNode(node:Node, recursive?:boolean):NodeData {
+ if (node === null)
+ return null;
+
+ var id = this.knownNodes.get(node);
+ if (id !== undefined) {
+ return { id: id };
+ }
+
+ var data:NodeData = {
+ nodeType: node.nodeType,
+ id: this.rememberNode(node)
+ };
+
+ switch(data.nodeType) {
+ case Node.DOCUMENT_TYPE_NODE:
+ var docType = node;
+ data.name = docType.name;
+ data.publicId = docType.publicId;
+ data.systemId = docType.systemId;
+ break;
+
+ case Node.COMMENT_NODE:
+ case Node.TEXT_NODE:
+ data.textContent = node.textContent;
+ break;
+
+ case Node.ELEMENT_NODE:
+ var elm = node;
+ data.tagName = elm.tagName;
+ data.attributes = {};
+ for (var i = 0; i < elm.attributes.length; i++) {
+ var attr = elm.attributes[i];
+ data.attributes[attr.name] = attr.value;
+ }
+
+ if (recursive && elm.childNodes.length) {
+ data.childNodes = [];
+
+ for (var child = elm.firstChild; child; child = child.nextSibling)
+ data.childNodes.push(this.serializeNode(child, true));
+ }
+ break;
+ }
+
+ return data;
+ }
+
+ private serializeAddedAndMoved(added:Node[],
+ reparented:Node[],
+ reordered:Node[]):PositionData[] {
+ var all = added.concat(reparented).concat(reordered);
+
+ var parentMap = new MutationSummary.NodeMap>();
+
+ all.forEach((node) => {
+ var parent = node.parentNode;
+ var children = parentMap.get(parent)
+ if (!children) {
+ children = new MutationSummary.NodeMap();
+ parentMap.set(parent, children);
+ }
+
+ children.set(node, true);
+ });
+
+ var moved:PositionData[] = [];
+
+ parentMap.keys().forEach((parent) => {
+ var children = parentMap.get(parent);
+
+ var keys = children.keys();
+ while (keys.length) {
+ var node = keys[0];
+ while (node.previousSibling && children.has(node.previousSibling))
+ node = node.previousSibling;
+
+ while (node && children.has(node)) {
+ var data = this.serializeNode(node);
+ data.previousSibling = this.serializeNode(node.previousSibling);
+ data.parentNode = this.serializeNode(node.parentNode);
+ moved.push(data);
+ children.delete(node);
+ node = node.nextSibling;
+ }
+
+ var keys = children.keys();
+ }
+ });
+
+ return moved;
+ }
+
+ private serializeAttributeChanges(attributeChanged:StringMap):AttributeData[] {
+ var map = new MutationSummary.NodeMap();
+
+ Object.keys(attributeChanged).forEach((attrName) => {
+ attributeChanged[attrName].forEach((element) => {
+ var record = map.get(element);
+ if (!record) {
+ record = this.serializeNode(element);
+ record.attributes = {};
+ map.set(element, record);
+ }
+
+ record.attributes[attrName] = element.getAttribute(attrName);
+ });
+ });
+
+ return map.keys().map((node:Node) => {
+ return map.get(node);
+ });
+ }
+
+ applyChanged(summaries:Summary[]) {
+ var summary:Summary = summaries[0]
+
+ var removed:NodeData[] = summary.removed.map((node:Node) => {
+ return this.serializeNode(node);
+ });
+
+ var moved:PositionData[] =
+ this.serializeAddedAndMoved(summary.added,
+ summary.reparented,
+ summary.reordered);
+
+ var attributes:AttributeData[] =
+ this.serializeAttributeChanges(summary.attributeChanged);
+
+ var text:TextData[] = summary.characterDataChanged.map((node:Node) => {
+ var data = this.serializeNode(node);
+ data.textContent = node.textContent;
+ return data;
+ });
+
+ this.mirror.applyChanged(removed, moved, attributes, text);
+
+ summary.removed.forEach((node:Node) => {
+ this.forgetNode(node);
+ });
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/CHANGELOG.md b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/CHANGELOG.md
new file mode 100644
index 0000000..6d9eb1e
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/CHANGELOG.md
@@ -0,0 +1,642 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+The document follows the conventions described in [“Keep a CHANGELOG”](http://keepachangelog.com).
+
+
+====
+
+
+## UNRELEASED 3.0.0
+
+### Added
+- added `'random'` option and `randomize()` method to `SVG.Color` -> __TODO!__
+- added `precision()` method to round numeric element attributes -> __TODO!__
+- added specs for `SVG.FX` -> __TODO!__
+
+### Changed
+- made transform-methods relative as default (breaking change)
+- changed SVG() to use querySelector instead of getElementById (breaking change) -> __TODO!__
+- made `parents()` method on `SVG.Element` return an instance of SVG.Set (breaking change) -> __TODO!__
+- replaced static reference to `masker` in `SVG.Mask` with the `masker()` method (breaking change) -> __TODO!__
+- replaced static reference to `clipper` in `SVG.ClipPath` with the `clipper()` method (breaking change) -> __TODO!__
+- replaced static reference to `targets` in `SVG.Mask` and `SVG.ClipPath` with the `targets()` method (breaking change) -> __TODO!__
+- moved all regexes to `SVG.regex` (in color, element, pointarray, style, transform and viewbox) -> __TODO!__
+
+### Fixed
+- fixed a bug in clipping and masking where empty nodes persists after removal -> __TODO!__
+- fixed a bug in IE11 with `mouseenter` and `mouseleave` -> __TODO!__
+
+
+## [2.6.1] - 2017-04-25
+
+### Fixed
+- fixed a bug in path parser which made it stop parsing when hitting z command (#665)
+
+## [2.6.0] - 2017-04-21
+
+### Added
+- added `options` object to `SVG.on()` and `el.on()` (#661)
+
+### Changed
+- back to sloppy mode because of problems with plugins (#660)
+
+
+## [2.5.3] - 2017-04-15
+
+### Added
+- added gitter badge in readme
+
+
+### Fixed
+- fixed svg.js.d.ts (#644 #648)
+- fixed bug in `el.flip()` which causes an error when calling flip without any argument
+
+### Removed
+- component.json (#652)
+
+
+## [2.5.2] - 2017-04-11
+
+### Changed
+- SVG.js is now running in strict mode
+
+### Fixed
+- `clear()` does not remove the parser in svg documents anymore
+- `len` not declared in FX module, making it a global variable (9737e8a)
+- `bbox` not declared in SVG.Box.transform in the Box module (131df0f)
+- `namespace` not declared in the Event module (e89c97e)
+
+
+## [2.5.1] - 2017-03-27
+
+### Changed
+- make svgjs ready to be used on the server
+
+### Fixed
+- fixed `SVG.PathArray.parse` that did not correctly parsed flat arrays
+- prevented unnecessary parsing of point or path strings
+
+
+## [2.5.0] - 2017-03-10
+
+### Added
+- added a plot and array method to `SVG.TextPath` (#582)
+- added `clone()` method to `SVG.Array/PointArray/PathArray` (#590)
+- added `font()` method to `SVG.Tspan`
+- added `SVG.Box()`
+- added `transform()` method to boxes
+- added `event()` to `SVG.Element` to retrieve the event that was fired last on the element (#550)
+
+### Changed
+- changed CHANGELOG to follow the conventions described in [“Keep a CHANGELOG”](http://keepachangelog.com) (#578)
+- make the method plot a getter when no parameter is passed for `SVG.Polyline`,`SVG.Polygon`, `SVG.Line`, `SVG.Path` (related #547)
+- allow `SVG.PointArray` to be passed flat array
+- change the regexp `SVG.PointArray` use to parse string to allow more flexibility in the way spaces and commas can be used
+- allow `plot` to be called with 4 parameters when animating an `SVG.Line`
+- relative value for `SVG.Number` are now calculated in its `morph` method (related #547)
+- clean up the implementation of the `initAnimation` method of the FX module (#547, #552, #584)
+- deprecated `.tbox()`. `.tbox()` now map to `.rbox()`. If you are using `.tbox()`, you can substitute it with `.rbox()` (#594, #602)
+- all boxes now accept 4 values or an object on creation
+- `el.rbox()` now always returns the right boxes in screen coordinates and has an additional paramater to transform the box into other coordinate systems
+- `font()` method can now be used like `attr()` method (#620)
+- events are now cancelable by default (#550)
+
+### Fixed
+- fixed a bug in the plain morphing part of `SVG.MorphObj` that is in the FX module
+- fixed bug which produces an error when removing an event from a node which was formerly removed with a global `off()` (#518)
+- fixed a bug in `size()` for poly elements when their height/width is zero (#505)
+- viewbox now also accepts strings and arrays as constructor arguments
+- `SVG.Array` now accepts a comma seperated string and returns array of numbers instead of strings
+- `SVG.Matrix` now accepts an array as input
+- `SVG.Element.matrix()` now accepts also 6 values
+- `dx()/dy()` now accepts percentage values, too but only if the value on the element is already percentage
+- `flip()` now flips on both axis when no parameter is passed
+- fixed bug with `documentElement.contains()` in IE
+- fixed offset produced by svg parser (#553)
+- fixed a bug with clone which didnt copy over dom data (#621)
+
+
+## [2.4.0] - 2017-01-14
+
+### Added
+- added support for basic path animations (#561)
+
+
+## [2.3.7] - 2017-01-14
+
+### Added
+- added code coverage https://coveralls.io/github/svgdotjs/svg.js (3e614d4)
+- added `npm run test:quick` which aim at being fast rather than correct - great for git hooks (981ce24)
+
+### Changed
+- moved project to [svgdotjs](https://github.com/svgdotjs)
+- made matrixify work with transformation chain separated by commas (#543)
+- updated dev dependencies; request and gulp-chmod - `npm run build` now requires nodejs 4.x+
+
+### Fixed
+- fixed `SVG.Matrix.skew()` (#545)
+- fixed broken animations, if using polyfills for es6/7 proposals (#504)
+- fixed and improved `SVG.FX.dequeue()` (#546)
+- fixed an error in `SVG.FX.step`, if custom properties is added to `Array.prototype` (#549)
+
+
+## [2.3.6] - 2016-10-21
+
+### Changed
+- make SVG.FX.loop modify the last situation instead of the current one (#532)
+
+### Fixed
+- fixed leading and trailing space in SVG.PointArray would return NaN for some points (695f26a) (#529)
+- fixed test of `SVG.FX.afterAll` (#534)
+- fixed `SVG.FX.speed()` (#536)
+
+
+## [2.3.5] - 2016-10-13
+
+### Added
+- added automated unit tests via [Travis](https://travis-ci.org/svgdotjs/svg.js) (#527)
+- added `npm run build` to build a new version of SVG.js without requiring gulp to be globally installed
+
+### Changed
+- calling `fill()`, `stroke()` without an argument is now a nop
+- Polygon now accepts comma less points to achieve parity with Adobe Illustrator (#529)
+- updated dependencies
+
+
+## [2.3.4] - 2016-08-04
+
+### Changed
+- reworked parent module for speed improvemenents
+- reworked `filterSVGElements` utility to use a for loop instead of the native filter function
+
+
+## [2.3.3] - 2016-08-02
+
+### Added
+- add error callback on image loading (#508)
+
+### Fixed
+- fixed bug when getting bbox of text elements which are not in the dom (#514)
+- fixed bug when getting bbox of element which is hidden with css (#516)
+
+
+## [2.3.2] - 2016-06-21
+
+### Added
+- added specs for `SVG.ViewBox`
+- added `parent` parameter for `clone()`
+- added spec for mentioned issue
+
+### Fixed
+- fixed string parsing in viewbox (#483)
+- fixed bbox when element is not in the dom (#480)
+- fixed line constructor which doesn't work with Array as input (#487)
+- fixed problem in IE with `document.contains` (#490) related to (#480)
+- fixed `undo` when undoing transformations (#494)
+
+
+## [2.3.1] - 2016-05-05
+
+### Added
+- added typings for svg.js (#470)
+
+### Fixed
+- fixed `SVG.morph()` (#473)
+- fixed parser error (#471)
+- fixed bug in `SVG.Color` with new fx
+- fixed `radius()` for circles when animating and other related code (#477)
+- fixed bug where `stop(true)` throws an error when element is not animated (#475)
+- fixed bug in `add()` when altering svgs with whitespaces
+- fixed bug in `SVG.Doc().create` where size was set to 100% even if size was already specified
+- fixed bug in `parse()` from `SVG.PathArray` which does not correctly handled `S` and `T` (#485)
+
+
+## [2.3.0] - 2016-03-30
+
+### Added
+- added `SVG.Point` which serves as Wrapper to the native `SVGPoint` (#437)
+- added `element.point(x,y)` which transforms a point from screen coordinates to the elements space (#403)
+- added `element.is()` which helps to check for the object instance faster (instanceof check)
+- added more fx specs
+
+### Changed
+- textpath now is a parent element, the lines method of text will return the tspans inside the textpath (#450)
+- fx module rewritten to support animation chaining and several other stuff (see docs)
+
+### Fixed
+- fixed `svgjs:data` attribute which was not set properly in all browsers (#428)
+- fixed `isNumber` and `numberAndUnit` regex (#405)
+- fixed error where a parent node is not found when loading an image but the canvas was cleared (#447)
+- fixed absolute transformation animations (not perfect but better)
+- fixed event listeners which didnt work correctly when identic funtions used
+
+
+## [2.2.5] - 2015-12-29
+
+### Added
+- added check for existence of node (#431)
+
+### Changed
+- `group.move()` now allows string numbers as input (#433)
+- `matrixify()` will not apply the calculated matrix to the node anymore
+
+
+## [2.2.4] - 2015-12-12
+
+### Fixed
+- fixed `transform()` which returns the matrix values (a-f) now, too (#423)
+- double newlines (\n\n) are correctly handled as blank line from `text()`
+- fixed use of scrollX vs pageXOffset in `rbox()` (#425)
+- fixed target array in mask and clip which was removed instead of reinitialized (#429)
+
+
+## [2.2.3] - 2015-11-30
+
+### Fixed
+- fixed null check in image (see 2.2.2)
+- fixed bug related to the new path parser (see 2.2.2)
+- fixed amd loader (#412)
+
+
+## [2.2.2] - 2015-11-28
+
+### Added
+- added null check in image onload callback (#415)
+
+### Changed
+- documentation rework (#407) [thanks @snowyplover]
+
+### Fixed
+- fixed leading point bug in path parsing (#416)
+
+
+## [2.2.1] - 2015-11-18
+
+### Added
+- added workaround for `SvgPathSeg` which is removed in Chrome 48 (#409)
+- added `gbox()` to group to get bbox with translation included (#405)
+
+### Fixed
+- fixed dom data which was not cleaned up properly (#398)
+
+
+## [2.2.0] - 2015-11-06
+
+### Added
+- added `ungroup()/flatten()` (#238), `toParent()` and `toDoc()`
+- added UMD-Wrapper with possibility to pass custom window object (#352)
+- added `morph()` method for paths via plugin [svg.pathmorphing.js](https://github.com/Fuzzyma/svg.pathmorphing.js)
+- added support for css selectors within the `parent()` method
+- added `parents()` method to get an array of all parenting elements
+
+### Changed
+- svgjs now saves crucial data in the dom before export and restores them when element is adopted
+
+### Fixed
+- fixed pattern and gradient animation (#385)
+- fixed mask animation in Firefox (#287)
+- fixed return value of `text()` after import/clone (#393)
+
+
+## [2.1.1] - 2015-10-03
+
+### Added
+- added custom context binding to event callback (default is the element the event is bound to)
+
+
+## [2.1.0] - 2015-09-20
+
+### Added
+- added transform to pattern and gradients (#383)
+
+### Fixed
+- fixed clone of textnodes (#369)
+- fixed transformlists in IE (#372)
+- fixed typo that leads to broken gradients (#370)
+- fixed animate radius for circles (#367)
+
+
+## [2.0.2] - 2015-06-22
+
+### Fixed
+- Fixed zoom consideration in circle and ellipse
+
+
+## [2.0.1] - 2015-06-21
+
+### Added
+- added possibility to remove all events from a certain namespace
+
+### Fixed
+- fixed bug with `doc()` which always should return root svg
+- fixed bug in `SVG.FX` when animating with `plot()`
+
+### Removed
+- removed target reference from use which caused bugs in `dmove()` and `use()` with external file
+- removed scale consideration in `move()` duo to incompatibilities with other move-functions e.g. in `SVG.PointArray`
+
+
+## [2.0.0] - 2015-06-11
+
+### Added
+- implemented an SVG adoption system to be able to manipulate existing SVG's not created with svg.js
+- added polyfill for IE9 and IE10 custom events [thanks @Fuzzyma]
+- added DOM query selector with the `select()` method globally or on parent elements
+- added the intentionally neglected `SVG.Circle` element
+- added `rx()` and `ry()` to `SVG.Rect`, `SVG.Circle`, `SVG.Ellispe` and `SVG.FX`
+- added support to clone manually built text elements
+- added `svg.wiml.js` plugin to plugins list
+- added `ctm()` method to for matrix-centric transformations
+- added `morph()` method to `SVG.Matrix`
+- added support for new matrix system to `SVG.FX`
+- added `native()` method to elements and matrix to get to the native api
+- added `untransform()` method to remove all transformations
+- added raw svg import functionality with the `svg()` method
+- added coding style description to README
+- added reverse functionality for animations
+- documented the `situation` object in `SVG.FX`
+- added distinction between relative and absolute matrix transformations
+- implemented the `element()` method using the `SVG.Bare` class to create elements that are not described by SVG.js
+- added `w` and `h` properties as shorthand for `width` and `height` to `SVG.BBox`
+- added `SVG.TBox` to get a bounding box that is affected by transformation values
+- added event-based or complete detaching of event listeners in `off()` method
+
+### Changed
+- changed `parent` reference on elements to `parent()` method
+- using `CustomEvent` instead of `Event` to be able to fire events with a `detail` object [thanks @Fuzzyma]
+- renamed `SVG.TSpan` class to `SVG.Tspan` to play nice with the adoption system
+- completely reworked `clone()` method to use the adoption system
+- completely reworked transformations to be chainable and more true to their nature
+- changed `lines` reference to `lines()` on `SVG.Text`
+- changed `track` reference to `track()` on `SVG.Text`
+- changed `textPath` reference to `textPath()` on `SVG.Text`
+- changed `array` reference to `array()` method on `SVG.Polyline`, `SVG.Polygon` and `SVG.Path`
+- reworked sup-pixel offset implementation to be more compact
+- switched from Ruby's `rake` to Node's `gulp` for building [thanks to Alex Ewerlöf]
+- changed `to()` method to `at()` method in `SVG.FX`
+- renamed `SVG.SetFX` to `SVG.FX.Set`
+- reworked `SVG.Number` to return new instances with calculations rather than itself
+- reworked animatable matrix rotations
+- removed `SVG.Symbol` but kept the `symbol()` method using the new `element()` method
+
+### Fixed
+- fixed bug in `radius()` method when `y` value equals `0`
+- fixed a bug where events are not detached properly
+
+
+## [1.0.0-rc.9] - 2014-06-17
+
+### Added
+- added `SVG.Marker`
+- added `SVG.Symbol`
+- added `first()` and `last()` methods to `SVG.Set`
+- added `length()` method to `SVG.Text` and `SVG.TSpan` to calculate total text length
+- added `reference()` method to get referenced elements from a given attribute value
+
+### Changed
+- `SVG.get()` will now also fetch elements with a `xlink:href="#elementId"` or `url(#elementId)` value given
+
+### Fixed
+- fixed infinite loop in viewbox when element has a percentage width / height [thanks @shabegger]
+
+
+## [1.0.0-rc.8] - 2014-06-12
+
+### Fixed
+- fixed bug in `SVG.off`
+- fixed offset by window scroll position in `rbox()` [thanks @bryhoyt]
+
+
+## [1.0.0-rc.7] - 2014-06-11
+
+### Added
+- added `classes()`, `hasClass()`, `addClass()`, `removeClass()` and `toggleClass()` [thanks @pklingem]
+
+### Changed
+- binding events listeners to svg.js instance
+- calling `after()` when calling `stop(true)` (fulfill flag) [thanks @vird]
+- text element fires `rebuild` event whenever the `rebuild()` method is called
+
+### Fixed
+- fixed a bug where `Element#style()` would not save empty values in IE11 [thanks @Shtong]
+- fixed `SVG is not defined error` [thanks @anvaka]
+- fixed a bug in `move()`on text elements with a string based value
+- fix for `text()` method on text element when acting as getter [thanks @Lochemage]
+- fix in `style()` method with a css string [thanks @TobiasHeckel]
+
+
+## [1.0.0-rc.6] - 2014-03-03
+
+### Added
+- added `leading()` method to `SVG.FX`
+- added `reverse()` method to `SVG.Array` (and thereby also to `SVG.PointArray` and `SVG.PathArray`)
+- added `fulfill` option to `stop()` method in `SVG.FX` to finalise animations
+- added more output values to `bbox()` and `rbox()` methods
+
+### Changed
+- fine-tuned text element positioning
+- calling `at()` method directly on morphable svg.js instances in `SVG.FX` module
+- moved most `_private` methods to local named functions
+- moved helpers to a separate file
+
+### Fixed
+- fixed a bug in text `dy()` method
+
+### Removed
+- removed internal representation for `style`
+
+
+## [1.0.0-rc.5] - 2014-02-14
+
+### Added
+- added `plain()` method to `SVG.Text` element to add plain text content, without tspans
+- added `plain()` method to parent elements to create a text element without tspans
+- added `build()` to enable/disable build mode
+
+### Changed
+- updated `SVG.TSpan` to accept nested tspan elements, not unlike the `text()` method in `SVG.Text`
+- removed the `relative()` method in favour of `dx()`, `dy()` and `dmove()`
+- switched form objects to arrays in `SVG.PathArray` for compatibility with other libraries and better performance on parsing and rendering (up-to 48% faster than 1.0.0-rc.4)
+- refined docs on element-specific methods and `SVG.PathArray` structure
+- reworked `leading()` implementation to be more font-size "aware"
+- refactored the `attr` method on `SVG.Element`
+- applied Helvetica as default font
+- building `SVG.FX` class with `SVG.invent()` function
+
+### Removed
+- removed verbose style application to tspans
+
+
+## [1.0.0-rc.4] - 2014-02-04
+
+### Added
+- automatic pattern creation by passing an image url or instance as `fill` attribute on elements
+- added `loaded()` method to image tag
+- added `pointAt()` method to `SVG.Path`, wrapping the native `getPointAtLength()`
+
+### Changed
+- switched to `MAJOR`.`MINOR`.`PATCH` versioning format to play nice with package managers
+- made svg.pattern.js part of the core library
+- moved `length()` method to sugar module
+
+### Fixed
+- fix in `animate('=').to()`
+- fix for arcs in patharray `toString()` method [thanks @dotnetCarpenter]
+
+
+## [v1.0rc3] - 2014-02-03
+
+### Added
+- added the `SVG.invent` function to ease invention of new elements
+- added second values for `animate('2s')`
+- added `length()` mehtod to path, wrapping the native `getTotalLength()`
+
+### Changed
+- using `SVG.invent` to generate core shapes as well for leaner code
+
+### Fixed
+- fix for html-less documents
+- fix for arcs in patharray `toString()` method
+
+
+## [v1.0rc2] - 2014-02-01
+
+### Added
+- added `index()` method to `SVG.Parent` and `SVG.Set`
+- added `morph()` and `at()` methods to `SVG.Number` for unit morphing
+
+### Changed
+- modified `cx()` and `cy()` methods on elements with native `x`, `y`, `width` and `height` attributes for better performance
+
+
+## [v1.0rc1] - 2014-01-31
+
+### Added
+- added `SVG.PathArray` for real path transformations
+- added `bbox()` method to `SVG.Set`
+- added `relative()` method for moves relative to the current position
+- added `morph()` and `at()` methods to `SVG.Color` for color morphing
+
+### Changed
+- enabled proportional resizing on `size()` method with `null` for either `width` or `height` values
+- moved data module to separate file
+- `data()` method now accepts object for for multiple key / value assignments
+
+### Removed
+- removed `unbiased` system for paths
+
+
+## [v0.38] - 2014-01-28
+
+### Added
+- added `loop()` method to `SVG.FX`
+
+### Changed
+- switched from `setInterval` to `requestAnimFrame` for animations
+
+
+## [v0.37] - 2014-01-26
+
+### Added
+- added `get()` to `SVG.Set`
+
+### Changed
+- moved `SVG.PointArray` to a separate file
+
+
+## [v0.36] - 2014-01-25
+
+### Added
+- added `linkTo()`, `addTo()` and `putIn()` methods on `SVG.Element`
+
+### Changed
+- provided more detailed documentation on parent elements
+
+### Fixed
+
+
+## [v0.35] - 2014-01-23
+
+### Added
+- added `SVG.A` element with the `link()`
+
+
+## [v0.34] - 2014-01-23
+
+### Added
+- added `pause()` and `play()` to `SVG.FX`
+
+### Changed
+- storing animation values in `situation` object
+
+
+## [v0.33] - 2014-01-22
+
+### Added
+- added `has()` method to `SVG.Set`
+- added `width()` and `height()` as setter and getter methods on all shapes
+- added `replace()` method to elements
+- added `radius()` method to `SVG.Rect` and `SVG.Ellipse`
+- added reference to parent node in defs
+
+### Changed
+- moved sub-pixel offset fix to be an optional method (e.g. `SVG('drawing').fixSubPixelOffset()`)
+- merged plotable.js and path.js
+
+
+## [v0.32]
+
+### Added
+- added library to [cdnjs](http://cdnjs.com)
+
+
+
+[2.6.0]: https://github.com/svgdotjs/svg.js/releases/tag/2.6.0
+[2.5.3]: https://github.com/svgdotjs/svg.js/releases/tag/2.5.3
+[2.5.2]: https://github.com/svgdotjs/svg.js/releases/tag/2.5.2
+[2.5.1]: https://github.com/svgdotjs/svg.js/releases/tag/2.5.1
+[2.5.0]: https://github.com/svgdotjs/svg.js/releases/tag/2.5.0
+[2.4.0]: https://github.com/svgdotjs/svg.js/releases/tag/2.4.0
+
+[2.3.7]: https://github.com/svgdotjs/svg.js/releases/tag/2.3.7
+[2.3.6]: https://github.com/svgdotjs/svg.js/releases/tag/2.3.6
+[2.3.5]: https://github.com/svgdotjs/svg.js/releases/tag/2.3.5
+[2.3.4]: https://github.com/svgdotjs/svg.js/releases/tag/2.3.4
+[2.3.3]: https://github.com/svgdotjs/svg.js/releases/tag/2.3.3
+[2.3.2]: https://github.com/svgdotjs/svg.js/releases/tag/2.3.2
+[2.3.1]: https://github.com/svgdotjs/svg.js/releases/tag/2.3.1
+[2.3.0]: https://github.com/svgdotjs/svg.js/releases/tag/2.3.0
+
+[2.2.5]: https://github.com/svgdotjs/svg.js/releases/tag/2.2.5
+[2.2.4]: https://github.com/svgdotjs/svg.js/releases/tag/2.2.4
+[2.2.3]: https://github.com/svgdotjs/svg.js/releases/tag/2.2.3
+[2.2.2]: https://github.com/svgdotjs/svg.js/releases/tag/2.2.2
+[2.2.1]: https://github.com/svgdotjs/svg.js/releases/tag/2.2.1
+[2.2.0]: https://github.com/svgdotjs/svg.js/releases/tag/2.2.0
+
+[2.1.1]: https://github.com/svgdotjs/svg.js/releases/tag/2.1.1
+[2.1.0]: https://github.com/svgdotjs/svg.js/releases/tag/2.1.0
+
+[2.0.2]: https://github.com/svgdotjs/svg.js/releases/tag/2.0.2
+[2.0.1]: https://github.com/svgdotjs/svg.js/releases/tag/2.0.1
+[2.0.0]: https://github.com/svgdotjs/svg.js/releases/tag/2.0.0
+
+[1.0.0-rc.9]: https://github.com/svgdotjs/svg.js/releases/tag/1.0.0-rc.9
+[1.0.0-rc.8]: https://github.com/svgdotjs/svg.js/releases/tag/1.0.0-rc.8
+[1.0.0-rc.7]: https://github.com/svgdotjs/svg.js/releases/tag/1.0.0-rc.7
+[1.0.0-rc.6]: https://github.com/svgdotjs/svg.js/releases/tag/1.0.0-rc.6
+[1.0.0-rc.5]: https://github.com/svgdotjs/svg.js/releases/tag/1.0.0-rc.5
+[1.0.0-rc.4]: https://github.com/svgdotjs/svg.js/releases/tag/1.0.0-rc.4
+[v1.0rc3]: https://github.com/svgdotjs/svg.js/releases/tag/v1.0rc3
+[v1.0rc2]: https://github.com/svgdotjs/svg.js/releases/tag/v1.0rc2
+[v1.0rc1]: https://github.com/svgdotjs/svg.js/releases/tag/v1.0rc1
+
+[v0.38]: https://github.com/svgdotjs/svg.js/releases/tag/v0.38
+[v0.37]: https://github.com/svgdotjs/svg.js/releases/tag/v0.37
+[v0.36]: https://github.com/svgdotjs/svg.js/releases/tag/v0.36
+[v0.35]: https://github.com/svgdotjs/svg.js/releases/tag/v0.35
+[v0.34]: https://github.com/svgdotjs/svg.js/releases/tag/v0.34
+[v0.33]: https://github.com/svgdotjs/svg.js/releases/tag/v0.33
+[v0.32]: https://github.com/svgdotjs/svg.js/releases/tag/v0.32
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/LICENSE.txt b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/LICENSE.txt
new file mode 100644
index 0000000..148b70a
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/LICENSE.txt
@@ -0,0 +1,21 @@
+Copyright (c) 2012-2017 Wout Fierens
+https://svgdotjs.github.io/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/README.md b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/README.md
new file mode 100644
index 0000000..b88c5a5
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/README.md
@@ -0,0 +1,29 @@
+# SVG.js
+
+[](https://travis-ci.org/svgdotjs/svg.js)
+[](https://coveralls.io/github/svgdotjs/svg.js?branch=master)
+[](https://cdnjs.com/libraries/svg.js)
+[](https://gitter.im/svgdotjs/svg.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+__A lightweight library for manipulating and animating SVG, without any dependencies.__
+
+SVG.js is licensed under the terms of the MIT License.
+
+## Installation
+
+#### Bower:
+
+`bower install svg.js`
+
+#### Node:
+
+`npm install svg.js`
+
+#### Cdnjs:
+
+[https://cdnjs.com/libraries/svg.js](https://cdnjs.com/libraries/svg.js)
+
+## Documentation
+Check [https://svgdotjs.github.io](https://svgdotjs.github.io/) to learn more.
+
+[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=pay%40woutfierens.com&lc=US&item_name=SVG.JS¤cy_code=EUR&bn=PP-DonationsBF%3Abtn_donate_74x21.png%3ANonHostedGuest)
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/dist/svg.js b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/dist/svg.js
new file mode 100644
index 0000000..d2fd5d3
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/dist/svg.js
@@ -0,0 +1,5518 @@
+/*!
+* svg.js - A lightweight library for manipulating and animating SVG.
+* @version 2.6.1
+* https://svgdotjs.github.io/
+*
+* @copyright Wout Fierens
+* @license MIT
+*
+* BUILT: Tue Apr 25 2017 11:58:09 GMT+0200 (Mitteleuropäische Sommerzeit)
+*/;
+(function(root, factory) {
+ /* istanbul ignore next */
+ if (typeof define === 'function' && define.amd) {
+ define(function(){
+ return factory(root, root.document)
+ })
+ } else if (typeof exports === 'object') {
+ module.exports = root.document ? factory(root, root.document) : function(w){ return factory(w, w.document) }
+ } else {
+ root.SVG = factory(root, root.document)
+ }
+}(typeof window !== "undefined" ? window : this, function(window, document) {
+
+// The main wrapping element
+var SVG = this.SVG = function(element) {
+ if (SVG.supported) {
+ element = new SVG.Doc(element)
+
+ if(!SVG.parser.draw)
+ SVG.prepare()
+
+ return element
+ }
+}
+
+// Default namespaces
+SVG.ns = 'http://www.w3.org/2000/svg'
+SVG.xmlns = 'http://www.w3.org/2000/xmlns/'
+SVG.xlink = 'http://www.w3.org/1999/xlink'
+SVG.svgjs = 'http://svgjs.com/svgjs'
+
+// Svg support test
+SVG.supported = (function() {
+ return !! document.createElementNS &&
+ !! document.createElementNS(SVG.ns,'svg').createSVGRect
+})()
+
+// Don't bother to continue if SVG is not supported
+if (!SVG.supported) return false
+
+// Element id sequence
+SVG.did = 1000
+
+// Get next named element id
+SVG.eid = function(name) {
+ return 'Svgjs' + capitalize(name) + (SVG.did++)
+}
+
+// Method for element creation
+SVG.create = function(name) {
+ // create element
+ var element = document.createElementNS(this.ns, name)
+
+ // apply unique id
+ element.setAttribute('id', this.eid(name))
+
+ return element
+}
+
+// Method for extending objects
+SVG.extend = function() {
+ var modules, methods, key, i
+
+ // Get list of modules
+ modules = [].slice.call(arguments)
+
+ // Get object with extensions
+ methods = modules.pop()
+
+ for (i = modules.length - 1; i >= 0; i--)
+ if (modules[i])
+ for (key in methods)
+ modules[i].prototype[key] = methods[key]
+
+ // Make sure SVG.Set inherits any newly added methods
+ if (SVG.Set && SVG.Set.inherit)
+ SVG.Set.inherit()
+}
+
+// Invent new element
+SVG.invent = function(config) {
+ // Create element initializer
+ var initializer = typeof config.create == 'function' ?
+ config.create :
+ function() {
+ this.constructor.call(this, SVG.create(config.create))
+ }
+
+ // Inherit prototype
+ if (config.inherit)
+ initializer.prototype = new config.inherit
+
+ // Extend with methods
+ if (config.extend)
+ SVG.extend(initializer, config.extend)
+
+ // Attach construct method to parent
+ if (config.construct)
+ SVG.extend(config.parent || SVG.Container, config.construct)
+
+ return initializer
+}
+
+// Adopt existing svg elements
+SVG.adopt = function(node) {
+ // check for presence of node
+ if (!node) return null
+
+ // make sure a node isn't already adopted
+ if (node.instance) return node.instance
+
+ // initialize variables
+ var element
+
+ // adopt with element-specific settings
+ if (node.nodeName == 'svg')
+ element = node.parentNode instanceof window.SVGElement ? new SVG.Nested : new SVG.Doc
+ else if (node.nodeName == 'linearGradient')
+ element = new SVG.Gradient('linear')
+ else if (node.nodeName == 'radialGradient')
+ element = new SVG.Gradient('radial')
+ else if (SVG[capitalize(node.nodeName)])
+ element = new SVG[capitalize(node.nodeName)]
+ else
+ element = new SVG.Element(node)
+
+ // ensure references
+ element.type = node.nodeName
+ element.node = node
+ node.instance = element
+
+ // SVG.Class specific preparations
+ if (element instanceof SVG.Doc)
+ element.namespace().defs()
+
+ // pull svgjs data from the dom (getAttributeNS doesn't work in html5)
+ element.setData(JSON.parse(node.getAttribute('svgjs:data')) || {})
+
+ return element
+}
+
+// Initialize parsing element
+SVG.prepare = function() {
+ // Select document body and create invisible svg element
+ var body = document.getElementsByTagName('body')[0]
+ , draw = (body ? new SVG.Doc(body) : SVG.adopt(document.documentElement).nested()).size(2, 0)
+
+ // Create parser object
+ SVG.parser = {
+ body: body || document.documentElement
+ , draw: draw.style('opacity:0;position:absolute;left:-100%;top:-100%;overflow:hidden').node
+ , poly: draw.polyline().node
+ , path: draw.path().node
+ , native: SVG.create('svg')
+ }
+}
+
+SVG.parser = {
+ native: SVG.create('svg')
+}
+
+document.addEventListener('DOMContentLoaded', function() {
+ if(!SVG.parser.draw)
+ SVG.prepare()
+}, false)
+
+// Storage for regular expressions
+SVG.regex = {
+ // Parse unit value
+ numberAndUnit: /^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i
+
+ // Parse hex value
+, hex: /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i
+
+ // Parse rgb value
+, rgb: /rgb\((\d+),(\d+),(\d+)\)/
+
+ // Parse reference id
+, reference: /#([a-z0-9\-_]+)/i
+
+ // splits a transformation chain
+, transforms: /\)\s*,?\s*/
+
+ // Whitespace
+, whitespace: /\s/g
+
+ // Test hex value
+, isHex: /^#[a-f0-9]{3,6}$/i
+
+ // Test rgb value
+, isRgb: /^rgb\(/
+
+ // Test css declaration
+, isCss: /[^:]+:[^;]+;?/
+
+ // Test for blank string
+, isBlank: /^(\s+)?$/
+
+ // Test for numeric string
+, isNumber: /^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i
+
+ // Test for percent value
+, isPercent: /^-?[\d\.]+%$/
+
+ // Test for image url
+, isImage: /\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i
+
+ // split at whitespace and comma
+, delimiter: /[\s,]+/
+
+ // The following regex are used to parse the d attribute of a path
+
+ // Matches all hyphens which are not after an exponent
+, hyphen: /([^e])\-/gi
+
+ // Replaces and tests for all path letters
+, pathLetters: /[MLHVCSQTAZ]/gi
+
+ // yes we need this one, too
+, isPathLetter: /[MLHVCSQTAZ]/i
+
+ // matches 0.154.23.45
+, numbersWithDots: /((\d?\.\d+(?:e[+-]?\d+)?)((?:\.\d+(?:e[+-]?\d+)?)+))+/gi
+
+ // matches .
+, dots: /\./g
+}
+
+SVG.utils = {
+ // Map function
+ map: function(array, block) {
+ var i
+ , il = array.length
+ , result = []
+
+ for (i = 0; i < il; i++)
+ result.push(block(array[i]))
+
+ return result
+ }
+
+ // Filter function
+, filter: function(array, block) {
+ var i
+ , il = array.length
+ , result = []
+
+ for (i = 0; i < il; i++)
+ if (block(array[i]))
+ result.push(array[i])
+
+ return result
+ }
+
+ // Degrees to radians
+, radians: function(d) {
+ return d % 360 * Math.PI / 180
+ }
+
+ // Radians to degrees
+, degrees: function(r) {
+ return r * 180 / Math.PI % 360
+ }
+
+, filterSVGElements: function(nodes) {
+ return this.filter( nodes, function(el) { return el instanceof window.SVGElement })
+ }
+
+}
+
+SVG.defaults = {
+ // Default attribute values
+ attrs: {
+ // fill and stroke
+ 'fill-opacity': 1
+ , 'stroke-opacity': 1
+ , 'stroke-width': 0
+ , 'stroke-linejoin': 'miter'
+ , 'stroke-linecap': 'butt'
+ , fill: '#000000'
+ , stroke: '#000000'
+ , opacity: 1
+ // position
+ , x: 0
+ , y: 0
+ , cx: 0
+ , cy: 0
+ // size
+ , width: 0
+ , height: 0
+ // radius
+ , r: 0
+ , rx: 0
+ , ry: 0
+ // gradient
+ , offset: 0
+ , 'stop-opacity': 1
+ , 'stop-color': '#000000'
+ // text
+ , 'font-size': 16
+ , 'font-family': 'Helvetica, Arial, sans-serif'
+ , 'text-anchor': 'start'
+ }
+
+}
+// Module for color convertions
+SVG.Color = function(color) {
+ var match
+
+ // initialize defaults
+ this.r = 0
+ this.g = 0
+ this.b = 0
+
+ if(!color) return
+
+ // parse color
+ if (typeof color === 'string') {
+ if (SVG.regex.isRgb.test(color)) {
+ // get rgb values
+ match = SVG.regex.rgb.exec(color.replace(SVG.regex.whitespace,''))
+
+ // parse numeric values
+ this.r = parseInt(match[1])
+ this.g = parseInt(match[2])
+ this.b = parseInt(match[3])
+
+ } else if (SVG.regex.isHex.test(color)) {
+ // get hex values
+ match = SVG.regex.hex.exec(fullHex(color))
+
+ // parse numeric values
+ this.r = parseInt(match[1], 16)
+ this.g = parseInt(match[2], 16)
+ this.b = parseInt(match[3], 16)
+
+ }
+
+ } else if (typeof color === 'object') {
+ this.r = color.r
+ this.g = color.g
+ this.b = color.b
+
+ }
+
+}
+
+SVG.extend(SVG.Color, {
+ // Default to hex conversion
+ toString: function() {
+ return this.toHex()
+ }
+ // Build hex value
+, toHex: function() {
+ return '#'
+ + compToHex(this.r)
+ + compToHex(this.g)
+ + compToHex(this.b)
+ }
+ // Build rgb value
+, toRgb: function() {
+ return 'rgb(' + [this.r, this.g, this.b].join() + ')'
+ }
+ // Calculate true brightness
+, brightness: function() {
+ return (this.r / 255 * 0.30)
+ + (this.g / 255 * 0.59)
+ + (this.b / 255 * 0.11)
+ }
+ // Make color morphable
+, morph: function(color) {
+ this.destination = new SVG.Color(color)
+
+ return this
+ }
+ // Get morphed color at given position
+, at: function(pos) {
+ // make sure a destination is defined
+ if (!this.destination) return this
+
+ // normalise pos
+ pos = pos < 0 ? 0 : pos > 1 ? 1 : pos
+
+ // generate morphed color
+ return new SVG.Color({
+ r: ~~(this.r + (this.destination.r - this.r) * pos)
+ , g: ~~(this.g + (this.destination.g - this.g) * pos)
+ , b: ~~(this.b + (this.destination.b - this.b) * pos)
+ })
+ }
+
+})
+
+// Testers
+
+// Test if given value is a color string
+SVG.Color.test = function(color) {
+ color += ''
+ return SVG.regex.isHex.test(color)
+ || SVG.regex.isRgb.test(color)
+}
+
+// Test if given value is a rgb object
+SVG.Color.isRgb = function(color) {
+ return color && typeof color.r == 'number'
+ && typeof color.g == 'number'
+ && typeof color.b == 'number'
+}
+
+// Test if given value is a color
+SVG.Color.isColor = function(color) {
+ return SVG.Color.isRgb(color) || SVG.Color.test(color)
+}
+// Module for array conversion
+SVG.Array = function(array, fallback) {
+ array = (array || []).valueOf()
+
+ // if array is empty and fallback is provided, use fallback
+ if (array.length == 0 && fallback)
+ array = fallback.valueOf()
+
+ // parse array
+ this.value = this.parse(array)
+}
+
+SVG.extend(SVG.Array, {
+ // Make array morphable
+ morph: function(array) {
+ this.destination = this.parse(array)
+
+ // normalize length of arrays
+ if (this.value.length != this.destination.length) {
+ var lastValue = this.value[this.value.length - 1]
+ , lastDestination = this.destination[this.destination.length - 1]
+
+ while(this.value.length > this.destination.length)
+ this.destination.push(lastDestination)
+ while(this.value.length < this.destination.length)
+ this.value.push(lastValue)
+ }
+
+ return this
+ }
+ // Clean up any duplicate points
+, settle: function() {
+ // find all unique values
+ for (var i = 0, il = this.value.length, seen = []; i < il; i++)
+ if (seen.indexOf(this.value[i]) == -1)
+ seen.push(this.value[i])
+
+ // set new value
+ return this.value = seen
+ }
+ // Get morphed array at given position
+, at: function(pos) {
+ // make sure a destination is defined
+ if (!this.destination) return this
+
+ // generate morphed array
+ for (var i = 0, il = this.value.length, array = []; i < il; i++)
+ array.push(this.value[i] + (this.destination[i] - this.value[i]) * pos)
+
+ return new SVG.Array(array)
+ }
+ // Convert array to string
+, toString: function() {
+ return this.value.join(' ')
+ }
+ // Real value
+, valueOf: function() {
+ return this.value
+ }
+ // Parse whitespace separated string
+, parse: function(array) {
+ array = array.valueOf()
+
+ // if already is an array, no need to parse it
+ if (Array.isArray(array)) return array
+
+ return this.split(array)
+ }
+ // Strip unnecessary whitespace
+, split: function(string) {
+ return string.trim().split(SVG.regex.delimiter).map(parseFloat)
+ }
+ // Reverse array
+, reverse: function() {
+ this.value.reverse()
+
+ return this
+ }
+, clone: function() {
+ var clone = new this.constructor()
+ clone.value = array_clone(this.value)
+ return clone
+ }
+})
+// Poly points array
+SVG.PointArray = function(array, fallback) {
+ SVG.Array.call(this, array, fallback || [[0,0]])
+}
+
+// Inherit from SVG.Array
+SVG.PointArray.prototype = new SVG.Array
+SVG.PointArray.prototype.constructor = SVG.PointArray
+
+SVG.extend(SVG.PointArray, {
+ // Convert array to string
+ toString: function() {
+ // convert to a poly point string
+ for (var i = 0, il = this.value.length, array = []; i < il; i++)
+ array.push(this.value[i].join(','))
+
+ return array.join(' ')
+ }
+ // Convert array to line object
+, toLine: function() {
+ return {
+ x1: this.value[0][0]
+ , y1: this.value[0][1]
+ , x2: this.value[1][0]
+ , y2: this.value[1][1]
+ }
+ }
+ // Get morphed array at given position
+, at: function(pos) {
+ // make sure a destination is defined
+ if (!this.destination) return this
+
+ // generate morphed point string
+ for (var i = 0, il = this.value.length, array = []; i < il; i++)
+ array.push([
+ this.value[i][0] + (this.destination[i][0] - this.value[i][0]) * pos
+ , this.value[i][1] + (this.destination[i][1] - this.value[i][1]) * pos
+ ])
+
+ return new SVG.PointArray(array)
+ }
+ // Parse point string and flat array
+, parse: function(array) {
+ var points = []
+
+ array = array.valueOf()
+
+ // if it is an array
+ if (Array.isArray(array)) {
+ // and it is not flat, there is no need to parse it
+ if(Array.isArray(array[0])) {
+ return array
+ }
+ } else { // Else, it is considered as a string
+ // parse points
+ array = array.trim().split(SVG.regex.delimiter).map(parseFloat)
+ }
+
+ // validate points - https://svgwg.org/svg2-draft/shapes.html#DataTypePoints
+ // Odd number of coordinates is an error. In such cases, drop the last odd coordinate.
+ if (array.length % 2 !== 0) array.pop()
+
+ // wrap points in two-tuples and parse points as floats
+ for(var i = 0, len = array.length; i < len; i = i + 2)
+ points.push([ array[i], array[i+1] ])
+
+ return points
+ }
+ // Move point string
+, move: function(x, y) {
+ var box = this.bbox()
+
+ // get relative offset
+ x -= box.x
+ y -= box.y
+
+ // move every point
+ if (!isNaN(x) && !isNaN(y))
+ for (var i = this.value.length - 1; i >= 0; i--)
+ this.value[i] = [this.value[i][0] + x, this.value[i][1] + y]
+
+ return this
+ }
+ // Resize poly string
+, size: function(width, height) {
+ var i, box = this.bbox()
+
+ // recalculate position of all points according to new size
+ for (i = this.value.length - 1; i >= 0; i--) {
+ if(box.width) this.value[i][0] = ((this.value[i][0] - box.x) * width) / box.width + box.x
+ if(box.height) this.value[i][1] = ((this.value[i][1] - box.y) * height) / box.height + box.y
+ }
+
+ return this
+ }
+ // Get bounding box of points
+, bbox: function() {
+ SVG.parser.poly.setAttribute('points', this.toString())
+
+ return SVG.parser.poly.getBBox()
+ }
+})
+
+var pathHandlers = {
+ M: function(c, p, p0) {
+ p.x = p0.x = c[0]
+ p.y = p0.y = c[1]
+
+ return ['M', p.x, p.y]
+ },
+ L: function(c, p) {
+ p.x = c[0]
+ p.y = c[1]
+ return ['L', c[0], c[1]]
+ },
+ H: function(c, p) {
+ p.x = c[0]
+ return ['H', c[0]]
+ },
+ V: function(c, p) {
+ p.y = c[0]
+ return ['V', c[0]]
+ },
+ C: function(c, p) {
+ p.x = c[4]
+ p.y = c[5]
+ return ['C', c[0], c[1], c[2], c[3], c[4], c[5]]
+ },
+ S: function(c, p) {
+ p.x = c[2]
+ p.y = c[3]
+ return ['S', c[0], c[1], c[2], c[3]]
+ },
+ Q: function(c, p) {
+ p.x = c[2]
+ p.y = c[3]
+ return ['Q', c[0], c[1], c[2], c[3]]
+ },
+ T: function(c, p) {
+ p.x = c[0]
+ p.y = c[1]
+ return ['T', c[0], c[1]]
+ },
+ Z: function(c, p, p0) {
+ p.x = p0.x
+ p.y = p0.y
+ return ['Z']
+ },
+ A: function(c, p) {
+ p.x = c[5]
+ p.y = c[6]
+ return ['A', c[0], c[1], c[2], c[3], c[4], c[5], c[6]]
+ }
+}
+
+var mlhvqtcsa = 'mlhvqtcsaz'.split('')
+
+for(var i = 0, il = mlhvqtcsa.length; i < il; ++i){
+ pathHandlers[mlhvqtcsa[i]] = (function(i){
+ return function(c, p, p0) {
+ if(i == 'H') c[0] = c[0] + p.x
+ else if(i == 'V') c[0] = c[0] + p.y
+ else if(i == 'A'){
+ c[5] = c[5] + p.x,
+ c[6] = c[6] + p.y
+ }
+ else
+ for(var j = 0, jl = c.length; j < jl; ++j) {
+ c[j] = c[j] + (j%2 ? p.y : p.x)
+ }
+
+ return pathHandlers[i](c, p, p0)
+ }
+ })(mlhvqtcsa[i].toUpperCase())
+}
+
+// Path points array
+SVG.PathArray = function(array, fallback) {
+ SVG.Array.call(this, array, fallback || [['M', 0, 0]])
+}
+
+// Inherit from SVG.Array
+SVG.PathArray.prototype = new SVG.Array
+SVG.PathArray.prototype.constructor = SVG.PathArray
+
+SVG.extend(SVG.PathArray, {
+ // Convert array to string
+ toString: function() {
+ return arrayToString(this.value)
+ }
+ // Move path string
+, move: function(x, y) {
+ // get bounding box of current situation
+ var box = this.bbox()
+
+ // get relative offset
+ x -= box.x
+ y -= box.y
+
+ if (!isNaN(x) && !isNaN(y)) {
+ // move every point
+ for (var l, i = this.value.length - 1; i >= 0; i--) {
+ l = this.value[i][0]
+
+ if (l == 'M' || l == 'L' || l == 'T') {
+ this.value[i][1] += x
+ this.value[i][2] += y
+
+ } else if (l == 'H') {
+ this.value[i][1] += x
+
+ } else if (l == 'V') {
+ this.value[i][1] += y
+
+ } else if (l == 'C' || l == 'S' || l == 'Q') {
+ this.value[i][1] += x
+ this.value[i][2] += y
+ this.value[i][3] += x
+ this.value[i][4] += y
+
+ if (l == 'C') {
+ this.value[i][5] += x
+ this.value[i][6] += y
+ }
+
+ } else if (l == 'A') {
+ this.value[i][6] += x
+ this.value[i][7] += y
+ }
+
+ }
+ }
+
+ return this
+ }
+ // Resize path string
+, size: function(width, height) {
+ // get bounding box of current situation
+ var i, l, box = this.bbox()
+
+ // recalculate position of all points according to new size
+ for (i = this.value.length - 1; i >= 0; i--) {
+ l = this.value[i][0]
+
+ if (l == 'M' || l == 'L' || l == 'T') {
+ this.value[i][1] = ((this.value[i][1] - box.x) * width) / box.width + box.x
+ this.value[i][2] = ((this.value[i][2] - box.y) * height) / box.height + box.y
+
+ } else if (l == 'H') {
+ this.value[i][1] = ((this.value[i][1] - box.x) * width) / box.width + box.x
+
+ } else if (l == 'V') {
+ this.value[i][1] = ((this.value[i][1] - box.y) * height) / box.height + box.y
+
+ } else if (l == 'C' || l == 'S' || l == 'Q') {
+ this.value[i][1] = ((this.value[i][1] - box.x) * width) / box.width + box.x
+ this.value[i][2] = ((this.value[i][2] - box.y) * height) / box.height + box.y
+ this.value[i][3] = ((this.value[i][3] - box.x) * width) / box.width + box.x
+ this.value[i][4] = ((this.value[i][4] - box.y) * height) / box.height + box.y
+
+ if (l == 'C') {
+ this.value[i][5] = ((this.value[i][5] - box.x) * width) / box.width + box.x
+ this.value[i][6] = ((this.value[i][6] - box.y) * height) / box.height + box.y
+ }
+
+ } else if (l == 'A') {
+ // resize radii
+ this.value[i][1] = (this.value[i][1] * width) / box.width
+ this.value[i][2] = (this.value[i][2] * height) / box.height
+
+ // move position values
+ this.value[i][6] = ((this.value[i][6] - box.x) * width) / box.width + box.x
+ this.value[i][7] = ((this.value[i][7] - box.y) * height) / box.height + box.y
+ }
+
+ }
+
+ return this
+ }
+ // Test if the passed path array use the same path data commands as this path array
+, equalCommands: function(pathArray) {
+ var i, il, equalCommands
+
+ pathArray = new SVG.PathArray(pathArray)
+
+ equalCommands = this.value.length === pathArray.value.length
+ for(i = 0, il = this.value.length; equalCommands && i < il; i++) {
+ equalCommands = this.value[i][0] === pathArray.value[i][0]
+ }
+
+ return equalCommands
+ }
+ // Make path array morphable
+, morph: function(pathArray) {
+ pathArray = new SVG.PathArray(pathArray)
+
+ if(this.equalCommands(pathArray)) {
+ this.destination = pathArray
+ } else {
+ this.destination = null
+ }
+
+ return this
+ }
+ // Get morphed path array at given position
+, at: function(pos) {
+ // make sure a destination is defined
+ if (!this.destination) return this
+
+ var sourceArray = this.value
+ , destinationArray = this.destination.value
+ , array = [], pathArray = new SVG.PathArray()
+ , i, il, j, jl
+
+ // Animate has specified in the SVG spec
+ // See: https://www.w3.org/TR/SVG11/paths.html#PathElement
+ for (i = 0, il = sourceArray.length; i < il; i++) {
+ array[i] = [sourceArray[i][0]]
+ for(j = 1, jl = sourceArray[i].length; j < jl; j++) {
+ array[i][j] = sourceArray[i][j] + (destinationArray[i][j] - sourceArray[i][j]) * pos
+ }
+ // For the two flags of the elliptical arc command, the SVG spec say:
+ // Flags and booleans are interpolated as fractions between zero and one, with any non-zero value considered to be a value of one/true
+ // Elliptical arc command as an array followed by corresponding indexes:
+ // ['A', rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y]
+ // 0 1 2 3 4 5 6 7
+ if(array[i][0] === 'A') {
+ array[i][4] = +(array[i][4] != 0)
+ array[i][5] = +(array[i][5] != 0)
+ }
+ }
+
+ // Directly modify the value of a path array, this is done this way for performance
+ pathArray.value = array
+ return pathArray
+ }
+ // Absolutize and parse path to array
+, parse: function(array) {
+ // if it's already a patharray, no need to parse it
+ if (array instanceof SVG.PathArray) return array.valueOf()
+
+ // prepare for parsing
+ var i, x0, y0, s, seg, arr
+ , x = 0
+ , y = 0
+ , paramCnt = { 'M':2, 'L':2, 'H':1, 'V':1, 'C':6, 'S':4, 'Q':4, 'T':2, 'A':7, 'Z':0 }
+
+ if(typeof array == 'string'){
+
+ array = array
+ .replace(SVG.regex.numbersWithDots, pathRegReplace) // convert 45.123.123 to 45.123 .123
+ .replace(SVG.regex.pathLetters, ' $& ') // put some room between letters and numbers
+ .replace(SVG.regex.hyphen, '$1 -') // add space before hyphen
+ .trim() // trim
+ .split(SVG.regex.delimiter) // split into array
+
+ }else{
+ array = array.reduce(function(prev, curr){
+ return [].concat.call(prev, curr)
+ }, [])
+ }
+
+ // array now is an array containing all parts of a path e.g. ['M', '0', '0', 'L', '30', '30' ...]
+ var arr = []
+ , p = new SVG.Point()
+ , p0 = new SVG.Point()
+ , index = 0
+ , len = array.length
+
+ do{
+ // Test if we have a path letter
+ if(SVG.regex.isPathLetter.test(array[index])){
+ s = array[index]
+ ++index
+ // If last letter was a move command and we got no new, it defaults to [L]ine
+ }else if(s == 'M'){
+ s = 'L'
+ }else if(s == 'm'){
+ s = 'l'
+ }
+
+ arr.push(pathHandlers[s].call(null,
+ array.slice(index, (index = index + paramCnt[s.toUpperCase()])).map(parseFloat),
+ p, p0
+ )
+ )
+
+ }while(len > index)
+
+ return arr
+
+ }
+ // Get bounding box of path
+, bbox: function() {
+ SVG.parser.path.setAttribute('d', this.toString())
+
+ return SVG.parser.path.getBBox()
+ }
+
+})
+
+// Module for unit convertions
+SVG.Number = SVG.invent({
+ // Initialize
+ create: function(value, unit) {
+ // initialize defaults
+ this.value = 0
+ this.unit = unit || ''
+
+ // parse value
+ if (typeof value === 'number') {
+ // ensure a valid numeric value
+ this.value = isNaN(value) ? 0 : !isFinite(value) ? (value < 0 ? -3.4e+38 : +3.4e+38) : value
+
+ } else if (typeof value === 'string') {
+ unit = value.match(SVG.regex.numberAndUnit)
+
+ if (unit) {
+ // make value numeric
+ this.value = parseFloat(unit[1])
+
+ // normalize
+ if (unit[5] == '%')
+ this.value /= 100
+ else if (unit[5] == 's')
+ this.value *= 1000
+
+ // store unit
+ this.unit = unit[5]
+ }
+
+ } else {
+ if (value instanceof SVG.Number) {
+ this.value = value.valueOf()
+ this.unit = value.unit
+ }
+ }
+
+ }
+ // Add methods
+, extend: {
+ // Stringalize
+ toString: function() {
+ return (
+ this.unit == '%' ?
+ ~~(this.value * 1e8) / 1e6:
+ this.unit == 's' ?
+ this.value / 1e3 :
+ this.value
+ ) + this.unit
+ }
+ , toJSON: function() {
+ return this.toString()
+ }
+ , // Convert to primitive
+ valueOf: function() {
+ return this.value
+ }
+ // Add number
+ , plus: function(number) {
+ number = new SVG.Number(number)
+ return new SVG.Number(this + number, this.unit || number.unit)
+ }
+ // Subtract number
+ , minus: function(number) {
+ number = new SVG.Number(number)
+ return new SVG.Number(this - number, this.unit || number.unit)
+ }
+ // Multiply number
+ , times: function(number) {
+ number = new SVG.Number(number)
+ return new SVG.Number(this * number, this.unit || number.unit)
+ }
+ // Divide number
+ , divide: function(number) {
+ number = new SVG.Number(number)
+ return new SVG.Number(this / number, this.unit || number.unit)
+ }
+ // Convert to different unit
+ , to: function(unit) {
+ var number = new SVG.Number(this)
+
+ if (typeof unit === 'string')
+ number.unit = unit
+
+ return number
+ }
+ // Make number morphable
+ , morph: function(number) {
+ this.destination = new SVG.Number(number)
+
+ if(number.relative) {
+ this.destination.value += this.value
+ }
+
+ return this
+ }
+ // Get morphed number at given position
+ , at: function(pos) {
+ // Make sure a destination is defined
+ if (!this.destination) return this
+
+ // Generate new morphed number
+ return new SVG.Number(this.destination)
+ .minus(this)
+ .times(pos)
+ .plus(this)
+ }
+
+ }
+})
+
+
+SVG.Element = SVG.invent({
+ // Initialize node
+ create: function(node) {
+ // make stroke value accessible dynamically
+ this._stroke = SVG.defaults.attrs.stroke
+ this._event = null
+
+ // initialize data object
+ this.dom = {}
+
+ // create circular reference
+ if (this.node = node) {
+ this.type = node.nodeName
+ this.node.instance = this
+
+ // store current attribute value
+ this._stroke = node.getAttribute('stroke') || this._stroke
+ }
+ }
+
+ // Add class methods
+, extend: {
+ // Move over x-axis
+ x: function(x) {
+ return this.attr('x', x)
+ }
+ // Move over y-axis
+ , y: function(y) {
+ return this.attr('y', y)
+ }
+ // Move by center over x-axis
+ , cx: function(x) {
+ return x == null ? this.x() + this.width() / 2 : this.x(x - this.width() / 2)
+ }
+ // Move by center over y-axis
+ , cy: function(y) {
+ return y == null ? this.y() + this.height() / 2 : this.y(y - this.height() / 2)
+ }
+ // Move element to given x and y values
+ , move: function(x, y) {
+ return this.x(x).y(y)
+ }
+ // Move element by its center
+ , center: function(x, y) {
+ return this.cx(x).cy(y)
+ }
+ // Set width of element
+ , width: function(width) {
+ return this.attr('width', width)
+ }
+ // Set height of element
+ , height: function(height) {
+ return this.attr('height', height)
+ }
+ // Set element size to given width and height
+ , size: function(width, height) {
+ var p = proportionalSize(this, width, height)
+
+ return this
+ .width(new SVG.Number(p.width))
+ .height(new SVG.Number(p.height))
+ }
+ // Clone element
+ , clone: function(parent, withData) {
+ // write dom data to the dom so the clone can pickup the data
+ this.writeDataToDom()
+
+ // clone element and assign new id
+ var clone = assignNewId(this.node.cloneNode(true))
+
+ // insert the clone in the given parent or after myself
+ if(parent) parent.add(clone)
+ else this.after(clone)
+
+ return clone
+ }
+ // Remove element
+ , remove: function() {
+ if (this.parent())
+ this.parent().removeElement(this)
+
+ return this
+ }
+ // Replace element
+ , replace: function(element) {
+ this.after(element).remove()
+
+ return element
+ }
+ // Add element to given container and return self
+ , addTo: function(parent) {
+ return parent.put(this)
+ }
+ // Add element to given container and return container
+ , putIn: function(parent) {
+ return parent.add(this)
+ }
+ // Get / set id
+ , id: function(id) {
+ return this.attr('id', id)
+ }
+ // Checks whether the given point inside the bounding box of the element
+ , inside: function(x, y) {
+ var box = this.bbox()
+
+ return x > box.x
+ && y > box.y
+ && x < box.x + box.width
+ && y < box.y + box.height
+ }
+ // Show element
+ , show: function() {
+ return this.style('display', '')
+ }
+ // Hide element
+ , hide: function() {
+ return this.style('display', 'none')
+ }
+ // Is element visible?
+ , visible: function() {
+ return this.style('display') != 'none'
+ }
+ // Return id on string conversion
+ , toString: function() {
+ return this.attr('id')
+ }
+ // Return array of classes on the node
+ , classes: function() {
+ var attr = this.attr('class')
+
+ return attr == null ? [] : attr.trim().split(SVG.regex.delimiter)
+ }
+ // Return true if class exists on the node, false otherwise
+ , hasClass: function(name) {
+ return this.classes().indexOf(name) != -1
+ }
+ // Add class to the node
+ , addClass: function(name) {
+ if (!this.hasClass(name)) {
+ var array = this.classes()
+ array.push(name)
+ this.attr('class', array.join(' '))
+ }
+
+ return this
+ }
+ // Remove class from the node
+ , removeClass: function(name) {
+ if (this.hasClass(name)) {
+ this.attr('class', this.classes().filter(function(c) {
+ return c != name
+ }).join(' '))
+ }
+
+ return this
+ }
+ // Toggle the presence of a class on the node
+ , toggleClass: function(name) {
+ return this.hasClass(name) ? this.removeClass(name) : this.addClass(name)
+ }
+ // Get referenced element form attribute value
+ , reference: function(attr) {
+ return SVG.get(this.attr(attr))
+ }
+ // Returns the parent element instance
+ , parent: function(type) {
+ var parent = this
+
+ // check for parent
+ if(!parent.node.parentNode) return null
+
+ // get parent element
+ parent = SVG.adopt(parent.node.parentNode)
+
+ if(!type) return parent
+
+ // loop trough ancestors if type is given
+ while(parent && parent.node instanceof window.SVGElement){
+ if(typeof type === 'string' ? parent.matches(type) : parent instanceof type) return parent
+ parent = SVG.adopt(parent.node.parentNode)
+ }
+ }
+ // Get parent document
+ , doc: function() {
+ return this instanceof SVG.Doc ? this : this.parent(SVG.Doc)
+ }
+ // return array of all ancestors of given type up to the root svg
+ , parents: function(type) {
+ var parents = [], parent = this
+
+ do{
+ parent = parent.parent(type)
+ if(!parent || !parent.node) break
+
+ parents.push(parent)
+ } while(parent.parent)
+
+ return parents
+ }
+ // matches the element vs a css selector
+ , matches: function(selector){
+ return matches(this.node, selector)
+ }
+ // Returns the svg node to call native svg methods on it
+ , native: function() {
+ return this.node
+ }
+ // Import raw svg
+ , svg: function(svg) {
+ // create temporary holder
+ var well = document.createElement('svg')
+
+ // act as a setter if svg is given
+ if (svg && this instanceof SVG.Parent) {
+ // dump raw svg
+ well.innerHTML = '' + svg.replace(/\n/, '').replace(/<(\w+)([^<]+?)\/>/g, '<$1$2>$1>') + ' '
+
+ // transplant nodes
+ for (var i = 0, il = well.firstChild.childNodes.length; i < il; i++)
+ this.node.appendChild(well.firstChild.firstChild)
+
+ // otherwise act as a getter
+ } else {
+ // create a wrapping svg element in case of partial content
+ well.appendChild(svg = document.createElement('svg'))
+
+ // write svgjs data to the dom
+ this.writeDataToDom()
+
+ // insert a copy of this node
+ svg.appendChild(this.node.cloneNode(true))
+
+ // return target element
+ return well.innerHTML.replace(/^/, '').replace(/<\/svg>$/, '')
+ }
+
+ return this
+ }
+ // write svgjs data to the dom
+ , writeDataToDom: function() {
+
+ // dump variables recursively
+ if(this.each || this.lines){
+ var fn = this.each ? this : this.lines();
+ fn.each(function(){
+ this.writeDataToDom()
+ })
+ }
+
+ // remove previously set data
+ this.node.removeAttribute('svgjs:data')
+
+ if(Object.keys(this.dom).length)
+ this.node.setAttribute('svgjs:data', JSON.stringify(this.dom)) // see #428
+
+ return this
+ }
+ // set given data to the elements data property
+ , setData: function(o){
+ this.dom = o
+ return this
+ }
+ , is: function(obj){
+ return is(this, obj)
+ }
+ }
+})
+
+SVG.easing = {
+ '-': function(pos){return pos}
+, '<>':function(pos){return -Math.cos(pos * Math.PI) / 2 + 0.5}
+, '>': function(pos){return Math.sin(pos * Math.PI / 2)}
+, '<': function(pos){return -Math.cos(pos * Math.PI / 2) + 1}
+}
+
+SVG.morph = function(pos){
+ return function(from, to) {
+ return new SVG.MorphObj(from, to).at(pos)
+ }
+}
+
+SVG.Situation = SVG.invent({
+
+ create: function(o){
+ this.init = false
+ this.reversed = false
+ this.reversing = false
+
+ this.duration = new SVG.Number(o.duration).valueOf()
+ this.delay = new SVG.Number(o.delay).valueOf()
+
+ this.start = +new Date() + this.delay
+ this.finish = this.start + this.duration
+ this.ease = o.ease
+
+ // this.loop is incremented from 0 to this.loops
+ // it is also incremented when in an infinite loop (when this.loops is true)
+ this.loop = 0
+ this.loops = false
+
+ this.animations = {
+ // functionToCall: [list of morphable objects]
+ // e.g. move: [SVG.Number, SVG.Number]
+ }
+
+ this.attrs = {
+ // holds all attributes which are not represented from a function svg.js provides
+ // e.g. someAttr: SVG.Number
+ }
+
+ this.styles = {
+ // holds all styles which should be animated
+ // e.g. fill-color: SVG.Color
+ }
+
+ this.transforms = [
+ // holds all transformations as transformation objects
+ // e.g. [SVG.Rotate, SVG.Translate, SVG.Matrix]
+ ]
+
+ this.once = {
+ // functions to fire at a specific position
+ // e.g. "0.5": function foo(){}
+ }
+
+ }
+
+})
+
+
+SVG.FX = SVG.invent({
+
+ create: function(element) {
+ this._target = element
+ this.situations = []
+ this.active = false
+ this.situation = null
+ this.paused = false
+ this.lastPos = 0
+ this.pos = 0
+ // The absolute position of an animation is its position in the context of its complete duration (including delay and loops)
+ // When performing a delay, absPos is below 0 and when performing a loop, its value is above 1
+ this.absPos = 0
+ this._speed = 1
+ }
+
+, extend: {
+
+ /**
+ * sets or returns the target of this animation
+ * @param o object || number In case of Object it holds all parameters. In case of number its the duration of the animation
+ * @param ease function || string Function which should be used for easing or easing keyword
+ * @param delay Number indicating the delay before the animation starts
+ * @return target || this
+ */
+ animate: function(o, ease, delay){
+
+ if(typeof o == 'object'){
+ ease = o.ease
+ delay = o.delay
+ o = o.duration
+ }
+
+ var situation = new SVG.Situation({
+ duration: o || 1000,
+ delay: delay || 0,
+ ease: SVG.easing[ease || '-'] || ease
+ })
+
+ this.queue(situation)
+
+ return this
+ }
+
+ /**
+ * sets a delay before the next element of the queue is called
+ * @param delay Duration of delay in milliseconds
+ * @return this.target()
+ */
+ , delay: function(delay){
+ // The delay is performed by an empty situation with its duration
+ // attribute set to the duration of the delay
+ var situation = new SVG.Situation({
+ duration: delay,
+ delay: 0,
+ ease: SVG.easing['-']
+ })
+
+ return this.queue(situation)
+ }
+
+ /**
+ * sets or returns the target of this animation
+ * @param null || target SVG.Element which should be set as new target
+ * @return target || this
+ */
+ , target: function(target){
+ if(target && target instanceof SVG.Element){
+ this._target = target
+ return this
+ }
+
+ return this._target
+ }
+
+ // returns the absolute position at a given time
+ , timeToAbsPos: function(timestamp){
+ return (timestamp - this.situation.start) / (this.situation.duration/this._speed)
+ }
+
+ // returns the timestamp from a given absolute positon
+ , absPosToTime: function(absPos){
+ return this.situation.duration/this._speed * absPos + this.situation.start
+ }
+
+ // starts the animationloop
+ , startAnimFrame: function(){
+ this.stopAnimFrame()
+ this.animationFrame = window.requestAnimationFrame(function(){ this.step() }.bind(this))
+ }
+
+ // cancels the animationframe
+ , stopAnimFrame: function(){
+ window.cancelAnimationFrame(this.animationFrame)
+ }
+
+ // kicks off the animation - only does something when the queue is currently not active and at least one situation is set
+ , start: function(){
+ // dont start if already started
+ if(!this.active && this.situation){
+ this.active = true
+ this.startCurrent()
+ }
+
+ return this
+ }
+
+ // start the current situation
+ , startCurrent: function(){
+ this.situation.start = +new Date + this.situation.delay/this._speed
+ this.situation.finish = this.situation.start + this.situation.duration/this._speed
+ return this.initAnimations().step()
+ }
+
+ /**
+ * adds a function / Situation to the animation queue
+ * @param fn function / situation to add
+ * @return this
+ */
+ , queue: function(fn){
+ if(typeof fn == 'function' || fn instanceof SVG.Situation)
+ this.situations.push(fn)
+
+ if(!this.situation) this.situation = this.situations.shift()
+
+ return this
+ }
+
+ /**
+ * pulls next element from the queue and execute it
+ * @return this
+ */
+ , dequeue: function(){
+ // stop current animation
+ this.stop()
+
+ // get next animation from queue
+ this.situation = this.situations.shift()
+
+ if(this.situation){
+ if(this.situation instanceof SVG.Situation) {
+ this.start()
+ } else {
+ // If it is not a SVG.Situation, then it is a function, we execute it
+ this.situation.call(this)
+ }
+ }
+
+ return this
+ }
+
+ // updates all animations to the current state of the element
+ // this is important when one property could be changed from another property
+ , initAnimations: function() {
+ var i, source
+ var s = this.situation
+
+ if(s.init) return this
+
+ for(i in s.animations){
+ source = this.target()[i]()
+
+ // The condition is because some methods return a normal number instead
+ // of a SVG.Number
+ if(s.animations[i] instanceof SVG.Number)
+ source = new SVG.Number(source)
+
+ s.animations[i] = source.morph(s.animations[i])
+ }
+
+ for(i in s.attrs){
+ s.attrs[i] = new SVG.MorphObj(this.target().attr(i), s.attrs[i])
+ }
+
+ for(i in s.styles){
+ s.styles[i] = new SVG.MorphObj(this.target().style(i), s.styles[i])
+ }
+
+ s.initialTransformation = this.target().matrixify()
+
+ s.init = true
+ return this
+ }
+ , clearQueue: function(){
+ this.situations = []
+ return this
+ }
+ , clearCurrent: function(){
+ this.situation = null
+ return this
+ }
+ /** stops the animation immediately
+ * @param jumpToEnd A Boolean indicating whether to complete the current animation immediately.
+ * @param clearQueue A Boolean indicating whether to remove queued animation as well.
+ * @return this
+ */
+ , stop: function(jumpToEnd, clearQueue){
+ var active = this.active
+ this.active = false
+
+ if(clearQueue){
+ this.clearQueue()
+ }
+
+ if(jumpToEnd && this.situation){
+ // initialize the situation if it was not
+ !active && this.startCurrent()
+ this.atEnd()
+ }
+
+ this.stopAnimFrame()
+
+ return this.clearCurrent()
+ }
+
+ /** resets the element to the state where the current element has started
+ * @return this
+ */
+ , reset: function(){
+ if(this.situation){
+ var temp = this.situation
+ this.stop()
+ this.situation = temp
+ this.atStart()
+ }
+ return this
+ }
+
+ // Stop the currently-running animation, remove all queued animations, and complete all animations for the element.
+ , finish: function(){
+
+ this.stop(true, false)
+
+ while(this.dequeue().situation && this.stop(true, false));
+
+ this.clearQueue().clearCurrent()
+
+ return this
+ }
+
+ // set the internal animation pointer at the start position, before any loops, and updates the visualisation
+ , atStart: function() {
+ return this.at(0, true)
+ }
+
+ // set the internal animation pointer at the end position, after all the loops, and updates the visualisation
+ , atEnd: function() {
+ if (this.situation.loops === true) {
+ // If in a infinite loop, we end the current iteration
+ this.situation.loops = this.situation.loop + 1
+ }
+
+ if(typeof this.situation.loops == 'number') {
+ // If performing a finite number of loops, we go after all the loops
+ return this.at(this.situation.loops, true)
+ } else {
+ // If no loops, we just go at the end
+ return this.at(1, true)
+ }
+ }
+
+ // set the internal animation pointer to the specified position and updates the visualisation
+ // if isAbsPos is true, pos is treated as an absolute position
+ , at: function(pos, isAbsPos){
+ var durDivSpd = this.situation.duration/this._speed
+
+ this.absPos = pos
+ // If pos is not an absolute position, we convert it into one
+ if (!isAbsPos) {
+ if (this.situation.reversed) this.absPos = 1 - this.absPos
+ this.absPos += this.situation.loop
+ }
+
+ this.situation.start = +new Date - this.absPos * durDivSpd
+ this.situation.finish = this.situation.start + durDivSpd
+
+ return this.step(true)
+ }
+
+ /**
+ * sets or returns the speed of the animations
+ * @param speed null || Number The new speed of the animations
+ * @return Number || this
+ */
+ , speed: function(speed){
+ if (speed === 0) return this.pause()
+
+ if (speed) {
+ this._speed = speed
+ // We use an absolute position here so that speed can affect the delay before the animation
+ return this.at(this.absPos, true)
+ } else return this._speed
+ }
+
+ // Make loopable
+ , loop: function(times, reverse) {
+ var c = this.last()
+
+ // store total loops
+ c.loops = (times != null) ? times : true
+ c.loop = 0
+
+ if(reverse) c.reversing = true
+ return this
+ }
+
+ // pauses the animation
+ , pause: function(){
+ this.paused = true
+ this.stopAnimFrame()
+
+ return this
+ }
+
+ // unpause the animation
+ , play: function(){
+ if(!this.paused) return this
+ this.paused = false
+ // We use an absolute position here so that the delay before the animation can be paused
+ return this.at(this.absPos, true)
+ }
+
+ /**
+ * toggle or set the direction of the animation
+ * true sets direction to backwards while false sets it to forwards
+ * @param reversed Boolean indicating whether to reverse the animation or not (default: toggle the reverse status)
+ * @return this
+ */
+ , reverse: function(reversed){
+ var c = this.last()
+
+ if(typeof reversed == 'undefined') c.reversed = !c.reversed
+ else c.reversed = reversed
+
+ return this
+ }
+
+
+ /**
+ * returns a float from 0-1 indicating the progress of the current animation
+ * @param eased Boolean indicating whether the returned position should be eased or not
+ * @return number
+ */
+ , progress: function(easeIt){
+ return easeIt ? this.situation.ease(this.pos) : this.pos
+ }
+
+ /**
+ * adds a callback function which is called when the current animation is finished
+ * @param fn Function which should be executed as callback
+ * @return number
+ */
+ , after: function(fn){
+ var c = this.last()
+ , wrapper = function wrapper(e){
+ if(e.detail.situation == c){
+ fn.call(this, c)
+ this.off('finished.fx', wrapper) // prevent memory leak
+ }
+ }
+
+ this.target().on('finished.fx', wrapper)
+
+ return this._callStart()
+ }
+
+ // adds a callback which is called whenever one animation step is performed
+ , during: function(fn){
+ var c = this.last()
+ , wrapper = function(e){
+ if(e.detail.situation == c){
+ fn.call(this, e.detail.pos, SVG.morph(e.detail.pos), e.detail.eased, c)
+ }
+ }
+
+ // see above
+ this.target().off('during.fx', wrapper).on('during.fx', wrapper)
+
+ this.after(function(){
+ this.off('during.fx', wrapper)
+ })
+
+ return this._callStart()
+ }
+
+ // calls after ALL animations in the queue are finished
+ , afterAll: function(fn){
+ var wrapper = function wrapper(e){
+ fn.call(this)
+ this.off('allfinished.fx', wrapper)
+ }
+
+ // see above
+ this.target().off('allfinished.fx', wrapper).on('allfinished.fx', wrapper)
+
+ return this._callStart()
+ }
+
+ // calls on every animation step for all animations
+ , duringAll: function(fn){
+ var wrapper = function(e){
+ fn.call(this, e.detail.pos, SVG.morph(e.detail.pos), e.detail.eased, e.detail.situation)
+ }
+
+ this.target().off('during.fx', wrapper).on('during.fx', wrapper)
+
+ this.afterAll(function(){
+ this.off('during.fx', wrapper)
+ })
+
+ return this._callStart()
+ }
+
+ , last: function(){
+ return this.situations.length ? this.situations[this.situations.length-1] : this.situation
+ }
+
+ // adds one property to the animations
+ , add: function(method, args, type){
+ this.last()[type || 'animations'][method] = args
+ return this._callStart()
+ }
+
+ /** perform one step of the animation
+ * @param ignoreTime Boolean indicating whether to ignore time and use position directly or recalculate position based on time
+ * @return this
+ */
+ , step: function(ignoreTime){
+
+ // convert current time to an absolute position
+ if(!ignoreTime) this.absPos = this.timeToAbsPos(+new Date)
+
+ // This part convert an absolute position to a position
+ if(this.situation.loops !== false) {
+ var absPos, absPosInt, lastLoop
+
+ // If the absolute position is below 0, we just treat it as if it was 0
+ absPos = Math.max(this.absPos, 0)
+ absPosInt = Math.floor(absPos)
+
+ if(this.situation.loops === true || absPosInt < this.situation.loops) {
+ this.pos = absPos - absPosInt
+ lastLoop = this.situation.loop
+ this.situation.loop = absPosInt
+ } else {
+ this.absPos = this.situation.loops
+ this.pos = 1
+ // The -1 here is because we don't want to toggle reversed when all the loops have been completed
+ lastLoop = this.situation.loop - 1
+ this.situation.loop = this.situation.loops
+ }
+
+ if(this.situation.reversing) {
+ // Toggle reversed if an odd number of loops as occured since the last call of step
+ this.situation.reversed = this.situation.reversed != Boolean((this.situation.loop - lastLoop) % 2)
+ }
+
+ } else {
+ // If there are no loop, the absolute position must not be above 1
+ this.absPos = Math.min(this.absPos, 1)
+ this.pos = this.absPos
+ }
+
+ // while the absolute position can be below 0, the position must not be below 0
+ if(this.pos < 0) this.pos = 0
+
+ if(this.situation.reversed) this.pos = 1 - this.pos
+
+
+ // apply easing
+ var eased = this.situation.ease(this.pos)
+
+ // call once-callbacks
+ for(var i in this.situation.once){
+ if(i > this.lastPos && i <= eased){
+ this.situation.once[i].call(this.target(), this.pos, eased)
+ delete this.situation.once[i]
+ }
+ }
+
+ // fire during callback with position, eased position and current situation as parameter
+ if(this.active) this.target().fire('during', {pos: this.pos, eased: eased, fx: this, situation: this.situation})
+
+ // the user may call stop or finish in the during callback
+ // so make sure that we still have a valid situation
+ if(!this.situation){
+ return this
+ }
+
+ // apply the actual animation to every property
+ this.eachAt()
+
+ // do final code when situation is finished
+ if((this.pos == 1 && !this.situation.reversed) || (this.situation.reversed && this.pos == 0)){
+
+ // stop animation callback
+ this.stopAnimFrame()
+
+ // fire finished callback with current situation as parameter
+ this.target().fire('finished', {fx:this, situation: this.situation})
+
+ if(!this.situations.length){
+ this.target().fire('allfinished')
+ this.target().off('.fx') // there shouldnt be any binding left, but to make sure...
+ this.active = false
+ }
+
+ // start next animation
+ if(this.active) this.dequeue()
+ else this.clearCurrent()
+
+ }else if(!this.paused && this.active){
+ // we continue animating when we are not at the end
+ this.startAnimFrame()
+ }
+
+ // save last eased position for once callback triggering
+ this.lastPos = eased
+ return this
+
+ }
+
+ // calculates the step for every property and calls block with it
+ , eachAt: function(){
+ var i, len, at, self = this, target = this.target(), s = this.situation
+
+ // apply animations which can be called trough a method
+ for(i in s.animations){
+
+ at = [].concat(s.animations[i]).map(function(el){
+ return typeof el !== 'string' && el.at ? el.at(s.ease(self.pos), self.pos) : el
+ })
+
+ target[i].apply(target, at)
+
+ }
+
+ // apply animation which has to be applied with attr()
+ for(i in s.attrs){
+
+ at = [i].concat(s.attrs[i]).map(function(el){
+ return typeof el !== 'string' && el.at ? el.at(s.ease(self.pos), self.pos) : el
+ })
+
+ target.attr.apply(target, at)
+
+ }
+
+ // apply animation which has to be applied with style()
+ for(i in s.styles){
+
+ at = [i].concat(s.styles[i]).map(function(el){
+ return typeof el !== 'string' && el.at ? el.at(s.ease(self.pos), self.pos) : el
+ })
+
+ target.style.apply(target, at)
+
+ }
+
+ // animate initialTransformation which has to be chained
+ if(s.transforms.length){
+
+ // get initial initialTransformation
+ at = s.initialTransformation
+ for(i = 0, len = s.transforms.length; i < len; i++){
+
+ // get next transformation in chain
+ var a = s.transforms[i]
+
+ // multiply matrix directly
+ if(a instanceof SVG.Matrix){
+
+ if(a.relative){
+ at = at.multiply(new SVG.Matrix().morph(a).at(s.ease(this.pos)))
+ }else{
+ at = at.morph(a).at(s.ease(this.pos))
+ }
+ continue
+ }
+
+ // when transformation is absolute we have to reset the needed transformation first
+ if(!a.relative)
+ a.undo(at.extract())
+
+ // and reapply it after
+ at = at.multiply(a.at(s.ease(this.pos)))
+
+ }
+
+ // set new matrix on element
+ target.matrix(at)
+ }
+
+ return this
+
+ }
+
+
+ // adds an once-callback which is called at a specific position and never again
+ , once: function(pos, fn, isEased){
+
+ if(!isEased)pos = this.situation.ease(pos)
+
+ this.situation.once[pos] = fn
+
+ return this
+ }
+
+ , _callStart: function() {
+ setTimeout(function(){this.start()}.bind(this), 0)
+ return this
+ }
+
+ }
+
+, parent: SVG.Element
+
+ // Add method to parent elements
+, construct: {
+ // Get fx module or create a new one, then animate with given duration and ease
+ animate: function(o, ease, delay) {
+ return (this.fx || (this.fx = new SVG.FX(this))).animate(o, ease, delay)
+ }
+ , delay: function(delay){
+ return (this.fx || (this.fx = new SVG.FX(this))).delay(delay)
+ }
+ , stop: function(jumpToEnd, clearQueue) {
+ if (this.fx)
+ this.fx.stop(jumpToEnd, clearQueue)
+
+ return this
+ }
+ , finish: function() {
+ if (this.fx)
+ this.fx.finish()
+
+ return this
+ }
+ // Pause current animation
+ , pause: function() {
+ if (this.fx)
+ this.fx.pause()
+
+ return this
+ }
+ // Play paused current animation
+ , play: function() {
+ if (this.fx)
+ this.fx.play()
+
+ return this
+ }
+ // Set/Get the speed of the animations
+ , speed: function(speed) {
+ if (this.fx)
+ if (speed == null)
+ return this.fx.speed()
+ else
+ this.fx.speed(speed)
+
+ return this
+ }
+ }
+
+})
+
+// MorphObj is used whenever no morphable object is given
+SVG.MorphObj = SVG.invent({
+
+ create: function(from, to){
+ // prepare color for morphing
+ if(SVG.Color.isColor(to)) return new SVG.Color(from).morph(to)
+ // prepare number for morphing
+ if(SVG.regex.numberAndUnit.test(to)) return new SVG.Number(from).morph(to)
+
+ // prepare for plain morphing
+ this.value = from
+ this.destination = to
+ }
+
+, extend: {
+ at: function(pos, real){
+ return real < 1 ? this.value : this.destination
+ },
+
+ valueOf: function(){
+ return this.value
+ }
+ }
+
+})
+
+SVG.extend(SVG.FX, {
+ // Add animatable attributes
+ attr: function(a, v, relative) {
+ // apply attributes individually
+ if (typeof a == 'object') {
+ for (var key in a)
+ this.attr(key, a[key])
+
+ } else {
+ this.add(a, v, 'attrs')
+ }
+
+ return this
+ }
+ // Add animatable styles
+, style: function(s, v) {
+ if (typeof s == 'object')
+ for (var key in s)
+ this.style(key, s[key])
+
+ else
+ this.add(s, v, 'styles')
+
+ return this
+ }
+ // Animatable x-axis
+, x: function(x, relative) {
+ if(this.target() instanceof SVG.G){
+ this.transform({x:x}, relative)
+ return this
+ }
+
+ var num = new SVG.Number(x)
+ num.relative = relative
+ return this.add('x', num)
+ }
+ // Animatable y-axis
+, y: function(y, relative) {
+ if(this.target() instanceof SVG.G){
+ this.transform({y:y}, relative)
+ return this
+ }
+
+ var num = new SVG.Number(y)
+ num.relative = relative
+ return this.add('y', num)
+ }
+ // Animatable center x-axis
+, cx: function(x) {
+ return this.add('cx', new SVG.Number(x))
+ }
+ // Animatable center y-axis
+, cy: function(y) {
+ return this.add('cy', new SVG.Number(y))
+ }
+ // Add animatable move
+, move: function(x, y) {
+ return this.x(x).y(y)
+ }
+ // Add animatable center
+, center: function(x, y) {
+ return this.cx(x).cy(y)
+ }
+ // Add animatable size
+, size: function(width, height) {
+ if (this.target() instanceof SVG.Text) {
+ // animate font size for Text elements
+ this.attr('font-size', width)
+
+ } else {
+ // animate bbox based size for all other elements
+ var box
+
+ if(!width || !height){
+ box = this.target().bbox()
+ }
+
+ if(!width){
+ width = box.width / box.height * height
+ }
+
+ if(!height){
+ height = box.height / box.width * width
+ }
+
+ this.add('width' , new SVG.Number(width))
+ .add('height', new SVG.Number(height))
+
+ }
+
+ return this
+ }
+ // Add animatable plot
+, plot: function() {
+ // We use arguments here since SVG.Line's plot method can be passed 4 parameters
+ return this.add('plot', arguments.length > 1 ? [].slice.call(arguments) : arguments[0])
+ }
+ // Add leading method
+, leading: function(value) {
+ return this.target().leading ?
+ this.add('leading', new SVG.Number(value)) :
+ this
+ }
+ // Add animatable viewbox
+, viewbox: function(x, y, width, height) {
+ if (this.target() instanceof SVG.Container) {
+ this.add('viewbox', new SVG.ViewBox(x, y, width, height))
+ }
+
+ return this
+ }
+, update: function(o) {
+ if (this.target() instanceof SVG.Stop) {
+ if (typeof o == 'number' || o instanceof SVG.Number) {
+ return this.update({
+ offset: arguments[0]
+ , color: arguments[1]
+ , opacity: arguments[2]
+ })
+ }
+
+ if (o.opacity != null) this.attr('stop-opacity', o.opacity)
+ if (o.color != null) this.attr('stop-color', o.color)
+ if (o.offset != null) this.attr('offset', o.offset)
+ }
+
+ return this
+ }
+})
+
+SVG.Box = SVG.invent({
+ create: function(x, y, width, height) {
+ if (typeof x == 'object' && !(x instanceof SVG.Element)) {
+ // chromes getBoundingClientRect has no x and y property
+ return SVG.Box.call(this, x.left != null ? x.left : x.x , x.top != null ? x.top : x.y, x.width, x.height)
+ } else if (arguments.length == 4) {
+ this.x = x
+ this.y = y
+ this.width = width
+ this.height = height
+ }
+
+ // add center, right, bottom...
+ fullBox(this)
+ }
+, extend: {
+ // Merge rect box with another, return a new instance
+ merge: function(box) {
+ var b = new this.constructor()
+
+ // merge boxes
+ b.x = Math.min(this.x, box.x)
+ b.y = Math.min(this.y, box.y)
+ b.width = Math.max(this.x + this.width, box.x + box.width) - b.x
+ b.height = Math.max(this.y + this.height, box.y + box.height) - b.y
+
+ return fullBox(b)
+ }
+
+ , transform: function(m) {
+ var xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, p, bbox
+
+ var pts = [
+ new SVG.Point(this.x, this.y),
+ new SVG.Point(this.x2, this.y),
+ new SVG.Point(this.x, this.y2),
+ new SVG.Point(this.x2, this.y2)
+ ]
+
+ pts.forEach(function(p) {
+ p = p.transform(m)
+ xMin = Math.min(xMin,p.x)
+ xMax = Math.max(xMax,p.x)
+ yMin = Math.min(yMin,p.y)
+ yMax = Math.max(yMax,p.y)
+ })
+
+ bbox = new this.constructor()
+ bbox.x = xMin
+ bbox.width = xMax-xMin
+ bbox.y = yMin
+ bbox.height = yMax-yMin
+
+ fullBox(bbox)
+
+ return bbox
+ }
+ }
+})
+
+SVG.BBox = SVG.invent({
+ // Initialize
+ create: function(element) {
+ SVG.Box.apply(this, [].slice.call(arguments))
+
+ // get values if element is given
+ if (element instanceof SVG.Element) {
+ var box
+
+ // yes this is ugly, but Firefox can be a bitch when it comes to elements that are not yet rendered
+ try {
+
+ if (!document.documentElement.contains){
+ // This is IE - it does not support contains() for top-level SVGs
+ var topParent = element.node
+ while (topParent.parentNode){
+ topParent = topParent.parentNode
+ }
+ if (topParent != document) throw new Exception('Element not in the dom')
+ } else {
+ // the element is NOT in the dom, throw error
+ if(!document.documentElement.contains(element.node)) throw new Exception('Element not in the dom')
+ }
+
+ // find native bbox
+ box = element.node.getBBox()
+ } catch(e) {
+ if(element instanceof SVG.Shape){
+ var clone = element.clone(SVG.parser.draw.instance).show()
+ box = clone.node.getBBox()
+ clone.remove()
+ }else{
+ box = {
+ x: element.node.clientLeft
+ , y: element.node.clientTop
+ , width: element.node.clientWidth
+ , height: element.node.clientHeight
+ }
+ }
+ }
+
+ SVG.Box.call(this, box)
+ }
+
+ }
+
+ // Define ancestor
+, inherit: SVG.Box
+
+ // Define Parent
+, parent: SVG.Element
+
+ // Constructor
+, construct: {
+ // Get bounding box
+ bbox: function() {
+ return new SVG.BBox(this)
+ }
+ }
+
+})
+
+SVG.BBox.prototype.constructor = SVG.BBox
+
+
+SVG.extend(SVG.Element, {
+ tbox: function(){
+ console.warn('Use of TBox is deprecated and mapped to RBox. Use .rbox() instead.')
+ return this.rbox(this.doc())
+ }
+})
+
+SVG.RBox = SVG.invent({
+ // Initialize
+ create: function(element) {
+ SVG.Box.apply(this, [].slice.call(arguments))
+
+ if (element instanceof SVG.Element) {
+ SVG.Box.call(this, element.node.getBoundingClientRect())
+ }
+ }
+
+, inherit: SVG.Box
+
+ // define Parent
+, parent: SVG.Element
+
+, extend: {
+ addOffset: function() {
+ // offset by window scroll position, because getBoundingClientRect changes when window is scrolled
+ this.x += window.pageXOffset
+ this.y += window.pageYOffset
+ return this
+ }
+ }
+
+ // Constructor
+, construct: {
+ // Get rect box
+ rbox: function(el) {
+ if (el) return new SVG.RBox(this).transform(el.screenCTM().inverse())
+ return new SVG.RBox(this).addOffset()
+ }
+ }
+
+})
+
+SVG.RBox.prototype.constructor = SVG.RBox
+
+SVG.Matrix = SVG.invent({
+ // Initialize
+ create: function(source) {
+ var i, base = arrayToMatrix([1, 0, 0, 1, 0, 0])
+
+ // ensure source as object
+ source = source instanceof SVG.Element ?
+ source.matrixify() :
+ typeof source === 'string' ?
+ arrayToMatrix(source.split(SVG.regex.delimiter).map(parseFloat)) :
+ arguments.length == 6 ?
+ arrayToMatrix([].slice.call(arguments)) :
+ Array.isArray(source) ?
+ arrayToMatrix(source) :
+ typeof source === 'object' ?
+ source : base
+
+ // merge source
+ for (i = abcdef.length - 1; i >= 0; --i)
+ this[abcdef[i]] = source && typeof source[abcdef[i]] === 'number' ?
+ source[abcdef[i]] : base[abcdef[i]]
+ }
+
+ // Add methods
+, extend: {
+ // Extract individual transformations
+ extract: function() {
+ // find delta transform points
+ var px = deltaTransformPoint(this, 0, 1)
+ , py = deltaTransformPoint(this, 1, 0)
+ , skewX = 180 / Math.PI * Math.atan2(px.y, px.x) - 90
+
+ return {
+ // translation
+ x: this.e
+ , y: this.f
+ , transformedX:(this.e * Math.cos(skewX * Math.PI / 180) + this.f * Math.sin(skewX * Math.PI / 180)) / Math.sqrt(this.a * this.a + this.b * this.b)
+ , transformedY:(this.f * Math.cos(skewX * Math.PI / 180) + this.e * Math.sin(-skewX * Math.PI / 180)) / Math.sqrt(this.c * this.c + this.d * this.d)
+ // skew
+ , skewX: -skewX
+ , skewY: 180 / Math.PI * Math.atan2(py.y, py.x)
+ // scale
+ , scaleX: Math.sqrt(this.a * this.a + this.b * this.b)
+ , scaleY: Math.sqrt(this.c * this.c + this.d * this.d)
+ // rotation
+ , rotation: skewX
+ , a: this.a
+ , b: this.b
+ , c: this.c
+ , d: this.d
+ , e: this.e
+ , f: this.f
+ , matrix: new SVG.Matrix(this)
+ }
+ }
+ // Clone matrix
+ , clone: function() {
+ return new SVG.Matrix(this)
+ }
+ // Morph one matrix into another
+ , morph: function(matrix) {
+ // store new destination
+ this.destination = new SVG.Matrix(matrix)
+
+ return this
+ }
+ // Get morphed matrix at a given position
+ , at: function(pos) {
+ // make sure a destination is defined
+ if (!this.destination) return this
+
+ // calculate morphed matrix at a given position
+ var matrix = new SVG.Matrix({
+ a: this.a + (this.destination.a - this.a) * pos
+ , b: this.b + (this.destination.b - this.b) * pos
+ , c: this.c + (this.destination.c - this.c) * pos
+ , d: this.d + (this.destination.d - this.d) * pos
+ , e: this.e + (this.destination.e - this.e) * pos
+ , f: this.f + (this.destination.f - this.f) * pos
+ })
+
+ return matrix
+ }
+ // Multiplies by given matrix
+ , multiply: function(matrix) {
+ return new SVG.Matrix(this.native().multiply(parseMatrix(matrix).native()))
+ }
+ // Inverses matrix
+ , inverse: function() {
+ return new SVG.Matrix(this.native().inverse())
+ }
+ // Translate matrix
+ , translate: function(x, y) {
+ return new SVG.Matrix(this.native().translate(x || 0, y || 0))
+ }
+ // Scale matrix
+ , scale: function(x, y, cx, cy) {
+ // support uniformal scale
+ if (arguments.length == 1) {
+ y = x
+ } else if (arguments.length == 3) {
+ cy = cx
+ cx = y
+ y = x
+ }
+
+ return this.around(cx, cy, new SVG.Matrix(x, 0, 0, y, 0, 0))
+ }
+ // Rotate matrix
+ , rotate: function(r, cx, cy) {
+ // convert degrees to radians
+ r = SVG.utils.radians(r)
+
+ return this.around(cx, cy, new SVG.Matrix(Math.cos(r), Math.sin(r), -Math.sin(r), Math.cos(r), 0, 0))
+ }
+ // Flip matrix on x or y, at a given offset
+ , flip: function(a, o) {
+ return a == 'x' ?
+ this.scale(-1, 1, o, 0) :
+ a == 'y' ?
+ this.scale(1, -1, 0, o) :
+ this.scale(-1, -1, a, o != null ? o : a)
+ }
+ // Skew
+ , skew: function(x, y, cx, cy) {
+ // support uniformal skew
+ if (arguments.length == 1) {
+ y = x
+ } else if (arguments.length == 3) {
+ cy = cx
+ cx = y
+ y = x
+ }
+
+ // convert degrees to radians
+ x = SVG.utils.radians(x)
+ y = SVG.utils.radians(y)
+
+ return this.around(cx, cy, new SVG.Matrix(1, Math.tan(y), Math.tan(x), 1, 0, 0))
+ }
+ // SkewX
+ , skewX: function(x, cx, cy) {
+ return this.skew(x, 0, cx, cy)
+ }
+ // SkewY
+ , skewY: function(y, cx, cy) {
+ return this.skew(0, y, cx, cy)
+ }
+ // Transform around a center point
+ , around: function(cx, cy, matrix) {
+ return this
+ .multiply(new SVG.Matrix(1, 0, 0, 1, cx || 0, cy || 0))
+ .multiply(matrix)
+ .multiply(new SVG.Matrix(1, 0, 0, 1, -cx || 0, -cy || 0))
+ }
+ // Convert to native SVGMatrix
+ , native: function() {
+ // create new matrix
+ var matrix = SVG.parser.native.createSVGMatrix()
+
+ // update with current values
+ for (var i = abcdef.length - 1; i >= 0; i--)
+ matrix[abcdef[i]] = this[abcdef[i]]
+
+ return matrix
+ }
+ // Convert matrix to string
+ , toString: function() {
+ return 'matrix(' + this.a + ',' + this.b + ',' + this.c + ',' + this.d + ',' + this.e + ',' + this.f + ')'
+ }
+ }
+
+ // Define parent
+, parent: SVG.Element
+
+ // Add parent method
+, construct: {
+ // Get current matrix
+ ctm: function() {
+ return new SVG.Matrix(this.node.getCTM())
+ },
+ // Get current screen matrix
+ screenCTM: function() {
+ /* https://bugzilla.mozilla.org/show_bug.cgi?id=1344537
+ This is needed because FF does not return the transformation matrix
+ for the inner coordinate system when getScreenCTM() is called on nested svgs.
+ However all other Browsers do that */
+ if(this instanceof SVG.Nested) {
+ var rect = this.rect(1,1)
+ var m = rect.node.getScreenCTM()
+ rect.remove()
+ return new SVG.Matrix(m)
+ }
+ return new SVG.Matrix(this.node.getScreenCTM())
+ }
+
+ }
+
+})
+
+SVG.Point = SVG.invent({
+ // Initialize
+ create: function(x,y) {
+ var i, source, base = {x:0, y:0}
+
+ // ensure source as object
+ source = Array.isArray(x) ?
+ {x:x[0], y:x[1]} :
+ typeof x === 'object' ?
+ {x:x.x, y:x.y} :
+ x != null ?
+ {x:x, y:(y != null ? y : x)} : base // If y has no value, then x is used has its value
+
+ // merge source
+ this.x = source.x
+ this.y = source.y
+ }
+
+ // Add methods
+, extend: {
+ // Clone point
+ clone: function() {
+ return new SVG.Point(this)
+ }
+ // Morph one point into another
+ , morph: function(x, y) {
+ // store new destination
+ this.destination = new SVG.Point(x, y)
+
+ return this
+ }
+ // Get morphed point at a given position
+ , at: function(pos) {
+ // make sure a destination is defined
+ if (!this.destination) return this
+
+ // calculate morphed matrix at a given position
+ var point = new SVG.Point({
+ x: this.x + (this.destination.x - this.x) * pos
+ , y: this.y + (this.destination.y - this.y) * pos
+ })
+
+ return point
+ }
+ // Convert to native SVGPoint
+ , native: function() {
+ // create new point
+ var point = SVG.parser.native.createSVGPoint()
+
+ // update with current values
+ point.x = this.x
+ point.y = this.y
+
+ return point
+ }
+ // transform point with matrix
+ , transform: function(matrix) {
+ return new SVG.Point(this.native().matrixTransform(matrix.native()))
+ }
+
+ }
+
+})
+
+SVG.extend(SVG.Element, {
+
+ // Get point
+ point: function(x, y) {
+ return new SVG.Point(x,y).transform(this.screenCTM().inverse());
+ }
+
+})
+
+SVG.extend(SVG.Element, {
+ // Set svg element attribute
+ attr: function(a, v, n) {
+ // act as full getter
+ if (a == null) {
+ // get an object of attributes
+ a = {}
+ v = this.node.attributes
+ for (n = v.length - 1; n >= 0; n--)
+ a[v[n].nodeName] = SVG.regex.isNumber.test(v[n].nodeValue) ? parseFloat(v[n].nodeValue) : v[n].nodeValue
+
+ return a
+
+ } else if (typeof a == 'object') {
+ // apply every attribute individually if an object is passed
+ for (v in a) this.attr(v, a[v])
+
+ } else if (v === null) {
+ // remove value
+ this.node.removeAttribute(a)
+
+ } else if (v == null) {
+ // act as a getter if the first and only argument is not an object
+ v = this.node.getAttribute(a)
+ return v == null ?
+ SVG.defaults.attrs[a] :
+ SVG.regex.isNumber.test(v) ?
+ parseFloat(v) : v
+
+ } else {
+ // BUG FIX: some browsers will render a stroke if a color is given even though stroke width is 0
+ if (a == 'stroke-width')
+ this.attr('stroke', parseFloat(v) > 0 ? this._stroke : null)
+ else if (a == 'stroke')
+ this._stroke = v
+
+ // convert image fill and stroke to patterns
+ if (a == 'fill' || a == 'stroke') {
+ if (SVG.regex.isImage.test(v))
+ v = this.doc().defs().image(v, 0, 0)
+
+ if (v instanceof SVG.Image)
+ v = this.doc().defs().pattern(0, 0, function() {
+ this.add(v)
+ })
+ }
+
+ // ensure correct numeric values (also accepts NaN and Infinity)
+ if (typeof v === 'number')
+ v = new SVG.Number(v)
+
+ // ensure full hex color
+ else if (SVG.Color.isColor(v))
+ v = new SVG.Color(v)
+
+ // parse array values
+ else if (Array.isArray(v))
+ v = new SVG.Array(v)
+
+ // if the passed attribute is leading...
+ if (a == 'leading') {
+ // ... call the leading method instead
+ if (this.leading)
+ this.leading(v)
+ } else {
+ // set given attribute on node
+ typeof n === 'string' ?
+ this.node.setAttributeNS(n, a, v.toString()) :
+ this.node.setAttribute(a, v.toString())
+ }
+
+ // rebuild if required
+ if (this.rebuild && (a == 'font-size' || a == 'x'))
+ this.rebuild(a, v)
+ }
+
+ return this
+ }
+})
+SVG.extend(SVG.Element, {
+ // Add transformations
+ transform: function(o, relative) {
+ // get target in case of the fx module, otherwise reference this
+ var target = this
+ , matrix, bbox
+
+ // act as a getter
+ if (typeof o !== 'object') {
+ // get current matrix
+ matrix = new SVG.Matrix(target).extract()
+
+ return typeof o === 'string' ? matrix[o] : matrix
+ }
+
+ // get current matrix
+ matrix = new SVG.Matrix(target)
+
+ // ensure relative flag
+ relative = !!relative || !!o.relative
+
+ // act on matrix
+ if (o.a != null) {
+ matrix = relative ?
+ // relative
+ matrix.multiply(new SVG.Matrix(o)) :
+ // absolute
+ new SVG.Matrix(o)
+
+ // act on rotation
+ } else if (o.rotation != null) {
+ // ensure centre point
+ ensureCentre(o, target)
+
+ // apply transformation
+ matrix = relative ?
+ // relative
+ matrix.rotate(o.rotation, o.cx, o.cy) :
+ // absolute
+ matrix.rotate(o.rotation - matrix.extract().rotation, o.cx, o.cy)
+
+ // act on scale
+ } else if (o.scale != null || o.scaleX != null || o.scaleY != null) {
+ // ensure centre point
+ ensureCentre(o, target)
+
+ // ensure scale values on both axes
+ o.scaleX = o.scale != null ? o.scale : o.scaleX != null ? o.scaleX : 1
+ o.scaleY = o.scale != null ? o.scale : o.scaleY != null ? o.scaleY : 1
+
+ if (!relative) {
+ // absolute; multiply inversed values
+ var e = matrix.extract()
+ o.scaleX = o.scaleX * 1 / e.scaleX
+ o.scaleY = o.scaleY * 1 / e.scaleY
+ }
+
+ matrix = matrix.scale(o.scaleX, o.scaleY, o.cx, o.cy)
+
+ // act on skew
+ } else if (o.skew != null || o.skewX != null || o.skewY != null) {
+ // ensure centre point
+ ensureCentre(o, target)
+
+ // ensure skew values on both axes
+ o.skewX = o.skew != null ? o.skew : o.skewX != null ? o.skewX : 0
+ o.skewY = o.skew != null ? o.skew : o.skewY != null ? o.skewY : 0
+
+ if (!relative) {
+ // absolute; reset skew values
+ var e = matrix.extract()
+ matrix = matrix.multiply(new SVG.Matrix().skew(e.skewX, e.skewY, o.cx, o.cy).inverse())
+ }
+
+ matrix = matrix.skew(o.skewX, o.skewY, o.cx, o.cy)
+
+ // act on flip
+ } else if (o.flip) {
+ if(o.flip == 'x' || o.flip == 'y') {
+ o.offset = o.offset == null ? target.bbox()['c' + o.flip] : o.offset
+ } else {
+ if(o.offset == null) {
+ bbox = target.bbox()
+ o.flip = bbox.cx
+ o.offset = bbox.cy
+ } else {
+ o.flip = o.offset
+ }
+ }
+
+ matrix = new SVG.Matrix().flip(o.flip, o.offset)
+
+ // act on translate
+ } else if (o.x != null || o.y != null) {
+ if (relative) {
+ // relative
+ matrix = matrix.translate(o.x, o.y)
+ } else {
+ // absolute
+ if (o.x != null) matrix.e = o.x
+ if (o.y != null) matrix.f = o.y
+ }
+ }
+
+ return this.attr('transform', matrix)
+ }
+})
+
+SVG.extend(SVG.FX, {
+ transform: function(o, relative) {
+ // get target in case of the fx module, otherwise reference this
+ var target = this.target()
+ , matrix, bbox
+
+ // act as a getter
+ if (typeof o !== 'object') {
+ // get current matrix
+ matrix = new SVG.Matrix(target).extract()
+
+ return typeof o === 'string' ? matrix[o] : matrix
+ }
+
+ // ensure relative flag
+ relative = !!relative || !!o.relative
+
+ // act on matrix
+ if (o.a != null) {
+ matrix = new SVG.Matrix(o)
+
+ // act on rotation
+ } else if (o.rotation != null) {
+ // ensure centre point
+ ensureCentre(o, target)
+
+ // apply transformation
+ matrix = new SVG.Rotate(o.rotation, o.cx, o.cy)
+
+ // act on scale
+ } else if (o.scale != null || o.scaleX != null || o.scaleY != null) {
+ // ensure centre point
+ ensureCentre(o, target)
+
+ // ensure scale values on both axes
+ o.scaleX = o.scale != null ? o.scale : o.scaleX != null ? o.scaleX : 1
+ o.scaleY = o.scale != null ? o.scale : o.scaleY != null ? o.scaleY : 1
+
+ matrix = new SVG.Scale(o.scaleX, o.scaleY, o.cx, o.cy)
+
+ // act on skew
+ } else if (o.skewX != null || o.skewY != null) {
+ // ensure centre point
+ ensureCentre(o, target)
+
+ // ensure skew values on both axes
+ o.skewX = o.skewX != null ? o.skewX : 0
+ o.skewY = o.skewY != null ? o.skewY : 0
+
+ matrix = new SVG.Skew(o.skewX, o.skewY, o.cx, o.cy)
+
+ // act on flip
+ } else if (o.flip) {
+ if(o.flip == 'x' || o.flip == 'y') {
+ o.offset = o.offset == null ? target.bbox()['c' + o.flip] : o.offset
+ } else {
+ if(o.offset == null) {
+ bbox = target.bbox()
+ o.flip = bbox.cx
+ o.offset = bbox.cy
+ } else {
+ o.flip = o.offset
+ }
+ }
+
+ matrix = new SVG.Matrix().flip(o.flip, o.offset)
+
+ // act on translate
+ } else if (o.x != null || o.y != null) {
+ matrix = new SVG.Translate(o.x, o.y)
+ }
+
+ if(!matrix) return this
+
+ matrix.relative = relative
+
+ this.last().transforms.push(matrix)
+
+ return this._callStart()
+ }
+})
+
+SVG.extend(SVG.Element, {
+ // Reset all transformations
+ untransform: function() {
+ return this.attr('transform', null)
+ },
+ // merge the whole transformation chain into one matrix and returns it
+ matrixify: function() {
+
+ var matrix = (this.attr('transform') || '')
+ // split transformations
+ .split(SVG.regex.transforms).slice(0,-1).map(function(str){
+ // generate key => value pairs
+ var kv = str.trim().split('(')
+ return [kv[0], kv[1].split(SVG.regex.delimiter).map(function(str){ return parseFloat(str) })]
+ })
+ // merge every transformation into one matrix
+ .reduce(function(matrix, transform){
+
+ if(transform[0] == 'matrix') return matrix.multiply(arrayToMatrix(transform[1]))
+ return matrix[transform[0]].apply(matrix, transform[1])
+
+ }, new SVG.Matrix())
+
+ return matrix
+ },
+ // add an element to another parent without changing the visual representation on the screen
+ toParent: function(parent) {
+ if(this == parent) return this
+ var ctm = this.screenCTM()
+ var pCtm = parent.screenCTM().inverse()
+
+ this.addTo(parent).untransform().transform(pCtm.multiply(ctm))
+
+ return this
+ },
+ // same as above with parent equals root-svg
+ toDoc: function() {
+ return this.toParent(this.doc())
+ }
+
+})
+
+SVG.Transformation = SVG.invent({
+
+ create: function(source, inversed){
+
+ if(arguments.length > 1 && typeof inversed != 'boolean'){
+ return this.constructor.call(this, [].slice.call(arguments))
+ }
+
+ if(Array.isArray(source)){
+ for(var i = 0, len = this.arguments.length; i < len; ++i){
+ this[this.arguments[i]] = source[i]
+ }
+ } else if(typeof source == 'object'){
+ for(var i = 0, len = this.arguments.length; i < len; ++i){
+ this[this.arguments[i]] = source[this.arguments[i]]
+ }
+ }
+
+ this.inversed = false
+
+ if(inversed === true){
+ this.inversed = true
+ }
+
+ }
+
+, extend: {
+
+ arguments: []
+ , method: ''
+
+ , at: function(pos){
+
+ var params = []
+
+ for(var i = 0, len = this.arguments.length; i < len; ++i){
+ params.push(this[this.arguments[i]])
+ }
+
+ var m = this._undo || new SVG.Matrix()
+
+ m = new SVG.Matrix().morph(SVG.Matrix.prototype[this.method].apply(m, params)).at(pos)
+
+ return this.inversed ? m.inverse() : m
+
+ }
+
+ , undo: function(o){
+ for(var i = 0, len = this.arguments.length; i < len; ++i){
+ o[this.arguments[i]] = typeof this[this.arguments[i]] == 'undefined' ? 0 : o[this.arguments[i]]
+ }
+
+ // The method SVG.Matrix.extract which was used before calling this
+ // method to obtain a value for the parameter o doesn't return a cx and
+ // a cy so we use the ones that were provided to this object at its creation
+ o.cx = this.cx
+ o.cy = this.cy
+
+ this._undo = new SVG[capitalize(this.method)](o, true).at(1)
+
+ return this
+ }
+
+ }
+
+})
+
+SVG.Translate = SVG.invent({
+
+ parent: SVG.Matrix
+, inherit: SVG.Transformation
+
+, create: function(source, inversed){
+ this.constructor.apply(this, [].slice.call(arguments))
+ }
+
+, extend: {
+ arguments: ['transformedX', 'transformedY']
+ , method: 'translate'
+ }
+
+})
+
+SVG.Rotate = SVG.invent({
+
+ parent: SVG.Matrix
+, inherit: SVG.Transformation
+
+, create: function(source, inversed){
+ this.constructor.apply(this, [].slice.call(arguments))
+ }
+
+, extend: {
+ arguments: ['rotation', 'cx', 'cy']
+ , method: 'rotate'
+ , at: function(pos){
+ var m = new SVG.Matrix().rotate(new SVG.Number().morph(this.rotation - (this._undo ? this._undo.rotation : 0)).at(pos), this.cx, this.cy)
+ return this.inversed ? m.inverse() : m
+ }
+ , undo: function(o){
+ this._undo = o
+ return this
+ }
+ }
+
+})
+
+SVG.Scale = SVG.invent({
+
+ parent: SVG.Matrix
+, inherit: SVG.Transformation
+
+, create: function(source, inversed){
+ this.constructor.apply(this, [].slice.call(arguments))
+ }
+
+, extend: {
+ arguments: ['scaleX', 'scaleY', 'cx', 'cy']
+ , method: 'scale'
+ }
+
+})
+
+SVG.Skew = SVG.invent({
+
+ parent: SVG.Matrix
+, inherit: SVG.Transformation
+
+, create: function(source, inversed){
+ this.constructor.apply(this, [].slice.call(arguments))
+ }
+
+, extend: {
+ arguments: ['skewX', 'skewY', 'cx', 'cy']
+ , method: 'skew'
+ }
+
+})
+
+SVG.extend(SVG.Element, {
+ // Dynamic style generator
+ style: function(s, v) {
+ if (arguments.length == 0) {
+ // get full style
+ return this.node.style.cssText || ''
+
+ } else if (arguments.length < 2) {
+ // apply every style individually if an object is passed
+ if (typeof s == 'object') {
+ for (v in s) this.style(v, s[v])
+
+ } else if (SVG.regex.isCss.test(s)) {
+ // parse css string
+ s = s.split(/\s*;\s*/)
+ // filter out suffix ; and stuff like ;;
+ .filter(function(e) { return !!e })
+ .map(function(e){ return e.split(/\s*:\s*/) })
+
+ // apply every definition individually
+ while (v = s.pop()) {
+ this.style(v[0], v[1])
+ }
+ } else {
+ // act as a getter if the first and only argument is not an object
+ return this.node.style[camelCase(s)]
+ }
+
+ } else {
+ this.node.style[camelCase(s)] = v === null || SVG.regex.isBlank.test(v) ? '' : v
+ }
+
+ return this
+ }
+})
+SVG.Parent = SVG.invent({
+ // Initialize node
+ create: function(element) {
+ this.constructor.call(this, element)
+ }
+
+ // Inherit from
+, inherit: SVG.Element
+
+ // Add class methods
+, extend: {
+ // Returns all child elements
+ children: function() {
+ return SVG.utils.map(SVG.utils.filterSVGElements(this.node.childNodes), function(node) {
+ return SVG.adopt(node)
+ })
+ }
+ // Add given element at a position
+ , add: function(element, i) {
+ if (i == null)
+ this.node.appendChild(element.node)
+ else if (element.node != this.node.childNodes[i])
+ this.node.insertBefore(element.node, this.node.childNodes[i])
+
+ return this
+ }
+ // Basically does the same as `add()` but returns the added element instead
+ , put: function(element, i) {
+ this.add(element, i)
+ return element
+ }
+ // Checks if the given element is a child
+ , has: function(element) {
+ return this.index(element) >= 0
+ }
+ // Gets index of given element
+ , index: function(element) {
+ return [].slice.call(this.node.childNodes).indexOf(element.node)
+ }
+ // Get a element at the given index
+ , get: function(i) {
+ return SVG.adopt(this.node.childNodes[i])
+ }
+ // Get first child
+ , first: function() {
+ return this.get(0)
+ }
+ // Get the last child
+ , last: function() {
+ return this.get(this.node.childNodes.length - 1)
+ }
+ // Iterates over all children and invokes a given block
+ , each: function(block, deep) {
+ var i, il
+ , children = this.children()
+
+ for (i = 0, il = children.length; i < il; i++) {
+ if (children[i] instanceof SVG.Element)
+ block.apply(children[i], [i, children])
+
+ if (deep && (children[i] instanceof SVG.Container))
+ children[i].each(block, deep)
+ }
+
+ return this
+ }
+ // Remove a given child
+ , removeElement: function(element) {
+ this.node.removeChild(element.node)
+
+ return this
+ }
+ // Remove all elements in this container
+ , clear: function() {
+ // remove children
+ while(this.node.hasChildNodes())
+ this.node.removeChild(this.node.lastChild)
+
+ // remove defs reference
+ delete this._defs
+
+ return this
+ }
+ , // Get defs
+ defs: function() {
+ return this.doc().defs()
+ }
+ }
+
+})
+
+SVG.extend(SVG.Parent, {
+
+ ungroup: function(parent, depth) {
+ if(depth === 0 || this instanceof SVG.Defs || this.node == SVG.parser.draw) return this
+
+ parent = parent || (this instanceof SVG.Doc ? this : this.parent(SVG.Parent))
+ depth = depth || Infinity
+
+ this.each(function(){
+ if(this instanceof SVG.Defs) return this
+ if(this instanceof SVG.Parent) return this.ungroup(parent, depth-1)
+ return this.toParent(parent)
+ })
+
+ this.node.firstChild || this.remove()
+
+ return this
+ },
+
+ flatten: function(parent, depth) {
+ return this.ungroup(parent, depth)
+ }
+
+})
+SVG.Container = SVG.invent({
+ // Initialize node
+ create: function(element) {
+ this.constructor.call(this, element)
+ }
+
+ // Inherit from
+, inherit: SVG.Parent
+
+})
+
+SVG.ViewBox = SVG.invent({
+
+ create: function(source) {
+ var i, base = [0, 0, 0, 0]
+
+ var x, y, width, height, box, view, we, he
+ , wm = 1 // width multiplier
+ , hm = 1 // height multiplier
+ , reg = /[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?/gi
+
+ if(source instanceof SVG.Element){
+
+ we = source
+ he = source
+ view = (source.attr('viewBox') || '').match(reg)
+ box = source.bbox
+
+ // get dimensions of current node
+ width = new SVG.Number(source.width())
+ height = new SVG.Number(source.height())
+
+ // find nearest non-percentual dimensions
+ while (width.unit == '%') {
+ wm *= width.value
+ width = new SVG.Number(we instanceof SVG.Doc ? we.parent().offsetWidth : we.parent().width())
+ we = we.parent()
+ }
+ while (height.unit == '%') {
+ hm *= height.value
+ height = new SVG.Number(he instanceof SVG.Doc ? he.parent().offsetHeight : he.parent().height())
+ he = he.parent()
+ }
+
+ // ensure defaults
+ this.x = 0
+ this.y = 0
+ this.width = width * wm
+ this.height = height * hm
+ this.zoom = 1
+
+ if (view) {
+ // get width and height from viewbox
+ x = parseFloat(view[0])
+ y = parseFloat(view[1])
+ width = parseFloat(view[2])
+ height = parseFloat(view[3])
+
+ // calculate zoom accoring to viewbox
+ this.zoom = ((this.width / this.height) > (width / height)) ?
+ this.height / height :
+ this.width / width
+
+ // calculate real pixel dimensions on parent SVG.Doc element
+ this.x = x
+ this.y = y
+ this.width = width
+ this.height = height
+
+ }
+
+ }else{
+
+ // ensure source as object
+ source = typeof source === 'string' ?
+ source.match(reg).map(function(el){ return parseFloat(el) }) :
+ Array.isArray(source) ?
+ source :
+ typeof source == 'object' ?
+ [source.x, source.y, source.width, source.height] :
+ arguments.length == 4 ?
+ [].slice.call(arguments) :
+ base
+
+ this.x = source[0]
+ this.y = source[1]
+ this.width = source[2]
+ this.height = source[3]
+ }
+
+
+ }
+
+, extend: {
+
+ toString: function() {
+ return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height
+ }
+ , morph: function(x, y, width, height){
+ this.destination = new SVG.ViewBox(x, y, width, height)
+ return this
+ }
+
+ , at: function(pos) {
+
+ if(!this.destination) return this
+
+ return new SVG.ViewBox([
+ this.x + (this.destination.x - this.x) * pos
+ , this.y + (this.destination.y - this.y) * pos
+ , this.width + (this.destination.width - this.width) * pos
+ , this.height + (this.destination.height - this.height) * pos
+ ])
+
+ }
+
+ }
+
+ // Define parent
+, parent: SVG.Container
+
+ // Add parent method
+, construct: {
+
+ // get/set viewbox
+ viewbox: function(x, y, width, height) {
+ if (arguments.length == 0)
+ // act as a getter if there are no arguments
+ return new SVG.ViewBox(this)
+
+ // otherwise act as a setter
+ return this.attr('viewBox', new SVG.ViewBox(x, y, width, height))
+ }
+
+ }
+
+})
+// Add events to elements
+;[ 'click'
+ , 'dblclick'
+ , 'mousedown'
+ , 'mouseup'
+ , 'mouseover'
+ , 'mouseout'
+ , 'mousemove'
+ // , 'mouseenter' -> not supported by IE
+ // , 'mouseleave' -> not supported by IE
+ , 'touchstart'
+ , 'touchmove'
+ , 'touchleave'
+ , 'touchend'
+ , 'touchcancel' ].forEach(function(event) {
+
+ // add event to SVG.Element
+ SVG.Element.prototype[event] = function(f) {
+ // bind event to element rather than element node
+ SVG.on(this.node, event, f)
+ return this
+ }
+})
+
+// Initialize listeners stack
+SVG.listeners = []
+SVG.handlerMap = []
+SVG.listenerId = 0
+
+// Add event binder in the SVG namespace
+SVG.on = function(node, event, listener, binding, options) {
+ // create listener, get object-index
+ var l = listener.bind(binding || node.instance || node)
+ , index = (SVG.handlerMap.indexOf(node) + 1 || SVG.handlerMap.push(node)) - 1
+ , ev = event.split('.')[0]
+ , ns = event.split('.')[1] || '*'
+
+
+ // ensure valid object
+ SVG.listeners[index] = SVG.listeners[index] || {}
+ SVG.listeners[index][ev] = SVG.listeners[index][ev] || {}
+ SVG.listeners[index][ev][ns] = SVG.listeners[index][ev][ns] || {}
+
+ if(!listener._svgjsListenerId)
+ listener._svgjsListenerId = ++SVG.listenerId
+
+ // reference listener
+ SVG.listeners[index][ev][ns][listener._svgjsListenerId] = l
+
+ // add listener
+ node.addEventListener(ev, l, options || false)
+}
+
+// Add event unbinder in the SVG namespace
+SVG.off = function(node, event, listener) {
+ var index = SVG.handlerMap.indexOf(node)
+ , ev = event && event.split('.')[0]
+ , ns = event && event.split('.')[1]
+ , namespace = ''
+
+ if(index == -1) return
+
+ if (listener) {
+ if(typeof listener == 'function') listener = listener._svgjsListenerId
+ if(!listener) return
+
+ // remove listener reference
+ if (SVG.listeners[index][ev] && SVG.listeners[index][ev][ns || '*']) {
+ // remove listener
+ node.removeEventListener(ev, SVG.listeners[index][ev][ns || '*'][listener], false)
+
+ delete SVG.listeners[index][ev][ns || '*'][listener]
+ }
+
+ } else if (ns && ev) {
+ // remove all listeners for a namespaced event
+ if (SVG.listeners[index][ev] && SVG.listeners[index][ev][ns]) {
+ for (listener in SVG.listeners[index][ev][ns])
+ SVG.off(node, [ev, ns].join('.'), listener)
+
+ delete SVG.listeners[index][ev][ns]
+ }
+
+ } else if (ns){
+ // remove all listeners for a specific namespace
+ for(event in SVG.listeners[index]){
+ for(namespace in SVG.listeners[index][event]){
+ if(ns === namespace){
+ SVG.off(node, [event, ns].join('.'))
+ }
+ }
+ }
+
+ } else if (ev) {
+ // remove all listeners for the event
+ if (SVG.listeners[index][ev]) {
+ for (namespace in SVG.listeners[index][ev])
+ SVG.off(node, [ev, namespace].join('.'))
+
+ delete SVG.listeners[index][ev]
+ }
+
+ } else {
+ // remove all listeners on a given node
+ for (event in SVG.listeners[index])
+ SVG.off(node, event)
+
+ delete SVG.listeners[index]
+ delete SVG.handlerMap[index]
+
+ }
+}
+
+//
+SVG.extend(SVG.Element, {
+ // Bind given event to listener
+ on: function(event, listener, binding, options) {
+ SVG.on(this.node, event, listener, binding, options)
+
+ return this
+ }
+ // Unbind event from listener
+, off: function(event, listener) {
+ SVG.off(this.node, event, listener)
+
+ return this
+ }
+ // Fire given event
+, fire: function(event, data) {
+
+ // Dispatch event
+ if(event instanceof window.Event){
+ this.node.dispatchEvent(event)
+ }else{
+ this.node.dispatchEvent(event = new window.CustomEvent(event, {detail:data, cancelable: true}))
+ }
+
+ this._event = event
+ return this
+ }
+, event: function() {
+ return this._event
+ }
+})
+
+
+SVG.Defs = SVG.invent({
+ // Initialize node
+ create: 'defs'
+
+ // Inherit from
+, inherit: SVG.Container
+
+})
+SVG.G = SVG.invent({
+ // Initialize node
+ create: 'g'
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add class methods
+, extend: {
+ // Move over x-axis
+ x: function(x) {
+ return x == null ? this.transform('x') : this.transform({ x: x - this.x() }, true)
+ }
+ // Move over y-axis
+ , y: function(y) {
+ return y == null ? this.transform('y') : this.transform({ y: y - this.y() }, true)
+ }
+ // Move by center over x-axis
+ , cx: function(x) {
+ return x == null ? this.gbox().cx : this.x(x - this.gbox().width / 2)
+ }
+ // Move by center over y-axis
+ , cy: function(y) {
+ return y == null ? this.gbox().cy : this.y(y - this.gbox().height / 2)
+ }
+ , gbox: function() {
+
+ var bbox = this.bbox()
+ , trans = this.transform()
+
+ bbox.x += trans.x
+ bbox.x2 += trans.x
+ bbox.cx += trans.x
+
+ bbox.y += trans.y
+ bbox.y2 += trans.y
+ bbox.cy += trans.y
+
+ return bbox
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create a group element
+ group: function() {
+ return this.put(new SVG.G)
+ }
+ }
+})
+
+// ### This module adds backward / forward functionality to elements.
+
+//
+SVG.extend(SVG.Element, {
+ // Get all siblings, including myself
+ siblings: function() {
+ return this.parent().children()
+ }
+ // Get the curent position siblings
+, position: function() {
+ return this.parent().index(this)
+ }
+ // Get the next element (will return null if there is none)
+, next: function() {
+ return this.siblings()[this.position() + 1]
+ }
+ // Get the next element (will return null if there is none)
+, previous: function() {
+ return this.siblings()[this.position() - 1]
+ }
+ // Send given element one step forward
+, forward: function() {
+ var i = this.position() + 1
+ , p = this.parent()
+
+ // move node one step forward
+ p.removeElement(this).add(this, i)
+
+ // make sure defs node is always at the top
+ if (p instanceof SVG.Doc)
+ p.node.appendChild(p.defs().node)
+
+ return this
+ }
+ // Send given element one step backward
+, backward: function() {
+ var i = this.position()
+
+ if (i > 0)
+ this.parent().removeElement(this).add(this, i - 1)
+
+ return this
+ }
+ // Send given element all the way to the front
+, front: function() {
+ var p = this.parent()
+
+ // Move node forward
+ p.node.appendChild(this.node)
+
+ // Make sure defs node is always at the top
+ if (p instanceof SVG.Doc)
+ p.node.appendChild(p.defs().node)
+
+ return this
+ }
+ // Send given element all the way to the back
+, back: function() {
+ if (this.position() > 0)
+ this.parent().removeElement(this).add(this, 0)
+
+ return this
+ }
+ // Inserts a given element before the targeted element
+, before: function(element) {
+ element.remove()
+
+ var i = this.position()
+
+ this.parent().add(element, i)
+
+ return this
+ }
+ // Insters a given element after the targeted element
+, after: function(element) {
+ element.remove()
+
+ var i = this.position()
+
+ this.parent().add(element, i + 1)
+
+ return this
+ }
+
+})
+SVG.Mask = SVG.invent({
+ // Initialize node
+ create: function() {
+ this.constructor.call(this, SVG.create('mask'))
+
+ // keep references to masked elements
+ this.targets = []
+ }
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add class methods
+, extend: {
+ // Unmask all masked elements and remove itself
+ remove: function() {
+ // unmask all targets
+ for (var i = this.targets.length - 1; i >= 0; i--)
+ if (this.targets[i])
+ this.targets[i].unmask()
+ this.targets = []
+
+ // remove mask from parent
+ this.parent().removeElement(this)
+
+ return this
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create masking element
+ mask: function() {
+ return this.defs().put(new SVG.Mask)
+ }
+ }
+})
+
+
+SVG.extend(SVG.Element, {
+ // Distribute mask to svg element
+ maskWith: function(element) {
+ // use given mask or create a new one
+ this.masker = element instanceof SVG.Mask ? element : this.parent().mask().add(element)
+
+ // store reverence on self in mask
+ this.masker.targets.push(this)
+
+ // apply mask
+ return this.attr('mask', 'url("#' + this.masker.attr('id') + '")')
+ }
+ // Unmask element
+, unmask: function() {
+ delete this.masker
+ return this.attr('mask', null)
+ }
+
+})
+
+SVG.ClipPath = SVG.invent({
+ // Initialize node
+ create: function() {
+ this.constructor.call(this, SVG.create('clipPath'))
+
+ // keep references to clipped elements
+ this.targets = []
+ }
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add class methods
+, extend: {
+ // Unclip all clipped elements and remove itself
+ remove: function() {
+ // unclip all targets
+ for (var i = this.targets.length - 1; i >= 0; i--)
+ if (this.targets[i])
+ this.targets[i].unclip()
+ this.targets = []
+
+ // remove clipPath from parent
+ this.parent().removeElement(this)
+
+ return this
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create clipping element
+ clip: function() {
+ return this.defs().put(new SVG.ClipPath)
+ }
+ }
+})
+
+//
+SVG.extend(SVG.Element, {
+ // Distribute clipPath to svg element
+ clipWith: function(element) {
+ // use given clip or create a new one
+ this.clipper = element instanceof SVG.ClipPath ? element : this.parent().clip().add(element)
+
+ // store reverence on self in mask
+ this.clipper.targets.push(this)
+
+ // apply mask
+ return this.attr('clip-path', 'url("#' + this.clipper.attr('id') + '")')
+ }
+ // Unclip element
+, unclip: function() {
+ delete this.clipper
+ return this.attr('clip-path', null)
+ }
+
+})
+SVG.Gradient = SVG.invent({
+ // Initialize node
+ create: function(type) {
+ this.constructor.call(this, SVG.create(type + 'Gradient'))
+
+ // store type
+ this.type = type
+ }
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add class methods
+, extend: {
+ // Add a color stop
+ at: function(offset, color, opacity) {
+ return this.put(new SVG.Stop).update(offset, color, opacity)
+ }
+ // Update gradient
+ , update: function(block) {
+ // remove all stops
+ this.clear()
+
+ // invoke passed block
+ if (typeof block == 'function')
+ block.call(this, this)
+
+ return this
+ }
+ // Return the fill id
+ , fill: function() {
+ return 'url(#' + this.id() + ')'
+ }
+ // Alias string convertion to fill
+ , toString: function() {
+ return this.fill()
+ }
+ // custom attr to handle transform
+ , attr: function(a, b, c) {
+ if(a == 'transform') a = 'gradientTransform'
+ return SVG.Container.prototype.attr.call(this, a, b, c)
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create gradient element in defs
+ gradient: function(type, block) {
+ return this.defs().gradient(type, block)
+ }
+ }
+})
+
+// Add animatable methods to both gradient and fx module
+SVG.extend(SVG.Gradient, SVG.FX, {
+ // From position
+ from: function(x, y) {
+ return (this._target || this).type == 'radial' ?
+ this.attr({ fx: new SVG.Number(x), fy: new SVG.Number(y) }) :
+ this.attr({ x1: new SVG.Number(x), y1: new SVG.Number(y) })
+ }
+ // To position
+, to: function(x, y) {
+ return (this._target || this).type == 'radial' ?
+ this.attr({ cx: new SVG.Number(x), cy: new SVG.Number(y) }) :
+ this.attr({ x2: new SVG.Number(x), y2: new SVG.Number(y) })
+ }
+})
+
+// Base gradient generation
+SVG.extend(SVG.Defs, {
+ // define gradient
+ gradient: function(type, block) {
+ return this.put(new SVG.Gradient(type)).update(block)
+ }
+
+})
+
+SVG.Stop = SVG.invent({
+ // Initialize node
+ create: 'stop'
+
+ // Inherit from
+, inherit: SVG.Element
+
+ // Add class methods
+, extend: {
+ // add color stops
+ update: function(o) {
+ if (typeof o == 'number' || o instanceof SVG.Number) {
+ o = {
+ offset: arguments[0]
+ , color: arguments[1]
+ , opacity: arguments[2]
+ }
+ }
+
+ // set attributes
+ if (o.opacity != null) this.attr('stop-opacity', o.opacity)
+ if (o.color != null) this.attr('stop-color', o.color)
+ if (o.offset != null) this.attr('offset', new SVG.Number(o.offset))
+
+ return this
+ }
+ }
+
+})
+
+SVG.Pattern = SVG.invent({
+ // Initialize node
+ create: 'pattern'
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add class methods
+, extend: {
+ // Return the fill id
+ fill: function() {
+ return 'url(#' + this.id() + ')'
+ }
+ // Update pattern by rebuilding
+ , update: function(block) {
+ // remove content
+ this.clear()
+
+ // invoke passed block
+ if (typeof block == 'function')
+ block.call(this, this)
+
+ return this
+ }
+ // Alias string convertion to fill
+ , toString: function() {
+ return this.fill()
+ }
+ // custom attr to handle transform
+ , attr: function(a, b, c) {
+ if(a == 'transform') a = 'patternTransform'
+ return SVG.Container.prototype.attr.call(this, a, b, c)
+ }
+
+ }
+
+ // Add parent method
+, construct: {
+ // Create pattern element in defs
+ pattern: function(width, height, block) {
+ return this.defs().pattern(width, height, block)
+ }
+ }
+})
+
+SVG.extend(SVG.Defs, {
+ // Define gradient
+ pattern: function(width, height, block) {
+ return this.put(new SVG.Pattern).update(block).attr({
+ x: 0
+ , y: 0
+ , width: width
+ , height: height
+ , patternUnits: 'userSpaceOnUse'
+ })
+ }
+
+})
+SVG.Doc = SVG.invent({
+ // Initialize node
+ create: function(element) {
+ if (element) {
+ // ensure the presence of a dom element
+ element = typeof element == 'string' ?
+ document.getElementById(element) :
+ element
+
+ // If the target is an svg element, use that element as the main wrapper.
+ // This allows svg.js to work with svg documents as well.
+ if (element.nodeName == 'svg') {
+ this.constructor.call(this, element)
+ } else {
+ this.constructor.call(this, SVG.create('svg'))
+ element.appendChild(this.node)
+ this.size('100%', '100%')
+ }
+
+ // set svg element attributes and ensure defs node
+ this.namespace().defs()
+ }
+ }
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add class methods
+, extend: {
+ // Add namespaces
+ namespace: function() {
+ return this
+ .attr({ xmlns: SVG.ns, version: '1.1' })
+ .attr('xmlns:xlink', SVG.xlink, SVG.xmlns)
+ .attr('xmlns:svgjs', SVG.svgjs, SVG.xmlns)
+ }
+ // Creates and returns defs element
+ , defs: function() {
+ if (!this._defs) {
+ var defs
+
+ // Find or create a defs element in this instance
+ if (defs = this.node.getElementsByTagName('defs')[0])
+ this._defs = SVG.adopt(defs)
+ else
+ this._defs = new SVG.Defs
+
+ // Make sure the defs node is at the end of the stack
+ this.node.appendChild(this._defs.node)
+ }
+
+ return this._defs
+ }
+ // custom parent method
+ , parent: function() {
+ return this.node.parentNode.nodeName == '#document' ? null : this.node.parentNode
+ }
+ // Fix for possible sub-pixel offset. See:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=608812
+ , spof: function(spof) {
+ var pos = this.node.getScreenCTM()
+
+ if (pos)
+ this
+ .style('left', (-pos.e % 1) + 'px')
+ .style('top', (-pos.f % 1) + 'px')
+
+ return this
+ }
+
+ // Removes the doc from the DOM
+ , remove: function() {
+ if(this.parent()) {
+ this.parent().removeChild(this.node)
+ }
+
+ return this
+ }
+ , clear: function() {
+ // remove children
+ while(this.node.hasChildNodes())
+ this.node.removeChild(this.node.lastChild)
+
+ // remove defs reference
+ delete this._defs
+
+ // add back parser
+ if(!SVG.parser.draw.parentNode)
+ this.node.appendChild(SVG.parser.draw)
+
+ return this
+ }
+ }
+
+})
+
+SVG.Shape = SVG.invent({
+ // Initialize node
+ create: function(element) {
+ this.constructor.call(this, element)
+ }
+
+ // Inherit from
+, inherit: SVG.Element
+
+})
+
+SVG.Bare = SVG.invent({
+ // Initialize
+ create: function(element, inherit) {
+ // construct element
+ this.constructor.call(this, SVG.create(element))
+
+ // inherit custom methods
+ if (inherit)
+ for (var method in inherit.prototype)
+ if (typeof inherit.prototype[method] === 'function')
+ this[method] = inherit.prototype[method]
+ }
+
+ // Inherit from
+, inherit: SVG.Element
+
+ // Add methods
+, extend: {
+ // Insert some plain text
+ words: function(text) {
+ // remove contents
+ while (this.node.hasChildNodes())
+ this.node.removeChild(this.node.lastChild)
+
+ // create text node
+ this.node.appendChild(document.createTextNode(text))
+
+ return this
+ }
+ }
+})
+
+
+SVG.extend(SVG.Parent, {
+ // Create an element that is not described by SVG.js
+ element: function(element, inherit) {
+ return this.put(new SVG.Bare(element, inherit))
+ }
+})
+
+SVG.Symbol = SVG.invent({
+ // Initialize node
+ create: 'symbol'
+
+ // Inherit from
+, inherit: SVG.Container
+
+, construct: {
+ // create symbol
+ symbol: function() {
+ return this.put(new SVG.Symbol)
+ }
+ }
+})
+
+SVG.Use = SVG.invent({
+ // Initialize node
+ create: 'use'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add class methods
+, extend: {
+ // Use element as a reference
+ element: function(element, file) {
+ // Set lined element
+ return this.attr('href', (file || '') + '#' + element, SVG.xlink)
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create a use element
+ use: function(element, file) {
+ return this.put(new SVG.Use).element(element, file)
+ }
+ }
+})
+SVG.Rect = SVG.invent({
+ // Initialize node
+ create: 'rect'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add parent method
+, construct: {
+ // Create a rect element
+ rect: function(width, height) {
+ return this.put(new SVG.Rect()).size(width, height)
+ }
+ }
+})
+SVG.Circle = SVG.invent({
+ // Initialize node
+ create: 'circle'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add parent method
+, construct: {
+ // Create circle element, based on ellipse
+ circle: function(size) {
+ return this.put(new SVG.Circle).rx(new SVG.Number(size).divide(2)).move(0, 0)
+ }
+ }
+})
+
+SVG.extend(SVG.Circle, SVG.FX, {
+ // Radius x value
+ rx: function(rx) {
+ return this.attr('r', rx)
+ }
+ // Alias radius x value
+, ry: function(ry) {
+ return this.rx(ry)
+ }
+})
+
+SVG.Ellipse = SVG.invent({
+ // Initialize node
+ create: 'ellipse'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add parent method
+, construct: {
+ // Create an ellipse
+ ellipse: function(width, height) {
+ return this.put(new SVG.Ellipse).size(width, height).move(0, 0)
+ }
+ }
+})
+
+SVG.extend(SVG.Ellipse, SVG.Rect, SVG.FX, {
+ // Radius x value
+ rx: function(rx) {
+ return this.attr('rx', rx)
+ }
+ // Radius y value
+, ry: function(ry) {
+ return this.attr('ry', ry)
+ }
+})
+
+// Add common method
+SVG.extend(SVG.Circle, SVG.Ellipse, {
+ // Move over x-axis
+ x: function(x) {
+ return x == null ? this.cx() - this.rx() : this.cx(x + this.rx())
+ }
+ // Move over y-axis
+ , y: function(y) {
+ return y == null ? this.cy() - this.ry() : this.cy(y + this.ry())
+ }
+ // Move by center over x-axis
+ , cx: function(x) {
+ return x == null ? this.attr('cx') : this.attr('cx', x)
+ }
+ // Move by center over y-axis
+ , cy: function(y) {
+ return y == null ? this.attr('cy') : this.attr('cy', y)
+ }
+ // Set width of element
+ , width: function(width) {
+ return width == null ? this.rx() * 2 : this.rx(new SVG.Number(width).divide(2))
+ }
+ // Set height of element
+ , height: function(height) {
+ return height == null ? this.ry() * 2 : this.ry(new SVG.Number(height).divide(2))
+ }
+ // Custom size function
+ , size: function(width, height) {
+ var p = proportionalSize(this, width, height)
+
+ return this
+ .rx(new SVG.Number(p.width).divide(2))
+ .ry(new SVG.Number(p.height).divide(2))
+ }
+})
+SVG.Line = SVG.invent({
+ // Initialize node
+ create: 'line'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add class methods
+, extend: {
+ // Get array
+ array: function() {
+ return new SVG.PointArray([
+ [ this.attr('x1'), this.attr('y1') ]
+ , [ this.attr('x2'), this.attr('y2') ]
+ ])
+ }
+ // Overwrite native plot() method
+ , plot: function(x1, y1, x2, y2) {
+ if (x1 == null)
+ return this.array()
+ else if (typeof y1 !== 'undefined')
+ x1 = { x1: x1, y1: y1, x2: x2, y2: y2 }
+ else
+ x1 = new SVG.PointArray(x1).toLine()
+
+ return this.attr(x1)
+ }
+ // Move by left top corner
+ , move: function(x, y) {
+ return this.attr(this.array().move(x, y).toLine())
+ }
+ // Set element size to given width and height
+ , size: function(width, height) {
+ var p = proportionalSize(this, width, height)
+
+ return this.attr(this.array().size(p.width, p.height).toLine())
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create a line element
+ line: function(x1, y1, x2, y2) {
+ // make sure plot is called as a setter
+ // x1 is not necessarily a number, it can also be an array, a string and a SVG.PointArray
+ return SVG.Line.prototype.plot.apply(
+ this.put(new SVG.Line)
+ , x1 != null ? [x1, y1, x2, y2] : [0, 0, 0, 0]
+ )
+ }
+ }
+})
+
+SVG.Polyline = SVG.invent({
+ // Initialize node
+ create: 'polyline'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add parent method
+, construct: {
+ // Create a wrapped polyline element
+ polyline: function(p) {
+ // make sure plot is called as a setter
+ return this.put(new SVG.Polyline).plot(p || new SVG.PointArray)
+ }
+ }
+})
+
+SVG.Polygon = SVG.invent({
+ // Initialize node
+ create: 'polygon'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add parent method
+, construct: {
+ // Create a wrapped polygon element
+ polygon: function(p) {
+ // make sure plot is called as a setter
+ return this.put(new SVG.Polygon).plot(p || new SVG.PointArray)
+ }
+ }
+})
+
+// Add polygon-specific functions
+SVG.extend(SVG.Polyline, SVG.Polygon, {
+ // Get array
+ array: function() {
+ return this._array || (this._array = new SVG.PointArray(this.attr('points')))
+ }
+ // Plot new path
+, plot: function(p) {
+ return (p == null) ?
+ this.array() :
+ this.clear().attr('points', typeof p == 'string' ? p : (this._array = new SVG.PointArray(p)))
+ }
+ // Clear array cache
+, clear: function() {
+ delete this._array
+ return this
+ }
+ // Move by left top corner
+, move: function(x, y) {
+ return this.attr('points', this.array().move(x, y))
+ }
+ // Set element size to given width and height
+, size: function(width, height) {
+ var p = proportionalSize(this, width, height)
+
+ return this.attr('points', this.array().size(p.width, p.height))
+ }
+
+})
+
+// unify all point to point elements
+SVG.extend(SVG.Line, SVG.Polyline, SVG.Polygon, {
+ // Define morphable array
+ morphArray: SVG.PointArray
+ // Move by left top corner over x-axis
+, x: function(x) {
+ return x == null ? this.bbox().x : this.move(x, this.bbox().y)
+ }
+ // Move by left top corner over y-axis
+, y: function(y) {
+ return y == null ? this.bbox().y : this.move(this.bbox().x, y)
+ }
+ // Set width of element
+, width: function(width) {
+ var b = this.bbox()
+
+ return width == null ? b.width : this.size(width, b.height)
+ }
+ // Set height of element
+, height: function(height) {
+ var b = this.bbox()
+
+ return height == null ? b.height : this.size(b.width, height)
+ }
+})
+SVG.Path = SVG.invent({
+ // Initialize node
+ create: 'path'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add class methods
+, extend: {
+ // Define morphable array
+ morphArray: SVG.PathArray
+ // Get array
+ , array: function() {
+ return this._array || (this._array = new SVG.PathArray(this.attr('d')))
+ }
+ // Plot new path
+ , plot: function(d) {
+ return (d == null) ?
+ this.array() :
+ this.clear().attr('d', typeof d == 'string' ? d : (this._array = new SVG.PathArray(d)))
+ }
+ // Clear array cache
+ , clear: function() {
+ delete this._array
+ return this
+ }
+ // Move by left top corner
+ , move: function(x, y) {
+ return this.attr('d', this.array().move(x, y))
+ }
+ // Move by left top corner over x-axis
+ , x: function(x) {
+ return x == null ? this.bbox().x : this.move(x, this.bbox().y)
+ }
+ // Move by left top corner over y-axis
+ , y: function(y) {
+ return y == null ? this.bbox().y : this.move(this.bbox().x, y)
+ }
+ // Set element size to given width and height
+ , size: function(width, height) {
+ var p = proportionalSize(this, width, height)
+
+ return this.attr('d', this.array().size(p.width, p.height))
+ }
+ // Set width of element
+ , width: function(width) {
+ return width == null ? this.bbox().width : this.size(width, this.bbox().height)
+ }
+ // Set height of element
+ , height: function(height) {
+ return height == null ? this.bbox().height : this.size(this.bbox().width, height)
+ }
+
+ }
+
+ // Add parent method
+, construct: {
+ // Create a wrapped path element
+ path: function(d) {
+ // make sure plot is called as a setter
+ return this.put(new SVG.Path).plot(d || new SVG.PathArray)
+ }
+ }
+})
+
+SVG.Image = SVG.invent({
+ // Initialize node
+ create: 'image'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add class methods
+, extend: {
+ // (re)load image
+ load: function(url) {
+ if (!url) return this
+
+ var self = this
+ , img = new window.Image()
+
+ // preload image
+ SVG.on(img, 'load', function() {
+ var p = self.parent(SVG.Pattern)
+
+ if(p === null) return
+
+ // ensure image size
+ if (self.width() == 0 && self.height() == 0)
+ self.size(img.width, img.height)
+
+ // ensure pattern size if not set
+ if (p && p.width() == 0 && p.height() == 0)
+ p.size(self.width(), self.height())
+
+ // callback
+ if (typeof self._loaded === 'function')
+ self._loaded.call(self, {
+ width: img.width
+ , height: img.height
+ , ratio: img.width / img.height
+ , url: url
+ })
+ })
+
+ SVG.on(img, 'error', function(e){
+ if (typeof self._error === 'function'){
+ self._error.call(self, e)
+ }
+ })
+
+ return this.attr('href', (img.src = this.src = url), SVG.xlink)
+ }
+ // Add loaded callback
+ , loaded: function(loaded) {
+ this._loaded = loaded
+ return this
+ }
+
+ , error: function(error) {
+ this._error = error
+ return this
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // create image element, load image and set its size
+ image: function(source, width, height) {
+ return this.put(new SVG.Image).load(source).size(width || 0, height || width || 0)
+ }
+ }
+
+})
+SVG.Text = SVG.invent({
+ // Initialize node
+ create: function() {
+ this.constructor.call(this, SVG.create('text'))
+
+ this.dom.leading = new SVG.Number(1.3) // store leading value for rebuilding
+ this._rebuild = true // enable automatic updating of dy values
+ this._build = false // disable build mode for adding multiple lines
+
+ // set default font
+ this.attr('font-family', SVG.defaults.attrs['font-family'])
+ }
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add class methods
+, extend: {
+ // Move over x-axis
+ x: function(x) {
+ // act as getter
+ if (x == null)
+ return this.attr('x')
+
+ return this.attr('x', x)
+ }
+ // Move over y-axis
+ , y: function(y) {
+ var oy = this.attr('y')
+ , o = typeof oy === 'number' ? oy - this.bbox().y : 0
+
+ // act as getter
+ if (y == null)
+ return typeof oy === 'number' ? oy - o : oy
+
+ return this.attr('y', typeof y === 'number' ? y + o : y)
+ }
+ // Move center over x-axis
+ , cx: function(x) {
+ return x == null ? this.bbox().cx : this.x(x - this.bbox().width / 2)
+ }
+ // Move center over y-axis
+ , cy: function(y) {
+ return y == null ? this.bbox().cy : this.y(y - this.bbox().height / 2)
+ }
+ // Set the text content
+ , text: function(text) {
+ // act as getter
+ if (typeof text === 'undefined'){
+ var text = ''
+ var children = this.node.childNodes
+ for(var i = 0, len = children.length; i < len; ++i){
+
+ // add newline if its not the first child and newLined is set to true
+ if(i != 0 && children[i].nodeType != 3 && SVG.adopt(children[i]).dom.newLined == true){
+ text += '\n'
+ }
+
+ // add content of this node
+ text += children[i].textContent
+ }
+
+ return text
+ }
+
+ // remove existing content
+ this.clear().build(true)
+
+ if (typeof text === 'function') {
+ // call block
+ text.call(this, this)
+
+ } else {
+ // store text and make sure text is not blank
+ text = text.split('\n')
+
+ // build new lines
+ for (var i = 0, il = text.length; i < il; i++)
+ this.tspan(text[i]).newLine()
+ }
+
+ // disable build mode and rebuild lines
+ return this.build(false).rebuild()
+ }
+ // Set font size
+ , size: function(size) {
+ return this.attr('font-size', size).rebuild()
+ }
+ // Set / get leading
+ , leading: function(value) {
+ // act as getter
+ if (value == null)
+ return this.dom.leading
+
+ // act as setter
+ this.dom.leading = new SVG.Number(value)
+
+ return this.rebuild()
+ }
+ // Get all the first level lines
+ , lines: function() {
+ var node = (this.textPath && this.textPath() || this).node
+
+ // filter tspans and map them to SVG.js instances
+ var lines = SVG.utils.map(SVG.utils.filterSVGElements(node.childNodes), function(el){
+ return SVG.adopt(el)
+ })
+
+ // return an instance of SVG.set
+ return new SVG.Set(lines)
+ }
+ // Rebuild appearance type
+ , rebuild: function(rebuild) {
+ // store new rebuild flag if given
+ if (typeof rebuild == 'boolean')
+ this._rebuild = rebuild
+
+ // define position of all lines
+ if (this._rebuild) {
+ var self = this
+ , blankLineOffset = 0
+ , dy = this.dom.leading * new SVG.Number(this.attr('font-size'))
+
+ this.lines().each(function() {
+ if (this.dom.newLined) {
+ if (!self.textPath())
+ this.attr('x', self.attr('x'))
+ if(this.text() == '\n') {
+ blankLineOffset += dy
+ }else{
+ this.attr('dy', dy + blankLineOffset)
+ blankLineOffset = 0
+ }
+ }
+ })
+
+ this.fire('rebuild')
+ }
+
+ return this
+ }
+ // Enable / disable build mode
+ , build: function(build) {
+ this._build = !!build
+ return this
+ }
+ // overwrite method from parent to set data properly
+ , setData: function(o){
+ this.dom = o
+ this.dom.leading = new SVG.Number(o.leading || 1.3)
+ return this
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create text element
+ text: function(text) {
+ return this.put(new SVG.Text).text(text)
+ }
+ // Create plain text element
+ , plain: function(text) {
+ return this.put(new SVG.Text).plain(text)
+ }
+ }
+
+})
+
+SVG.Tspan = SVG.invent({
+ // Initialize node
+ create: 'tspan'
+
+ // Inherit from
+, inherit: SVG.Shape
+
+ // Add class methods
+, extend: {
+ // Set text content
+ text: function(text) {
+ if(text == null) return this.node.textContent + (this.dom.newLined ? '\n' : '')
+
+ typeof text === 'function' ? text.call(this, this) : this.plain(text)
+
+ return this
+ }
+ // Shortcut dx
+ , dx: function(dx) {
+ return this.attr('dx', dx)
+ }
+ // Shortcut dy
+ , dy: function(dy) {
+ return this.attr('dy', dy)
+ }
+ // Create new line
+ , newLine: function() {
+ // fetch text parent
+ var t = this.parent(SVG.Text)
+
+ // mark new line
+ this.dom.newLined = true
+
+ // apply new hy¡n
+ return this.dy(t.dom.leading * t.attr('font-size')).attr('x', t.x())
+ }
+ }
+
+})
+
+SVG.extend(SVG.Text, SVG.Tspan, {
+ // Create plain text node
+ plain: function(text) {
+ // clear if build mode is disabled
+ if (this._build === false)
+ this.clear()
+
+ // create text node
+ this.node.appendChild(document.createTextNode(text))
+
+ return this
+ }
+ // Create a tspan
+, tspan: function(text) {
+ var node = (this.textPath && this.textPath() || this).node
+ , tspan = new SVG.Tspan
+
+ // clear if build mode is disabled
+ if (this._build === false)
+ this.clear()
+
+ // add new tspan
+ node.appendChild(tspan.node)
+
+ return tspan.text(text)
+ }
+ // Clear all lines
+, clear: function() {
+ var node = (this.textPath && this.textPath() || this).node
+
+ // remove existing child nodes
+ while (node.hasChildNodes())
+ node.removeChild(node.lastChild)
+
+ return this
+ }
+ // Get length of text element
+, length: function() {
+ return this.node.getComputedTextLength()
+ }
+})
+
+SVG.TextPath = SVG.invent({
+ // Initialize node
+ create: 'textPath'
+
+ // Inherit from
+, inherit: SVG.Parent
+
+ // Define parent class
+, parent: SVG.Text
+
+ // Add parent method
+, construct: {
+ // Create path for text to run on
+ path: function(d) {
+ // create textPath element
+ var path = new SVG.TextPath
+ , track = this.doc().defs().path(d)
+
+ // move lines to textpath
+ while (this.node.hasChildNodes())
+ path.node.appendChild(this.node.firstChild)
+
+ // add textPath element as child node
+ this.node.appendChild(path.node)
+
+ // link textPath to path and add content
+ path.attr('href', '#' + track, SVG.xlink)
+
+ return this
+ }
+ // return the array of the path track element
+ , array: function() {
+ var track = this.track()
+
+ return track ? track.array() : null
+ }
+ // Plot path if any
+ , plot: function(d) {
+ var track = this.track()
+ , pathArray = null
+
+ if (track) {
+ pathArray = track.plot(d)
+ }
+
+ return (d == null) ? pathArray : this
+ }
+ // Get the path track element
+ , track: function() {
+ var path = this.textPath()
+
+ if (path)
+ return path.reference('href')
+ }
+ // Get the textPath child
+ , textPath: function() {
+ if (this.node.firstChild && this.node.firstChild.nodeName == 'textPath')
+ return SVG.adopt(this.node.firstChild)
+ }
+ }
+})
+
+SVG.Nested = SVG.invent({
+ // Initialize node
+ create: function() {
+ this.constructor.call(this, SVG.create('svg'))
+
+ this.style('overflow', 'visible')
+ }
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add parent method
+, construct: {
+ // Create nested svg document
+ nested: function() {
+ return this.put(new SVG.Nested)
+ }
+ }
+})
+SVG.A = SVG.invent({
+ // Initialize node
+ create: 'a'
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add class methods
+, extend: {
+ // Link url
+ to: function(url) {
+ return this.attr('href', url, SVG.xlink)
+ }
+ // Link show attribute
+ , show: function(target) {
+ return this.attr('show', target, SVG.xlink)
+ }
+ // Link target attribute
+ , target: function(target) {
+ return this.attr('target', target)
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create a hyperlink element
+ link: function(url) {
+ return this.put(new SVG.A).to(url)
+ }
+ }
+})
+
+SVG.extend(SVG.Element, {
+ // Create a hyperlink element
+ linkTo: function(url) {
+ var link = new SVG.A
+
+ if (typeof url == 'function')
+ url.call(link, link)
+ else
+ link.to(url)
+
+ return this.parent().put(link).put(this)
+ }
+
+})
+SVG.Marker = SVG.invent({
+ // Initialize node
+ create: 'marker'
+
+ // Inherit from
+, inherit: SVG.Container
+
+ // Add class methods
+, extend: {
+ // Set width of element
+ width: function(width) {
+ return this.attr('markerWidth', width)
+ }
+ // Set height of element
+ , height: function(height) {
+ return this.attr('markerHeight', height)
+ }
+ // Set marker refX and refY
+ , ref: function(x, y) {
+ return this.attr('refX', x).attr('refY', y)
+ }
+ // Update marker
+ , update: function(block) {
+ // remove all content
+ this.clear()
+
+ // invoke passed block
+ if (typeof block == 'function')
+ block.call(this, this)
+
+ return this
+ }
+ // Return the fill id
+ , toString: function() {
+ return 'url(#' + this.id() + ')'
+ }
+ }
+
+ // Add parent method
+, construct: {
+ marker: function(width, height, block) {
+ // Create marker element in defs
+ return this.defs().marker(width, height, block)
+ }
+ }
+
+})
+
+SVG.extend(SVG.Defs, {
+ // Create marker
+ marker: function(width, height, block) {
+ // Set default viewbox to match the width and height, set ref to cx and cy and set orient to auto
+ return this.put(new SVG.Marker)
+ .size(width, height)
+ .ref(width / 2, height / 2)
+ .viewbox(0, 0, width, height)
+ .attr('orient', 'auto')
+ .update(block)
+ }
+
+})
+
+SVG.extend(SVG.Line, SVG.Polyline, SVG.Polygon, SVG.Path, {
+ // Create and attach markers
+ marker: function(marker, width, height, block) {
+ var attr = ['marker']
+
+ // Build attribute name
+ if (marker != 'all') attr.push(marker)
+ attr = attr.join('-')
+
+ // Set marker attribute
+ marker = arguments[1] instanceof SVG.Marker ?
+ arguments[1] :
+ this.doc().marker(width, height, block)
+
+ return this.attr(attr, marker)
+ }
+
+})
+// Define list of available attributes for stroke and fill
+var sugar = {
+ stroke: ['color', 'width', 'opacity', 'linecap', 'linejoin', 'miterlimit', 'dasharray', 'dashoffset']
+, fill: ['color', 'opacity', 'rule']
+, prefix: function(t, a) {
+ return a == 'color' ? t : t + '-' + a
+ }
+}
+
+// Add sugar for fill and stroke
+;['fill', 'stroke'].forEach(function(m) {
+ var i, extension = {}
+
+ extension[m] = function(o) {
+ if (typeof o == 'undefined')
+ return this
+ if (typeof o == 'string' || SVG.Color.isRgb(o) || (o && typeof o.fill === 'function'))
+ this.attr(m, o)
+
+ else
+ // set all attributes from sugar.fill and sugar.stroke list
+ for (i = sugar[m].length - 1; i >= 0; i--)
+ if (o[sugar[m][i]] != null)
+ this.attr(sugar.prefix(m, sugar[m][i]), o[sugar[m][i]])
+
+ return this
+ }
+
+ SVG.extend(SVG.Element, SVG.FX, extension)
+
+})
+
+SVG.extend(SVG.Element, SVG.FX, {
+ // Map rotation to transform
+ rotate: function(d, cx, cy) {
+ return this.transform({ rotation: d, cx: cx, cy: cy })
+ }
+ // Map skew to transform
+, skew: function(x, y, cx, cy) {
+ return arguments.length == 1 || arguments.length == 3 ?
+ this.transform({ skew: x, cx: y, cy: cx }) :
+ this.transform({ skewX: x, skewY: y, cx: cx, cy: cy })
+ }
+ // Map scale to transform
+, scale: function(x, y, cx, cy) {
+ return arguments.length == 1 || arguments.length == 3 ?
+ this.transform({ scale: x, cx: y, cy: cx }) :
+ this.transform({ scaleX: x, scaleY: y, cx: cx, cy: cy })
+ }
+ // Map translate to transform
+, translate: function(x, y) {
+ return this.transform({ x: x, y: y })
+ }
+ // Map flip to transform
+, flip: function(a, o) {
+ o = typeof a == 'number' ? a : o
+ return this.transform({ flip: a || 'both', offset: o })
+ }
+ // Map matrix to transform
+, matrix: function(m) {
+ return this.attr('transform', new SVG.Matrix(arguments.length == 6 ? [].slice.call(arguments) : m))
+ }
+ // Opacity
+, opacity: function(value) {
+ return this.attr('opacity', value)
+ }
+ // Relative move over x axis
+, dx: function(x) {
+ return this.x(new SVG.Number(x).plus(this instanceof SVG.FX ? 0 : this.x()), true)
+ }
+ // Relative move over y axis
+, dy: function(y) {
+ return this.y(new SVG.Number(y).plus(this instanceof SVG.FX ? 0 : this.y()), true)
+ }
+ // Relative move over x and y axes
+, dmove: function(x, y) {
+ return this.dx(x).dy(y)
+ }
+})
+
+SVG.extend(SVG.Rect, SVG.Ellipse, SVG.Circle, SVG.Gradient, SVG.FX, {
+ // Add x and y radius
+ radius: function(x, y) {
+ var type = (this._target || this).type;
+ return type == 'radial' || type == 'circle' ?
+ this.attr('r', new SVG.Number(x)) :
+ this.rx(x).ry(y == null ? x : y)
+ }
+})
+
+SVG.extend(SVG.Path, {
+ // Get path length
+ length: function() {
+ return this.node.getTotalLength()
+ }
+ // Get point at length
+, pointAt: function(length) {
+ return this.node.getPointAtLength(length)
+ }
+})
+
+SVG.extend(SVG.Parent, SVG.Text, SVG.Tspan, SVG.FX, {
+ // Set font
+ font: function(a, v) {
+ if (typeof a == 'object') {
+ for (v in a) this.font(v, a[v])
+ }
+
+ return a == 'leading' ?
+ this.leading(v) :
+ a == 'anchor' ?
+ this.attr('text-anchor', v) :
+ a == 'size' || a == 'family' || a == 'weight' || a == 'stretch' || a == 'variant' || a == 'style' ?
+ this.attr('font-'+ a, v) :
+ this.attr(a, v)
+ }
+})
+
+SVG.Set = SVG.invent({
+ // Initialize
+ create: function(members) {
+ // Set initial state
+ Array.isArray(members) ? this.members = members : this.clear()
+ }
+
+ // Add class methods
+, extend: {
+ // Add element to set
+ add: function() {
+ var i, il, elements = [].slice.call(arguments)
+
+ for (i = 0, il = elements.length; i < il; i++)
+ this.members.push(elements[i])
+
+ return this
+ }
+ // Remove element from set
+ , remove: function(element) {
+ var i = this.index(element)
+
+ // remove given child
+ if (i > -1)
+ this.members.splice(i, 1)
+
+ return this
+ }
+ // Iterate over all members
+ , each: function(block) {
+ for (var i = 0, il = this.members.length; i < il; i++)
+ block.apply(this.members[i], [i, this.members])
+
+ return this
+ }
+ // Restore to defaults
+ , clear: function() {
+ // initialize store
+ this.members = []
+
+ return this
+ }
+ // Get the length of a set
+ , length: function() {
+ return this.members.length
+ }
+ // Checks if a given element is present in set
+ , has: function(element) {
+ return this.index(element) >= 0
+ }
+ // retuns index of given element in set
+ , index: function(element) {
+ return this.members.indexOf(element)
+ }
+ // Get member at given index
+ , get: function(i) {
+ return this.members[i]
+ }
+ // Get first member
+ , first: function() {
+ return this.get(0)
+ }
+ // Get last member
+ , last: function() {
+ return this.get(this.members.length - 1)
+ }
+ // Default value
+ , valueOf: function() {
+ return this.members
+ }
+ // Get the bounding box of all members included or empty box if set has no items
+ , bbox: function(){
+ // return an empty box of there are no members
+ if (this.members.length == 0)
+ return new SVG.RBox()
+
+ // get the first rbox and update the target bbox
+ var rbox = this.members[0].rbox(this.members[0].doc())
+
+ this.each(function() {
+ // user rbox for correct position and visual representation
+ rbox = rbox.merge(this.rbox(this.doc()))
+ })
+
+ return rbox
+ }
+ }
+
+ // Add parent method
+, construct: {
+ // Create a new set
+ set: function(members) {
+ return new SVG.Set(members)
+ }
+ }
+})
+
+SVG.FX.Set = SVG.invent({
+ // Initialize node
+ create: function(set) {
+ // store reference to set
+ this.set = set
+ }
+
+})
+
+// Alias methods
+SVG.Set.inherit = function() {
+ var m
+ , methods = []
+
+ // gather shape methods
+ for(var m in SVG.Shape.prototype)
+ if (typeof SVG.Shape.prototype[m] == 'function' && typeof SVG.Set.prototype[m] != 'function')
+ methods.push(m)
+
+ // apply shape aliasses
+ methods.forEach(function(method) {
+ SVG.Set.prototype[method] = function() {
+ for (var i = 0, il = this.members.length; i < il; i++)
+ if (this.members[i] && typeof this.members[i][method] == 'function')
+ this.members[i][method].apply(this.members[i], arguments)
+
+ return method == 'animate' ? (this.fx || (this.fx = new SVG.FX.Set(this))) : this
+ }
+ })
+
+ // clear methods for the next round
+ methods = []
+
+ // gather fx methods
+ for(var m in SVG.FX.prototype)
+ if (typeof SVG.FX.prototype[m] == 'function' && typeof SVG.FX.Set.prototype[m] != 'function')
+ methods.push(m)
+
+ // apply fx aliasses
+ methods.forEach(function(method) {
+ SVG.FX.Set.prototype[method] = function() {
+ for (var i = 0, il = this.set.members.length; i < il; i++)
+ this.set.members[i].fx[method].apply(this.set.members[i].fx, arguments)
+
+ return this
+ }
+ })
+}
+
+
+
+
+SVG.extend(SVG.Element, {
+ // Store data values on svg nodes
+ data: function(a, v, r) {
+ if (typeof a == 'object') {
+ for (v in a)
+ this.data(v, a[v])
+
+ } else if (arguments.length < 2) {
+ try {
+ return JSON.parse(this.attr('data-' + a))
+ } catch(e) {
+ return this.attr('data-' + a)
+ }
+
+ } else {
+ this.attr(
+ 'data-' + a
+ , v === null ?
+ null :
+ r === true || typeof v === 'string' || typeof v === 'number' ?
+ v :
+ JSON.stringify(v)
+ )
+ }
+
+ return this
+ }
+})
+SVG.extend(SVG.Element, {
+ // Remember arbitrary data
+ remember: function(k, v) {
+ // remember every item in an object individually
+ if (typeof arguments[0] == 'object')
+ for (var v in k)
+ this.remember(v, k[v])
+
+ // retrieve memory
+ else if (arguments.length == 1)
+ return this.memory()[k]
+
+ // store memory
+ else
+ this.memory()[k] = v
+
+ return this
+ }
+
+ // Erase a given memory
+, forget: function() {
+ if (arguments.length == 0)
+ this._memory = {}
+ else
+ for (var i = arguments.length - 1; i >= 0; i--)
+ delete this.memory()[arguments[i]]
+
+ return this
+ }
+
+ // Initialize or return local memory object
+, memory: function() {
+ return this._memory || (this._memory = {})
+ }
+
+})
+// Method for getting an element by id
+SVG.get = function(id) {
+ var node = document.getElementById(idFromReference(id) || id)
+ return SVG.adopt(node)
+}
+
+// Select elements by query string
+SVG.select = function(query, parent) {
+ return new SVG.Set(
+ SVG.utils.map((parent || document).querySelectorAll(query), function(node) {
+ return SVG.adopt(node)
+ })
+ )
+}
+
+SVG.extend(SVG.Parent, {
+ // Scoped select method
+ select: function(query) {
+ return SVG.select(query, this.node)
+ }
+
+})
+function pathRegReplace(a, b, c, d) {
+ return c + d.replace(SVG.regex.dots, ' .')
+}
+
+// creates deep clone of array
+function array_clone(arr){
+ var clone = arr.slice(0)
+ for(var i = clone.length; i--;){
+ if(Array.isArray(clone[i])){
+ clone[i] = array_clone(clone[i])
+ }
+ }
+ return clone
+}
+
+// tests if a given element is instance of an object
+function is(el, obj){
+ return el instanceof obj
+}
+
+// tests if a given selector matches an element
+function matches(el, selector) {
+ return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
+}
+
+// Convert dash-separated-string to camelCase
+function camelCase(s) {
+ return s.toLowerCase().replace(/-(.)/g, function(m, g) {
+ return g.toUpperCase()
+ })
+}
+
+// Capitalize first letter of a string
+function capitalize(s) {
+ return s.charAt(0).toUpperCase() + s.slice(1)
+}
+
+// Ensure to six-based hex
+function fullHex(hex) {
+ return hex.length == 4 ?
+ [ '#',
+ hex.substring(1, 2), hex.substring(1, 2)
+ , hex.substring(2, 3), hex.substring(2, 3)
+ , hex.substring(3, 4), hex.substring(3, 4)
+ ].join('') : hex
+}
+
+// Component to hex value
+function compToHex(comp) {
+ var hex = comp.toString(16)
+ return hex.length == 1 ? '0' + hex : hex
+}
+
+// Calculate proportional width and height values when necessary
+function proportionalSize(element, width, height) {
+ if (width == null || height == null) {
+ var box = element.bbox()
+
+ if (width == null)
+ width = box.width / box.height * height
+ else if (height == null)
+ height = box.height / box.width * width
+ }
+
+ return {
+ width: width
+ , height: height
+ }
+}
+
+// Delta transform point
+function deltaTransformPoint(matrix, x, y) {
+ return {
+ x: x * matrix.a + y * matrix.c + 0
+ , y: x * matrix.b + y * matrix.d + 0
+ }
+}
+
+// Map matrix array to object
+function arrayToMatrix(a) {
+ return { a: a[0], b: a[1], c: a[2], d: a[3], e: a[4], f: a[5] }
+}
+
+// Parse matrix if required
+function parseMatrix(matrix) {
+ if (!(matrix instanceof SVG.Matrix))
+ matrix = new SVG.Matrix(matrix)
+
+ return matrix
+}
+
+// Add centre point to transform object
+function ensureCentre(o, target) {
+ o.cx = o.cx == null ? target.bbox().cx : o.cx
+ o.cy = o.cy == null ? target.bbox().cy : o.cy
+}
+
+// PathArray Helpers
+function arrayToString(a) {
+ for (var i = 0, il = a.length, s = ''; i < il; i++) {
+ s += a[i][0]
+
+ if (a[i][1] != null) {
+ s += a[i][1]
+
+ if (a[i][2] != null) {
+ s += ' '
+ s += a[i][2]
+
+ if (a[i][3] != null) {
+ s += ' '
+ s += a[i][3]
+ s += ' '
+ s += a[i][4]
+
+ if (a[i][5] != null) {
+ s += ' '
+ s += a[i][5]
+ s += ' '
+ s += a[i][6]
+
+ if (a[i][7] != null) {
+ s += ' '
+ s += a[i][7]
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return s + ' '
+}
+
+// Deep new id assignment
+function assignNewId(node) {
+ // do the same for SVG child nodes as well
+ for (var i = node.childNodes.length - 1; i >= 0; i--)
+ if (node.childNodes[i] instanceof window.SVGElement)
+ assignNewId(node.childNodes[i])
+
+ return SVG.adopt(node).id(SVG.eid(node.nodeName))
+}
+
+// Add more bounding box properties
+function fullBox(b) {
+ if (b.x == null) {
+ b.x = 0
+ b.y = 0
+ b.width = 0
+ b.height = 0
+ }
+
+ b.w = b.width
+ b.h = b.height
+ b.x2 = b.x + b.width
+ b.y2 = b.y + b.height
+ b.cx = b.x + b.width / 2
+ b.cy = b.y + b.height / 2
+
+ return b
+}
+
+// Get id from reference string
+function idFromReference(url) {
+ var m = url.toString().match(SVG.regex.reference)
+
+ if (m) return m[1]
+}
+
+// Create matrix array for looping
+var abcdef = 'abcdef'.split('')
+// Add CustomEvent to IE9 and IE10
+if (typeof window.CustomEvent !== 'function') {
+ // Code from: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
+ var CustomEvent = function(event, options) {
+ options = options || { bubbles: false, cancelable: false, detail: undefined }
+ var e = document.createEvent('CustomEvent')
+ e.initCustomEvent(event, options.bubbles, options.cancelable, options.detail)
+ return e
+ }
+
+ CustomEvent.prototype = window.Event.prototype
+
+ window.CustomEvent = CustomEvent
+}
+
+// requestAnimationFrame / cancelAnimationFrame Polyfill with fallback based on Paul Irish
+(function(w) {
+ var lastTime = 0
+ var vendors = ['moz', 'webkit']
+
+ for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ w.requestAnimationFrame = w[vendors[x] + 'RequestAnimationFrame']
+ w.cancelAnimationFrame = w[vendors[x] + 'CancelAnimationFrame'] ||
+ w[vendors[x] + 'CancelRequestAnimationFrame']
+ }
+
+ w.requestAnimationFrame = w.requestAnimationFrame ||
+ function(callback) {
+ var currTime = new Date().getTime()
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime))
+
+ var id = w.setTimeout(function() {
+ callback(currTime + timeToCall)
+ }, timeToCall)
+
+ lastTime = currTime + timeToCall
+ return id
+ }
+
+ w.cancelAnimationFrame = w.cancelAnimationFrame || w.clearTimeout;
+
+}(window))
+
+return SVG
+
+}));
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/dist/svg.min.js b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/dist/svg.min.js
new file mode 100644
index 0000000..29ce812
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/dist/svg.min.js
@@ -0,0 +1,3 @@
+/*! svg.js v2.6.1 MIT*/;!function(t,e){"function"==typeof define&&define.amd?define(function(){return e(t,t.document)}):"object"==typeof exports?module.exports=t.document?e(t,t.document):function(t){return e(t,t.document)}:t.SVG=e(t,t.document)}("undefined"!=typeof window?window:this,function(t,e){function i(t,e,i,n){return i+n.replace(g.regex.dots," .")}function n(t){for(var e=t.slice(0),i=e.length;i--;)Array.isArray(e[i])&&(e[i]=n(e[i]));return e}function r(t,e){return t instanceof e}function s(t,e){return(t.matches||t.matchesSelector||t.msMatchesSelector||t.mozMatchesSelector||t.webkitMatchesSelector||t.oMatchesSelector).call(t,e)}function o(t){return t.toLowerCase().replace(/-(.)/g,function(t,e){return e.toUpperCase()})}function a(t){return t.charAt(0).toUpperCase()+t.slice(1)}function h(t){return 4==t.length?["#",t.substring(1,2),t.substring(1,2),t.substring(2,3),t.substring(2,3),t.substring(3,4),t.substring(3,4)].join(""):t}function u(t){var e=t.toString(16);return 1==e.length?"0"+e:e}function l(t,e,i){if(null==e||null==i){var n=t.bbox();null==e?e=n.width/n.height*i:null==i&&(i=n.height/n.width*e)}return{width:e,height:i}}function c(t,e,i){return{x:e*t.a+i*t.c+0,y:e*t.b+i*t.d+0}}function f(t){return{a:t[0],b:t[1],c:t[2],d:t[3],e:t[4],f:t[5]}}function d(t){return t instanceof g.Matrix||(t=new g.Matrix(t)),t}function p(t,e){t.cx=null==t.cx?e.bbox().cx:t.cx,t.cy=null==t.cy?e.bbox().cy:t.cy}function m(t){for(var e=0,i=t.length,n="";e=0;i--)e.childNodes[i]instanceof t.SVGElement&&x(e.childNodes[i]);return g.adopt(e).id(g.eid(e.nodeName))}function y(t){return null==t.x&&(t.x=0,t.y=0,t.width=0,t.height=0),t.w=t.width,t.h=t.height,t.x2=t.x+t.width,t.y2=t.y+t.height,t.cx=t.x+t.width/2,t.cy=t.y+t.height/2,t}function v(t){var e=t.toString().match(g.regex.reference);if(e)return e[1]}var g=this.SVG=function(t){if(g.supported)return t=new g.Doc(t),g.parser.draw||g.prepare(),t};if(g.ns="http://www.w3.org/2000/svg",g.xmlns="http://www.w3.org/2000/xmlns/",g.xlink="http://www.w3.org/1999/xlink",g.svgjs="http://svgjs.com/svgjs",g.supported=function(){return!!e.createElementNS&&!!e.createElementNS(g.ns,"svg").createSVGRect}(),!g.supported)return!1;g.did=1e3,g.eid=function(t){return"Svgjs"+a(t)+g.did++},g.create=function(t){var i=e.createElementNS(this.ns,t);return i.setAttribute("id",this.eid(t)),i},g.extend=function(){var t,e,i,n;for(t=[].slice.call(arguments),e=t.pop(),n=t.length-1;n>=0;n--)if(t[n])for(i in e)t[n].prototype[i]=e[i];g.Set&&g.Set.inherit&&g.Set.inherit()},g.invent=function(t){var e="function"==typeof t.create?t.create:function(){this.constructor.call(this,g.create(t.create))};return t.inherit&&(e.prototype=new t.inherit),t.extend&&g.extend(e,t.extend),t.construct&&g.extend(t.parent||g.Container,t.construct),e},g.adopt=function(e){if(!e)return null;if(e.instance)return e.instance;var i;return i="svg"==e.nodeName?e.parentNode instanceof t.SVGElement?new g.Nested:new g.Doc:"linearGradient"==e.nodeName?new g.Gradient("linear"):"radialGradient"==e.nodeName?new g.Gradient("radial"):g[a(e.nodeName)]?new(g[a(e.nodeName)]):new g.Element(e),i.type=e.nodeName,i.node=e,e.instance=i,i instanceof g.Doc&&i.namespace().defs(),i.setData(JSON.parse(e.getAttribute("svgjs:data"))||{}),i},g.prepare=function(){var t=e.getElementsByTagName("body")[0],i=(t?new g.Doc(t):g.adopt(e.documentElement).nested()).size(2,0);g.parser={body:t||e.documentElement,draw:i.style("opacity:0;position:absolute;left:-100%;top:-100%;overflow:hidden").node,poly:i.polyline().node,path:i.path().node,native:g.create("svg")}},g.parser={native:g.create("svg")},e.addEventListener("DOMContentLoaded",function(){g.parser.draw||g.prepare()},!1),g.regex={numberAndUnit:/^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i,hex:/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,rgb:/rgb\((\d+),(\d+),(\d+)\)/,reference:/#([a-z0-9\-_]+)/i,transforms:/\)\s*,?\s*/,whitespace:/\s/g,isHex:/^#[a-f0-9]{3,6}$/i,isRgb:/^rgb\(/,isCss:/[^:]+:[^;]+;?/,isBlank:/^(\s+)?$/,isNumber:/^[+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,isPercent:/^-?[\d\.]+%$/,isImage:/\.(jpg|jpeg|png|gif|svg)(\?[^=]+.*)?/i,delimiter:/[\s,]+/,hyphen:/([^e])\-/gi,pathLetters:/[MLHVCSQTAZ]/gi,isPathLetter:/[MLHVCSQTAZ]/i,numbersWithDots:/((\d?\.\d+(?:e[+-]?\d+)?)((?:\.\d+(?:e[+-]?\d+)?)+))+/gi,dots:/\./g},g.utils={map:function(t,e){var i,n=t.length,r=[];for(i=0;i1?1:t,new g.Color({r:~~(this.r+(this.destination.r-this.r)*t),g:~~(this.g+(this.destination.g-this.g)*t),b:~~(this.b+(this.destination.b-this.b)*t)})):this}}),g.Color.test=function(t){return t+="",g.regex.isHex.test(t)||g.regex.isRgb.test(t)},g.Color.isRgb=function(t){return t&&"number"==typeof t.r&&"number"==typeof t.g&&"number"==typeof t.b},g.Color.isColor=function(t){return g.Color.isRgb(t)||g.Color.test(t)},g.Array=function(t,e){t=(t||[]).valueOf(),0==t.length&&e&&(t=e.valueOf()),this.value=this.parse(t)},g.extend(g.Array,{morph:function(t){if(this.destination=this.parse(t),this.value.length!=this.destination.length){for(var e=this.value[this.value.length-1],i=this.destination[this.destination.length-1];this.value.length>this.destination.length;)this.destination.push(i);for(;this.value.length=0;n--)this.value[n]=[this.value[n][0]+t,this.value[n][1]+e];return this},size:function(t,e){var i,n=this.bbox();for(i=this.value.length-1;i>=0;i--)n.width&&(this.value[i][0]=(this.value[i][0]-n.x)*t/n.width+n.x),n.height&&(this.value[i][1]=(this.value[i][1]-n.y)*e/n.height+n.y);return this},bbox:function(){return g.parser.poly.setAttribute("points",this.toString()),g.parser.poly.getBBox()}});for(var w={M:function(t,e,i){return e.x=i.x=t[0],e.y=i.y=t[1],["M",e.x,e.y]},L:function(t,e){return e.x=t[0],e.y=t[1],["L",t[0],t[1]]},H:function(t,e){return e.x=t[0],["H",t[0]]},V:function(t,e){return e.y=t[0],["V",t[0]]},C:function(t,e){return e.x=t[4],e.y=t[5],["C",t[0],t[1],t[2],t[3],t[4],t[5]]},S:function(t,e){return e.x=t[2],e.y=t[3],["S",t[0],t[1],t[2],t[3]]},Q:function(t,e){return e.x=t[2],e.y=t[3],["Q",t[0],t[1],t[2],t[3]]},T:function(t,e){return e.x=t[0],e.y=t[1],["T",t[0],t[1]]},Z:function(t,e,i){return e.x=i.x,e.y=i.y,["Z"]},A:function(t,e){return e.x=t[5],e.y=t[6],["A",t[0],t[1],t[2],t[3],t[4],t[5],t[6]]}},b="mlhvqtcsaz".split(""),C=0,N=b.length;C=0;r--)n=this.value[r][0],"M"==n||"L"==n||"T"==n?(this.value[r][1]+=t,this.value[r][2]+=e):"H"==n?this.value[r][1]+=t:"V"==n?this.value[r][1]+=e:"C"==n||"S"==n||"Q"==n?(this.value[r][1]+=t,this.value[r][2]+=e,this.value[r][3]+=t,this.value[r][4]+=e,"C"==n&&(this.value[r][5]+=t,this.value[r][6]+=e)):"A"==n&&(this.value[r][6]+=t,this.value[r][7]+=e);return this},size:function(t,e){var i,n,r=this.bbox();for(i=this.value.length-1;i>=0;i--)n=this.value[i][0],"M"==n||"L"==n||"T"==n?(this.value[i][1]=(this.value[i][1]-r.x)*t/r.width+r.x,this.value[i][2]=(this.value[i][2]-r.y)*e/r.height+r.y):"H"==n?this.value[i][1]=(this.value[i][1]-r.x)*t/r.width+r.x:"V"==n?this.value[i][1]=(this.value[i][1]-r.y)*e/r.height+r.y:"C"==n||"S"==n||"Q"==n?(this.value[i][1]=(this.value[i][1]-r.x)*t/r.width+r.x,this.value[i][2]=(this.value[i][2]-r.y)*e/r.height+r.y,this.value[i][3]=(this.value[i][3]-r.x)*t/r.width+r.x,this.value[i][4]=(this.value[i][4]-r.y)*e/r.height+r.y,"C"==n&&(this.value[i][5]=(this.value[i][5]-r.x)*t/r.width+r.x,this.value[i][6]=(this.value[i][6]-r.y)*e/r.height+r.y)):"A"==n&&(this.value[i][1]=this.value[i][1]*t/r.width,this.value[i][2]=this.value[i][2]*e/r.height,this.value[i][6]=(this.value[i][6]-r.x)*t/r.width+r.x,this.value[i][7]=(this.value[i][7]-r.y)*e/r.height+r.y);return this},equalCommands:function(t){var e,i,n;for(t=new g.PathArray(t),n=this.value.length===t.value.length,e=0,i=this.value.length;n&&ea);return n},bbox:function(){return g.parser.path.setAttribute("d",this.toString()),g.parser.path.getBBox()}}),g.Number=g.invent({create:function(t,e){this.value=0,this.unit=e||"","number"==typeof t?this.value=isNaN(t)?0:isFinite(t)?t:t<0?-3.4e38:3.4e38:"string"==typeof t?(e=t.match(g.regex.numberAndUnit),e&&(this.value=parseFloat(e[1]),"%"==e[5]?this.value/=100:"s"==e[5]&&(this.value*=1e3),this.unit=e[5])):t instanceof g.Number&&(this.value=t.valueOf(),this.unit=t.unit)},extend:{toString:function(){return("%"==this.unit?~~(1e8*this.value)/1e6:"s"==this.unit?this.value/1e3:this.value)+this.unit},toJSON:function(){return this.toString()},valueOf:function(){return this.value},plus:function(t){return t=new g.Number(t),new g.Number(this+t,this.unit||t.unit)},minus:function(t){return t=new g.Number(t),new g.Number(this-t,this.unit||t.unit)},times:function(t){return t=new g.Number(t),new g.Number(this*t,this.unit||t.unit)},divide:function(t){return t=new g.Number(t),new g.Number(this/t,this.unit||t.unit)},to:function(t){var e=new g.Number(this);return"string"==typeof t&&(e.unit=t),e},morph:function(t){return this.destination=new g.Number(t),t.relative&&(this.destination.value+=this.value),this},at:function(t){return this.destination?new g.Number(this.destination).minus(this).times(t).plus(this):this}}}),g.Element=g.invent({create:function(t){this._stroke=g.defaults.attrs.stroke,this._event=null,this.dom={},(this.node=t)&&(this.type=t.nodeName,this.node.instance=this,this._stroke=t.getAttribute("stroke")||this._stroke)},extend:{x:function(t){return this.attr("x",t)},y:function(t){return this.attr("y",t)},cx:function(t){return null==t?this.x()+this.width()/2:this.x(t-this.width()/2)},cy:function(t){return null==t?this.y()+this.height()/2:this.y(t-this.height()/2)},move:function(t,e){return this.x(t).y(e)},center:function(t,e){return this.cx(t).cy(e)},width:function(t){return this.attr("width",t)},height:function(t){return this.attr("height",t)},size:function(t,e){var i=l(this,t,e);return this.width(new g.Number(i.width)).height(new g.Number(i.height))},clone:function(t,e){this.writeDataToDom();var i=x(this.node.cloneNode(!0));return t?t.add(i):this.after(i),i},remove:function(){return this.parent()&&this.parent().removeElement(this),this},replace:function(t){return this.after(t).remove(),t},addTo:function(t){return t.put(this)},putIn:function(t){return t.add(this)},id:function(t){return this.attr("id",t)},inside:function(t,e){var i=this.bbox();return t>i.x&&e>i.y&&t/,"").replace(/<\/svg>$/,"");i.innerHTML=""+t.replace(/\n/,"").replace(/<(\w+)([^<]+?)\/>/g,"<$1$2>$1>")+" ";for(var n=0,r=i.firstChild.childNodes.length;n":function(t){return-Math.cos(t*Math.PI)/2+.5},">":function(t){return Math.sin(t*Math.PI/2)},"<":function(t){return-Math.cos(t*Math.PI/2)+1}},g.morph=function(t){return function(e,i){return new g.MorphObj(e,i).at(t)}},g.Situation=g.invent({create:function(t){this.init=!1,this.reversed=!1,this.reversing=!1,this.duration=new g.Number(t.duration).valueOf(),this.delay=new g.Number(t.delay).valueOf(),this.start=+new Date+this.delay,this.finish=this.start+this.duration,this.ease=t.ease,this.loop=0,this.loops=!1,this.animations={},this.attrs={},this.styles={},this.transforms=[],this.once={}}}),g.FX=g.invent({create:function(t){this._target=t,this.situations=[],this.active=!1,this.situation=null,this.paused=!1,this.lastPos=0,this.pos=0,this.absPos=0,this._speed=1},extend:{animate:function(t,e,i){"object"==typeof t&&(e=t.ease,i=t.delay,t=t.duration);var n=new g.Situation({duration:t||1e3,delay:i||0,ease:g.easing[e||"-"]||e});return this.queue(n),this},delay:function(t){var e=new g.Situation({duration:t,delay:0,ease:g.easing["-"]});return this.queue(e)},target:function(t){return t&&t instanceof g.Element?(this._target=t,this):this._target},timeToAbsPos:function(t){return(t-this.situation.start)/(this.situation.duration/this._speed)},absPosToTime:function(t){return this.situation.duration/this._speed*t+this.situation.start},startAnimFrame:function(){this.stopAnimFrame(),this.animationFrame=t.requestAnimationFrame(function(){this.step()}.bind(this))},stopAnimFrame:function(){t.cancelAnimationFrame(this.animationFrame)},start:function(){return!this.active&&this.situation&&(this.active=!0,this.startCurrent()),this},startCurrent:function(){return this.situation.start=+new Date+this.situation.delay/this._speed,this.situation.finish=this.situation.start+this.situation.duration/this._speed,this.initAnimations().step()},queue:function(t){return("function"==typeof t||t instanceof g.Situation)&&this.situations.push(t),this.situation||(this.situation=this.situations.shift()),this},dequeue:function(){return this.stop(),this.situation=this.situations.shift(),this.situation&&(this.situation instanceof g.Situation?this.start():this.situation.call(this)),this},initAnimations:function(){var t,e,i=this.situation;if(i.init)return this;for(t in i.animations)e=this.target()[t](),i.animations[t]instanceof g.Number&&(e=new g.Number(e)),i.animations[t]=e.morph(i.animations[t]);for(t in i.attrs)i.attrs[t]=new g.MorphObj(this.target().attr(t),i.attrs[t]);for(t in i.styles)i.styles[t]=new g.MorphObj(this.target().style(t),i.styles[t]);return i.initialTransformation=this.target().matrixify(),i.init=!0,this},clearQueue:function(){return this.situations=[],this},clearCurrent:function(){return this.situation=null,this},stop:function(t,e){var i=this.active;return this.active=!1,e&&this.clearQueue(),t&&this.situation&&(!i&&this.startCurrent(),this.atEnd()),this.stopAnimFrame(),this.clearCurrent()},reset:function(){if(this.situation){var t=this.situation;this.stop(),this.situation=t,this.atStart()}return this},finish:function(){for(this.stop(!0,!1);this.dequeue().situation&&this.stop(!0,!1););return this.clearQueue().clearCurrent(),this},atStart:function(){return this.at(0,!0)},atEnd:function(){return this.situation.loops===!0&&(this.situation.loops=this.situation.loop+1),"number"==typeof this.situation.loops?this.at(this.situation.loops,!0):this.at(1,!0)},at:function(t,e){var i=this.situation.duration/this._speed;return this.absPos=t,e||(this.situation.reversed&&(this.absPos=1-this.absPos),this.absPos+=this.situation.loop),this.situation.start=+new Date-this.absPos*i,this.situation.finish=this.situation.start+i,this.step(!0)},speed:function(t){return 0===t?this.pause():t?(this._speed=t,this.at(this.absPos,!0)):this._speed},loop:function(t,e){var i=this.last();return i.loops=null==t||t,i.loop=0,e&&(i.reversing=!0),this},pause:function(){return this.paused=!0,this.stopAnimFrame(),this},play:function(){return this.paused?(this.paused=!1,this.at(this.absPos,!0)):this},reverse:function(t){var e=this.last();return"undefined"==typeof t?e.reversed=!e.reversed:e.reversed=t,this},progress:function(t){return t?this.situation.ease(this.pos):this.pos},after:function(t){var e=this.last(),i=function i(n){n.detail.situation==e&&(t.call(this,e),this.off("finished.fx",i))};return this.target().on("finished.fx",i),this._callStart()},during:function(t){var e=this.last(),i=function(i){i.detail.situation==e&&t.call(this,i.detail.pos,g.morph(i.detail.pos),i.detail.eased,e)};return this.target().off("during.fx",i).on("during.fx",i),this.after(function(){this.off("during.fx",i)}),this._callStart()},afterAll:function(t){var e=function e(i){t.call(this),this.off("allfinished.fx",e)};return this.target().off("allfinished.fx",e).on("allfinished.fx",e),this._callStart()},duringAll:function(t){var e=function(e){t.call(this,e.detail.pos,g.morph(e.detail.pos),e.detail.eased,e.detail.situation)};return this.target().off("during.fx",e).on("during.fx",e),this.afterAll(function(){this.off("during.fx",e)}),this._callStart()},last:function(){return this.situations.length?this.situations[this.situations.length-1]:this.situation},add:function(t,e,i){return this.last()[i||"animations"][t]=e,this._callStart()},step:function(t){if(t||(this.absPos=this.timeToAbsPos(+new Date)),this.situation.loops!==!1){var e,i,n;e=Math.max(this.absPos,0),i=Math.floor(e),this.situation.loops===!0||ithis.lastPos&&s<=r&&(this.situation.once[s].call(this.target(),this.pos,r),delete this.situation.once[s]);return this.active&&this.target().fire("during",{pos:this.pos,eased:r,fx:this,situation:this.situation}),this.situation?(this.eachAt(),1==this.pos&&!this.situation.reversed||this.situation.reversed&&0==this.pos?(this.stopAnimFrame(),this.target().fire("finished",{fx:this,situation:this.situation}),this.situations.length||(this.target().fire("allfinished"),this.target().off(".fx"),this.active=!1),this.active?this.dequeue():this.clearCurrent()):!this.paused&&this.active&&this.startAnimFrame(),this.lastPos=r,this):this},eachAt:function(){var t,e,i,n=this,r=this.target(),s=this.situation;for(t in s.animations)i=[].concat(s.animations[t]).map(function(t){return"string"!=typeof t&&t.at?t.at(s.ease(n.pos),n.pos):t}),r[t].apply(r,i);for(t in s.attrs)i=[t].concat(s.attrs[t]).map(function(t){return"string"!=typeof t&&t.at?t.at(s.ease(n.pos),n.pos):t}),r.attr.apply(r,i);for(t in s.styles)i=[t].concat(s.styles[t]).map(function(t){return"string"!=typeof t&&t.at?t.at(s.ease(n.pos),n.pos):t}),r.style.apply(r,i);if(s.transforms.length){for(i=s.initialTransformation,t=0,e=s.transforms.length;t1?[].slice.call(arguments):arguments[0])},leading:function(t){return this.target().leading?this.add("leading",new g.Number(t)):this},viewbox:function(t,e,i,n){return this.target()instanceof g.Container&&this.add("viewbox",new g.ViewBox(t,e,i,n)),this},update:function(t){if(this.target()instanceof g.Stop){if("number"==typeof t||t instanceof g.Number)return this.update({offset:arguments[0],color:arguments[1],opacity:arguments[2]});null!=t.opacity&&this.attr("stop-opacity",t.opacity),null!=t.color&&this.attr("stop-color",t.color),null!=t.offset&&this.attr("offset",t.offset)}return this}}),g.Box=g.invent({create:function(t,e,i,n){return"object"!=typeof t||t instanceof g.Element?(4==arguments.length&&(this.x=t,this.y=e,this.width=i,this.height=n),void y(this)):g.Box.call(this,null!=t.left?t.left:t.x,null!=t.top?t.top:t.y,t.width,t.height)},extend:{merge:function(t){var e=new this.constructor;return e.x=Math.min(this.x,t.x),e.y=Math.min(this.y,t.y),e.width=Math.max(this.x+this.width,t.x+t.width)-e.x,e.height=Math.max(this.y+this.height,t.y+t.height)-e.y,y(e)},transform:function(t){var e,i=1/0,n=-(1/0),r=1/0,s=-(1/0),o=[new g.Point(this.x,this.y),new g.Point(this.x2,this.y),new g.Point(this.x,this.y2),new g.Point(this.x2,this.y2)];return o.forEach(function(e){e=e.transform(t),i=Math.min(i,e.x),n=Math.max(n,e.x),r=Math.min(r,e.y),s=Math.max(s,e.y)}),e=new this.constructor,e.x=i,e.width=n-i,e.y=r,e.height=s-r,y(e),e}}}),g.BBox=g.invent({create:function(t){if(g.Box.apply(this,[].slice.call(arguments)),t instanceof g.Element){var i;try{if(e.documentElement.contains){if(!e.documentElement.contains(t.node))throw new Exception("Element not in the dom")}else{for(var n=t.node;n.parentNode;)n=n.parentNode;if(n!=e)throw new Exception("Element not in the dom")}i=t.node.getBBox()}catch(e){if(t instanceof g.Shape){var r=t.clone(g.parser.draw.instance).show();i=r.node.getBBox(),r.remove()}else i={x:t.node.clientLeft,y:t.node.clientTop,width:t.node.clientWidth,height:t.node.clientHeight}}g.Box.call(this,i)}},inherit:g.Box,parent:g.Element,construct:{bbox:function(){return new g.BBox(this)}}}),g.BBox.prototype.constructor=g.BBox,g.extend(g.Element,{tbox:function(){return console.warn("Use of TBox is deprecated and mapped to RBox. Use .rbox() instead."),this.rbox(this.doc())}}),g.RBox=g.invent({create:function(t){g.Box.apply(this,[].slice.call(arguments)),t instanceof g.Element&&g.Box.call(this,t.node.getBoundingClientRect())},inherit:g.Box,parent:g.Element,extend:{addOffset:function(){return this.x+=t.pageXOffset,this.y+=t.pageYOffset,this}},construct:{rbox:function(t){return t?new g.RBox(this).transform(t.screenCTM().inverse()):new g.RBox(this).addOffset()}}}),g.RBox.prototype.constructor=g.RBox,g.Matrix=g.invent({create:function(t){var e,i=f([1,0,0,1,0,0]);for(t=t instanceof g.Element?t.matrixify():"string"==typeof t?f(t.split(g.regex.delimiter).map(parseFloat)):6==arguments.length?f([].slice.call(arguments)):Array.isArray(t)?f(t):"object"==typeof t?t:i,e=P.length-1;e>=0;--e)this[P[e]]=t&&"number"==typeof t[P[e]]?t[P[e]]:i[P[e]]},extend:{extract:function(){var t=c(this,0,1),e=c(this,1,0),i=180/Math.PI*Math.atan2(t.y,t.x)-90;return{x:this.e,y:this.f,transformedX:(this.e*Math.cos(i*Math.PI/180)+this.f*Math.sin(i*Math.PI/180))/Math.sqrt(this.a*this.a+this.b*this.b),transformedY:(this.f*Math.cos(i*Math.PI/180)+this.e*Math.sin(-i*Math.PI/180))/Math.sqrt(this.c*this.c+this.d*this.d),skewX:-i,skewY:180/Math.PI*Math.atan2(e.y,e.x),scaleX:Math.sqrt(this.a*this.a+this.b*this.b),scaleY:Math.sqrt(this.c*this.c+this.d*this.d),rotation:i,a:this.a,b:this.b,c:this.c,d:this.d,e:this.e,f:this.f,matrix:new g.Matrix(this)}},clone:function(){return new g.Matrix(this)},morph:function(t){return this.destination=new g.Matrix(t),this},at:function(t){if(!this.destination)return this;var e=new g.Matrix({a:this.a+(this.destination.a-this.a)*t,b:this.b+(this.destination.b-this.b)*t,c:this.c+(this.destination.c-this.c)*t,d:this.d+(this.destination.d-this.d)*t,e:this.e+(this.destination.e-this.e)*t,f:this.f+(this.destination.f-this.f)*t});return e},multiply:function(t){return new g.Matrix(this.native().multiply(d(t).native()))},inverse:function(){return new g.Matrix(this.native().inverse())},translate:function(t,e){return new g.Matrix(this.native().translate(t||0,e||0))},scale:function(t,e,i,n){return 1==arguments.length?e=t:3==arguments.length&&(n=i,i=e,e=t),this.around(i,n,new g.Matrix(t,0,0,e,0,0))},rotate:function(t,e,i){return t=g.utils.radians(t),this.around(e,i,new g.Matrix(Math.cos(t),Math.sin(t),-Math.sin(t),Math.cos(t),0,0))},flip:function(t,e){return"x"==t?this.scale(-1,1,e,0):"y"==t?this.scale(1,-1,0,e):this.scale(-1,-1,t,null!=e?e:t)},skew:function(t,e,i,n){return 1==arguments.length?e=t:3==arguments.length&&(n=i,i=e,e=t),t=g.utils.radians(t),e=g.utils.radians(e),this.around(i,n,new g.Matrix(1,Math.tan(e),Math.tan(t),1,0,0))},skewX:function(t,e,i){return this.skew(t,0,e,i)},skewY:function(t,e,i){return this.skew(0,t,e,i)},around:function(t,e,i){return this.multiply(new g.Matrix(1,0,0,1,t||0,e||0)).multiply(i).multiply(new g.Matrix(1,0,0,1,-t||0,-e||0))},native:function(){for(var t=g.parser.native.createSVGMatrix(),e=P.length-1;e>=0;e--)t[P[e]]=this[P[e]];return t},toString:function(){return"matrix("+this.a+","+this.b+","+this.c+","+this.d+","+this.e+","+this.f+")"}},parent:g.Element,construct:{ctm:function(){return new g.Matrix(this.node.getCTM());
+},screenCTM:function(){if(this instanceof g.Nested){var t=this.rect(1,1),e=t.node.getScreenCTM();return t.remove(),new g.Matrix(e)}return new g.Matrix(this.node.getScreenCTM())}}}),g.Point=g.invent({create:function(t,e){var i,n={x:0,y:0};i=Array.isArray(t)?{x:t[0],y:t[1]}:"object"==typeof t?{x:t.x,y:t.y}:null!=t?{x:t,y:null!=e?e:t}:n,this.x=i.x,this.y=i.y},extend:{clone:function(){return new g.Point(this)},morph:function(t,e){return this.destination=new g.Point(t,e),this},at:function(t){if(!this.destination)return this;var e=new g.Point({x:this.x+(this.destination.x-this.x)*t,y:this.y+(this.destination.y-this.y)*t});return e},native:function(){var t=g.parser.native.createSVGPoint();return t.x=this.x,t.y=this.y,t},transform:function(t){return new g.Point(this.native().matrixTransform(t.native()))}}}),g.extend(g.Element,{point:function(t,e){return new g.Point(t,e).transform(this.screenCTM().inverse())}}),g.extend(g.Element,{attr:function(t,e,i){if(null==t){for(t={},e=this.node.attributes,i=e.length-1;i>=0;i--)t[e[i].nodeName]=g.regex.isNumber.test(e[i].nodeValue)?parseFloat(e[i].nodeValue):e[i].nodeValue;return t}if("object"==typeof t)for(e in t)this.attr(e,t[e]);else if(null===e)this.node.removeAttribute(t);else{if(null==e)return e=this.node.getAttribute(t),null==e?g.defaults.attrs[t]:g.regex.isNumber.test(e)?parseFloat(e):e;"stroke-width"==t?this.attr("stroke",parseFloat(e)>0?this._stroke:null):"stroke"==t&&(this._stroke=e),"fill"!=t&&"stroke"!=t||(g.regex.isImage.test(e)&&(e=this.doc().defs().image(e,0,0)),e instanceof g.Image&&(e=this.doc().defs().pattern(0,0,function(){this.add(e)}))),"number"==typeof e?e=new g.Number(e):g.Color.isColor(e)?e=new g.Color(e):Array.isArray(e)&&(e=new g.Array(e)),"leading"==t?this.leading&&this.leading(e):"string"==typeof i?this.node.setAttributeNS(i,t,e.toString()):this.node.setAttribute(t,e.toString()),!this.rebuild||"font-size"!=t&&"x"!=t||this.rebuild(t,e)}return this}}),g.extend(g.Element,{transform:function(t,e){var i,n,r=this;if("object"!=typeof t)return i=new g.Matrix(r).extract(),"string"==typeof t?i[t]:i;if(i=new g.Matrix(r),e=!!e||!!t.relative,null!=t.a)i=e?i.multiply(new g.Matrix(t)):new g.Matrix(t);else if(null!=t.rotation)p(t,r),i=e?i.rotate(t.rotation,t.cx,t.cy):i.rotate(t.rotation-i.extract().rotation,t.cx,t.cy);else if(null!=t.scale||null!=t.scaleX||null!=t.scaleY){if(p(t,r),t.scaleX=null!=t.scale?t.scale:null!=t.scaleX?t.scaleX:1,t.scaleY=null!=t.scale?t.scale:null!=t.scaleY?t.scaleY:1,!e){var s=i.extract();t.scaleX=1*t.scaleX/s.scaleX,t.scaleY=1*t.scaleY/s.scaleY}i=i.scale(t.scaleX,t.scaleY,t.cx,t.cy)}else if(null!=t.skew||null!=t.skewX||null!=t.skewY){if(p(t,r),t.skewX=null!=t.skew?t.skew:null!=t.skewX?t.skewX:0,t.skewY=null!=t.skew?t.skew:null!=t.skewY?t.skewY:0,!e){var s=i.extract();i=i.multiply((new g.Matrix).skew(s.skewX,s.skewY,t.cx,t.cy).inverse())}i=i.skew(t.skewX,t.skewY,t.cx,t.cy)}else t.flip?("x"==t.flip||"y"==t.flip?t.offset=null==t.offset?r.bbox()["c"+t.flip]:t.offset:null==t.offset?(n=r.bbox(),t.flip=n.cx,t.offset=n.cy):t.flip=t.offset,i=(new g.Matrix).flip(t.flip,t.offset)):null==t.x&&null==t.y||(e?i=i.translate(t.x,t.y):(null!=t.x&&(i.e=t.x),null!=t.y&&(i.f=t.y)));return this.attr("transform",i)}}),g.extend(g.FX,{transform:function(t,e){var i,n,r=this.target();return"object"!=typeof t?(i=new g.Matrix(r).extract(),"string"==typeof t?i[t]:i):(e=!!e||!!t.relative,null!=t.a?i=new g.Matrix(t):null!=t.rotation?(p(t,r),i=new g.Rotate(t.rotation,t.cx,t.cy)):null!=t.scale||null!=t.scaleX||null!=t.scaleY?(p(t,r),t.scaleX=null!=t.scale?t.scale:null!=t.scaleX?t.scaleX:1,t.scaleY=null!=t.scale?t.scale:null!=t.scaleY?t.scaleY:1,i=new g.Scale(t.scaleX,t.scaleY,t.cx,t.cy)):null!=t.skewX||null!=t.skewY?(p(t,r),t.skewX=null!=t.skewX?t.skewX:0,t.skewY=null!=t.skewY?t.skewY:0,i=new g.Skew(t.skewX,t.skewY,t.cx,t.cy)):t.flip?("x"==t.flip||"y"==t.flip?t.offset=null==t.offset?r.bbox()["c"+t.flip]:t.offset:null==t.offset?(n=r.bbox(),t.flip=n.cx,t.offset=n.cy):t.flip=t.offset,i=(new g.Matrix).flip(t.flip,t.offset)):null==t.x&&null==t.y||(i=new g.Translate(t.x,t.y)),i?(i.relative=e,this.last().transforms.push(i),this._callStart()):this)}}),g.extend(g.Element,{untransform:function(){return this.attr("transform",null)},matrixify:function(){var t=(this.attr("transform")||"").split(g.regex.transforms).slice(0,-1).map(function(t){var e=t.trim().split("(");return[e[0],e[1].split(g.regex.delimiter).map(function(t){return parseFloat(t)})]}).reduce(function(t,e){return"matrix"==e[0]?t.multiply(f(e[1])):t[e[0]].apply(t,e[1])},new g.Matrix);return t},toParent:function(t){if(this==t)return this;var e=this.screenCTM(),i=t.screenCTM().inverse();return this.addTo(t).untransform().transform(i.multiply(e)),this},toDoc:function(){return this.toParent(this.doc())}}),g.Transformation=g.invent({create:function(t,e){if(arguments.length>1&&"boolean"!=typeof e)return this.constructor.call(this,[].slice.call(arguments));if(Array.isArray(t))for(var i=0,n=this.arguments.length;i=0},index:function(t){return[].slice.call(this.node.childNodes).indexOf(t.node)},get:function(t){return g.adopt(this.node.childNodes[t])},first:function(){return this.get(0)},last:function(){return this.get(this.node.childNodes.length-1)},each:function(t,e){var i,n,r=this.children();for(i=0,n=r.length;in/r?this.height/r:this.width/n,this.x=e,this.y=i,this.width=n,this.height=r)}else t="string"==typeof t?t.match(f).map(function(t){return parseFloat(t)}):Array.isArray(t)?t:"object"==typeof t?[t.x,t.y,t.width,t.height]:4==arguments.length?[].slice.call(arguments):u,this.x=t[0],this.y=t[1],this.width=t[2],this.height=t[3]},extend:{toString:function(){return this.x+" "+this.y+" "+this.width+" "+this.height},morph:function(t,e,i,n){return this.destination=new g.ViewBox(t,e,i,n),this},at:function(t){return this.destination?new g.ViewBox([this.x+(this.destination.x-this.x)*t,this.y+(this.destination.y-this.y)*t,this.width+(this.destination.width-this.width)*t,this.height+(this.destination.height-this.height)*t]):this}},parent:g.Container,construct:{viewbox:function(t,e,i,n){return 0==arguments.length?new g.ViewBox(this):this.attr("viewBox",new g.ViewBox(t,e,i,n))}}}),["click","dblclick","mousedown","mouseup","mouseover","mouseout","mousemove","touchstart","touchmove","touchleave","touchend","touchcancel"].forEach(function(t){g.Element.prototype[t]=function(e){return g.on(this.node,t,e),this}}),g.listeners=[],g.handlerMap=[],g.listenerId=0,g.on=function(t,e,i,n,r){var s=i.bind(n||t.instance||t),o=(g.handlerMap.indexOf(t)+1||g.handlerMap.push(t))-1,a=e.split(".")[0],h=e.split(".")[1]||"*";g.listeners[o]=g.listeners[o]||{},g.listeners[o][a]=g.listeners[o][a]||{},g.listeners[o][a][h]=g.listeners[o][a][h]||{},i._svgjsListenerId||(i._svgjsListenerId=++g.listenerId),g.listeners[o][a][h][i._svgjsListenerId]=s,t.addEventListener(a,s,r||!1)},g.off=function(t,e,i){var n=g.handlerMap.indexOf(t),r=e&&e.split(".")[0],s=e&&e.split(".")[1],o="";if(n!=-1)if(i){if("function"==typeof i&&(i=i._svgjsListenerId),!i)return;g.listeners[n][r]&&g.listeners[n][r][s||"*"]&&(t.removeEventListener(r,g.listeners[n][r][s||"*"][i],!1),delete g.listeners[n][r][s||"*"][i])}else if(s&&r){if(g.listeners[n][r]&&g.listeners[n][r][s]){for(i in g.listeners[n][r][s])g.off(t,[r,s].join("."),i);delete g.listeners[n][r][s]}}else if(s)for(e in g.listeners[n])for(o in g.listeners[n][e])s===o&&g.off(t,[e,s].join("."));else if(r){if(g.listeners[n][r]){for(o in g.listeners[n][r])g.off(t,[r,o].join("."));delete g.listeners[n][r]}}else{for(e in g.listeners[n])g.off(t,e);delete g.listeners[n],delete g.handlerMap[n]}},g.extend(g.Element,{on:function(t,e,i,n){return g.on(this.node,t,e,i,n),this},off:function(t,e){return g.off(this.node,t,e),this},fire:function(e,i){return e instanceof t.Event?this.node.dispatchEvent(e):this.node.dispatchEvent(e=new t.CustomEvent(e,{detail:i,cancelable:!0})),this._event=e,this},event:function(){return this._event}}),g.Defs=g.invent({create:"defs",inherit:g.Container}),g.G=g.invent({create:"g",inherit:g.Container,extend:{x:function(t){return null==t?this.transform("x"):this.transform({x:t-this.x()},!0)},y:function(t){return null==t?this.transform("y"):this.transform({y:t-this.y()},!0)},cx:function(t){return null==t?this.gbox().cx:this.x(t-this.gbox().width/2)},cy:function(t){return null==t?this.gbox().cy:this.y(t-this.gbox().height/2)},gbox:function(){var t=this.bbox(),e=this.transform();return t.x+=e.x,t.x2+=e.x,t.cx+=e.x,t.y+=e.y,t.y2+=e.y,t.cy+=e.y,t}},construct:{group:function(){return this.put(new g.G)}}}),g.extend(g.Element,{siblings:function(){return this.parent().children()},position:function(){return this.parent().index(this)},next:function(){return this.siblings()[this.position()+1]},previous:function(){return this.siblings()[this.position()-1]},forward:function(){var t=this.position()+1,e=this.parent();return e.removeElement(this).add(this,t),e instanceof g.Doc&&e.node.appendChild(e.defs().node),this},backward:function(){var t=this.position();return t>0&&this.parent().removeElement(this).add(this,t-1),this},front:function(){var t=this.parent();return t.node.appendChild(this.node),t instanceof g.Doc&&t.node.appendChild(t.defs().node),this},back:function(){return this.position()>0&&this.parent().removeElement(this).add(this,0),this},before:function(t){t.remove();var e=this.position();return this.parent().add(t,e),this},after:function(t){t.remove();var e=this.position();return this.parent().add(t,e+1),this}}),g.Mask=g.invent({create:function(){this.constructor.call(this,g.create("mask")),this.targets=[]},inherit:g.Container,extend:{remove:function(){for(var t=this.targets.length-1;t>=0;t--)this.targets[t]&&this.targets[t].unmask();return this.targets=[],this.parent().removeElement(this),this}},construct:{mask:function(){return this.defs().put(new g.Mask)}}}),g.extend(g.Element,{maskWith:function(t){return this.masker=t instanceof g.Mask?t:this.parent().mask().add(t),this.masker.targets.push(this),this.attr("mask",'url("#'+this.masker.attr("id")+'")')},unmask:function(){return delete this.masker,this.attr("mask",null)}}),g.ClipPath=g.invent({create:function(){this.constructor.call(this,g.create("clipPath")),this.targets=[]},inherit:g.Container,extend:{remove:function(){for(var t=this.targets.length-1;t>=0;t--)this.targets[t]&&this.targets[t].unclip();return this.targets=[],this.parent().removeElement(this),this}},construct:{clip:function(){return this.defs().put(new g.ClipPath)}}}),g.extend(g.Element,{clipWith:function(t){return this.clipper=t instanceof g.ClipPath?t:this.parent().clip().add(t),this.clipper.targets.push(this),this.attr("clip-path",'url("#'+this.clipper.attr("id")+'")')},unclip:function(){return delete this.clipper,this.attr("clip-path",null)}}),g.Gradient=g.invent({create:function(t){this.constructor.call(this,g.create(t+"Gradient")),this.type=t},inherit:g.Container,extend:{at:function(t,e,i){return this.put(new g.Stop).update(t,e,i)},update:function(t){return this.clear(),"function"==typeof t&&t.call(this,this),this},fill:function(){return"url(#"+this.id()+")"},toString:function(){return this.fill()},attr:function(t,e,i){return"transform"==t&&(t="gradientTransform"),g.Container.prototype.attr.call(this,t,e,i)}},construct:{gradient:function(t,e){return this.defs().gradient(t,e)}}}),g.extend(g.Gradient,g.FX,{from:function(t,e){return"radial"==(this._target||this).type?this.attr({fx:new g.Number(t),fy:new g.Number(e)}):this.attr({x1:new g.Number(t),y1:new g.Number(e)})},to:function(t,e){return"radial"==(this._target||this).type?this.attr({cx:new g.Number(t),cy:new g.Number(e)}):this.attr({x2:new g.Number(t),y2:new g.Number(e)})}}),g.extend(g.Defs,{gradient:function(t,e){return this.put(new g.Gradient(t)).update(e)}}),g.Stop=g.invent({create:"stop",inherit:g.Element,extend:{update:function(t){return("number"==typeof t||t instanceof g.Number)&&(t={offset:arguments[0],color:arguments[1],opacity:arguments[2]}),null!=t.opacity&&this.attr("stop-opacity",t.opacity),null!=t.color&&this.attr("stop-color",t.color),null!=t.offset&&this.attr("offset",new g.Number(t.offset)),this}}}),g.Pattern=g.invent({create:"pattern",inherit:g.Container,extend:{fill:function(){return"url(#"+this.id()+")"},update:function(t){return this.clear(),"function"==typeof t&&t.call(this,this),this},toString:function(){return this.fill()},attr:function(t,e,i){return"transform"==t&&(t="patternTransform"),g.Container.prototype.attr.call(this,t,e,i)}},construct:{pattern:function(t,e,i){return this.defs().pattern(t,e,i)}}}),g.extend(g.Defs,{pattern:function(t,e,i){return this.put(new g.Pattern).update(i).attr({x:0,y:0,width:t,height:e,patternUnits:"userSpaceOnUse"})}}),g.Doc=g.invent({create:function(t){t&&(t="string"==typeof t?e.getElementById(t):t,"svg"==t.nodeName?this.constructor.call(this,t):(this.constructor.call(this,g.create("svg")),t.appendChild(this.node),this.size("100%","100%")),this.namespace().defs())},inherit:g.Container,extend:{namespace:function(){return this.attr({xmlns:g.ns,version:"1.1"}).attr("xmlns:xlink",g.xlink,g.xmlns).attr("xmlns:svgjs",g.svgjs,g.xmlns)},defs:function(){if(!this._defs){var t;(t=this.node.getElementsByTagName("defs")[0])?this._defs=g.adopt(t):this._defs=new g.Defs,this.node.appendChild(this._defs.node)}return this._defs},parent:function(){return"#document"==this.node.parentNode.nodeName?null:this.node.parentNode},spof:function(t){var e=this.node.getScreenCTM();return e&&this.style("left",-e.e%1+"px").style("top",-e.f%1+"px"),this},remove:function(){return this.parent()&&this.parent().removeChild(this.node),this},clear:function(){for(;this.node.hasChildNodes();)this.node.removeChild(this.node.lastChild);return delete this._defs,g.parser.draw.parentNode||this.node.appendChild(g.parser.draw),this}}}),g.Shape=g.invent({create:function(t){this.constructor.call(this,t)},inherit:g.Element}),g.Bare=g.invent({create:function(t,e){if(this.constructor.call(this,g.create(t)),e)for(var i in e.prototype)"function"==typeof e.prototype[i]&&(this[i]=e.prototype[i])},inherit:g.Element,extend:{words:function(t){for(;this.node.hasChildNodes();)this.node.removeChild(this.node.lastChild);return this.node.appendChild(e.createTextNode(t)),this}}}),g.extend(g.Parent,{element:function(t,e){return this.put(new g.Bare(t,e))}}),g.Symbol=g.invent({create:"symbol",inherit:g.Container,construct:{symbol:function(){return this.put(new g.Symbol)}}}),g.Use=g.invent({create:"use",inherit:g.Shape,extend:{element:function(t,e){return this.attr("href",(e||"")+"#"+t,g.xlink)}},construct:{use:function(t,e){return this.put(new g.Use).element(t,e)}}}),g.Rect=g.invent({create:"rect",inherit:g.Shape,construct:{rect:function(t,e){return this.put(new g.Rect).size(t,e)}}}),g.Circle=g.invent({create:"circle",inherit:g.Shape,construct:{circle:function(t){return this.put(new g.Circle).rx(new g.Number(t).divide(2)).move(0,0)}}}),g.extend(g.Circle,g.FX,{rx:function(t){return this.attr("r",t)},ry:function(t){return this.rx(t)}}),g.Ellipse=g.invent({create:"ellipse",inherit:g.Shape,construct:{ellipse:function(t,e){return this.put(new g.Ellipse).size(t,e).move(0,0)}}}),g.extend(g.Ellipse,g.Rect,g.FX,{rx:function(t){return this.attr("rx",t)},ry:function(t){return this.attr("ry",t)}}),g.extend(g.Circle,g.Ellipse,{x:function(t){return null==t?this.cx()-this.rx():this.cx(t+this.rx())},y:function(t){return null==t?this.cy()-this.ry():this.cy(t+this.ry())},cx:function(t){return null==t?this.attr("cx"):this.attr("cx",t)},cy:function(t){return null==t?this.attr("cy"):this.attr("cy",t)},width:function(t){return null==t?2*this.rx():this.rx(new g.Number(t).divide(2))},height:function(t){return null==t?2*this.ry():this.ry(new g.Number(t).divide(2))},size:function(t,e){var i=l(this,t,e);return this.rx(new g.Number(i.width).divide(2)).ry(new g.Number(i.height).divide(2))}}),g.Line=g.invent({create:"line",inherit:g.Shape,extend:{array:function(){return new g.PointArray([[this.attr("x1"),this.attr("y1")],[this.attr("x2"),this.attr("y2")]])},plot:function(t,e,i,n){return null==t?this.array():(t="undefined"!=typeof e?{x1:t,y1:e,x2:i,y2:n}:new g.PointArray(t).toLine(),this.attr(t))},move:function(t,e){return this.attr(this.array().move(t,e).toLine())},size:function(t,e){var i=l(this,t,e);return this.attr(this.array().size(i.width,i.height).toLine())}},construct:{line:function(t,e,i,n){return g.Line.prototype.plot.apply(this.put(new g.Line),null!=t?[t,e,i,n]:[0,0,0,0])}}}),g.Polyline=g.invent({create:"polyline",inherit:g.Shape,construct:{polyline:function(t){return this.put(new g.Polyline).plot(t||new g.PointArray)}}}),g.Polygon=g.invent({create:"polygon",inherit:g.Shape,construct:{polygon:function(t){return this.put(new g.Polygon).plot(t||new g.PointArray)}}}),g.extend(g.Polyline,g.Polygon,{array:function(){return this._array||(this._array=new g.PointArray(this.attr("points")))},plot:function(t){return null==t?this.array():this.clear().attr("points","string"==typeof t?t:this._array=new g.PointArray(t))},clear:function(){return delete this._array,this},move:function(t,e){return this.attr("points",this.array().move(t,e))},size:function(t,e){var i=l(this,t,e);return this.attr("points",this.array().size(i.width,i.height))}}),g.extend(g.Line,g.Polyline,g.Polygon,{morphArray:g.PointArray,x:function(t){return null==t?this.bbox().x:this.move(t,this.bbox().y)},y:function(t){return null==t?this.bbox().y:this.move(this.bbox().x,t)},width:function(t){var e=this.bbox();return null==t?e.width:this.size(t,e.height)},height:function(t){var e=this.bbox();return null==t?e.height:this.size(e.width,t)}}),g.Path=g.invent({create:"path",inherit:g.Shape,extend:{morphArray:g.PathArray,array:function(){return this._array||(this._array=new g.PathArray(this.attr("d")))},plot:function(t){return null==t?this.array():this.clear().attr("d","string"==typeof t?t:this._array=new g.PathArray(t))},clear:function(){return delete this._array,this},move:function(t,e){return this.attr("d",this.array().move(t,e))},x:function(t){return null==t?this.bbox().x:this.move(t,this.bbox().y)},y:function(t){return null==t?this.bbox().y:this.move(this.bbox().x,t)},size:function(t,e){var i=l(this,t,e);return this.attr("d",this.array().size(i.width,i.height))},width:function(t){return null==t?this.bbox().width:this.size(t,this.bbox().height)},height:function(t){return null==t?this.bbox().height:this.size(this.bbox().width,t)}},construct:{path:function(t){return this.put(new g.Path).plot(t||new g.PathArray)}}}),g.Image=g.invent({create:"image",inherit:g.Shape,extend:{load:function(e){if(!e)return this;var i=this,n=new t.Image;return g.on(n,"load",function(){var t=i.parent(g.Pattern);null!==t&&(0==i.width()&&0==i.height()&&i.size(n.width,n.height),t&&0==t.width()&&0==t.height()&&t.size(i.width(),i.height()),"function"==typeof i._loaded&&i._loaded.call(i,{width:n.width,height:n.height,ratio:n.width/n.height,url:e}))}),g.on(n,"error",function(t){"function"==typeof i._error&&i._error.call(i,t)}),this.attr("href",n.src=this.src=e,g.xlink)},loaded:function(t){return this._loaded=t,this},error:function(t){return this._error=t,this}},construct:{image:function(t,e,i){return this.put(new g.Image).load(t).size(e||0,i||e||0)}}}),g.Text=g.invent({create:function(){this.constructor.call(this,g.create("text")),this.dom.leading=new g.Number(1.3),this._rebuild=!0,this._build=!1,this.attr("font-family",g.defaults.attrs["font-family"])},inherit:g.Shape,extend:{x:function(t){return null==t?this.attr("x"):this.attr("x",t)},y:function(t){var e=this.attr("y"),i="number"==typeof e?e-this.bbox().y:0;return null==t?"number"==typeof e?e-i:e:this.attr("y","number"==typeof t?t+i:t)},cx:function(t){return null==t?this.bbox().cx:this.x(t-this.bbox().width/2)},cy:function(t){return null==t?this.bbox().cy:this.y(t-this.bbox().height/2)},text:function(t){if("undefined"==typeof t){for(var t="",e=this.node.childNodes,i=0,n=e.length;i=0;e--)null!=i[M[t][e]]&&this.attr(M.prefix(t,M[t][e]),i[M[t][e]]);return this},g.extend(g.Element,g.FX,i)}),g.extend(g.Element,g.FX,{rotate:function(t,e,i){return this.transform({rotation:t,cx:e,cy:i})},skew:function(t,e,i,n){return 1==arguments.length||3==arguments.length?this.transform({skew:t,cx:e,cy:i}):this.transform({skewX:t,skewY:e,cx:i,cy:n})},scale:function(t,e,i,n){return 1==arguments.length||3==arguments.length?this.transform({scale:t,cx:e,cy:i}):this.transform({scaleX:t,scaleY:e,cx:i,cy:n})},translate:function(t,e){return this.transform({x:t,y:e})},flip:function(t,e){return e="number"==typeof t?t:e,this.transform({flip:t||"both",offset:e})},matrix:function(t){return this.attr("transform",new g.Matrix(6==arguments.length?[].slice.call(arguments):t))},opacity:function(t){return this.attr("opacity",t)},dx:function(t){return this.x(new g.Number(t).plus(this instanceof g.FX?0:this.x()),!0)},dy:function(t){return this.y(new g.Number(t).plus(this instanceof g.FX?0:this.y()),!0)},dmove:function(t,e){return this.dx(t).dy(e)}}),g.extend(g.Rect,g.Ellipse,g.Circle,g.Gradient,g.FX,{radius:function(t,e){var i=(this._target||this).type;return"radial"==i||"circle"==i?this.attr("r",new g.Number(t)):this.rx(t).ry(null==e?t:e)}}),g.extend(g.Path,{length:function(){return this.node.getTotalLength()},pointAt:function(t){return this.node.getPointAtLength(t)}}),g.extend(g.Parent,g.Text,g.Tspan,g.FX,{font:function(t,e){if("object"==typeof t)for(e in t)this.font(e,t[e]);return"leading"==t?this.leading(e):"anchor"==t?this.attr("text-anchor",e):"size"==t||"family"==t||"weight"==t||"stretch"==t||"variant"==t||"style"==t?this.attr("font-"+t,e):this.attr(t,e)}}),g.Set=g.invent({create:function(t){Array.isArray(t)?this.members=t:this.clear()},extend:{add:function(){var t,e,i=[].slice.call(arguments);for(t=0,e=i.length;t-1&&this.members.splice(e,1),this},each:function(t){for(var e=0,i=this.members.length;e=0},index:function(t){return this.members.indexOf(t)},get:function(t){return this.members[t]},first:function(){return this.get(0)},last:function(){return this.get(this.members.length-1)},valueOf:function(){return this.members},bbox:function(){if(0==this.members.length)return new g.RBox;var t=this.members[0].rbox(this.members[0].doc());return this.each(function(){t=t.merge(this.rbox(this.doc()))}),t}},construct:{set:function(t){return new g.Set(t)}}}),g.FX.Set=g.invent({create:function(t){this.set=t}}),g.Set.inherit=function(){var t,e=[];for(var t in g.Shape.prototype)"function"==typeof g.Shape.prototype[t]&&"function"!=typeof g.Set.prototype[t]&&e.push(t);e.forEach(function(t){g.Set.prototype[t]=function(){for(var e=0,i=this.members.length;e=0;t--)delete this.memory()[arguments[t]];return this},memory:function(){return this._memory||(this._memory={})}}),g.get=function(t){var i=e.getElementById(v(t)||t);return g.adopt(i)},g.select=function(t,i){return new g.Set(g.utils.map((i||e).querySelectorAll(t),function(t){return g.adopt(t)}))},g.extend(g.Parent,{select:function(t){return g.select(t,this.node)}});var P="abcdef".split("");if("function"!=typeof t.CustomEvent){var A=function(t,i){i=i||{bubbles:!1,cancelable:!1,detail:void 0};var n=e.createEvent("CustomEvent");return n.initCustomEvent(t,i.bubbles,i.cancelable,i.detail),n};A.prototype=t.Event.prototype,t.CustomEvent=A}return function(e){for(var i=0,n=["moz","webkit"],r=0;r",
+ "main": "dist/svg.js",
+ "jam": {
+ "include": [
+ "dist/svg.js",
+ "README.md",
+ "LICENSE.txt"
+ ]
+ },
+ "maintainers": [
+ {
+ "name": "Wout Fierens",
+ "email": "wout@mick-wout.com",
+ "web": "https://svgdotjs.github.io/"
+ },
+ {
+ "name": "Alex Ewerlöf",
+ "email": "alex@userpixel.com",
+ "web": "http://www.ewerlof.name"
+ },
+ {
+ "name": "Ulrich-Matthias Schäfer",
+ "email": "ulima.ums@googlemail.com"
+ },
+ {
+ "name": "Jon Ege Ronnenberg",
+ "email": "jon@svgjs.com",
+ "url": "https://keybase.io/dotnetcarpenter"
+ }
+ ],
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "http://www.opensource.org/licenses/mit-license.php"
+ }
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/svgdotjs/svg.js.git"
+ },
+ "github": "https://github.com/svgdotjs/svg.js",
+ "license": "MIT",
+ "typings": "./svg.js.d.ts",
+ "scripts": {
+ "build": "gulp",
+ "build:test": "gulp unify",
+ "test": "karma start .config/karma.conf.js --single-run",
+ "test:quick": "karma start .config/karma.quick.js"
+ },
+ "devDependencies": {
+ "coveralls": "^2.11.15",
+ "del": "^2.2.0",
+ "gulp": "^3.8.6",
+ "gulp-chmod": "^2.0.0",
+ "gulp-cli": "^1.2.2",
+ "gulp-concat": "^2.3.3",
+ "gulp-header": "^1.0.5",
+ "gulp-rename": "^1.2.0",
+ "gulp-size": "^2.1.0",
+ "gulp-trimlines": "^1.0.0",
+ "gulp-uglify": "^2.0.0",
+ "gulp-wrap": "^0.13.0",
+ "jasmine-core": "^2.5.2",
+ "karma": "^1.3.0",
+ "karma-coverage": "^1.1.1",
+ "karma-firefox-launcher": "^1.0.0",
+ "karma-jasmine": "^1.0.2",
+ "karma-phantomjs-launcher": "^1.0.2",
+ "request": "^2.78.0",
+ "svgdom": "latest"
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/package-lock.json b/files/plugin-HeatmapSessionRecording-5.2.4/package-lock.json
new file mode 100644
index 0000000..fd1c5a8
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/package-lock.json
@@ -0,0 +1,71 @@
+{
+ "name": "heatmapsessionrecording",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "heatmapsessionrecording",
+ "version": "1.0.0",
+ "license": "GPL-3.0+",
+ "dependencies": {
+ "@types/heatmap.js": "^2.0.37",
+ "heatmap.js": "^2.0.5"
+ }
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.8",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
+ "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="
+ },
+ "node_modules/@types/heatmap.js": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/@types/heatmap.js/-/heatmap.js-2.0.37.tgz",
+ "integrity": "sha512-Zd1m6WaRSPnXcR1fETGnIvyRSE2rcQK21S0zIU/LWjwsrNyKBA3xdckrQhQpIdG+UTeu7WODv237s30Ky7IVXg==",
+ "dependencies": {
+ "@types/leaflet": "^0"
+ }
+ },
+ "node_modules/@types/leaflet": {
+ "version": "0.7.35",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-0.7.35.tgz",
+ "integrity": "sha512-BK+pa9a9dYC1qJyYQulqkRI9N+ZnV4ycAmNSOUmom7C6xaAdmrhOoiCiDMhSQklyjPpasy3KWRTkTRTJuDbBSw==",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/heatmap.js": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/heatmap.js/-/heatmap.js-2.0.5.tgz",
+ "integrity": "sha1-Rm07hlE/XUkRKknSVwCrJzAUkVM="
+ }
+ },
+ "dependencies": {
+ "@types/geojson": {
+ "version": "7946.0.8",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz",
+ "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="
+ },
+ "@types/heatmap.js": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/@types/heatmap.js/-/heatmap.js-2.0.37.tgz",
+ "integrity": "sha512-Zd1m6WaRSPnXcR1fETGnIvyRSE2rcQK21S0zIU/LWjwsrNyKBA3xdckrQhQpIdG+UTeu7WODv237s30Ky7IVXg==",
+ "requires": {
+ "@types/leaflet": "^0"
+ }
+ },
+ "@types/leaflet": {
+ "version": "0.7.35",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-0.7.35.tgz",
+ "integrity": "sha512-BK+pa9a9dYC1qJyYQulqkRI9N+ZnV4ycAmNSOUmom7C6xaAdmrhOoiCiDMhSQklyjPpasy3KWRTkTRTJuDbBSw==",
+ "requires": {
+ "@types/geojson": "*"
+ }
+ },
+ "heatmap.js": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/heatmap.js/-/heatmap.js-2.0.5.tgz",
+ "integrity": "sha1-Rm07hlE/XUkRKknSVwCrJzAUkVM="
+ }
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/package.json b/files/plugin-HeatmapSessionRecording-5.2.4/package.json
new file mode 100644
index 0000000..d7cea0a
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "heatmapsessionrecording",
+ "version": "1.0.0",
+ "description": "## Description",
+ "main": "tracker.js",
+ "directories": {
+ "doc": "docs",
+ "test": "tests"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/innocraft/plugin-HeatmapSessionRecording.git"
+ },
+ "author": "",
+ "license": "InnoCraft EULA",
+ "bugs": {
+ "url": "https://github.com/innocraft/plugin-HeatmapSessionRecording/issues"
+ },
+ "homepage": "https://github.com/innocraft/plugin-HeatmapSessionRecording#readme",
+ "dependencies": {
+ "@types/heatmap.js": "^2.0.37",
+ "heatmap.js": "^2.0.5"
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/phpcs.xml b/files/plugin-HeatmapSessionRecording-5.2.4/phpcs.xml
new file mode 100644
index 0000000..269d510
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/phpcs.xml
@@ -0,0 +1,37 @@
+
+
+
+ Matomo Coding Standard for HeatmapSessionRecording plugin
+
+
+
+ .
+
+ tests/javascript/*
+ */vendor/*
+
+
+
+
+
+
+
+ tests/*
+
+
+
+
+ Updates/*
+
+
+
+
+ tests/*
+
+
+
+
+ tests/*
+ Tracker/Configs.php
+
+
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/plugin.json b/files/plugin-HeatmapSessionRecording-5.2.4/plugin.json
new file mode 100644
index 0000000..9eab077
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/plugin.json
@@ -0,0 +1,41 @@
+{
+ "name": "HeatmapSessionRecording",
+ "description": "Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions.",
+ "version": "5.2.4",
+ "theme": false,
+ "require": {
+ "matomo": ">=5.0.0-rc1,<6.0.0-b1"
+ },
+ "authors": [
+ {
+ "name": "InnoCraft",
+ "email": "contact@innocraft.com",
+ "homepage": "https:\/\/www.innocraft.com"
+ }
+ ],
+ "price": {
+ "base": 220
+ },
+ "archive": {
+ "exclude": ["/tracker.js"]
+ },
+ "preview": {
+ "video_url": "https://www.youtube-nocookie.com/embed/AUSXjH8U9fk"
+ },
+ "homepage": "https:\/\/www.heatmap-analytics.com",
+ "license": "InnoCraft EULA",
+ "keywords": [
+ "heatmap",
+ "session recording",
+ "session",
+ "recording",
+ "move",
+ "scroll",
+ "hover",
+ "click",
+ "user",
+ "visitor",
+ "video",
+ "visit"
+ ]
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/pull_request_template.md b/files/plugin-HeatmapSessionRecording-5.2.4/pull_request_template.md
new file mode 100644
index 0000000..e7d9cf5
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/pull_request_template.md
@@ -0,0 +1,26 @@
+## Description
+
+
+## Issue No
+
+
+## Steps to Replicate the Issue
+1.
+2.
+3.
+
+
+
+## Checklist
+- [✔/✖] Tested locally or on demo2/demo3?
+- [✔/✖/NA] New test case added/updated?
+- [✔/✖/NA] Are all newly added texts included via translation?
+- [✔/✖/NA] Are text sanitized properly? (Eg use of v-text v/s v-html for vue)
+- [✔/✖/NA] Version bumped?
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/edit-entities.less b/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/edit-entities.less
new file mode 100644
index 0000000..9d2fc7b
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/edit-entities.less
@@ -0,0 +1,55 @@
+
+.editHsr {
+ .icon-help {
+ color: #888;
+ cursor: help;
+ &:hover {
+ color: @theme-color-text;
+ }
+ }
+ .icon-minus,
+ .icon-plus {
+ cursor: pointer;
+ }
+ .icon-minus {
+ margin-left: 8px;
+ }
+
+ .matchPageRules > .row {
+ width: ~"calc(100% - 60px)";
+ margin: 0 !important;
+
+ > .col {
+ padding-left: 0;
+ }
+ }
+
+ .form-group {
+ margin-left: -.75rem;
+ margin-right: -.75rem;
+
+ &.hsrTargetTest {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+
+ .matchPageRules {
+ margin-top: 1em;
+
+ hr {
+ margin-bottom: 3em;
+ }
+
+ .form-group {
+ margin: 0;
+ }
+ .input-field {
+ margin-top: 0.2em;
+
+ input, select{
+ width: 100% !important;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/list-entities.less b/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/list-entities.less
new file mode 100644
index 0000000..c7c1189
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/list-entities.less
@@ -0,0 +1,35 @@
+.manageHsr {
+ .filterStatus, .hsrSearchFilter {
+ display: inline-block;
+ width:200px;
+ }
+
+ div.filterStatus {
+ margin: 0 -0.75em;
+ display: inline-block;
+
+ .theWidgetContent & {
+ margin: 0;
+ }
+ }
+
+ th.action, td.action {
+ width: 250px;
+
+ a {
+ color: black;
+ }
+ }
+
+ .index {
+ width: 60px;
+ }
+
+ a.table-action {
+ margin-right: 3.5px;
+ }
+
+ a.table-action:last-child {
+ margin-right: 0;
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/recordings.less b/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/recordings.less
new file mode 100644
index 0000000..a16bbc4
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/recordings.less
@@ -0,0 +1,154 @@
+#recordingPlayer {
+ display: block;
+ border: 1px solid #ccc;
+ pointer-events: none !important;
+ background: white;
+}
+
+.scrollHeatmapLeaf {
+ position:absolute;
+ z-index: 10;
+ opacity: 0.4;
+}
+#listOfPageviews {
+ table .inactive {
+ cursor: pointer;
+ }
+}
+.sessionRecording {
+ padding: 16px;
+ font-size: 14px;
+
+ .recordingPageviews {
+ cursor: pointer;
+ color: @theme-color-link;
+ }
+
+ .recordingPageviews,
+ .recordingResolution,
+ .recordingLogos,
+ .recordingUrl {
+ margin-left: 16px;
+ }
+
+ .recordingLogos {
+ img {
+ margin-right: 6px;
+ height: 14px;
+ }
+ .countryFlag {
+ border: 1px solid #d3d3d3;
+ height: 15px;
+ }
+ }
+
+ .openVisitorProfile {
+ cursor:pointer;
+ height: 15px !important;
+ }
+}
+
+.visitorLogReplaySession {
+ margin-top: 10px;
+ padding-bottom: 5px;
+ display: block;
+ width: auto!important;
+ &:hover {
+ color: @theme-color-brand !important;
+ }
+
+ .visitor-profile & {
+ display: none;
+ }
+}
+
+.visitorLogIconReplaySession {
+ display: block;
+ float: left;
+ font-size: 18px;
+ margin: 4px 10px 0 0;
+
+ .dataTableVizVisitorLog &, .visitor-profile-header & {
+ display: none;
+ }
+
+ &:hover {
+ text-decoration: none;
+ color: @theme-color-brand !important;
+ }
+}
+
+[data-report="HeatmapSessionRecording.getRecordedSessions"] {
+ .dataTableRowActions {
+
+ .actionHsrPlayRecording, .actionHsrVisitorProfile {
+ padding-right: 1rem;
+ }
+
+ .icon-play {
+ color: @theme-color-brand !important;
+ display: inline-block;
+ margin-top: 1px;
+ }
+
+ .icon-visitor-profile {
+ font-size: 21px !important;
+ }
+ }
+
+ table.subDataTable tr .label.column {
+ width: 400px;
+ }
+
+ .countryFlag {
+ border: 1px solid #d3d3d3;
+ }
+}
+
+.hsrLoadingOuter {
+ position: absolute;
+ z-index: 2;
+
+ .loadingUnderlay {
+ background: #000;
+ width:100%;
+ height:100%;
+ position: relative;
+ opacity: 0.6;
+ }
+
+ .loadingInner {
+ margin: 0 auto;
+ font-size: 28px;
+ color: white;
+ text-align:center;
+ top: 50px;
+ position: absolute;
+ width: 100%;
+ }
+
+ .loadingContent {
+ margin: 0 auto;
+ }
+}
+
+.heatmapVis .btn-flat {
+ background-color: @theme-color-brand;
+ opacity: 0.6;
+ color: #fff;
+ border: 0;
+ box-shadow: 0 2px 3px 0 rgba(0,0,0,0.16), 0 0 3px 0 rgba(0,0,0,0.12);
+
+ &:hover {
+ opacity: 1 !important;
+ }
+
+ img {
+ filter: invert(1);
+ }
+}
+
+.heatmapVis .visActive {
+ background-color: @theme-color-brand !important;
+ opacity: 1 !important;
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/templates/_detectAdBlocker.twig b/files/plugin-HeatmapSessionRecording-5.2.4/templates/_detectAdBlocker.twig
new file mode 100644
index 0000000..dfb74c4
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/templates/_detectAdBlocker.twig
@@ -0,0 +1,26 @@
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/templates/embedPage.twig b/files/plugin-HeatmapSessionRecording-5.2.4/templates/embedPage.twig
new file mode 100644
index 0000000..49c91d3
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/templates/embedPage.twig
@@ -0,0 +1,43 @@
+{% extends 'empty.twig' %}
+
+{% set title=('HeatmapSessionRecording_ReplayX'|translate('HeatmapSessionRecording_SessionRecording'|translate)) %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/templates/gettingStartedHeatmaps.twig b/files/plugin-HeatmapSessionRecording-5.2.4/templates/gettingStartedHeatmaps.twig
new file mode 100644
index 0000000..1ec5079
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/templates/gettingStartedHeatmaps.twig
@@ -0,0 +1,2 @@
+{{ 'HeatmapSessionRecording_NoHeatmapsConfiguredInfo'|translate }}
+{{ 'HeatmapSessionRecording_HeatmapUsageBenefits'|translate }}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/templates/gettingStartedSessions.twig b/files/plugin-HeatmapSessionRecording-5.2.4/templates/gettingStartedSessions.twig
new file mode 100644
index 0000000..234c4ee
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/templates/gettingStartedSessions.twig
@@ -0,0 +1,2 @@
+ {{ 'HeatmapSessionRecording_NoSessionRecordingsConfiguredInfo'|translate }}
+ {{ 'HeatmapSessionRecording_SessionRecordingsUsageBenefits'|translate }}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/templates/manageHeatmap.twig b/files/plugin-HeatmapSessionRecording-5.2.4/templates/manageHeatmap.twig
new file mode 100644
index 0000000..ee18072
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/templates/manageHeatmap.twig
@@ -0,0 +1,22 @@
+{% extends 'admin.twig' %}
+
+{% block topcontrols %}
+
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/templates/manageSessions.twig b/files/plugin-HeatmapSessionRecording-5.2.4/templates/manageSessions.twig
new file mode 100644
index 0000000..4fe301c
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/templates/manageSessions.twig
@@ -0,0 +1,20 @@
+{% extends 'admin.twig' %}
+
+{% block topcontrols %}
+
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/templates/replayRecording.twig b/files/plugin-HeatmapSessionRecording-5.2.4/templates/replayRecording.twig
new file mode 100644
index 0000000..c382d09
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/templates/replayRecording.twig
@@ -0,0 +1,60 @@
+{% extends 'layout.twig' %}
+
+{% set title=('HeatmapSessionRecording_ReplayX'|translate('HeatmapSessionRecording_SessionRecording'|translate)) %}
+
+{% block root %}
+ {% include '@HeatmapSessionRecording/_detectAdBlocker.twig' with {type: 'Session recordings'} %}
+
+
+
+
{{ recording.server_time_pretty }}
+
+ {% if recording.url %}
{{ recording.url|truncate(50) }} {% endif %}
+
+
{{ recording.viewport_w_px }} x{{ recording.viewport_h_px }}
+
{% if recording.numPageviews == 1 %}{{ 'HeatmapSessionRecording_OnePageview'|translate }}{% else %}{{ 'HeatmapSessionRecording_PageviewXofY'|translate(currentPage, recording.numPageviews) }} {% endif %}
+
+
+ {% if recording.location_logo %} {% endif %}
+ {% if recording.device_logo %} {% endif %}
+ {% if recording.os_logo %} {% endif %}
+ {% if recording.browser_logo %} {% endif %}
+ {% if recording.idvisitor and visitorProfileEnabled %} {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/templates/showHeatmap.twig b/files/plugin-HeatmapSessionRecording-5.2.4/templates/showHeatmap.twig
new file mode 100644
index 0000000..b4c65a5
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/templates/showHeatmap.twig
@@ -0,0 +1,21 @@
+{% include '@HeatmapSessionRecording/_detectAdBlocker.twig' with {type: 'Heatmaps'} %}
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/tracker.min.js b/files/plugin-HeatmapSessionRecording-5.2.4/tracker.min.js
new file mode 100644
index 0000000..91383a4
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/tracker.min.js
@@ -0,0 +1,125 @@
+(function(){var N=1;var aH=9;var o=10;var P=8;var w=3;var ax=["button","submit","reset"];
+/*!!
+ * Copyright (C) 2015 Pavel Savshenko
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
+ * Copyright (C) 2008 Matt Lilek
+ * Copyright (C) 2009 Joseph Pecoraro
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. 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.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS 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 APPLE OR ITS CONTRIBUTORS 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.
+ */
+;
+var i={};i.cssPath=function(aT,aR){if(aT.nodeType!==N){return""}var aQ=[];var aP=aT;while(aP){var aS=i._cssPathStep(aP,!!aR,aP===aT);if(!aS){break}aQ.push(aS);if(aS.optimized){break}aP=aP.parentNode}aQ.reverse();return aQ.join(" > ")};i._cssPathStep=function(a4,aW,a3){if(a4.nodeType!==N){return null}var a2=a4.getAttribute("id");if(aW){if(a2){return new i.DOMNodePathStep(ba(a2),true)}var aQ=a4.nodeName.toLowerCase();if(aQ==="body"||aQ==="head"||aQ==="html"){return new i.DOMNodePathStep(a4.nodeName.toLowerCase(),true)}}var aP=a4.nodeName.toLowerCase();if(a2){return new i.DOMNodePathStep(aP.toLowerCase()+ba(a2),true)}var aX=a4.parentNode;if(!aX||aX.nodeType===aH){return new i.DOMNodePathStep(aP.toLowerCase(),true)}function bg(bi){var bj=bi.getAttribute("class");if(!bj){return[]}return bj.split(/\s+/g).filter(Boolean).map(function(bk){return"$"+bk})}function ba(bi){return"#"+bf(bi)}function bf(bj){if(bb(bj)){return bj}var bi=/^(?:[0-9]|-[0-9-]?)/.test(bj);var bk=bj.length-1;return bj.replace(/./g,function(bm,bl){return((bi&&bl===0)||!aR(bm))?aT(bm,bl===bk):bm
+})}function aT(bj,bi){return"\\"+a1(bj)+(bi?"":" ")}function a1(bj){var bi=bj.charCodeAt(0).toString(16);if(bi.length===1){bi="0"+bi}return bi}function aR(bi){if(/[a-zA-Z0-9_\-]/.test(bi)){return true}return bi.charCodeAt(0)>=160}function bb(bi){return/^-?[a-zA-Z_][a-zA-Z0-9_\-]*$/.test(bi)}function a6(bi){var bk={},bj;for(bj=0;bj>>0};aP.prototype.nodeId=function(aQ){var aR=aQ[aP.ID_PROP];if(!aR){aR=aQ[aP.ID_PROP]=aP.nextId_++}return aR};aP.prototype.set=function(aQ,aR){var aS=this.nodeId(aQ);this.nodes[aS]=aQ;this.values[aS]=aR};aP.prototype.get=function(aQ){var aR=this.nodeId(aQ);return this.values[aR]};aP.prototype.has=function(aQ){return this.nodeId(aQ) in this.nodes};aP.prototype["delete"]=function(aQ){var aR=this.nodeId(aQ);delete this.nodes[aR];this.values[aR]=undefined};aP.prototype.keys=function(){var aQ=[];for(var aR in this.nodes){if(!this.isIndex(aR)){continue}aQ.push(this.nodes[aR])}return aQ};aP.ID_PROP="__mutation_summary_node_map_id__";aP.nextId_=1;return aP})();var aC;
+(function(aP){aP[aP.STAYED_OUT=0]="STAYED_OUT";aP[aP.ENTERED=1]="ENTERED";aP[aP.STAYED_IN=2]="STAYED_IN";aP[aP.REPARENTED=3]="REPARENTED";aP[aP.REORDERED=4]="REORDERED";aP[aP.EXITED=5]="EXITED"})(aC||(aC={}));function B(aP){return aP===aC.ENTERED||aP===aC.EXITED}var ab=(function(){function aP(aW,aV,aR,aT,aS,aU,aQ,aX){if(aV===void 0){aV=false}if(aR===void 0){aR=false}if(aT===void 0){aT=false}if(aS===void 0){aS=null}if(aU===void 0){aU=false}if(aQ===void 0){aQ=null}if(aX===void 0){aX=null}this.node=aW;this.childList=aV;this.attributes=aR;this.characterData=aT;this.oldParentNode=aS;this.added=aU;this.attributeOldValues=aQ;this.characterDataOldValue=aX;this.isCaseInsensitive=this.node.nodeType===N&&this.node instanceof HTMLElement&&this.node.ownerDocument instanceof HTMLDocument}aP.prototype.getAttributeOldValue=function(aQ){if(!this.attributeOldValues){return undefined}if(this.isCaseInsensitive){aQ=aQ.toLowerCase()}return this.attributeOldValues[aQ]};aP.prototype.getAttributeNamesMutated=function(){var aR=[];
+if(!this.attributeOldValues){return aR}for(var aQ in this.attributeOldValues){aR.push(aQ)}return aR};aP.prototype.attributeMutated=function(aR,aQ){this.attributes=true;this.attributeOldValues=this.attributeOldValues||{};if(aR in this.attributeOldValues){return}this.attributeOldValues[aR]=aQ};aP.prototype.characterDataMutated=function(aQ){if(this.characterData){return}this.characterData=true;this.characterDataOldValue=aQ};aP.prototype.removedFromParent=function(aQ){this.childList=true;if(this.added||this.oldParentNode){this.added=false}else{this.oldParentNode=aQ}};aP.prototype.insertedIntoParent=function(){this.childList=true;this.added=true};aP.prototype.getOldParent=function(){if(this.childList){if(this.oldParentNode){return this.oldParentNode}if(this.added){return null}}return this.node.parentNode};return aP})();var ao=(function(){function aP(){this.added=new J();this.removed=new J();this.maybeMoved=new J();this.oldPrevious=new J();this.moved=undefined}return aP})();var W=(function(aQ){b(aP,aQ);
+function aP(aU,aS){aQ.call(this);this.rootNode=aU;this.reachableCache=undefined;this.wasReachableCache=undefined;this.anyParentsChanged=false;this.anyAttributesChanged=false;this.anyCharacterDataChanged=false;for(var aR=0;aR1){throw Error("Invalid request option. all has no options.")}aU.queries.push({all:true});continue}if("attribute" in aT){var aV={attribute:j(aT.attribute)};aV.elementFilter=aG.parseSelectors("*["+aV.attribute+"]");if(Object.keys(aT).length>1){throw Error("Invalid request option. attribute has no options.")}aU.queries.push(aV);continue}if("element" in aT){var aS=Object.keys(aT).length;var aV={element:aT.element,elementFilter:aG.parseSelectors(aT.element)};if(aT.hasOwnProperty("elementAttributes")){aV.attributeList=az(aT.elementAttributes);aS--}if(aS>1){throw Error("Invalid request option. element only allows elementAttributes option.")}aU.queries.push(aV);continue}if(aT.characterData){if(Object.keys(aT).length>1){throw Error("Invalid request option. characterData has no options.")
+}aU.queries.push({characterData:true});continue}throw Error("Invalid request option. Unknown query request.")}return aU};aP.prototype.createSummaries=function(aR){if(!aR||!aR.length){return[]}var aQ=new K(this.root,aR,this.elementFilter,this.calcReordered,this.options.oldPreviousSibling);var aT=[];for(var aS=0;aS=0){aV={}}else{if(aU.attributes.name){var a3=String(aU.attributes.name.value).toLowerCase();if(a3.indexOf("twitter:")>=0||a3.indexOf("description")>=0||a3.indexOf("keywords")>=0){aV={}}}}}else{if("LINK"===aU.tagName){if(aU.attributes.rel){var a2=String(aU.attributes.rel.value).toLowerCase();
+var a1=["icon","preload","preconnect","dns-prefetch","next","prev","alternate","search"];if(a1.indexOf(a2)>=0){aV={}}}if(aU.attributes.href){var aT=String(aU.attributes.href.value).toLowerCase().indexOf(".scr.kaspersky-labs.com");if(aT>5&&aT<=20){aV={}}}if(aU.href){if(typeof I.URL==="function"){var aS=ak.onCssLoaded(aU.href);var aQ=3;if(!aS){aR(aU.href);function aR(a5){if(aQ>0){setTimeout(function(){aQ--;aS=ak.onCssLoaded(aU.href);if(!aS){aR(aU.href)}},300)}}}}aV.url=aU.href}}}}return aV};aP.prototype.serializeAddedAndMoved=function(aT,aQ,aU){var aW=this;var aS=aT.concat(aQ).concat(aU);var aV=new d.NodeMap();aS.forEach(function(aZ){var aY=aZ.parentNode;var aX=aV.get(aY);if(!aX){aX=new d.NodeMap();aV.set(aY,aX)}aX.set(aZ,true)});var aR=[];aV.keys().forEach(function(aY){var aX=aV.get(aY);var a0=aX.keys();while(a0.length){var aZ=a0[0];while(aZ.previousSibling&&aX.has(aZ.previousSibling)){aZ=aZ.previousSibling}while(aZ&&aX.has(aZ)){var a1=aW.serializeNode(aZ);a1.previousSibling=aW.serializeNode(aZ.previousSibling);
+a1.parentNode=aW.serializeNode(aZ.parentNode);aR.push(a1);aX["delete"](aZ);aZ=aZ.nextSibling}var a0=aX.keys()}});return aR};aP.prototype.serializeAttributeChanges=function(aQ){var aS=this;var aR=new d.NodeMap();Object.keys(aQ).forEach(function(aT){aQ[aT].forEach(function(aW){var aU=aR.get(aW);if(!aU){aU=aS.serializeNode(aW);aU.attributes={};aR.set(aW,aU)}var aV=aN.shouldMaskElementRecursive(aW);var aX=aS.getAttributesFromNode(aW,aV.isIgnoredField,aV.isIgnoredContent);aU.attributes[aT]=aT in aX?aX[aT]:null})});return aR.keys().map(function(aT){return aR.get(aT)})};aP.prototype.applyChanged=function(aT){var aW=this;var aR=aT[0];var aU=aR.removed.map(function(aX){return aW.serializeNode(aX)});var aS=this.serializeAddedAndMoved(aR.added,aR.reparented,aR.reordered);var aQ=this.serializeAttributeChanges(aR.attributeChanged);var aV=aR.characterDataChanged.map(function(aY){var aZ=aW.serializeNode(aY);if(aY.nodeType===w&&aY.parentNode){aY=aY.parentNode}var aX=aN.shouldMaskElementRecursive(aY,false,false);
+aZ.textContent=aN.getMaskedTextContent(aY,aX.isIgnoredField,aX.isIgnoredContent);return aZ});this.mirror.applyChanged(aU,aS,aQ,aV);aR.removed.forEach(function(aX){aW.forgetNode(aX)})};return aP})()}
+/*!!
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * All information contained herein is, and remains the property of InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+;var O=document;var I=window;var F=0;var l=false;var L=!u();var am=true;var n=null;var at=false;var ad="";var T=false;var g=15*60*1000;var aa=30*60*1000;var S=10;var av=(5*60*1000);var aA=2000;var C=1000;var H=100;var Y=500;var G=false;function u(){if("object"!==typeof JSON){return true}if("function"!==typeof Array.prototype.map||"function"!==typeof Array.prototype.filter||"function"!==typeof Array.prototype.indexOf){return true}if("function"!==typeof Element.prototype.getBoundingClientRect){return true}var aP=["cc.bingj.com"];
+if(aP.indexOf(O.domain)!==-1||String(O.domain).indexOf(".googleusercontent.com")!==-1){return true}var aR=/alexa|baidu|bing|bot|crawler|curl|crawling|duckduckgo|facebookexternalhit|feedburner|googlebot|google web preview|linkdex|nagios|postrank|pingdom|robot|slurp|spider|yahoo!|yandex|wget/i.test(navigator.userAgent);if(aR){return true}var aQ=String(O.referrer);if(aQ&&aQ.indexOf("module=Overlay&action=startOverlaySession")>=0){return true}return false}function U(){if(l&&"object"===typeof console){if(typeof console.debug==="function"){console.debug.apply(console,arguments)}else{if(typeof console.log==="function"){console.log.apply(console,arguments)}}}}var D=function(){return true};var s=1;var aJ=2;var h=3;var V=4;var aK=5;var ag=6;var a=7;var k=8;var e=9;var ar=10;var p=11;var ay=12;var aF=13;var aD=0;var af=1;var c=2;var aI=true;var Q=false;var an=false;var aO=true;var M=null;var A=false;var ae={};if("object"===typeof JSON){ae=JSON}var aj=false;var al=[];var aL={hasObserver:function(){if(typeof WebKitMutationObserver!=="undefined"){return true
+}else{if(typeof MutationObserver!=="undefined"){return true}}return false}};var ai=aL.hasObserver();var r={getScrollLeft:function(){return I.document.body.scrollLeft||I.document.documentElement.scrollLeft},getScrollTop:function(){return I.document.body.scrollTop||I.document.documentElement.scrollTop},getDocumentHeight:function(){return au.safeMathMax([O.body.offsetHeight,O.body.scrollHeight,O.documentElement.offsetHeight,O.documentElement.clientHeight,O.documentElement.scrollHeight,1])},getDocumentWidth:function(){return au.safeMathMax([O.body.offsetWidth,O.body.scrollWidth,O.documentElement.offsetWidth,O.documentElement.clientWidth,O.documentElement.scrollWidth,1])},getWindowSize:function(){var aP=I.innerHeight||O.documentElement.clientHeight||O.body.clientHeight;var aQ=I.innerWidth||O.documentElement.clientWidth||O.body.clientWidth;return{width:aQ,height:aP}}};var t={namespace:"hsr",set:function(aR,aV,aT){aV=parseInt(aV,10);aT=parseInt(aT,10);var aU="";var aQ=t.getHsrConfigs(aR);var aS=false;
+for(var aP=0;aP2){return true}}}if(aY){var aX=aP.parentNode?aP.parentNode:null;var aQ=false;while(aX){if(aN.hasAttribute(aX,"data-piwik-mask")||aN.hasAttribute(aX,"data-matomo-mask")){return true}else{if(!aQ&&aX&&aN.hasAttribute(aX,"data-matomo-unmask")){aQ=true}aX=aX.parentNode?aX.parentNode:null}}if(aQ){return false}}if(aN.hasAttribute(aP,"data-matomo-unmask")){return false}if(aT){return false}return true},shouldMaskContent:function(aR,aQ){if(!aR){return false}if(aR.tagName&&aR.tagName!=="FORM"&&aN.hasAttribute(aR,"data-matomo-mask")){return true}if(aR.tagName&&aR.tagName!=="FORM"&&aN.hasAttribute(aR,"data-matomo-unmask")){return false
+}if(aQ){var aP=aR.parentNode?aR.parentNode:null;while(aP){if(aR.nodeName==="#text"&&aN.hasAttribute(aP,"data-matomo-unmask")){return false}else{if(aP.tagName!=="FORM"&&aN.hasAttribute(aP,"data-matomo-mask")){return true}else{aP=aP.parentNode?aP.parentNode:null}}}}return false},isAllowedInputType:function(aP){return(aP.type&&ax.indexOf(aP.type)!==-1&&!aN.hasAttribute(aP,"data-piwik-mask")&&!aN.hasAttribute(aP,"data-matomo-mask"))}};var au={safeMathMax:function(aP){var aQ=[];var aR;for(aR=0;aR0}function x(){return f("pk_hsr_forcesample=1")||f("pk_hsr_capturescreen=1")}function v(){return f("pk_hsr_forcesample=0")}function Z(aP){if(x()){return true}if(v()){return false}if(aP>=100){return true}if(aP<=0){return false}if(aP>=1){return aP>=au.getRandomInt(1,H)}return(aP*10)>=au.getRandomInt(1,H*10)}function q(aP){if("undefined"!==typeof aP.HeatmapSessionRecording){return}aP.HeatmapSessionRecording={myId:au.generateUniqueId(),hasReceivedConfig:false,hasRequestedConfig:false,hasTrackedData:false,hasSentStopTrackingEvent:false,enabled:true,hsrIdsToGetDOM:[],disable:function(){this.enabled=false},enable:function(){this.enabled=true
+},isEnabled:function(){return L&&this.enabled},numSentTrackingRequests:0,Heatmap:{data:[],hsrids:[],configs:[],addConfig:function(aQ){if("object"!==typeof aQ||!aQ.id){return}aQ.id=parseInt(aQ.id,10);this.configs.push(aQ);if("undefined"===typeof aQ.sample_rate){aQ.sample_rate=H}else{aQ.sample_rate=Math.min(parseFloat(aQ.sample_rate),H)}if(aQ.id&&Z(aQ.sample_rate)&&D(aQ)){this.addHsrId(aQ.id);if((aQ.getdom&&!aQ.capture_manually)||f("pk_hsr_capturescreen=1")){aP.HeatmapSessionRecording.hsrIdsToGetDOM.push(aQ.id)}}},addHsrId:function(aQ){this.hsrids.push(aQ);if(aP.HeatmapSessionRecording.hasTrackedData){z.recordData(af,{ty:a,id:aQ})}}},Both:{data:[]},Session:{data:[],hsrids:[],configs:[],addConfig:function(aS){if("object"!==typeof aS||!aS.id){return}aS.id=parseInt(aS.id,10);if("undefined"===typeof aS.sample_rate){aS.sample_rate=H}else{aS.sample_rate=Math.min(parseFloat(aS.sample_rate),H)}aS.conditionsMet=false;this.configs.push(aS);var aR=parseInt(aP.getSiteId(),10);var aT=t.get(aP,aS.id);if(1===aT&&!v()){aS.sample_rate=H;
+aS.activity=false;aS.min_time=0}else{if(x()){}else{if(0===aT||!Z(aS.sample_rate)){t.set(aP,aS.id,0);return}}}this.checkConditionsMet();if(aS.min_time){var aQ=this;Piwik.DOM.onReady(function(){var aU=(aS.min_time*1000)-au.getTimeSincePageReady()+120;if(aU>=0){setTimeout(function(){aQ.checkConditionsMet()},aU)}else{aQ.checkConditionsMet()}})}},checkConditionsMet:function(){var aR;for(var aS=0;aS=au.roundTimeToSeconds(au.getTimeSincePageReady())){aQ=false}if(aR.activity&&!an){an=r.getDocumentHeight()<=r.getWindowSize().height}if(aR.activity&&(!Q||!an)){aQ=false}if(aQ){aR.conditionsMet=true;if(D(aR)){if("undefined"===typeof aR.keystrokes||!aR.keystrokes||aR.keystrokes==="0"){aI=false}this.addHsrId(aR.id)}}}}},addHsrId:function(aQ){this.hsrids.push(aQ);if(aP.HeatmapSessionRecording.hasTrackedData){z.recordData(c,{ty:a,id:aQ})}var aR=parseInt(aP.getSiteId(),10);t.set(aP,aQ,1)}},addConfig:function(aQ){this.hasRequestedConfig=true;
+this.hasReceivedConfig=true;if("undefined"===typeof aQ||!aQ){aM.checkAllConfigsReceived();return}if("object"===typeof aQ.heatmap){this.Heatmap.addConfig(aQ.heatmap)}var aR;if(aQ.heatmaps&&au.isArray(aQ.heatmaps)&&aQ.heatmaps.length){for(aR=0;aR=0;aR--){if(aQ[aR]&&aQ[aR].ty&&aQ[aR].ty===e){aQ.splice(aR,1)}}}}}if(aP.length&&aT.Both.data.length){aQ=aQ.concat(aT.Both.data);aT.Both.data=[]}if("undefined"===typeof aX){aX=this.shouldEndRecording(aW)}if(aX&&aT.hasTrackedData&&!aT.hasSentStopTrackingEvent&&aV){aQ.push({ty:p});aT.hasSentStopTrackingEvent=true}if(!aP||!aP.length||!aQ||!aQ.length){return}if(aW.HeatmapSessionRecording.hsrIdsToGetDOM&&aW.HeatmapSessionRecording.hsrIdsToGetDOM.length){if(!ak.initialDOM&&ai){var aU=new y(O,{initialize:function(aY,aZ){ak.initialDOM=ae.stringify({rootId:aY,children:aZ})}});aU.disconnect()}if(ak.initialDOM&&ai){for(var aS=0;aSM)&&aQ.ty&&aQ.ty!==ag){M=aQ.ti}if(aD===aP){aS.HeatmapSessionRecording.Both.data.push(aQ)}else{if(af===aP){aS.HeatmapSessionRecording.Heatmap.data.push(aQ)}else{if(c===aP){aS.HeatmapSessionRecording.Session.data.push(aQ)}}}}});if(l){U("recorddata",ae.stringify(aQ))}},stopSendingData:function(){var aP=z.getPiwikTrackers();aP.forEach(function(aQ){if(aQ.HeatmapSessionRecording){var aR=aQ.HeatmapSessionRecording;
+if("undefined"!==typeof aR.trackingInterval){clearInterval(aR.trackingInterval);delete aR.trackingInterval}}})},startSendingData:function(){var aP=z.getPiwikTrackers();aP.forEach(function(aQ){if(aQ.HeatmapSessionRecording&&"undefined"===typeof aQ.HeatmapSessionRecording.trackingInterval){var aR=au.getRandomInt(10250,11250);aQ.HeatmapSessionRecording.trackingInterval=setInterval(function(){z.sendQueuedData(aQ)},aR);z.sendQueuedData(aQ)}})}};function aE(){
+/*!!! hsrTrackerReadyHook */
+;if(typeof window==="object"&&"function"===typeof I.piwikHeatmapSessionRecordingAsyncInit){I.piwikHeatmapSessionRecordingAsyncInit()}if(typeof window==="object"&&"function"===typeof I.matomoHeatmapSessionRecordingAsyncInit){I.matomoHeatmapSessionRecordingAsyncInit()}var aQ=al;al=[];aj=true;for(var aP=0;aPY){aY=aY.substr(0,Y)}if(aN.shouldMaskField(aS,!aN.hasAttribute(aS,"data-matomo-unmask"))){aY=aN.maskFormField(aY,aN.getAttribute(aS,"type")==="password")}}else{if(aP===ar&&"undefined"!==typeof aS.value){aY=String(aS.value)}}}var aV={ti:aR,ty:aP,s:aU,te:aY};if(aU){z.recordData(c,aV)}else{U("No selector found for text input ",aX)
+}},onScroll:function(aP){if(!an){an=true;ak.checkTrackersIfConditionsMet()}var aT=au.getTimeSincePageReady();if(aP&&aP.type&&aP.type==="scroll"&&aP.target&&aP.target!==O){var aZ=aP.target;if("undefined"===typeof aZ.scrollTop){return}var aR=aZ.scrollTop;var aU=aZ.scrollLeft;var aS=aN.getWidth(aZ);var aQ=aN.getHeight(aZ);if(aS<=0||aQ<=0||!aS||!aQ){return}var aV=aN.getSelector(aZ);ak.lastElementScroll={time:aT,selector:aV,scrollY:parseInt((C*aR)/aQ,10),scrollX:parseInt((C*aU)/aS,10)};return}var aX=parseInt(r.getScrollTop(),10);var aW=parseInt(r.getScrollLeft(),10);var a1=r.getDocumentHeight();var aY=r.getDocumentWidth();ak.lastScroll={time:aT,scrollY:parseInt((C*aX)/a1,10),scrollX:parseInt((C*aW)/aY,10)};var a0=parseInt((C*(aX+r.getWindowSize().height))/a1,10);if(a0>ak.scrollMaxPercentage){ak.scrollMaxPercentage=a0}},checkTrackersIfConditionsMet:function(){var aQ=z.getPiwikTrackers();for(var aP=0;aPaa){g=aa}},setMaxTextInputLength:function(aQ){Y=aQ},disableCaptureKeystrokes:function(){aI=false},enableCaptureKeystrokes:function(){aI=true},setMatomoTrackers:function(aQ){this.setPiwikTrackers(aQ)},setPiwikTrackers:function(aQ){if(aQ===null){n=null;return}if(!au.isArray(aQ)){aQ=[aQ]
+}n=aQ;n.forEach(q);if(aj){if(A){this.enable()}else{if(L){aM.fetch()}}}},enableDebugMode:function(){l=true}};Piwik.DOM.onReady(function(){F=new Date().getTime()});Piwik.addPlugin("HeatmapSessionRecording",{log:function(aQ){if(aO){if(aQ.tracker&&aQ.tracker.getNumTrackedPageViews&&aQ.tracker.getNumTrackedPageViews()>1){setTimeout(function(){Piwik.HeatmapSessionRecording.setNewPageView(true)},10)}}return""},unload:function(){if(!u()){var aQ=z.getPiwikTrackers();z.stopSendingData();aQ.forEach(function(aS){var aR=false;z.sendQueuedData(aS,aR)})}}});if(I.Piwik.initialized){var aP=Piwik.getAsyncTrackers();aP.forEach(q);Piwik.on("TrackerSetup",q);Piwik.retryMissedPluginCalls();aE();aM.fetch();Piwik.on("TrackerAdded",function(){if(A){Piwik.HeatmapSessionRecording.enable()}else{aM.fetch()}})}else{Piwik.on("TrackerSetup",q);Piwik.on("MatomoInitialized",function(){aE();if(L||A){aM.fetch()}Piwik.on("TrackerAdded",function(){if(L){aM.fetch()}else{if(A){Piwik.HeatmapSessionRecording.enable()}}})})}}ad=au.generateUniqueId();
+if("object"===typeof I.Piwik){ah()}else{if("object"!==typeof I.matomoPluginAsyncInit){I.matomoPluginAsyncInit=[]}I.matomoPluginAsyncInit.push(ah)}})();
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/tsconfig.json b/files/plugin-HeatmapSessionRecording-5.2.4/tsconfig.json
new file mode 100644
index 0000000..a541624
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "typeRoots": [
+ "../../node_modules/@types",
+ "../../plugins/CoreVue/types/index.d.ts",
+ "./node_modules/@types"
+ ],
+ "types": [
+ "jquery",
+ "heatmap.js"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/HeatmapSessionRecording.umd.js b/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/HeatmapSessionRecording.umd.js
new file mode 100644
index 0000000..5468f48
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/HeatmapSessionRecording.umd.js
@@ -0,0 +1,5480 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory(require("CoreHome"), require("vue"), require("CorePluginsAdmin"));
+ else if(typeof define === 'function' && define.amd)
+ define(["CoreHome", , "CorePluginsAdmin"], factory);
+ else if(typeof exports === 'object')
+ exports["HeatmapSessionRecording"] = factory(require("CoreHome"), require("vue"), require("CorePluginsAdmin"));
+ else
+ root["HeatmapSessionRecording"] = factory(root["CoreHome"], root["Vue"], root["CorePluginsAdmin"]);
+})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__, __WEBPACK_EXTERNAL_MODULE_a5a2__) {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ }
+/******/ };
+/******/
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = function(exports) {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/
+/******/ // create a fake namespace object
+/******/ // mode & 1: value is a module id, require it
+/******/ // mode & 2: merge all properties of value into the ns
+/******/ // mode & 4: return value when already ns object
+/******/ // mode & 8|1: behave like require
+/******/ __webpack_require__.t = function(value, mode) {
+/******/ if(mode & 1) value = __webpack_require__(value);
+/******/ if(mode & 8) return value;
+/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ var ns = Object.create(null);
+/******/ __webpack_require__.r(ns);
+/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ return ns;
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "plugins/HeatmapSessionRecording/vue/dist/";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = "fae3");
+/******/ })
+/************************************************************************/
+/******/ ({
+
+/***/ "19dc":
+/***/ (function(module, exports) {
+
+module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__;
+
+/***/ }),
+
+/***/ "246e":
+/***/ (function(module, exports, __webpack_require__) {
+
+var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;/*
+ * heatmap.js v2.0.5 | JavaScript Heatmap Library
+ *
+ * Copyright 2008-2016 Patrick Wied - All rights reserved.
+ * Dual licensed under MIT and Beerware license
+ *
+ * :: 2016-09-05 01:16
+ */
+;(function (name, context, factory) {
+
+ // Supports UMD. AMD, CommonJS/Node.js and browser context
+ if ( true && module.exports) {
+ module.exports = factory();
+ } else if (true) {
+ !(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
+ __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
+ (__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :
+ __WEBPACK_AMD_DEFINE_FACTORY__),
+ __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else {}
+
+})("h337", this, function () {
+
+// Heatmap Config stores default values and will be merged with instance config
+var HeatmapConfig = {
+ defaultRadius: 40,
+ defaultRenderer: 'canvas2d',
+ defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"},
+ defaultMaxOpacity: 1,
+ defaultMinOpacity: 0,
+ defaultBlur: .85,
+ defaultXField: 'x',
+ defaultYField: 'y',
+ defaultValueField: 'value',
+ plugins: {}
+};
+var Store = (function StoreClosure() {
+
+ var Store = function Store(config) {
+ this._coordinator = {};
+ this._data = [];
+ this._radi = [];
+ this._min = 10;
+ this._max = 1;
+ this._xField = config['xField'] || config.defaultXField;
+ this._yField = config['yField'] || config.defaultYField;
+ this._valueField = config['valueField'] || config.defaultValueField;
+
+ if (config["radius"]) {
+ this._cfgRadius = config["radius"];
+ }
+ };
+
+ var defaultRadius = HeatmapConfig.defaultRadius;
+
+ Store.prototype = {
+ // when forceRender = false -> called from setData, omits renderall event
+ _organiseData: function(dataPoint, forceRender) {
+ var x = dataPoint[this._xField];
+ var y = dataPoint[this._yField];
+ var radi = this._radi;
+ var store = this._data;
+ var max = this._max;
+ var min = this._min;
+ var value = dataPoint[this._valueField] || 1;
+ var radius = dataPoint.radius || this._cfgRadius || defaultRadius;
+
+ if (!store[x]) {
+ store[x] = [];
+ radi[x] = [];
+ }
+
+ if (!store[x][y]) {
+ store[x][y] = value;
+ radi[x][y] = radius;
+ } else {
+ store[x][y] += value;
+ }
+ var storedVal = store[x][y];
+
+ if (storedVal > max) {
+ if (!forceRender) {
+ this._max = storedVal;
+ } else {
+ this.setDataMax(storedVal);
+ }
+ return false;
+ } else if (storedVal < min) {
+ if (!forceRender) {
+ this._min = storedVal;
+ } else {
+ this.setDataMin(storedVal);
+ }
+ return false;
+ } else {
+ return {
+ x: x,
+ y: y,
+ value: value,
+ radius: radius,
+ min: min,
+ max: max
+ };
+ }
+ },
+ _unOrganizeData: function() {
+ var unorganizedData = [];
+ var data = this._data;
+ var radi = this._radi;
+
+ for (var x in data) {
+ for (var y in data[x]) {
+
+ unorganizedData.push({
+ x: x,
+ y: y,
+ radius: radi[x][y],
+ value: data[x][y]
+ });
+
+ }
+ }
+ return {
+ min: this._min,
+ max: this._max,
+ data: unorganizedData
+ };
+ },
+ _onExtremaChange: function() {
+ this._coordinator.emit('extremachange', {
+ min: this._min,
+ max: this._max
+ });
+ },
+ addData: function() {
+ if (arguments[0].length > 0) {
+ var dataArr = arguments[0];
+ var dataLen = dataArr.length;
+ while (dataLen--) {
+ this.addData.call(this, dataArr[dataLen]);
+ }
+ } else {
+ // add to store
+ var organisedEntry = this._organiseData(arguments[0], true);
+ if (organisedEntry) {
+ // if it's the first datapoint initialize the extremas with it
+ if (this._data.length === 0) {
+ this._min = this._max = organisedEntry.value;
+ }
+ this._coordinator.emit('renderpartial', {
+ min: this._min,
+ max: this._max,
+ data: [organisedEntry]
+ });
+ }
+ }
+ return this;
+ },
+ setData: function(data) {
+ var dataPoints = data.data;
+ var pointsLen = dataPoints.length;
+
+
+ // reset data arrays
+ this._data = [];
+ this._radi = [];
+
+ for(var i = 0; i < pointsLen; i++) {
+ this._organiseData(dataPoints[i], false);
+ }
+ this._max = data.max;
+ this._min = data.min || 0;
+
+ this._onExtremaChange();
+ this._coordinator.emit('renderall', this._getInternalData());
+ return this;
+ },
+ removeData: function() {
+ // TODO: implement
+ },
+ setDataMax: function(max) {
+ this._max = max;
+ this._onExtremaChange();
+ this._coordinator.emit('renderall', this._getInternalData());
+ return this;
+ },
+ setDataMin: function(min) {
+ this._min = min;
+ this._onExtremaChange();
+ this._coordinator.emit('renderall', this._getInternalData());
+ return this;
+ },
+ setCoordinator: function(coordinator) {
+ this._coordinator = coordinator;
+ },
+ _getInternalData: function() {
+ return {
+ max: this._max,
+ min: this._min,
+ data: this._data,
+ radi: this._radi
+ };
+ },
+ getData: function() {
+ return this._unOrganizeData();
+ }/*,
+
+ TODO: rethink.
+
+ getValueAt: function(point) {
+ var value;
+ var radius = 100;
+ var x = point.x;
+ var y = point.y;
+ var data = this._data;
+
+ if (data[x] && data[x][y]) {
+ return data[x][y];
+ } else {
+ var values = [];
+ // radial search for datapoints based on default radius
+ for(var distance = 1; distance < radius; distance++) {
+ var neighbors = distance * 2 +1;
+ var startX = x - distance;
+ var startY = y - distance;
+
+ for(var i = 0; i < neighbors; i++) {
+ for (var o = 0; o < neighbors; o++) {
+ if ((i == 0 || i == neighbors-1) || (o == 0 || o == neighbors-1)) {
+ if (data[startY+i] && data[startY+i][startX+o]) {
+ values.push(data[startY+i][startX+o]);
+ }
+ } else {
+ continue;
+ }
+ }
+ }
+ }
+ if (values.length > 0) {
+ return Math.max.apply(Math, values);
+ }
+ }
+ return false;
+ }*/
+ };
+
+
+ return Store;
+})();
+
+var Canvas2dRenderer = (function Canvas2dRendererClosure() {
+
+ var _getColorPalette = function(config) {
+ var gradientConfig = config.gradient || config.defaultGradient;
+ var paletteCanvas = document.createElement('canvas');
+ var paletteCtx = paletteCanvas.getContext('2d');
+
+ paletteCanvas.width = 256;
+ paletteCanvas.height = 1;
+
+ var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
+ for (var key in gradientConfig) {
+ gradient.addColorStop(key, gradientConfig[key]);
+ }
+
+ paletteCtx.fillStyle = gradient;
+ paletteCtx.fillRect(0, 0, 256, 1);
+
+ return paletteCtx.getImageData(0, 0, 256, 1).data;
+ };
+
+ var _getPointTemplate = function(radius, blurFactor) {
+ var tplCanvas = document.createElement('canvas');
+ var tplCtx = tplCanvas.getContext('2d');
+ var x = radius;
+ var y = radius;
+ tplCanvas.width = tplCanvas.height = radius*2;
+
+ if (blurFactor == 1) {
+ tplCtx.beginPath();
+ tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ tplCtx.fillStyle = 'rgba(0,0,0,1)';
+ tplCtx.fill();
+ } else {
+ var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
+ gradient.addColorStop(0, 'rgba(0,0,0,1)');
+ gradient.addColorStop(1, 'rgba(0,0,0,0)');
+ tplCtx.fillStyle = gradient;
+ tplCtx.fillRect(0, 0, 2*radius, 2*radius);
+ }
+
+
+
+ return tplCanvas;
+ };
+
+ var _prepareData = function(data) {
+ var renderData = [];
+ var min = data.min;
+ var max = data.max;
+ var radi = data.radi;
+ var data = data.data;
+
+ var xValues = Object.keys(data);
+ var xValuesLen = xValues.length;
+
+ while(xValuesLen--) {
+ var xValue = xValues[xValuesLen];
+ var yValues = Object.keys(data[xValue]);
+ var yValuesLen = yValues.length;
+ while(yValuesLen--) {
+ var yValue = yValues[yValuesLen];
+ var value = data[xValue][yValue];
+ var radius = radi[xValue][yValue];
+ renderData.push({
+ x: xValue,
+ y: yValue,
+ value: value,
+ radius: radius
+ });
+ }
+ }
+
+ return {
+ min: min,
+ max: max,
+ data: renderData
+ };
+ };
+
+
+ function Canvas2dRenderer(config) {
+ var container = config.container;
+ var shadowCanvas = this.shadowCanvas = document.createElement('canvas');
+ var canvas = this.canvas = config.canvas || document.createElement('canvas');
+ var renderBoundaries = this._renderBoundaries = [10000, 10000, 0, 0];
+
+ var computed = getComputedStyle(config.container) || {};
+
+ canvas.className = 'heatmap-canvas';
+
+ this._width = canvas.width = shadowCanvas.width = config.width || +(computed.width.replace(/px/,''));
+ this._height = canvas.height = shadowCanvas.height = config.height || +(computed.height.replace(/px/,''));
+
+ this.shadowCtx = shadowCanvas.getContext('2d');
+ this.ctx = canvas.getContext('2d');
+
+ // @TODO:
+ // conditional wrapper
+
+ canvas.style.cssText = shadowCanvas.style.cssText = 'position:absolute;left:0;top:0;';
+
+ container.style.position = 'relative';
+ container.appendChild(canvas);
+
+ this._palette = _getColorPalette(config);
+ this._templates = {};
+
+ this._setStyles(config);
+ };
+
+ Canvas2dRenderer.prototype = {
+ renderPartial: function(data) {
+ if (data.data.length > 0) {
+ this._drawAlpha(data);
+ this._colorize();
+ }
+ },
+ renderAll: function(data) {
+ // reset render boundaries
+ this._clear();
+ if (data.data.length > 0) {
+ this._drawAlpha(_prepareData(data));
+ this._colorize();
+ }
+ },
+ _updateGradient: function(config) {
+ this._palette = _getColorPalette(config);
+ },
+ updateConfig: function(config) {
+ if (config['gradient']) {
+ this._updateGradient(config);
+ }
+ this._setStyles(config);
+ },
+ setDimensions: function(width, height) {
+ this._width = width;
+ this._height = height;
+ this.canvas.width = this.shadowCanvas.width = width;
+ this.canvas.height = this.shadowCanvas.height = height;
+ },
+ _clear: function() {
+ this.shadowCtx.clearRect(0, 0, this._width, this._height);
+ this.ctx.clearRect(0, 0, this._width, this._height);
+ },
+ _setStyles: function(config) {
+ this._blur = (config.blur == 0)?0:(config.blur || config.defaultBlur);
+
+ if (config.backgroundColor) {
+ this.canvas.style.backgroundColor = config.backgroundColor;
+ }
+
+ this._width = this.canvas.width = this.shadowCanvas.width = config.width || this._width;
+ this._height = this.canvas.height = this.shadowCanvas.height = config.height || this._height;
+
+
+ this._opacity = (config.opacity || 0) * 255;
+ this._maxOpacity = (config.maxOpacity || config.defaultMaxOpacity) * 255;
+ this._minOpacity = (config.minOpacity || config.defaultMinOpacity) * 255;
+ this._useGradientOpacity = !!config.useGradientOpacity;
+ },
+ _drawAlpha: function(data) {
+ var min = this._min = data.min;
+ var max = this._max = data.max;
+ var data = data.data || [];
+ var dataLen = data.length;
+ // on a point basis?
+ var blur = 1 - this._blur;
+
+ while(dataLen--) {
+
+ var point = data[dataLen];
+
+ var x = point.x;
+ var y = point.y;
+ var radius = point.radius;
+ // if value is bigger than max
+ // use max as value
+ var value = Math.min(point.value, max);
+ var rectX = x - radius;
+ var rectY = y - radius;
+ var shadowCtx = this.shadowCtx;
+
+
+
+
+ var tpl;
+ if (!this._templates[radius]) {
+ this._templates[radius] = tpl = _getPointTemplate(radius, blur);
+ } else {
+ tpl = this._templates[radius];
+ }
+ // value from minimum / value range
+ // => [0, 1]
+ var templateAlpha = (value-min)/(max-min);
+ // this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
+ shadowCtx.globalAlpha = templateAlpha < .01 ? .01 : templateAlpha;
+
+ shadowCtx.drawImage(tpl, rectX, rectY);
+
+ // update renderBoundaries
+ if (rectX < this._renderBoundaries[0]) {
+ this._renderBoundaries[0] = rectX;
+ }
+ if (rectY < this._renderBoundaries[1]) {
+ this._renderBoundaries[1] = rectY;
+ }
+ if (rectX + 2*radius > this._renderBoundaries[2]) {
+ this._renderBoundaries[2] = rectX + 2*radius;
+ }
+ if (rectY + 2*radius > this._renderBoundaries[3]) {
+ this._renderBoundaries[3] = rectY + 2*radius;
+ }
+
+ }
+ },
+ _colorize: function() {
+ var x = this._renderBoundaries[0];
+ var y = this._renderBoundaries[1];
+ var width = this._renderBoundaries[2] - x;
+ var height = this._renderBoundaries[3] - y;
+ var maxWidth = this._width;
+ var maxHeight = this._height;
+ var opacity = this._opacity;
+ var maxOpacity = this._maxOpacity;
+ var minOpacity = this._minOpacity;
+ var useGradientOpacity = this._useGradientOpacity;
+
+ if (x < 0) {
+ x = 0;
+ }
+ if (y < 0) {
+ y = 0;
+ }
+ if (x + width > maxWidth) {
+ width = maxWidth - x;
+ }
+ if (y + height > maxHeight) {
+ height = maxHeight - y;
+ }
+
+ var img = this.shadowCtx.getImageData(x, y, width, height);
+ var imgData = img.data;
+ var len = imgData.length;
+ var palette = this._palette;
+
+
+ for (var i = 3; i < len; i+= 4) {
+ var alpha = imgData[i];
+ var offset = alpha * 4;
+
+
+ if (!offset) {
+ continue;
+ }
+
+ var finalAlpha;
+ if (opacity > 0) {
+ finalAlpha = opacity;
+ } else {
+ if (alpha < maxOpacity) {
+ if (alpha < minOpacity) {
+ finalAlpha = minOpacity;
+ } else {
+ finalAlpha = alpha;
+ }
+ } else {
+ finalAlpha = maxOpacity;
+ }
+ }
+
+ imgData[i-3] = palette[offset];
+ imgData[i-2] = palette[offset + 1];
+ imgData[i-1] = palette[offset + 2];
+ imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha;
+
+ }
+
+ img.data = imgData;
+ this.ctx.putImageData(img, x, y);
+
+ this._renderBoundaries = [1000, 1000, 0, 0];
+
+ },
+ getValueAt: function(point) {
+ var value;
+ var shadowCtx = this.shadowCtx;
+ var img = shadowCtx.getImageData(point.x, point.y, 1, 1);
+ var data = img.data[3];
+ var max = this._max;
+ var min = this._min;
+
+ value = (Math.abs(max-min) * (data/255)) >> 0;
+
+ return value;
+ },
+ getDataURL: function() {
+ return this.canvas.toDataURL();
+ }
+ };
+
+
+ return Canvas2dRenderer;
+})();
+
+
+var Renderer = (function RendererClosure() {
+
+ var rendererFn = false;
+
+ if (HeatmapConfig['defaultRenderer'] === 'canvas2d') {
+ rendererFn = Canvas2dRenderer;
+ }
+
+ return rendererFn;
+})();
+
+
+var Util = {
+ merge: function() {
+ var merged = {};
+ var argsLen = arguments.length;
+ for (var i = 0; i < argsLen; i++) {
+ var obj = arguments[i]
+ for (var key in obj) {
+ merged[key] = obj[key];
+ }
+ }
+ return merged;
+ }
+};
+// Heatmap Constructor
+var Heatmap = (function HeatmapClosure() {
+
+ var Coordinator = (function CoordinatorClosure() {
+
+ function Coordinator() {
+ this.cStore = {};
+ };
+
+ Coordinator.prototype = {
+ on: function(evtName, callback, scope) {
+ var cStore = this.cStore;
+
+ if (!cStore[evtName]) {
+ cStore[evtName] = [];
+ }
+ cStore[evtName].push((function(data) {
+ return callback.call(scope, data);
+ }));
+ },
+ emit: function(evtName, data) {
+ var cStore = this.cStore;
+ if (cStore[evtName]) {
+ var len = cStore[evtName].length;
+ for (var i=0; i {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", {
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(["btn-flat", {
+ 'visActive': theHeatmapType.key === _ctx.heatmapType,
+ [`heatmapType${theHeatmapType.key}`]: true
+ }]),
+ onClick: $event => _ctx.changeHeatmapType(theHeatmapType.key),
+ key: theHeatmapType.key
+ }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(theHeatmapType.name), 11, _hoisted_5);
+ }), 128)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h4", _hoisted_6, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_DeviceType')), 1), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.deviceTypesWithSamples, theDeviceType => {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", {
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(["btn-flat", {
+ 'visActive': theDeviceType.key === _ctx.deviceType,
+ [`deviceType${theDeviceType.key}`]: true
+ }]),
+ title: theDeviceType.tooltip,
+ onClick: $event => _ctx.changeDeviceType(theDeviceType.key),
+ key: theDeviceType.key
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ height: "15",
+ src: theDeviceType.logo,
+ alt: `${_ctx.translate('DevicesDetection_Device')} ${theDeviceType.name}`
+ }, null, 8, _hoisted_8), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", _hoisted_9, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(theDeviceType.numSamples), 1)], 10, _hoisted_7);
+ }), 128)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_10, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h4", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Installation_Legend')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_11, [_hoisted_12, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ class: "gradient",
+ alt: "gradient",
+ src: _ctx.gradientImgData
+ }, null, 8, _hoisted_13), _hoisted_14])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_15, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ style: {
+ "margin-left": "2.5rem",
+ "margin-right": "13.5px"
+ },
+ textContent: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_Width'))
+ }, null, 8, _hoisted_16), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "iframewidth",
+ "model-value": _ctx.customIframeWidth,
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => {
+ _ctx.customIframeWidth = $event;
+ _ctx.changeIframeWidth(_ctx.customIframeWidth, true);
+ }),
+ options: _ctx.iframeWidthOptions
+ }, null, 8, ["model-value", "options"])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_17, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_18, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_19, null, 512), _hoisted_20]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ class: "hsrLoadingOuter",
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])([{
+ "height": "400px"
+ }, {
+ width: _ctx.iframeWidth + 'px'
+ }])
+ }, [_hoisted_21, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_22, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_23, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Loading')), 1)])], 4), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isLoading]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ class: "aboveFoldLine",
+ title: _ctx.translate('HeatmapSessionRecording_AvgAboveFoldDescription'),
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])({
+ width: _ctx.iframeWidth + 'px',
+ top: _ctx.avgFold + 'px'
+ })
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_AvgAboveFoldTitle', _ctx.avgFold)), 1)], 12, _hoisted_24), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.avgFold]]), _ctx.embedUrl ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("iframe", {
+ key: 0,
+ id: "recordingPlayer",
+ ref: "recordingPlayer",
+ sandbox: "allow-scripts allow-same-origin",
+ referrerpolicy: "no-referrer",
+ onLoad: _cache[1] || (_cache[1] = $event => _ctx.onLoaded()),
+ height: "400",
+ src: _ctx.embedUrl,
+ width: _ctx.iframeWidth
+ }, null, 40, _hoisted_25)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)], 512), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_26, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_SaveButton, {
+ style: {
+ "display": "block !important"
+ },
+ loading: _ctx.isLoading,
+ onClick: _cache[2] || (_cache[2] = $event => _ctx.deleteScreenshot()),
+ value: _ctx.translate('HeatmapSessionRecording_DeleteScreenshot')
+ }, null, 8, ["loading", "value"])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.showDeleteScreenshot]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_27, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_DeleteHeatmapScreenshotConfirm')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "yes",
+ type: "button",
+ value: _ctx.translate('General_Yes')
+ }, null, 8, _hoisted_28), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "no",
+ type: "button",
+ value: _ctx.translate('General_No')
+ }, null, 8, _hoisted_29)], 512), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Tooltip, {
+ ref: "tooltip",
+ "click-count": _ctx.clickCount,
+ "click-rate": _ctx.clickRate,
+ "is-moves": _ctx.heatmapType === 1
+ }, null, 8, ["click-count", "click-rate", "is-moves"])]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVis.vue?vue&type=template&id=d1a5d966
+
+// EXTERNAL MODULE: ./plugins/HeatmapSessionRecording/node_modules/heatmap.js/build/heatmap.js
+var heatmap = __webpack_require__("246e");
+var heatmap_default = /*#__PURE__*/__webpack_require__.n(heatmap);
+
+// EXTERNAL MODULE: external "CoreHome"
+var external_CoreHome_ = __webpack_require__("19dc");
+
+// EXTERNAL MODULE: external "CorePluginsAdmin"
+var external_CorePluginsAdmin_ = __webpack_require__("a5a2");
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/getIframeWindow.ts
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function getIframeWindow(iframeElement) {
+ if (iframeElement && iframeElement.contentWindow) {
+ return iframeElement.contentWindow;
+ }
+ if (iframeElement && iframeElement.contentDocument && iframeElement.contentDocument.defaultView) {
+ return iframeElement.contentDocument.defaultView;
+ }
+ return undefined;
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/oneAtATime.ts
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function oneAtATime(method, options) {
+ let abortController = null;
+ return (params, postParams) => {
+ if (abortController) {
+ abortController.abort();
+ abortController = null;
+ }
+ abortController = new AbortController();
+ return external_CoreHome_["AjaxHelper"].post(Object.assign(Object.assign({}, params), {}, {
+ method
+ }), postParams, Object.assign(Object.assign({}, options), {}, {
+ abortController
+ })).finally(() => {
+ abortController = null;
+ });
+ };
+}
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/Tooltip/Tooltip.vue?vue&type=template&id=6a6ace20
+
+const Tooltipvue_type_template_id_6a6ace20_hoisted_1 = {
+ class: "tooltip-item"
+};
+const Tooltipvue_type_template_id_6a6ace20_hoisted_2 = {
+ class: "tooltip-label"
+};
+const Tooltipvue_type_template_id_6a6ace20_hoisted_3 = {
+ class: "tooltip-value"
+};
+const Tooltipvue_type_template_id_6a6ace20_hoisted_4 = {
+ class: "tooltip-item"
+};
+const Tooltipvue_type_template_id_6a6ace20_hoisted_5 = {
+ class: "tooltip-label"
+};
+const Tooltipvue_type_template_id_6a6ace20_hoisted_6 = {
+ class: "tooltip-value"
+};
+function Tooltipvue_type_template_id_6a6ace20_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", {
+ ref: "tooltipRef",
+ class: "tooltip",
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])(_ctx.tooltipStyle)
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Tooltipvue_type_template_id_6a6ace20_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Tooltipvue_type_template_id_6a6ace20_hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.getClickCountTranslation), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Tooltipvue_type_template_id_6a6ace20_hoisted_3, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.getClickCount), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Tooltipvue_type_template_id_6a6ace20_hoisted_4, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Tooltipvue_type_template_id_6a6ace20_hoisted_5, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.getClickRateTranslation), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Tooltipvue_type_template_id_6a6ace20_hoisted_6, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.getClickRate), 1)])], 4)), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.visible]]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/Tooltip/Tooltip.vue?vue&type=template&id=6a6ace20
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/Tooltip/Tooltip.vue?vue&type=script&lang=ts
+
+
+/* harmony default export */ var Tooltipvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ clickCount: {
+ type: Number,
+ required: true
+ },
+ clickRate: {
+ type: Number,
+ required: true
+ },
+ isMoves: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+ setup() {
+ const state = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["reactive"])({
+ visible: false,
+ position: {
+ top: 0,
+ left: 0
+ }
+ });
+ const tooltipRef = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["ref"])(null);
+ const tooltipStyle = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(() => ({
+ top: `${state.position.top}px`,
+ left: `${state.position.left}px`,
+ position: 'absolute',
+ zIndex: 1000
+ }));
+ function show(event) {
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
+ const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
+ state.position.top = event.clientY + scrollTop + 10;
+ state.position.left = event.clientX + scrollLeft + 10;
+ state.visible = true;
+ Object(external_commonjs_vue_commonjs2_vue_root_Vue_["nextTick"])(() => {
+ const tooltipElement = tooltipRef.value;
+ if (tooltipElement) {
+ const {
+ innerWidth,
+ innerHeight
+ } = window;
+ const tooltipRect = tooltipElement.getBoundingClientRect();
+ if (tooltipRect.right > innerWidth) {
+ state.position.left = event.clientX + scrollLeft - tooltipRect.width - 10;
+ }
+ if (tooltipRect.bottom > innerHeight) {
+ state.position.top = event.clientY + scrollTop - tooltipRect.height - 10;
+ }
+ const adjustedTooltipRect = tooltipElement.getBoundingClientRect();
+ if (adjustedTooltipRect.left < 0) {
+ state.position.left = scrollLeft + 10;
+ }
+ if (adjustedTooltipRect.top < 0) {
+ state.position.top = scrollTop + 10;
+ }
+ }
+ });
+ }
+ function hide() {
+ state.visible = false;
+ }
+ return Object.assign(Object.assign({}, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toRefs"])(state)), {}, {
+ tooltipRef,
+ show,
+ hide,
+ tooltipStyle,
+ translate: external_CoreHome_["translate"]
+ });
+ },
+ computed: {
+ getClickCount() {
+ return external_CoreHome_["NumberFormatter"].formatNumber(this.clickCount);
+ },
+ getClickRate() {
+ return external_CoreHome_["NumberFormatter"].formatPercent(this.clickRate);
+ },
+ getClickCountTranslation() {
+ const translation = this.isMoves ? 'HeatmapSessionRecording_Moves' : 'HeatmapSessionRecording_Clicks';
+ return Object(external_CoreHome_["translate"])(translation);
+ },
+ getClickRateTranslation() {
+ const translation = this.isMoves ? 'HeatmapSessionRecording_MoveRate' : 'HeatmapSessionRecording_ClickRate';
+ return Object(external_CoreHome_["translate"])(translation);
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/Tooltip/Tooltip.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/Tooltip/Tooltip.vue
+
+
+
+Tooltipvue_type_script_lang_ts.render = Tooltipvue_type_template_id_6a6ace20_render
+
+/* harmony default export */ var Tooltip = (Tooltipvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVis.vue?vue&type=script&lang=ts
+
+
+
+
+
+
+
+const {
+ $
+} = window;
+const deviceDesktop = 1;
+const deviceTablet = 2;
+const deviceMobile = 3;
+let heightPerHeatmap = 32000;
+const userAgent = String(window.navigator.userAgent).toLowerCase();
+if (userAgent.match(/(iPod|iPhone|iPad|Android|IEMobile|Windows Phone)/i)) {
+ heightPerHeatmap = 2000;
+} else if (userAgent.indexOf('msie ') > 0 || userAgent.indexOf('trident/') > 0 || userAgent.indexOf('edge') > 0) {
+ heightPerHeatmap = 8000;
+}
+function initHeatmap(recordingPlayer, heatmapContainer,
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+recordingIframe) {
+ const $iframe = $(recordingPlayer);
+ // we first set the iframe to the initial 400px again so we can for sure detect the current
+ // height of the inner iframe body correctly
+ $iframe.css('height', '400px');
+ const documentHeight = recordingIframe.getIframeHeight();
+ $iframe.css('height', `${documentHeight}px`);
+ $(heatmapContainer).css('height', `${documentHeight}px`).css('width', `${$iframe.width()}px`).empty();
+ const numHeatmaps = Math.ceil(documentHeight / heightPerHeatmap);
+ for (let i = 1; i <= numHeatmaps; i += 1) {
+ let height = heightPerHeatmap;
+ if (i === numHeatmaps) {
+ height = documentHeight % heightPerHeatmap;
+ }
+ $(heatmapContainer).append(`
`);
+ $(heatmapContainer).find(`#heatmap${i}`).css({
+ height: `${height}px`
+ });
+ }
+ return numHeatmaps;
+}
+function scrollHeatmap(iframeRecordingContainer, recordingPlayer,
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+recordingIframe, scrollReaches) {
+ const $iframe = $(recordingPlayer);
+ // we first set the iframe to the initial 400px again so we can for sure detect the current
+ // height of the inner iframe body correctly
+ $iframe.css('height', '400px');
+ const documentHeight = recordingIframe.getIframeHeight();
+ $iframe.css('height', `${documentHeight}px`);
+ const numIntervals = 1000;
+ const heightToIntervalRatio = documentHeight / numIntervals;
+ const numViewersTotal = scrollReaches.reduce((pv, cv) => pv + parseInt(cv.value, 10), 0);
+ const buckets = [];
+ let num_viewers = 0;
+ let lastBucket = null;
+ let percentage = 100;
+ let reachScrolledFromPosition = 0;
+ // reachScrolledFromPosition we start from 0, and then always paint to the next bucket. eg when
+ // scrollReach is 27 and scrollDepth is 35, then we know that 27 people have scrolled down to
+ // 3.5% of the page.
+ scrollReaches.forEach(scrollReachObj => {
+ // the number of people that reached this point
+ const scrollReach = parseInt(scrollReachObj.value, 10);
+ // how far down they scrolled
+ const scrollDepth = parseInt(scrollReachObj.label, 10);
+ const reachScrolledToPosition = Math.round(scrollDepth * heightToIntervalRatio);
+ if (lastBucket && lastBucket.position === reachScrolledToPosition) {
+ // when page is < 1000 we need to aggregate buckets
+ num_viewers += scrollReach;
+ } else {
+ if (numViewersTotal !== 0) {
+ percentage = (numViewersTotal - num_viewers) / numViewersTotal * 100;
+ }
+ num_viewers += scrollReach;
+ // percentage.toFixed(1) * 10 => convert eg 99.8 => 998
+ lastBucket = {
+ percentageValue: parseFloat(percentage.toFixed(1)) * 10,
+ position: reachScrolledFromPosition,
+ percent: percentage.toFixed(1)
+ };
+ buckets.push(lastBucket);
+ }
+ reachScrolledFromPosition = reachScrolledToPosition;
+ });
+ function map(value, istart, istop, ostart, ostop) {
+ return ostart + (ostop - ostart) * ((value - istart) / (istop - istart));
+ }
+ function mapColorIntensity(intensity, min, max) {
+ if (min === max || !min && !max) {
+ return [255, 255, 0];
+ }
+ const cint = map(intensity, min, max, 0, 255);
+ const step = (max - min) / 5;
+ if (cint > 204) {
+ return [255, map(intensity, max - step, max, 255, 0), 0];
+ }
+ if (cint > 153) {
+ return [map(intensity, max - 2 * step, max - step, 0, 255), 255, 0];
+ }
+ if (cint > 102) {
+ return [0, 255, map(intensity, max - 3 * step, max - 2 * step, 255, 0)];
+ }
+ if (cint > 51) {
+ return [0, map(intensity, max - 4 * step, max - 3 * step, 0, 255), 255];
+ }
+ return [map(intensity, min, max - 4 * step, 255, 0), 0, 255];
+ }
+ if (buckets.length) {
+ // we need to make sure to draw scroll heatmap over full page
+ const found = buckets.some(b => b.position === 0);
+ if (!found) {
+ buckets.unshift({
+ percent: '100.0',
+ percentageValue: 1000,
+ position: 0
+ });
+ }
+ } else {
+ // we'll show full page as not scrolled
+ buckets.push({
+ percent: '0',
+ percentageValue: 0,
+ position: 0
+ });
+ }
+ let minValue = 0;
+ const maxValue = 1000; // max value is always 1000 (=100%)
+ if (buckets && buckets.length && buckets[0]) {
+ minValue = buckets[buckets.length - 1].percentageValue;
+ }
+ const iframeWidth = $iframe.width();
+ let nextBucket = null;
+ for (let index = 0; index < buckets.length; index += 1) {
+ const bucket = buckets[index];
+ if (buckets[index + 1]) {
+ nextBucket = buckets[index + 1];
+ } else {
+ nextBucket = {
+ position: documentHeight
+ };
+ }
+ const top = bucket.position;
+ let height = nextBucket.position - bucket.position;
+ if (height === 0) {
+ height = 1; // make sure to draw at least one px
+ }
+ const percent = `${bucket.percent} percent reached this point`;
+ const colorValues = mapColorIntensity(bucket.percentageValue, minValue, maxValue);
+ const color = `rgb(${colorValues.join(',')})`;
+ $(iframeRecordingContainer).append(`
`);
+ }
+ $('.scrollHeatmapLeaf', iframeRecordingContainer).tooltip({
+ track: true,
+ items: '*',
+ tooltipClass: 'heatmapTooltip',
+ show: false,
+ hide: false
+ });
+ $('.legend-area .min').text(`${(minValue / 10).toFixed(1)}%`);
+ $('.legend-area .max').text(`${(maxValue / 10).toFixed(1)}%`);
+}
+function actualRenderHeatmap(recordingPlayer, heatmapContainer,
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+recordingIframe, dataPoints) {
+ const numHeatmaps = initHeatmap(recordingPlayer, heatmapContainer, recordingIframe);
+ const legendCanvas = document.createElement('canvas');
+ legendCanvas.width = 100;
+ legendCanvas.height = 10;
+ const min = document.querySelector('.legend-area .min');
+ const max = document.querySelector('.legend-area .max');
+ const gradientImg = document.querySelector('.legend-area .gradient');
+ const legendCtx = legendCanvas.getContext('2d');
+ let gradientCfg = {};
+ function updateLegend(data) {
+ // the onExtremaChange callback gives us min, max, and the gradientConfig
+ // so we can update the legend
+ min.innerHTML = `${data.min}`;
+ max.innerHTML = `${data.max}`;
+ // regenerate gradient image
+ if (data.gradient && data.gradient !== gradientCfg) {
+ gradientCfg = data.gradient;
+ const gradient = legendCtx.createLinearGradient(0, 0, 100, 1);
+ Object.keys(gradientCfg).forEach(key => {
+ gradient.addColorStop(parseFloat(key), gradientCfg[key]);
+ });
+ legendCtx.fillStyle = gradient;
+ legendCtx.fillRect(0, 0, 100, 10);
+ gradientImg.src = legendCanvas.toDataURL();
+ }
+ }
+ const heatmapInstances = [];
+ for (let i = 1; i <= numHeatmaps; i += 1) {
+ const dpoints = {
+ min: dataPoints.min,
+ max: dataPoints.max,
+ data: []
+ };
+ const config = {
+ container: document.getElementById(`heatmap${i}`),
+ radius: 10,
+ maxOpacity: 0.5,
+ minOpacity: 0,
+ blur: 0.75
+ };
+ if (i === 1) {
+ config.onExtremaChange = updateLegend; // typing is wrong here
+ }
+ if (dataPoints && dataPoints.data && dataPoints.data.length >= 20000) {
+ config.radius = 8;
+ } else if (dataPoints && dataPoints.data && dataPoints.data.length >= 2000) {
+ config.radius = 9;
+ }
+ if (numHeatmaps === 1) {
+ dpoints.data = dataPoints.data;
+ } else {
+ const lowerLimit = (i - 1) * heightPerHeatmap;
+ const upperLimit = lowerLimit + heightPerHeatmap - 1;
+ dataPoints.data.forEach(dp => {
+ if (dp.y >= lowerLimit && dp.y <= upperLimit) {
+ const thePoint = Object.assign(Object.assign({}, dp), {}, {
+ y: dp.y - lowerLimit
+ });
+ dpoints.data.push(thePoint);
+ }
+ });
+ }
+ const heatmapInstance = heatmap_default.a.create(config);
+ // heatmap type requires value to be number, but matomo sets it as string
+ heatmapInstance.setData(dpoints);
+ heatmapInstances.push(heatmapInstance);
+ }
+ return heatmapInstances;
+}
+/* harmony default export */ var HeatmapVisvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ idSiteHsr: {
+ type: Number,
+ required: true
+ },
+ deviceTypes: {
+ type: Array,
+ required: true
+ },
+ heatmapTypes: {
+ type: Array,
+ required: true
+ },
+ breakpointMobile: {
+ type: Number,
+ required: true
+ },
+ breakpointTablet: {
+ type: Number,
+ required: true
+ },
+ offsetAccuracy: {
+ type: Number,
+ required: true
+ },
+ heatmapPeriod: {
+ type: String,
+ required: true
+ },
+ heatmapDate: {
+ type: String,
+ required: true
+ },
+ url: {
+ type: String,
+ required: true
+ },
+ isActive: Boolean,
+ numSamples: {
+ type: Object,
+ required: true
+ },
+ excludedElements: {
+ type: String,
+ required: true
+ },
+ createdDate: {
+ type: String,
+ required: true
+ },
+ desktopPreviewSize: {
+ type: Number,
+ required: true
+ },
+ iframeResolutionsValues: {
+ type: Object,
+ required: true
+ }
+ },
+ components: {
+ Field: external_CorePluginsAdmin_["Field"],
+ SaveButton: external_CorePluginsAdmin_["SaveButton"],
+ Tooltip: Tooltip
+ },
+ data() {
+ return {
+ isLoading: false,
+ iframeWidth: this.desktopPreviewSize,
+ customIframeWidth: this.desktopPreviewSize,
+ avgFold: 0,
+ heatmapType: this.heatmapTypes[0].key,
+ deviceType: this.deviceTypes[0].key,
+ iframeResolutions: this.iframeResolutionsValues,
+ actualNumSamples: this.numSamples,
+ dataCoordinates: [],
+ currentElement: null,
+ totalClicks: 0,
+ tooltipShowTimeoutId: null,
+ clickCount: 0,
+ clickRate: 0
+ };
+ },
+ setup(props) {
+ const tooltip = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["ref"])(null);
+ let iframeLoadedResolve = null;
+ const iframeLoadedPromise = new Promise(resolve => {
+ iframeLoadedResolve = resolve;
+ });
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let recordingIframe = null;
+ const getRecordingIframe = recordingPlayer => {
+ if (!recordingIframe) {
+ recordingIframe = getIframeWindow(recordingPlayer).recordingFrame;
+ recordingIframe.excludeElements(props.excludedElements);
+ recordingIframe.addClass('html', 'piwikHeatmap');
+ recordingIframe.addClass('html', 'matomoHeatmap');
+ recordingIframe.addWorkaroundForSharepointHeatmaps();
+ }
+ return recordingIframe;
+ };
+ const heatmapInstances = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["ref"])(null);
+ const renderHeatmap = (recordingPlayer, heatmapContainer,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ theRecordingIframe, dataPoints) => {
+ heatmapInstances.value = actualRenderHeatmap(recordingPlayer, heatmapContainer, theRecordingIframe, dataPoints);
+ };
+ return {
+ iframeLoadedPromise,
+ onLoaded: iframeLoadedResolve,
+ getRecordedHeatmap: oneAtATime('HeatmapSessionRecording.getRecordedHeatmap'),
+ getRecordedHeatmapMetadata: oneAtATime('HeatmapSessionRecording.getRecordedHeatmapMetadata'),
+ getRecordingIframe,
+ heatmapInstances,
+ renderHeatmap,
+ tooltip
+ };
+ },
+ created() {
+ if (this.iframeResolutions.indexOf(this.breakpointMobile) === -1) {
+ this.iframeResolutions.push(this.breakpointMobile);
+ }
+ if (this.iframeResolutions.indexOf(this.breakpointTablet) === -1) {
+ this.iframeResolutions.push(this.breakpointTablet);
+ }
+ this.iframeResolutions = this.iframeResolutions.sort((a, b) => a - b);
+ this.fetchHeatmap();
+ // Hide the period selector since we don't filter the heatmap by period
+ external_CoreHome_["Matomo"].postEvent('hidePeriodSelector');
+ },
+ watch: {
+ isLoading() {
+ if (this.isLoading === true) {
+ return;
+ }
+ const heatmapContainer = window.document.getElementById('heatmapContainer');
+ if (!heatmapContainer) {
+ return;
+ }
+ heatmapContainer.addEventListener('mouseleave', event => {
+ // Stop processing tooltip when moving mouse out of parent element
+ if (this.tooltipShowTimeoutId) {
+ clearTimeout(this.tooltipShowTimeoutId);
+ this.tooltipShowTimeoutId = null;
+ }
+ // Reset the highlight and tooltip when leaving the container
+ this.currentElement = null;
+ this.handleTooltip(event, 0, 0, 'hide');
+ const highlightDiv = window.document.getElementById('highlightDiv');
+ if (!highlightDiv) {
+ return;
+ }
+ highlightDiv.hidden = true;
+ });
+ heatmapContainer.addEventListener('mousemove', e => {
+ this.handleMouseMove(e);
+ });
+ }
+ },
+ beforeUnmount() {
+ this.removeScrollHeatmap();
+ },
+ methods: {
+ removeScrollHeatmap() {
+ const element = this.$refs.iframeRecordingContainer;
+ $(element).find('.scrollHeatmapLeaf').remove();
+ },
+ deleteScreenshot() {
+ external_CoreHome_["Matomo"].helper.modalConfirm(this.$refs.confirmDeleteHeatmapScreenshot, {
+ yes: () => {
+ this.isLoading = true;
+ external_CoreHome_["AjaxHelper"].fetch({
+ method: 'HeatmapSessionRecording.deleteHeatmapScreenshot',
+ idSiteHsr: this.idSiteHsr
+ }).then(() => {
+ this.isLoading = false;
+ window.location.reload();
+ });
+ }
+ });
+ },
+ fetchHeatmap() {
+ this.removeScrollHeatmap();
+ if (this.heatmapInstances) {
+ const instances = this.heatmapInstances;
+ instances.forEach(heatmapInstance => {
+ heatmapInstance.setData({
+ max: 1,
+ min: 0,
+ data: []
+ });
+ });
+ }
+ this.isLoading = true;
+ this.avgFold = 0;
+ const segment = external_CoreHome_["MatomoUrl"].parsed.value.segment ? decodeURIComponent(external_CoreHome_["MatomoUrl"].parsed.value.segment) : undefined;
+ const requestParams = {
+ idSiteHsr: this.idSiteHsr,
+ heatmapType: this.heatmapType,
+ deviceType: this.deviceType,
+ period: this.heatmapPeriod,
+ date: this.heatmapDate,
+ filter_limit: -1,
+ segment
+ };
+ const heatmapDataPromise = this.getRecordedHeatmap(requestParams);
+ const heatmapMetaDataPromise = this.getRecordedHeatmapMetadata(requestParams);
+ Promise.all([heatmapDataPromise, heatmapMetaDataPromise, this.iframeLoadedPromise]).then(response => {
+ const iframeElement = this.$refs.recordingPlayer;
+ const recordingIframe = this.getRecordingIframe(iframeElement);
+ initHeatmap(this.$refs.recordingPlayer, this.$refs.heatmapContainer, recordingIframe);
+ this.removeScrollHeatmap();
+ const rows = response[0];
+ const numSamples = response[1];
+ if (Array.isArray(numSamples) && numSamples[0]) {
+ [this.actualNumSamples] = numSamples;
+ } else {
+ this.actualNumSamples = numSamples;
+ }
+ this.isLoading = false;
+ if (this.isScrollHeatmapType) {
+ scrollHeatmap(this.$refs.iframeRecordingContainer, iframeElement, recordingIframe, rows);
+ } else {
+ var _this$actualNumSample;
+ const dataPoints = {
+ min: 0,
+ max: 0,
+ data: []
+ };
+ for (let i = 0; i < rows.length; i += 1) {
+ const row = rows[i];
+ if (row.selector) {
+ const dataPoint = recordingIframe.getCoordinatesInFrame(row.selector, row.offset_x, row.offset_y, this.offsetAccuracy, true);
+ if (dataPoint) {
+ dataPoint.value = row.value;
+ dataPoints.data.push(dataPoint);
+ this.dataCoordinates.push(dataPoint);
+ this.totalClicks += parseInt(row.value, 10);
+ }
+ }
+ }
+ if (this.heatmapType === 2) {
+ // click
+ let numEntriesHigherThan1 = 0;
+ dataPoints.data.forEach(dp => {
+ if (dp !== null && dp !== void 0 && dp.value && parseInt(dp.value, 10) > 1) {
+ numEntriesHigherThan1 += 1;
+ }
+ });
+ if (numEntriesHigherThan1 / dataPoints.data.length >= 0.10 && dataPoints.data.length > 120) {
+ // if at least 10% have .value >= 2, then we set max to 2 to differntiate better
+ // between 1 and 2 clicks but only if we also have more than 80 data points
+ // ("randomly" chosen that threshold)
+ dataPoints.max = 2;
+ } else {
+ dataPoints.max = 1;
+ }
+ } else {
+ const LIMIT_MAX_DATA_POINT = 10;
+ const values = {};
+ dataPoints.data.forEach(dp => {
+ if (!dp || !dp.value) {
+ return;
+ }
+ let value = parseInt(dp.value, 10);
+ if (value > dataPoints.max) {
+ dataPoints.max = value;
+ }
+ if (value > LIMIT_MAX_DATA_POINT) {
+ value = LIMIT_MAX_DATA_POINT;
+ }
+ const valueStr = `${value}`;
+ if (valueStr in values) {
+ values[valueStr] += 1;
+ } else {
+ values[valueStr] = 0;
+ }
+ });
+ if (dataPoints.max > LIMIT_MAX_DATA_POINT) {
+ // we limit it to 10 otherwise many single points are not visible etc
+ // if there is no single entry having value 10, we set it to 9, 8 or 7
+ // to make sure there is actually a dataPoint for this max value.
+ let sumValuesAboveThreshold = 0;
+ for (let k = LIMIT_MAX_DATA_POINT; k > 1; k -= 1) {
+ const kStr = `${k}`;
+ if (kStr in values) {
+ // we need to aggregate the value
+ sumValuesAboveThreshold += values[kStr];
+ }
+ if (sumValuesAboveThreshold / dataPoints.data.length >= 0.2) {
+ // we make sure to have at least 20% of entries in that max value
+ dataPoints.max = k;
+ break;
+ }
+ // todo ideally in this case also require that akk 2 - (k-1) have a distribution
+ // of 0.2 to make sure we have enough values in between, and if not select k-1 or
+ // so. Otherwise we have maybe 75% with value 1, 20% with value 10, and only 5% in
+ // between... which would be barely visible those 75% maybe
+ }
+ if (dataPoints.max > LIMIT_MAX_DATA_POINT) {
+ // when no entry has more than 15% distribution, we set a default of 5
+ dataPoints.max = 5;
+ for (let k = 5; k > 0; k -= 1) {
+ const kStr = `${k}`;
+ if (kStr in values) {
+ // we limit it to 10 otherwise many single points are not visible etc
+ // also if there is no single entry having value 10, we set it to 9, 8 or 7
+ // to make sure there is actually a dataPoint for this max value.
+ dataPoints.max = k;
+ break;
+ }
+ }
+ }
+ }
+ }
+ this.renderHeatmap(this.$refs.recordingPlayer, this.$refs.heatmapContainer, recordingIframe, dataPoints);
+ if ((_this$actualNumSample = this.actualNumSamples) !== null && _this$actualNumSample !== void 0 && _this$actualNumSample[`avg_fold_device_${this.deviceType}`]) {
+ const avgFoldPercent = this.actualNumSamples[`avg_fold_device_${this.deviceType}`];
+ const height = recordingIframe.getIframeHeight();
+ if (height) {
+ this.avgFold = parseInt(`${avgFoldPercent / 100 * height}`, 10);
+ }
+ }
+ }
+ }).finally(() => {
+ this.isLoading = false;
+ });
+ },
+ changeDeviceType(deviceType) {
+ this.deviceType = deviceType;
+ if (this.deviceType === deviceDesktop) {
+ this.changeIframeWidth(this.desktopPreviewSize, false);
+ } else if (this.deviceType === deviceTablet) {
+ this.changeIframeWidth(this.breakpointTablet || 960, false);
+ } else if (this.deviceType === deviceMobile) {
+ this.changeIframeWidth(this.breakpointMobile || 600, false);
+ }
+ },
+ changeIframeWidth(iframeWidth, scrollToTop) {
+ this.iframeWidth = iframeWidth;
+ this.customIframeWidth = this.iframeWidth;
+ this.totalClicks = 0;
+ this.dataCoordinates = [];
+ this.fetchHeatmap();
+ if (scrollToTop) {
+ external_CoreHome_["Matomo"].helper.lazyScrollToContent();
+ }
+ },
+ changeHeatmapType(heatmapType) {
+ this.heatmapType = heatmapType;
+ this.totalClicks = 0;
+ this.clickCount = 0;
+ this.clickRate = 0;
+ this.dataCoordinates = [];
+ this.fetchHeatmap();
+ },
+ handleMouseMove(event) {
+ const highlightDiv = window.document.getElementById('highlightDiv');
+ if (!highlightDiv) {
+ return;
+ }
+ // Keep the tooltip from showing until the cursor has stopped moving
+ if (this.tooltipShowTimeoutId) {
+ clearTimeout(this.tooltipShowTimeoutId);
+ this.tooltipShowTimeoutId = null;
+ this.currentElement = null;
+ }
+ // If the highlight is visible, move the tooltip around with the cursor
+ if (!highlightDiv.hidden) {
+ this.handleTooltip(event, 0, 0, 'move');
+ }
+ const element = this.lookUpRecordedElementAtEventLocation(event);
+ // If there's no element, don't do anything else
+ // If the element hasn't changed, there's no need to do anything else
+ if (!element || element === this.currentElement) {
+ return;
+ }
+ this.handleTooltip(event, 0, 0, 'hide');
+ highlightDiv.hidden = true;
+ const elementRect = element.getBoundingClientRect();
+ let elementClicks = 0;
+ this.dataCoordinates.forEach(dataPoint => {
+ // Return if the dataPoint isn't within the element
+ if (dataPoint.y < elementRect.top || dataPoint.y > elementRect.bottom || dataPoint.x < elementRect.left || dataPoint.x > elementRect.right) {
+ return;
+ }
+ elementClicks += parseInt(dataPoint.value, 10);
+ });
+ // Have a slight delay so that it's not jarring when it displays
+ this.tooltipShowTimeoutId = setTimeout(() => {
+ this.currentElement = element;
+ highlightDiv.hidden = false;
+ // Multiplying by 10000 and then dividing by 100 to get 2 decimal points of precision
+ const clickRate = this.totalClicks ? Math.round(elementClicks / this.totalClicks * 10000) / 100 : 0;
+ const rect = element.getBoundingClientRect();
+ highlightDiv.style.top = `${rect.top}px`;
+ highlightDiv.style.left = `${rect.left}px`;
+ highlightDiv.style.width = `${rect.width}px`;
+ highlightDiv.style.height = `${rect.height}px`;
+ this.handleTooltip(event, elementClicks, clickRate, 'show');
+ this.tooltipShowTimeoutId = null;
+ }, 100);
+ },
+ lookUpRecordedElementAtEventLocation(event) {
+ const targetElement = event.target;
+ if (!targetElement) {
+ return null;
+ }
+ const frameElement = window.document.getElementById('recordingPlayer');
+ if (!frameElement) {
+ return null;
+ }
+ const frameRef = frameElement.contentWindow ? frameElement.contentWindow.document : frameElement.contentDocument;
+ if (!frameRef) {
+ return null;
+ }
+ const rect = targetElement.getBoundingClientRect();
+ return frameRef.elementFromPoint(event.clientX - rect.left, event.clientY - rect.top);
+ },
+ handleTooltip(event, clickCount, clickRate, action) {
+ if (this.tooltip) {
+ if (action === 'show') {
+ this.clickCount = clickCount;
+ this.clickRate = clickRate;
+ this.tooltip.show(event);
+ } else if (action === 'move') {
+ this.tooltip.show(event);
+ } else {
+ this.tooltip.hide();
+ }
+ }
+ }
+ },
+ computed: {
+ isScrollHeatmapType() {
+ return this.heatmapType === 3;
+ },
+ tokenAuth() {
+ return external_CoreHome_["MatomoUrl"].parsed.value.token_auth;
+ },
+ embedUrl() {
+ return `?${external_CoreHome_["MatomoUrl"].stringify({
+ module: 'HeatmapSessionRecording',
+ action: 'embedPage',
+ idSite: external_CoreHome_["Matomo"].idSite,
+ idSiteHsr: this.idSiteHsr,
+ token_auth: this.tokenAuth || undefined
+ })}`;
+ },
+ iframeWidthOptions() {
+ return this.iframeResolutions.map(width => ({
+ key: width,
+ value: `${width}px`
+ }));
+ },
+ recordedSamplesSince() {
+ const string1 = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_HeatmapXRecordedSamplesSince', `${this.actualNumSamples.nb_samples_device_all} `, this.createdDate);
+ const linkString = Object(external_CoreHome_["externalLink"])('https://matomo.org/faq/heatmap-session-recording/troubleshooting-heatmaps/');
+ const string2 = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_HeatmapTroubleshoot', linkString, '');
+ return `${string1} ${string2}`;
+ },
+ deviceTypesWithSamples() {
+ return this.deviceTypes.map(deviceType => {
+ let numSamples;
+ if (this.actualNumSamples[`nb_samples_device_${deviceType.key}`]) {
+ numSamples = this.actualNumSamples[`nb_samples_device_${deviceType.key}`];
+ } else {
+ numSamples = 0;
+ }
+ const tooltip = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_XSamples', `${deviceType.name} - ${numSamples}`);
+ return Object.assign(Object.assign({}, deviceType), {}, {
+ numSamples,
+ tooltip
+ });
+ });
+ },
+ hasWriteAccess() {
+ return !!(external_CoreHome_["Matomo"] !== null && external_CoreHome_["Matomo"] !== void 0 && external_CoreHome_["Matomo"].heatmapWriteAccess);
+ },
+ showDeleteScreenshot() {
+ return this.isActive && this.hasWriteAccess;
+ },
+ gradientImgData() {
+ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAKCAYAAABCHPt+AAAAnklEQVRYR+2WQQq' + 'DQBAES5wB/f8/Y05RcMWwSu6JIT0Dm4WlH1DUdHew7/z6WYFhhnGRpnlhAEaQpi/ADbh/np0MiBhGhW+2ymFU+DZ' + 'fg1EhaoB4jCFuMYYcQKZrXwPEVvm5Og0pcYakBvI35G1jNIZ4jCHexxjSpz9ZFUjAynLbpOvqteaODkm9sloz5JF' + '+ZTVmSAWSu9Qb65AvgDwBQoLgVDlWfAQAAAAASUVORK5CYII=';
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVis.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVis.vue
+
+
+
+HeatmapVisvue_type_script_lang_ts.render = render
+
+/* harmony default export */ var HeatmapVis = (HeatmapVisvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/SessionRecordingVis/SessionRecordingVis.vue?vue&type=template&id=6f77b61e
+
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_1 = {
+ class: "sessionRecordingPlayer"
+};
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_2 = {
+ class: "controls"
+};
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_3 = {
+ class: "playerActions"
+};
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_4 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_5 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_6 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_7 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_8 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_9 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_10 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_11 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_12 = {
+ version: "1.1",
+ xmlns: "http://www.w3.org/2000/svg",
+ width: "20",
+ height: "20",
+ viewBox: "0 0 768 768"
+};
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_13 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("path", {
+ d: "M480 576.5v-321h-64.5v129h-63v-129h-64.5v192h127.5v129h64.5zM607.5 127.999c34.5 0\n 64.5 30 64.5 64.5v447c0 34.5-30 64.5-64.5 64.5h-447c-34.5\n 0-64.5-30-64.5-64.5v-447c0-34.5 30-64.5 64.5-64.5h447z"
+}, null, -1);
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_14 = [SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_13];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_15 = {
+ version: "1.1",
+ xmlns: "http://www.w3.org/2000/svg",
+ width: "20",
+ height: "20",
+ viewBox: "0 0 768 768"
+};
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_16 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("path", {
+ d: "M448.5 576.5v-321h-129v64.5h64.5v256.5h64.5zM607.5 127.999c34.5 0 64.5 30 64.5\n 64.5v447c0 34.5-30 64.5-64.5 64.5h-447c-34.5 0-64.5-30-64.5-64.5v-447c0-34.5\n 30-64.5 64.5-64.5h447z"
+}, null, -1);
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_17 = [SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_16];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_18 = {
+ version: "1.1",
+ xmlns: "http://www.w3.org/2000/svg",
+ width: "20",
+ height: "20",
+ viewBox: "0 0 768 768"
+};
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_19 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("path", {
+ d: "M480 384.5v-64.5c0-36-30-64.5-64.5-64.5h-127.5v64.5h127.5v64.5h-63c-34.5 0-64.5\n 27-64.5 63v129h192v-64.5h-127.5v-64.5h63c34.5 0 64.5-27 64.5-63zM607.5 127.999c34.5\n 0 64.5 30 64.5 64.5v447c0 34.5-30 64.5-64.5 64.5h-447c-34.5\n 0-64.5-30-64.5-64.5v-447c0-34.5 30-64.5 64.5-64.5h447z"
+}, null, -1);
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_20 = [SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_19];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_21 = {
+ version: "1.1",
+ xmlns: "http://www.w3.org/2000/svg",
+ width: "20",
+ height: "20",
+ viewBox: "0 0 768 768"
+};
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_22 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("path", {
+ d: "M480 320v-64.5h-127.5c-34.5 0-64.5 28.5-64.5 64.5v192c0 36 30 64.5 64.5\n 64.5h63c34.5 0 64.5-28.5 64.5-64.5v-64.5c0-36-30-63-64.5-63h-63v-64.5h127.5zM607.5\n 127.999c34.5 0 64.5 30 64.5 64.5v447c0 34.5-30 64.5-64.5 64.5h-447c-34.5\n 0-64.5-30-64.5-64.5v-447c0-34.5 30-64.5 64.5-64.5h447zM352.5 512v-64.5h63v64.5h-63z"
+}, null, -1);
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_23 = [SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_22];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_24 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_25 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("svg", {
+ version: "1.1",
+ xmlns: "http://www.w3.org/2000/svg",
+ width: "20",
+ height: "20",
+ viewBox: "0 0 768 768"
+}, [/*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("path", {
+ d: "M223.5 415.5h111l-64.5-63h-46.5v63zM72 72l624 624-42 40.5-88.5-90c-51 36-114\n 57-181.5 57-177 0-319.5-142.5-319.5-319.5 0-67.5 21-130.5 57-181.5l-90-88.5zM544.5\n 352.5h-111l-231-231c51-36 114-57 181.5-57 177 0 319.5 142.5 319.5 319.5 0 67.5-21\n 130.5-57 181.5l-148.5-150h46.5v-63z"
+})], -1);
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_26 = [SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_25];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_27 = ["title"];
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_28 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("svg", {
+ version: "1.1",
+ xmlns: "http://www.w3.org/2000/svg",
+ width: "22",
+ height: "22",
+ viewBox: "0 0 768 768"
+}, [/*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("path", {
+ d: "M544.5 609v-129h63v192h-384v96l-127.5-127.5 127.5-127.5v96h321zM223.5\n 288v129h-63v-192h384v-96l127.5 127.5-127.5 127.5v-96h-321z"
+})], -1);
+const SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_29 = [SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_28];
+const _hoisted_30 = {
+ class: "duration"
+};
+const _hoisted_31 = {
+ class: "playerHelp"
+};
+const _hoisted_32 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "clickEvent"
+}, null, -1);
+const _hoisted_33 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "moveEvent"
+}, null, -1);
+const _hoisted_34 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "scrollEvent"
+}, null, -1);
+const _hoisted_35 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "resizeEvent"
+}, null, -1);
+const _hoisted_36 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "formChange"
+}, null, -1);
+const _hoisted_37 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "mutationEvent"
+}, null, -1);
+const _hoisted_38 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", {
+ style: {
+ "clear": "right"
+ }
+}, null, -1);
+const _hoisted_39 = ["title"];
+const _hoisted_40 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1);
+const _hoisted_41 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ class: "loadingUnderlay"
+}, null, -1);
+const _hoisted_42 = {
+ class: "valign-wrapper loadingInner"
+};
+const _hoisted_43 = {
+ class: "loadingContent"
+};
+const _hoisted_44 = ["src", "width", "height"];
+function SessionRecordingVisvue_type_template_id_6f77b61e_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_2, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "playerAction icon-skip-previous",
+ title: _ctx.skipPreviousButtonTitle,
+ onClick: _cache[0] || (_cache[0] = $event => _ctx.loadNewRecording(_ctx.previousRecordingId))
+ }, null, 8, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_4), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.previousRecordingId]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "playerAction icon-fast-rewind",
+ title: _ctx.translate('HeatmapSessionRecording_PlayerRewindFast', 10, 'J'),
+ onClick: _cache[1] || (_cache[1] = $event => _ctx.jumpRelative(10, false))
+ }, null, 8, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_5), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "playerAction icon-play",
+ title: _ctx.translate('HeatmapSessionRecording_PlayerPlay', 'K'),
+ onClick: _cache[2] || (_cache[2] = $event => _ctx.play())
+ }, null, 8, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_6), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], !_ctx.isPlaying && !_ctx.isFinished]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "playerAction icon-replay",
+ title: _ctx.translate('HeatmapSessionRecording_PlayerReplay', 'K'),
+ onClick: _cache[3] || (_cache[3] = $event => _ctx.replay())
+ }, null, 8, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_7), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], !_ctx.isPlaying && _ctx.isFinished]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "playerAction icon-pause",
+ title: _ctx.translate('HeatmapSessionRecording_PlayerPause', 'K'),
+ onClick: _cache[4] || (_cache[4] = $event => _ctx.pause())
+ }, null, 8, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_8), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isPlaying]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "playerAction icon-fast-forward",
+ title: _ctx.translate('HeatmapSessionRecording_PlayerForwardFast', 10, 'L'),
+ onClick: _cache[5] || (_cache[5] = $event => _ctx.jumpRelative(10, true))
+ }, null, 8, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_9), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "playerAction icon-skip-next",
+ title: _ctx.translate('HeatmapSessionRecording_PlayerPageViewNext', _ctx.nextRecordingInfo, 'N'),
+ onClick: _cache[6] || (_cache[6] = $event => _ctx.loadNewRecording(_ctx.nextRecordingId))
+ }, null, 8, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_10), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.nextRecordingId]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "changeReplaySpeed",
+ title: _ctx.translate('HeatmapSessionRecording_ChangeReplaySpeed', 'S'),
+ onClick: _cache[7] || (_cache[7] = $event => _ctx.increaseReplaySpeed())
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("svg", SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_12, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_14, 512)), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.actualReplaySpeed === 4]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("svg", SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_15, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_17, 512)), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.actualReplaySpeed === 1]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("svg", SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_18, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_20, 512)), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.actualReplaySpeed === 2]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("svg", SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_21, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_23, 512)), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.actualReplaySpeed === 6]])], 8, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_11), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(["toggleSkipPause", {
+ 'active': _ctx.actualSkipPausesEnabled
+ }]),
+ title: _ctx.translate('HeatmapSessionRecording_ClickToSkipPauses', _ctx.skipPausesEnabledText, 'B'),
+ onClick: _cache[8] || (_cache[8] = $event => _ctx.toggleSkipPauses())
+ }, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_26, 10, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_24), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(["toggleAutoPlay", {
+ 'active': _ctx.actualAutoPlayEnabled
+ }]),
+ title: _ctx.translate('HeatmapSessionRecording_AutoPlayNextPageview', _ctx.autoplayEnabledText, 'A'),
+ onClick: _cache[9] || (_cache[9] = $event => _ctx.toggleAutoPlay())
+ }, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_29, 10, SessionRecordingVisvue_type_template_id_6f77b61e_hoisted_27), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", _hoisted_30, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_PlayerDurationXofY', _ctx.positionPretty, _ctx.durationPretty)), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_31, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("ul", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", null, [_hoisted_32, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_ActivityClick')), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", null, [_hoisted_33, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_ActivityMove')), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", null, [_hoisted_34, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_ActivityScroll')), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", null, [_hoisted_35, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_ActivityResize')), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", null, [_hoisted_36, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_ActivityFormChange')), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("li", null, [_hoisted_37, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_ActivityPageChange')), 1)])])]), _hoisted_38]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ class: "timelineOuter",
+ onClick: _cache[10] || (_cache[10] = $event => _ctx.seekEvent($event)),
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])({
+ width: `${_ctx.replayWidth}px`
+ })
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ class: "timelineInner",
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])({
+ width: `${_ctx.progress}%`
+ })
+ }, null, 4), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.clues, (clue, index) => {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", {
+ title: clue.title,
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(clue.type),
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])({
+ left: `${clue.left}%`
+ }),
+ key: index
+ }, null, 14, _hoisted_39);
+ }), 128))], 4), _hoisted_40, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ class: "hsrLoadingOuter",
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])({
+ width: `${_ctx.replayWidth}px`,
+ height: `${_ctx.replayHeight}px`
+ })
+ }, [_hoisted_41, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_42, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_43, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Loading')), 1)])], 4), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isLoading]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ class: "replayContainerOuter",
+ onClick: _cache[12] || (_cache[12] = $event => _ctx.togglePlay()),
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])({
+ height: `${_ctx.replayHeight}px`,
+ width: `${_ctx.replayWidth}px`
+ })
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ class: "replayContainerInner",
+ style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])([{
+ "transform-origin": "0 0"
+ }, {
+ transform: `scale(${_ctx.replayScale})`,
+ 'margin-left': `${_ctx.replayMarginLeft}px`
+ }])
+ }, [_ctx.embedUrl ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("iframe", {
+ key: 0,
+ id: "recordingPlayer",
+ ref: "recordingPlayer",
+ onLoad: _cache[11] || (_cache[11] = $event => _ctx.onLoaded()),
+ scrolling: "no",
+ sandbox: "allow-scripts allow-same-origin",
+ referrerpolicy: "no-referrer",
+ src: _ctx.embedUrl,
+ width: _ctx.recording.viewport_w_px,
+ height: _ctx.recording.viewport_h_px
+ }, null, 40, _hoisted_44)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)], 4)], 4)]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/SessionRecordingVis/SessionRecordingVis.vue?vue&type=template&id=6f77b61e
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/SessionRecordingVis/SessionRecordingVis.vue?vue&type=script&lang=ts
+
+
+
+const FRAME_STEP = 20;
+const EVENT_TYPE_MOVEMENT = 1;
+const EVENT_TYPE_CLICK = 2;
+const EVENT_TYPE_SCROLL = 3;
+const EVENT_TYPE_RESIZE = 4;
+const EVENT_TYPE_INITIAL_DOM = 5;
+const EVENT_TYPE_MUTATION = 6;
+const EVENT_TYPE_FORM_TEXT = 9;
+const EVENT_TYPE_FORM_VALUE = 10;
+const EVENT_TYPE_SCROLL_ELEMENT = 12;
+const EVENT_TYPE_TO_NAME = {
+ [EVENT_TYPE_CLICK]: 'clickEvent',
+ [EVENT_TYPE_MOVEMENT]: 'moveEvent',
+ [EVENT_TYPE_SCROLL]: 'scrollEvent',
+ [EVENT_TYPE_SCROLL_ELEMENT]: 'scrollEvent',
+ [EVENT_TYPE_RESIZE]: 'resizeEvent',
+ [EVENT_TYPE_FORM_TEXT]: 'formChange',
+ [EVENT_TYPE_FORM_VALUE]: 'formChange',
+ [EVENT_TYPE_INITIAL_DOM]: 'mutationEvent',
+ [EVENT_TYPE_MUTATION]: 'mutationEvent'
+};
+const EVENT_TYPE_TO_TITLE = {
+ [EVENT_TYPE_CLICK]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityClick'),
+ [EVENT_TYPE_MOVEMENT]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityMove'),
+ [EVENT_TYPE_SCROLL]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityScroll'),
+ [EVENT_TYPE_SCROLL_ELEMENT]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityScroll'),
+ [EVENT_TYPE_RESIZE]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityResize'),
+ [EVENT_TYPE_FORM_TEXT]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityFormChange'),
+ [EVENT_TYPE_FORM_VALUE]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityFormChange'),
+ [EVENT_TYPE_INITIAL_DOM]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityPageChange'),
+ [EVENT_TYPE_MUTATION]: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ActivityPageChange')
+};
+const MOUSE_POINTER_HTML = `
+
+`;
+const {
+ $: SessionRecordingVisvue_type_script_lang_ts_$,
+ Mousetrap
+} = window;
+function intVal(v) {
+ return typeof v === 'number' ? v : parseInt(v, 10);
+}
+function getEventTypeId(event) {
+ if (!(event !== null && event !== void 0 && event.event_type)) {
+ return undefined;
+ }
+ return intVal(event.event_type);
+}
+function toPrettyTimeFormat(milliseconds) {
+ const durationSeconds = Math.floor(milliseconds / 1000);
+ let minutes = Math.floor(durationSeconds / 60);
+ let secondsLeft = durationSeconds % 60;
+ if (minutes < 10) {
+ minutes = `0${minutes}`;
+ }
+ if (secondsLeft < 10) {
+ secondsLeft = `0${secondsLeft}`;
+ }
+ return `${minutes}:${secondsLeft}`;
+}
+// TODO use something like command pattern and redo actions for each action maybe for more effecient
+// and better looking eeking to an earlier position in the video etc: Problem mutations can likely
+// not be "undone"
+/* harmony default export */ var SessionRecordingVisvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ offsetAccuracy: {
+ type: Number,
+ required: true
+ },
+ scrollAccuracy: {
+ type: Number,
+ required: true
+ },
+ autoPlayEnabled: Boolean,
+ skipPausesEnabled: Boolean,
+ replaySpeed: {
+ type: Number,
+ default: 1
+ }
+ },
+ data() {
+ return {
+ isPlaying: false,
+ progress: 0,
+ isFinished: false,
+ isLoading: true,
+ seekTimeout: null,
+ lastFramePainted: 0,
+ recording: JSON.parse(JSON.stringify(window.sessionRecordingData)),
+ positionPretty: '00:00',
+ previousRecordingId: null,
+ previousRecordingInfo: null,
+ nextRecordingId: null,
+ nextRecordingInfo: null,
+ frame: 0,
+ hasFoundPrevious: false,
+ hasFoundNext: false,
+ videoPlayerInterval: null,
+ lastCanvasCoordinates: false,
+ actualAutoPlayEnabled: !!this.autoPlayEnabled,
+ replayWidth: 0,
+ replayHeight: 0,
+ replayScale: 0,
+ replayMarginLeft: 0,
+ seek: seekToFrame => seekToFrame,
+ actualSkipPausesEnabled: !!this.skipPausesEnabled,
+ actualReplaySpeed: this.replaySpeed
+ };
+ },
+ setup() {
+ const iframeLoaded = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["ref"])(false);
+ let iframeLoadedResolve = null;
+ const iframeLoadedPromise = new Promise(resolve => {
+ iframeLoadedResolve = resolve;
+ iframeLoaded.value = true;
+ });
+ const onLoaded = () => {
+ setTimeout(() => {
+ // just to be sure we wait for another 500ms
+ iframeLoadedResolve('loaded');
+ }, 500);
+ };
+ return {
+ iframeLoadedPromise,
+ onLoaded,
+ iframeLoaded
+ };
+ },
+ created() {
+ this.recording.duration = intVal(this.recording.duration);
+ this.recording.pageviews.forEach(pageview => {
+ if (!pageview || !pageview.idloghsr) {
+ return;
+ }
+ if (`${pageview.idloghsr}` === `${this.recording.idLogHsr}`) {
+ this.hasFoundPrevious = true;
+ } else if (!this.hasFoundPrevious) {
+ this.previousRecordingId = pageview.idloghsr;
+ this.previousRecordingInfo = [pageview.label, pageview.server_time_pretty, pageview.time_on_page_pretty].join(' - ');
+ } else if (!this.hasFoundNext) {
+ this.hasFoundNext = true;
+ this.nextRecordingId = pageview.idloghsr;
+ this.nextRecordingInfo = [pageview.label, pageview.server_time_pretty, pageview.time_on_page_pretty].join(' - ');
+ }
+ });
+ },
+ mounted() {
+ Mousetrap.bind(['space', 'k'], () => {
+ this.togglePlay();
+ });
+ Mousetrap.bind('0', () => {
+ if (this.isFinished) {
+ this.replay();
+ }
+ });
+ Mousetrap.bind('p', () => {
+ this.loadNewRecording(this.previousRecordingId);
+ });
+ Mousetrap.bind('n', () => {
+ this.loadNewRecording(this.nextRecordingId);
+ });
+ Mousetrap.bind('s', () => {
+ this.increaseReplaySpeed();
+ });
+ Mousetrap.bind('a', () => {
+ this.toggleAutoPlay();
+ });
+ Mousetrap.bind('b', () => {
+ this.toggleSkipPauses();
+ });
+ Mousetrap.bind('left', () => {
+ const numSeconds = 5;
+ const jumpForward = false;
+ this.jumpRelative(numSeconds, jumpForward);
+ });
+ Mousetrap.bind('right', () => {
+ const numSeconds = 5;
+ const jumpForward = true;
+ this.jumpRelative(numSeconds, jumpForward);
+ });
+ Mousetrap.bind('j', () => {
+ const numSeconds = 10;
+ const jumpForward = false;
+ this.jumpRelative(numSeconds, jumpForward);
+ });
+ Mousetrap.bind('l', () => {
+ const numSeconds = 10;
+ const jumpForward = true;
+ this.jumpRelative(numSeconds, jumpForward);
+ });
+ this.initViewport();
+ SessionRecordingVisvue_type_script_lang_ts_$(window).on('resize', () => this.initViewport());
+ this.iframeLoadedPromise.then(() => {
+ this.initPlayer();
+ });
+ window.addEventListener('beforeunload', () => {
+ // should improve reload / go to next page performance
+ this.isPlaying = false;
+ if (this.videoPlayerInterval) {
+ clearInterval(this.videoPlayerInterval);
+ this.videoPlayerInterval = null;
+ }
+ });
+ },
+ methods: {
+ initPlayer() {
+ const iframeElement = this.$refs.recordingPlayer;
+ const recordingIframe = getIframeWindow(iframeElement).recordingFrame;
+ if (!recordingIframe || !recordingIframe.isSupportedBrowser()) {
+ return;
+ }
+ recordingIframe.addClass('html', 'piwikSessionRecording');
+ recordingIframe.addClass('html', 'matomoSessionRecording');
+ let $mousePointerNode = null;
+ const drawMouseLine = (coordinates, color) => {
+ if ($mousePointerNode) {
+ $mousePointerNode.css({
+ left: `${coordinates.x - 8}px`,
+ top: `${coordinates.y - 8}px`
+ });
+ }
+ if (!this.lastCanvasCoordinates) {
+ return;
+ }
+ recordingIframe.drawLine(this.lastCanvasCoordinates.x, this.lastCanvasCoordinates.y, coordinates.x, coordinates.y, color);
+ this.lastCanvasCoordinates = coordinates;
+ };
+ const scrollFrameTo = (xPos, yPos) => {
+ if (!this.lastCanvasCoordinates || !$mousePointerNode) {
+ // we cannot move the mouse pointer since we do not have the initial mouse position yet
+ // only perform scroll action instead
+ recordingIframe.scrollTo(xPos, yPos);
+ return;
+ }
+ // we only move the mouse pointer but not draw a line for the mouse movement eg when user
+ // scrolls we also make sure that when the next time the user moves the mouse the mouse
+ // move line will be drawn from this new position
+ const currentScrollTop = recordingIframe.getScrollTop();
+ const currentScrollLeft = recordingIframe.getScrollLeft();
+ recordingIframe.scrollTo(xPos, yPos);
+ // we detect how far down or up user scrolled (or to the left or right)
+ const diffScrollTop = yPos - currentScrollTop;
+ const diffScrollLeft = xPos - currentScrollLeft;
+ // if user scrolled eg 100px down, we also need to move the cursor down
+ let newMousePointerPosLeft = diffScrollLeft + this.lastCanvasCoordinates.x;
+ let newMousePointerPosTop = diffScrollTop + this.lastCanvasCoordinates.y;
+ if (newMousePointerPosLeft <= 0) {
+ newMousePointerPosLeft = 0;
+ }
+ if (newMousePointerPosTop <= 0) {
+ newMousePointerPosTop = 0;
+ }
+ // we make sure to draw the next mouse move line from this position. we use a blue line
+ // to indicate the mouse was moved by a scroll
+ drawMouseLine({
+ x: newMousePointerPosLeft,
+ y: newMousePointerPosTop
+ }, 'blue');
+ };
+ const scrollElementTo = (element, xPos, yPos) => {
+ if (element !== null && element !== void 0 && element.scrollTo) {
+ element.scrollTo(xPos, yPos);
+ } else {
+ element.scrollLeft = xPos;
+ element.scrollTop = yPos;
+ }
+ };
+ let moveMouseTo = null;
+ const replayEvent = event => {
+ // fixes some concurrency problems etc by not continueing in the player until the current
+ // action is drawn
+ const {
+ isPlaying
+ } = this;
+ this.isPlaying = false;
+ const eventType = getEventTypeId(event);
+ let offset = null;
+ if (eventType === EVENT_TYPE_MOVEMENT) {
+ if (event.selector) {
+ offset = recordingIframe.getCoordinatesInFrame(event.selector, event.x, event.y, this.offsetAccuracy, false);
+ if (offset) {
+ moveMouseTo(offset);
+ }
+ }
+ } else if (eventType === EVENT_TYPE_CLICK) {
+ if (event.selector) {
+ offset = recordingIframe.getCoordinatesInFrame(event.selector, event.x, event.y, this.offsetAccuracy, false);
+ if (offset) {
+ moveMouseTo(offset);
+ recordingIframe.drawCircle(offset.x, offset.y, '#ff9407');
+ }
+ }
+ } else if (eventType === EVENT_TYPE_MUTATION) {
+ if (event.text) {
+ recordingIframe.applyMutation(event.text);
+ }
+ } else if (eventType === EVENT_TYPE_SCROLL) {
+ const docHeight = recordingIframe.getIframeHeight();
+ const docWidth = recordingIframe.getIframeWidth();
+ const yPos = parseInt(`${docHeight / this.scrollAccuracy * intVal(event.y)}`, 10);
+ const xPos = parseInt(`${docWidth / this.scrollAccuracy * intVal(event.x)}`, 10);
+ scrollFrameTo(xPos, yPos);
+ } else if (eventType === EVENT_TYPE_SCROLL_ELEMENT) {
+ if (event.selector) {
+ const element = recordingIframe.findElement(event.selector);
+ if (element && element.length && element[0]) {
+ const eleHeight = Math.max(element[0].scrollHeight, element[0].offsetHeight, element.height(), 0);
+ const eleWidth = Math.max(element[0].scrollWidth, element[0].offsetWidth, element.width(), 0);
+ if (eleHeight && eleWidth) {
+ const yPos = parseInt(`${eleHeight / this.scrollAccuracy * intVal(event.y)}`, 10);
+ const xPos = parseInt(`${eleWidth / this.scrollAccuracy * intVal(event.x)}`, 10);
+ scrollElementTo(element[0], xPos, yPos);
+ }
+ }
+ }
+ } else if (eventType === EVENT_TYPE_RESIZE) {
+ this.setViewportResolution(event.x, event.y);
+ } else if (eventType === EVENT_TYPE_FORM_TEXT) {
+ if (event.selector) {
+ const formElement = recordingIframe.findElement(event.selector);
+ if (formElement.length) {
+ const formAttrType = formElement.attr('type');
+ if (formAttrType && `${formAttrType}`.toLowerCase() === 'file') {
+ // cannot be changed to local file, would result in error
+ } else {
+ formElement.val(event.text).change();
+ }
+ }
+ }
+ } else if (eventType === EVENT_TYPE_FORM_VALUE) {
+ if (event.selector) {
+ const $field = recordingIframe.findElement(event.selector);
+ if ($field.is('input')) {
+ $field.prop('checked', event.text === 1 || event.text === '1');
+ } else if ($field.is('select')) {
+ $field.val(event.text).change();
+ }
+ }
+ }
+ this.isPlaying = isPlaying;
+ };
+ moveMouseTo = coordinates => {
+ const resizeStage = () => {
+ const stageWidth = recordingIframe.getIframeWidth();
+ const stageHeight = recordingIframe.getIframeHeight();
+ recordingIframe.makeSvg(stageWidth, stageHeight);
+ for (let crtFrame = 0; crtFrame <= this.frame; crtFrame += FRAME_STEP) {
+ if (!this.timeFrameBuckets[crtFrame]) {
+ return;
+ }
+ this.timeFrameBuckets[crtFrame].forEach(event => {
+ const eventType = getEventTypeId(event);
+ if (eventType === EVENT_TYPE_MOVEMENT || eventType === EVENT_TYPE_SCROLL || eventType === EVENT_TYPE_SCROLL_ELEMENT || eventType === EVENT_TYPE_CLICK) {
+ this.lastFramePainted = crtFrame;
+ replayEvent(event);
+ }
+ });
+ }
+ };
+ // Runs each time the DOM window resize event fires.
+ // Resets the canvas dimensions to match window,
+ // then draws the new borders accordingly.
+ const iframeWindow = recordingIframe.getIframeWindow();
+ if (!this.lastCanvasCoordinates) {
+ const stageHeight = recordingIframe.getIframeHeight();
+ const stageWidth = recordingIframe.getIframeWidth();
+ recordingIframe.appendContent(MOUSE_POINTER_HTML);
+ $mousePointerNode = recordingIframe.findElement('.mousePointer');
+ recordingIframe.makeSvg(stageWidth, stageHeight);
+ iframeWindow.removeEventListener('resize', resizeStage, false);
+ iframeWindow.addEventListener('resize', resizeStage, false);
+ this.lastCanvasCoordinates = coordinates;
+ $mousePointerNode.css({
+ left: `${coordinates.x - 8}px`,
+ top: `${coordinates.y - 8}px`
+ });
+ return;
+ }
+ let scrollTop = recordingIframe.getScrollTop();
+ const scrollLeft = recordingIframe.getScrollLeft();
+ if (coordinates.y > scrollTop + intVal(this.recording.viewport_h_px)) {
+ recordingIframe.scrollTo(scrollLeft, coordinates.y - 10);
+ } else if (coordinates.y < scrollTop) {
+ recordingIframe.scrollTo(scrollLeft, coordinates.y - 10);
+ }
+ scrollTop = recordingIframe.getScrollTop();
+ if (coordinates.x > scrollLeft + intVal(this.recording.viewport_w_px)) {
+ recordingIframe.scrollTo(coordinates.x - 10, scrollTop);
+ } else if (coordinates.x < scrollLeft) {
+ recordingIframe.scrollTo(coordinates.x - 10, scrollTop);
+ }
+ drawMouseLine(coordinates, '#ff9407');
+ };
+ this.seek = seekToFrame => {
+ if (!this.iframeLoaded) {
+ return;
+ }
+ // this operation may take a while so we want to stop any interval and further action
+ // until this is completed
+ this.isLoading = true;
+ let previousFrame = this.frame;
+ const executeSeek = thePreviousFrame => {
+ for (let crtFrame = thePreviousFrame; crtFrame <= this.frame; crtFrame += FRAME_STEP) {
+ (this.timeFrameBuckets[crtFrame] || []).forEach(event => {
+ this.lastFramePainted = crtFrame;
+ replayEvent(event);
+ });
+ }
+ };
+ this.isFinished = false;
+ this.frame = seekToFrame - seekToFrame % FRAME_STEP;
+ this.progress = parseFloat(parseFloat(`${this.frame / intVal(this.recording.duration) * 100}`).toFixed(2));
+ this.positionPretty = toPrettyTimeFormat(this.frame);
+ if (previousFrame > this.frame) {
+ // we start replaying the video from the beginning
+ previousFrame = 0;
+ this.lastCanvasCoordinates = false;
+ if (this.initialMutation) {
+ recordingIframe.initialMutation(this.initialMutation.text);
+ }
+ recordingIframe.scrollTo(0, 0);
+ this.setViewportResolution(window.sessionRecordingData.viewport_w_px, window.sessionRecordingData.viewport_h_px);
+ if (this.seekTimeout) {
+ clearTimeout(this.seekTimeout);
+ this.seekTimeout = null;
+ // make sure when user goes to previous position and we have a timeout to not execute
+ // it multiple times
+ }
+ (thePreviousFrame => {
+ this.seekTimeout = setTimeout(() => {
+ executeSeek(thePreviousFrame);
+ this.isLoading = false;
+ }, 1050);
+ })(previousFrame);
+ } else {
+ // otherwise we instead play fast forward all new actions for faster performance and
+ // smoother visualization etc
+ if (this.seekTimeout) {
+ clearTimeout(this.seekTimeout);
+ this.seekTimeout = null;
+ }
+ executeSeek(previousFrame);
+ this.isLoading = false;
+ }
+ };
+ this.isLoading = false;
+ this.isPlaying = true;
+ let updateTimeCounter = 0;
+ const drawFrames = () => {
+ if (this.isPlaying && !this.isLoading) {
+ updateTimeCounter += 1;
+ const duration = intVal(this.recording.duration);
+ if (this.frame >= duration) {
+ this.isPlaying = false;
+ this.progress = 100;
+ this.isFinished = true;
+ this.positionPretty = this.durationPretty;
+ if (this.actualAutoPlayEnabled && this.nextRecordingId) {
+ this.loadNewRecording(this.nextRecordingId);
+ }
+ } else {
+ this.progress = parseFloat(parseFloat(`${this.frame / duration * 100}`).toFixed(2));
+ if (updateTimeCounter === 20) {
+ updateTimeCounter = 0;
+ this.positionPretty = toPrettyTimeFormat(this.frame);
+ }
+ }
+ (this.timeFrameBuckets[this.frame] || []).forEach(event => {
+ // remember when we last painted a frame
+ this.lastFramePainted = this.frame;
+ replayEvent(event);
+ });
+ if (this.actualSkipPausesEnabled && this.frame - this.lastFramePainted > 1800) {
+ // after 1.8 seconds of not painting anything, move forward to next action
+ let keys = Object.keys(this.timeFrameBuckets).map(k => parseInt(k, 10));
+ keys = keys.sort((a, b) => a - b);
+ const nextFrameKey = keys.find(key => key > this.frame);
+ const hasNextFrame = !!nextFrameKey;
+ if (nextFrameKey) {
+ const isMoreThan1SecInFuture = nextFrameKey - this.frame > 1000;
+ if (isMoreThan1SecInFuture) {
+ // we set the pointer foward to the next frame printable
+ // we only move forward if we can save at least one second.
+ // we set the cursor to shortly before the next action.
+ this.frame = nextFrameKey - 20 * FRAME_STEP;
+ }
+ }
+ // if no frame found, skip to the end of the recording
+ if (!hasNextFrame) {
+ const isMoreThan1SecInFuture = duration - this.frame > 1000;
+ if (isMoreThan1SecInFuture) {
+ // we don't set it to very end to still have something to play
+ this.frame = duration - 20 * FRAME_STEP;
+ }
+ }
+ }
+ this.frame += FRAME_STEP;
+ }
+ };
+ this.videoPlayerInterval = setInterval(() => {
+ for (let k = 1; k <= this.actualReplaySpeed; k += 1) {
+ drawFrames();
+ }
+ }, FRAME_STEP);
+ },
+ initViewport() {
+ this.replayHeight = SessionRecordingVisvue_type_script_lang_ts_$(window).height() - 48 - SessionRecordingVisvue_type_script_lang_ts_$('.sessionRecording .sessionRecordingHead').outerHeight(true) - SessionRecordingVisvue_type_script_lang_ts_$('.sessionRecordingPlayer .controls').outerHeight(true);
+ this.replayWidth = SessionRecordingVisvue_type_script_lang_ts_$(window).width() - 48;
+ const viewportwpx = intVal(this.recording.viewport_w_px);
+ const viewporthpx = intVal(this.recording.viewport_h_px);
+ const minReplayWidth = 400;
+ if (this.replayWidth < minReplayWidth && viewportwpx > minReplayWidth) {
+ this.replayWidth = minReplayWidth;
+ }
+ const minReplayHeight = 400;
+ if (this.replayHeight < minReplayHeight && viewporthpx > minReplayHeight) {
+ this.replayHeight = minReplayHeight;
+ }
+ let widthScale = 1;
+ let heightScale = 1;
+ if (viewportwpx > this.replayWidth) {
+ widthScale = parseFloat(parseFloat(`${this.replayWidth / viewportwpx}`).toFixed(4));
+ }
+ if (viewporthpx > this.replayHeight) {
+ heightScale = parseFloat(parseFloat(`${this.replayHeight / viewporthpx}`).toFixed(4));
+ }
+ this.replayScale = Math.min(widthScale, heightScale);
+ this.replayMarginLeft = (this.replayWidth - this.replayScale * viewportwpx) / 2;
+ },
+ setViewportResolution(widthPx, heightPx) {
+ this.recording.viewport_w_px = parseInt(`${widthPx}`, 10);
+ this.recording.viewport_h_px = parseInt(`${heightPx}`, 10);
+ SessionRecordingVisvue_type_script_lang_ts_$('.recordingWidth').text(widthPx);
+ SessionRecordingVisvue_type_script_lang_ts_$('.recordingHeight').text(heightPx);
+ this.initViewport();
+ },
+ increaseReplaySpeed() {
+ if (this.actualReplaySpeed === 1) {
+ this.actualReplaySpeed = 2;
+ } else if (this.actualReplaySpeed === 2) {
+ this.actualReplaySpeed = 4;
+ } else if (this.actualReplaySpeed === 4) {
+ this.actualReplaySpeed = 6;
+ } else {
+ this.actualReplaySpeed = 1;
+ }
+ this.updateSettings();
+ },
+ updateSettings() {
+ external_CoreHome_["AjaxHelper"].fetch({
+ module: 'HeatmapSessionRecording',
+ action: 'saveSessionRecordingSettings',
+ autoplay: this.actualAutoPlayEnabled ? 1 : 0,
+ skippauses: this.actualSkipPausesEnabled ? 1 : 0,
+ replayspeed: this.actualReplaySpeed
+ }, {
+ format: 'html'
+ });
+ },
+ toggleAutoPlay() {
+ this.actualAutoPlayEnabled = !this.actualAutoPlayEnabled;
+ this.updateSettings();
+ },
+ toggleSkipPauses() {
+ this.actualSkipPausesEnabled = !this.actualSkipPausesEnabled;
+ this.updateSettings();
+ },
+ loadNewRecording(idLogHsr) {
+ if (idLogHsr) {
+ this.isPlaying = false;
+ external_CoreHome_["MatomoUrl"].updateUrl(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value), {}, {
+ idLogHsr: parseInt(`${idLogHsr}`, 10),
+ updated: external_CoreHome_["MatomoUrl"].urlParsed.value.updated ? parseInt(external_CoreHome_["MatomoUrl"].urlParsed.value.updated, 10) + 1 : 1
+ }));
+ }
+ },
+ jumpRelative(numberSeconds, forward) {
+ const framesToJump = numberSeconds * 1000;
+ let newPosition;
+ if (forward) {
+ newPosition = this.frame + framesToJump;
+ if (newPosition > this.recording.duration) {
+ newPosition = intVal(this.recording.duration) - FRAME_STEP;
+ }
+ } else {
+ newPosition = this.frame - framesToJump;
+ if (newPosition < 0) {
+ newPosition = 0;
+ }
+ }
+ this.seek(newPosition);
+ },
+ replay() {
+ this.isFinished = false;
+ this.lastFramePainted = 0;
+ this.seek(0);
+ this.play();
+ },
+ pause() {
+ this.isPlaying = false;
+ },
+ togglePlay() {
+ if (this.isFinished) {
+ this.replay();
+ } else if (this.isPlaying) {
+ this.pause();
+ } else {
+ this.play();
+ }
+ },
+ seekEvent(event) {
+ const offset = SessionRecordingVisvue_type_script_lang_ts_$(event.currentTarget).offset();
+ const selectedPosition = event.pageX - offset.left;
+ const fullWidth = this.replayWidth;
+ const seekPercentage = selectedPosition / fullWidth;
+ const seekPositionTime = intVal(this.recording.duration) * seekPercentage;
+ this.seek(seekPositionTime);
+ },
+ play() {
+ this.isPlaying = true;
+ }
+ },
+ computed: {
+ durationPretty() {
+ return toPrettyTimeFormat(intVal(this.recording.duration));
+ },
+ embedUrl() {
+ return `?${external_CoreHome_["MatomoUrl"].stringify({
+ module: 'HeatmapSessionRecording',
+ action: 'embedPage',
+ idSite: this.recording.idSite,
+ idLogHsr: this.recording.idLogHsr,
+ idSiteHsr: this.recording.idSiteHsr,
+ // NOTE: important to get the token_auth from the URL directly, since if there is no
+ // token_auth there, we should send nothing. In this case, Matomo.token_auth will still
+ // be set, so we can't check that variable here.
+ token_auth: external_CoreHome_["MatomoUrl"].urlParsed.value.token_auth || undefined
+ })}`;
+ },
+ skipPreviousButtonTitle() {
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_PlayerPageViewPrevious', this.previousRecordingInfo || '', 'P');
+ },
+ skipPausesEnabledText() {
+ if (this.actualSkipPausesEnabled) {
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_disable');
+ }
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_enable');
+ },
+ autoplayEnabledText() {
+ if (this.actualAutoPlayEnabled) {
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_disable');
+ }
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_enable');
+ },
+ recordingEvents() {
+ if (!this.recording) {
+ return [];
+ }
+ return this.recording.events.map(theEvent => {
+ const eventType = getEventTypeId(theEvent);
+ let {
+ text
+ } = theEvent;
+ if ((eventType === EVENT_TYPE_INITIAL_DOM || eventType === EVENT_TYPE_MUTATION) && typeof text === 'string') {
+ text = JSON.parse(text);
+ }
+ return Object.assign(Object.assign({}, theEvent), {}, {
+ text
+ });
+ });
+ },
+ initialMutation() {
+ const initialEvent = this.recordingEvents.find(e => {
+ const eventType = getEventTypeId(e);
+ const isMutation = eventType === EVENT_TYPE_INITIAL_DOM || eventType === EVENT_TYPE_MUTATION;
+ const isInitialMutation = isMutation && (eventType === EVENT_TYPE_INITIAL_DOM || !e.time_since_load || e.time_since_load === '0');
+ return isInitialMutation;
+ });
+ return initialEvent;
+ },
+ timeFrameBuckets() {
+ const result = {};
+ this.recordingEvents.forEach(event => {
+ if (event === this.initialMutation) {
+ return;
+ }
+ const bucket = Math.round(intVal(event.time_since_load) / FRAME_STEP) * FRAME_STEP;
+ result[bucket] = result[bucket] || [];
+ result[bucket].push(event);
+ });
+ return result;
+ },
+ clues() {
+ const result = [];
+ this.recordingEvents.forEach(event => {
+ if (event === this.initialMutation) {
+ return;
+ }
+ const eventTypeId = getEventTypeId(event);
+ const eventType = EVENT_TYPE_TO_NAME[eventTypeId] || '';
+ const eventTitle = EVENT_TYPE_TO_TITLE[eventTypeId] || '';
+ if (eventType) {
+ if ((event.time_since_load === 0 || event.time_since_load === '0') && eventType === 'moveEvent') {
+ // this is the initial mouse position and we ignore it in the clues since we cannot
+ // draw a line to it
+ return;
+ }
+ result.push({
+ left: parseFloat(`${intVal(event.time_since_load) / intVal(this.recording.duration) * 100}`).toFixed(2),
+ type: eventType,
+ title: eventTitle
+ });
+ }
+ });
+ return result;
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/SessionRecordingVis/SessionRecordingVis.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/SessionRecordingVis/SessionRecordingVis.vue
+
+
+
+SessionRecordingVisvue_type_script_lang_ts.render = SessionRecordingVisvue_type_template_id_6f77b61e_render
+
+/* harmony default export */ var SessionRecordingVis = (SessionRecordingVisvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/HsrTargetTest/HsrTargetTest.vue?vue&type=template&id=6eb3a085
+
+const HsrTargetTestvue_type_template_id_6eb3a085_hoisted_1 = {
+ class: "form-group hsrTargetTest"
+};
+const HsrTargetTestvue_type_template_id_6eb3a085_hoisted_2 = {
+ class: "loadingPiwik loadingMatchingSteps"
+};
+const HsrTargetTestvue_type_template_id_6eb3a085_hoisted_3 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ src: "plugins/Morpheus/images/loading-blue.gif",
+ alt: ""
+}, null, -1);
+const HsrTargetTestvue_type_template_id_6eb3a085_hoisted_4 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", {
+ id: "hsrTargetValidationError"
+}, null, -1);
+function HsrTargetTestvue_type_template_id_6eb3a085_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", HsrTargetTestvue_type_template_id_6eb3a085_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("label", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("strong", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_TargetPageTestTitle')) + ":", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_TargetPageTestLabel')), 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ type: "text",
+ id: "urltargettest",
+ placeholder: "http://www.example.com/",
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => _ctx.url = $event),
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])({
+ 'invalid': _ctx.url && !_ctx.matches && _ctx.isValid
+ })
+ }, null, 2), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vModelText"], _ctx.url]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "testInfo"
+ }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_TargetPageTestErrorInvalidUrl')), 513), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.url && !_ctx.isValid]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "testInfo matches"
+ }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_TargetPageTestUrlMatches')), 513), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.url && _ctx.matches && _ctx.isValid]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "testInfo notMatches"
+ }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_TargetPageTestUrlNotMatches')), 513), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.url && !_ctx.matches && _ctx.isValid]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", HsrTargetTestvue_type_template_id_6eb3a085_hoisted_2, [HsrTargetTestvue_type_template_id_6eb3a085_hoisted_3, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_LoadingData')), 1)], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isLoadingTestMatchPage]])]), HsrTargetTestvue_type_template_id_6eb3a085_hoisted_4]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HsrTargetTest/HsrTargetTest.vue?vue&type=template&id=6eb3a085
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/HsrTargetTest/HsrTargetTest.vue?vue&type=script&lang=ts
+
+
+
+function isValidUrl(url) {
+ return url.indexOf('://') > 3;
+}
+/* harmony default export */ var HsrTargetTestvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ includedTargets: Array
+ },
+ data() {
+ return {
+ url: '',
+ matches: false,
+ isLoadingTestMatchPage: false
+ };
+ },
+ watch: {
+ isValid(newVal) {
+ if (!newVal) {
+ this.matches = false;
+ }
+ },
+ includedTargets() {
+ this.runTest();
+ },
+ url() {
+ this.runTest();
+ }
+ },
+ setup() {
+ return {
+ testUrlMatchPages: oneAtATime('HeatmapSessionRecording.testUrlMatchPages', {
+ errorElement: '#hsrTargetValidationError'
+ })
+ };
+ },
+ created() {
+ // we wait for 200ms before actually sending a request as user might be still typing
+ this.runTest = Object(external_CoreHome_["debounce"])(this.runTest, 200);
+ },
+ methods: {
+ checkIsMatchingUrl() {
+ if (!this.isValid) {
+ return;
+ }
+ const url = this.targetUrl;
+ const included = this.filteredIncludedTargets;
+ if (!(included !== null && included !== void 0 && included.length)) {
+ return;
+ }
+ this.isLoadingTestMatchPage = true;
+ this.testUrlMatchPages({
+ url
+ }, {
+ matchPageRules: included
+ }).then(response => {
+ var _this$filteredInclude;
+ if (!((_this$filteredInclude = this.filteredIncludedTargets) !== null && _this$filteredInclude !== void 0 && _this$filteredInclude.length) || (response === null || response === void 0 ? void 0 : response.url) !== this.targetUrl) {
+ return;
+ }
+ this.matches = response.matches;
+ }).finally(() => {
+ this.isLoadingTestMatchPage = false;
+ });
+ },
+ runTest() {
+ if (!this.isValid) {
+ return;
+ }
+ this.checkIsMatchingUrl();
+ }
+ },
+ computed: {
+ targetUrl() {
+ return (this.url || '').trim();
+ },
+ isValid() {
+ return this.targetUrl && isValidUrl(this.targetUrl);
+ },
+ filteredIncludedTargets() {
+ if (!this.includedTargets) {
+ return undefined;
+ }
+ return this.includedTargets.filter(target => (target === null || target === void 0 ? void 0 : target.value) || (target === null || target === void 0 ? void 0 : target.type) === 'any').map(target => Object.assign(Object.assign({}, target), {}, {
+ value: target.value ? target.value.trim() : ''
+ }));
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HsrTargetTest/HsrTargetTest.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HsrTargetTest/HsrTargetTest.vue
+
+
+
+HsrTargetTestvue_type_script_lang_ts.render = HsrTargetTestvue_type_template_id_6eb3a085_render
+
+/* harmony default export */ var HsrTargetTest = (HsrTargetTestvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/HsrUrlTarget/HsrUrlTarget.vue?vue&type=template&id=4c1d8b92
+
+const HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_1 = {
+ style: {
+ "width": "100%"
+ }
+};
+const HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_2 = {
+ name: "targetAttribute"
+};
+const HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_3 = {
+ name: "targetType"
+};
+const HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_4 = {
+ name: "targetValue"
+};
+const HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_5 = {
+ name: "targetValue2"
+};
+const HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_6 = ["title"];
+const HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_7 = ["title"];
+function HsrUrlTargetvue_type_template_id_4c1d8b92_render(_ctx, _cache, $props, $setup, $data, $options) {
+ const _component_Field = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Field");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", {
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(["form-group hsrUrltarget valign-wrapper", {
+ 'disabled': _ctx.disableIfNoValue && !_ctx.modelValue.value
+ }])
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_2, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "targetAttribute",
+ "model-value": _ctx.modelValue.attribute,
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => _ctx.$emit('update:modelValue', Object.assign(Object.assign({}, _ctx.modelValue), {}, {
+ attribute: $event
+ }))),
+ title: _ctx.translate('HeatmapSessionRecording_Rule'),
+ options: _ctx.targetAttributes,
+ "full-width": true
+ }, null, 8, ["model-value", "title", "options"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "targetType",
+ "model-value": _ctx.pattern_type,
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => {
+ _ctx.onTypeChange($event);
+ }),
+ options: _ctx.targetOptions[_ctx.modelValue.attribute],
+ "full-width": true
+ }, null, 8, ["model-value", "options"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_4, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "targetValue",
+ placeholder: `eg. ${_ctx.targetExamples[_ctx.modelValue.attribute]}`,
+ "model-value": _ctx.modelValue.value,
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = $event => _ctx.$emit('update:modelValue', Object.assign(Object.assign({}, _ctx.modelValue), {}, {
+ value: $event.trim()
+ }))),
+ maxlength: 500,
+ "full-width": true
+ }, null, 8, ["placeholder", "model-value"]), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.pattern_type !== 'any']])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_5, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "targetValue2",
+ "model-value": _ctx.modelValue.value2,
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = $event => _ctx.$emit('update:modelValue', Object.assign(Object.assign({}, _ctx.modelValue), {}, {
+ value2: $event.trim()
+ }))),
+ maxlength: 500,
+ "full-width": true,
+ placeholder: _ctx.translate('HeatmapSessionRecording_UrlParameterValueToMatchPlaceholder')
+ }, null, 8, ["model-value", "placeholder"]), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.modelValue.attribute === 'urlparam' && _ctx.pattern_type && _ctx.pattern_type !== 'exists' && _ctx.pattern_type !== 'not_exists']])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "icon-plus valign",
+ title: _ctx.translate('General_Add'),
+ onClick: _cache[4] || (_cache[4] = $event => _ctx.$emit('addUrl'))
+ }, null, 8, HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_6), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.showAddUrl]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "icon-minus valign",
+ title: _ctx.translate('General_Remove'),
+ onClick: _cache[5] || (_cache[5] = $event => _ctx.$emit('removeUrl'))
+ }, null, 8, HsrUrlTargetvue_type_template_id_4c1d8b92_hoisted_7), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.canBeRemoved]])], 2);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HsrUrlTarget/HsrUrlTarget.vue?vue&type=template&id=4c1d8b92
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HsrUrlTarget/AvailableTargetPageRules.store.ts
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+
+class AvailableTargetPageRules_store_AvailableTargetPageRulesStore {
+ constructor() {
+ _defineProperty(this, "privateState", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["reactive"])({
+ rules: []
+ }));
+ _defineProperty(this, "state", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(() => Object(external_commonjs_vue_commonjs2_vue_root_Vue_["readonly"])(this.privateState)));
+ _defineProperty(this, "rules", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(() => this.state.value.rules));
+ _defineProperty(this, "initPromise", null);
+ }
+ init() {
+ if (this.initPromise) {
+ return this.initPromise;
+ }
+ this.initPromise = external_CoreHome_["AjaxHelper"].fetch({
+ method: 'HeatmapSessionRecording.getAvailableTargetPageRules',
+ filter_limit: '-1'
+ }).then(response => {
+ this.privateState.rules = response;
+ return this.rules.value;
+ });
+ return this.initPromise;
+ }
+}
+/* harmony default export */ var AvailableTargetPageRules_store = (new AvailableTargetPageRules_store_AvailableTargetPageRulesStore());
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/HsrUrlTarget/HsrUrlTarget.vue?vue&type=script&lang=ts
+
+
+
+
+/* harmony default export */ var HsrUrlTargetvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ modelValue: {
+ type: Object,
+ required: true
+ },
+ canBeRemoved: Boolean,
+ disableIfNoValue: Boolean,
+ allowAny: Boolean,
+ showAddUrl: Boolean
+ },
+ components: {
+ Field: external_CorePluginsAdmin_["Field"]
+ },
+ emits: ['addUrl', 'removeUrl', 'update:modelValue'],
+ created() {
+ AvailableTargetPageRules_store.init();
+ },
+ watch: {
+ modelValue(newValue) {
+ if (!newValue.attribute) {
+ return;
+ }
+ const types = this.targetOptions[newValue.attribute];
+ const found = types.find(t => t.key === this.pattern_type);
+ if (!found && types[0]) {
+ this.onTypeChange(types[0].key);
+ }
+ }
+ },
+ computed: {
+ pattern_type() {
+ let result = this.modelValue.type;
+ if (this.modelValue.inverted && this.modelValue.inverted !== '0') {
+ result = `not_${this.modelValue.type}`;
+ }
+ return result;
+ },
+ targetAttributes() {
+ return AvailableTargetPageRules_store.rules.value.map(r => ({
+ key: r.value,
+ value: r.name
+ }));
+ },
+ targetOptions() {
+ const result = {};
+ AvailableTargetPageRules_store.rules.value.forEach(r => {
+ result[r.value] = [];
+ if (this.allowAny && r.value === 'url') {
+ result[r.value].push({
+ value: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_TargetTypeIsAny'),
+ key: 'any'
+ });
+ }
+ r.types.forEach(type => {
+ result[r.value].push({
+ value: type.name,
+ key: type.value
+ });
+ result[r.value].push({
+ value: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_TargetTypeIsNot', type.name),
+ key: `not_${type.value}`
+ });
+ });
+ });
+ return result;
+ },
+ targetExamples() {
+ const result = {};
+ AvailableTargetPageRules_store.rules.value.forEach(r => {
+ result[r.value] = r.example;
+ });
+ return result;
+ }
+ },
+ methods: {
+ onTypeChange(newType) {
+ let inverted = 0;
+ let type = newType;
+ if (newType.indexOf('not_') === 0) {
+ type = newType.substring('not_'.length);
+ inverted = 1;
+ }
+ this.$emit('update:modelValue', Object.assign(Object.assign({}, this.modelValue), {}, {
+ type,
+ inverted
+ }));
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HsrUrlTarget/HsrUrlTarget.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HsrUrlTarget/HsrUrlTarget.vue
+
+
+
+HsrUrlTargetvue_type_script_lang_ts.render = HsrUrlTargetvue_type_template_id_4c1d8b92_render
+
+/* harmony default export */ var HsrUrlTarget = (HsrUrlTargetvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Edit.vue?vue&type=template&id=635b8e28
+
+const Editvue_type_template_id_635b8e28_hoisted_1 = {
+ class: "loadingPiwik"
+};
+const Editvue_type_template_id_635b8e28_hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ src: "plugins/Morpheus/images/loading-blue.gif"
+}, null, -1);
+const Editvue_type_template_id_635b8e28_hoisted_3 = {
+ class: "loadingPiwik"
+};
+const Editvue_type_template_id_635b8e28_hoisted_4 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ src: "plugins/Morpheus/images/loading-blue.gif"
+}, null, -1);
+const Editvue_type_template_id_635b8e28_hoisted_5 = {
+ name: "name"
+};
+const Editvue_type_template_id_635b8e28_hoisted_6 = {
+ name: "sampleLimit"
+};
+const Editvue_type_template_id_635b8e28_hoisted_7 = {
+ class: "form-group row"
+};
+const Editvue_type_template_id_635b8e28_hoisted_8 = {
+ class: "col s12"
+};
+const Editvue_type_template_id_635b8e28_hoisted_9 = {
+ class: "col s12 m6",
+ style: {
+ "padding-left": "0"
+ }
+};
+const Editvue_type_template_id_635b8e28_hoisted_10 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("hr", null, null, -1);
+const Editvue_type_template_id_635b8e28_hoisted_11 = {
+ class: "col s12 m6"
+};
+const Editvue_type_template_id_635b8e28_hoisted_12 = {
+ class: "form-help"
+};
+const Editvue_type_template_id_635b8e28_hoisted_13 = {
+ class: "inline-help"
+};
+const Editvue_type_template_id_635b8e28_hoisted_14 = {
+ name: "sampleRate"
+};
+const Editvue_type_template_id_635b8e28_hoisted_15 = {
+ name: "excludedElements"
+};
+const Editvue_type_template_id_635b8e28_hoisted_16 = {
+ name: "screenshotUrl"
+};
+const Editvue_type_template_id_635b8e28_hoisted_17 = {
+ name: "breakpointMobile"
+};
+const Editvue_type_template_id_635b8e28_hoisted_18 = {
+ name: "breakpointTablet"
+};
+const Editvue_type_template_id_635b8e28_hoisted_19 = {
+ name: "trackManually"
+};
+const Editvue_type_template_id_635b8e28_hoisted_20 = ["innerHTML"];
+const Editvue_type_template_id_635b8e28_hoisted_21 = {
+ class: "entityCancel"
+};
+function Editvue_type_template_id_635b8e28_render(_ctx, _cache, $props, $setup, $data, $options) {
+ const _component_Field = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Field");
+ const _component_HsrUrlTarget = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("HsrUrlTarget");
+ const _component_HsrTargetTest = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("HsrTargetTest");
+ const _component_SaveButton = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("SaveButton");
+ const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, {
+ class: "editHsr",
+ "content-title": _ctx.contentTitle
+ }, {
+ default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Editvue_type_template_id_635b8e28_hoisted_1, [Editvue_type_template_id_635b8e28_hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_LoadingData')), 1)])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isLoading]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Editvue_type_template_id_635b8e28_hoisted_3, [Editvue_type_template_id_635b8e28_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_UpdatingData')), 1)])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isUpdating]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("form", {
+ onSubmit: _cache[12] || (_cache[12] = $event => _ctx.edit ? _ctx.updateHsr() : _ctx.createHsr())
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_5, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "name",
+ "model-value": _ctx.siteHsr.name,
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => {
+ _ctx.siteHsr.name = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('General_Name'),
+ maxlength: 50,
+ placeholder: _ctx.translate('HeatmapSessionRecording_FieldNamePlaceholder'),
+ "inline-help": _ctx.translate('HeatmapSessionRecording_HeatmapNameHelp')
+ }, null, 8, ["model-value", "title", "placeholder", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_6, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "sampleLimit",
+ "model-value": _ctx.siteHsr.sample_limit,
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => {
+ _ctx.siteHsr.sample_limit = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_HeatmapSampleLimit'),
+ options: _ctx.sampleLimits,
+ "inline-help": _ctx.translate('HeatmapSessionRecording_HeatmapSampleLimitHelp')
+ }, null, 8, ["model-value", "title", "options", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_8, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_TargetPage')) + ":", 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_9, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.siteHsr.match_page_rules, (url, index) => {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", {
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(`matchPageRules ${index} multiple`),
+ key: index
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_HsrUrlTarget, {
+ "model-value": url,
+ "onUpdate:modelValue": $event => _ctx.setMatchPageRule($event, index),
+ onAddUrl: _cache[2] || (_cache[2] = $event => _ctx.addMatchPageRule()),
+ onRemoveUrl: $event => _ctx.removeMatchPageRule(index),
+ onAnyChange: _cache[3] || (_cache[3] = $event => _ctx.setValueHasChanged()),
+ "allow-any": false,
+ "disable-if-no-value": index > 0,
+ "can-be-removed": index > 0,
+ "show-add-url": true
+ }, null, 8, ["model-value", "onUpdate:modelValue", "onRemoveUrl", "disable-if-no-value", "can-be-removed"])]), Editvue_type_template_id_635b8e28_hoisted_10], 2);
+ }), 128))]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_11, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_12, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Editvue_type_template_id_635b8e28_hoisted_13, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_FieldIncludedTargetsHelp')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_HsrTargetTest, {
+ "included-targets": _ctx.siteHsr.match_page_rules
+ }, null, 8, ["included-targets"])])])])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_14, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "sampleRate",
+ "model-value": _ctx.siteHsr.sample_rate,
+ "onUpdate:modelValue": _cache[4] || (_cache[4] = $event => {
+ _ctx.siteHsr.sample_rate = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_SampleRate'),
+ options: _ctx.sampleRates,
+ introduction: _ctx.translate('HeatmapSessionRecording_AdvancedOptions'),
+ "inline-help": _ctx.translate('HeatmapSessionRecording_HeatmapSampleRateHelp')
+ }, null, 8, ["model-value", "title", "options", "introduction", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_15, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "excludedElements",
+ "model-value": _ctx.siteHsr.excluded_elements,
+ "onUpdate:modelValue": _cache[5] || (_cache[5] = $event => {
+ _ctx.siteHsr.excluded_elements = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_ExcludedElements'),
+ maxlength: 1000,
+ "inline-help": _ctx.translate('HeatmapSessionRecording_ExcludedElementsHelp')
+ }, null, 8, ["model-value", "title", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_16, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "screenshotUrl",
+ "model-value": _ctx.siteHsr.screenshot_url,
+ "onUpdate:modelValue": _cache[6] || (_cache[6] = $event => {
+ _ctx.siteHsr.screenshot_url = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_ScreenshotUrl'),
+ maxlength: 300,
+ disabled: !!_ctx.siteHsr.page_treemirror,
+ "inline-help": _ctx.translate('HeatmapSessionRecording_ScreenshotUrlHelp')
+ }, null, 8, ["model-value", "title", "disabled", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_17, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "breakpointMobile",
+ "model-value": _ctx.siteHsr.breakpoint_mobile,
+ "onUpdate:modelValue": _cache[7] || (_cache[7] = $event => {
+ _ctx.siteHsr.breakpoint_mobile = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_BreakpointX', _ctx.translate('General_Mobile')),
+ maxlength: 4,
+ "inline-help": _ctx.breakpointMobileInlineHelp
+ }, null, 8, ["model-value", "title", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_18, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "breakpointTablet",
+ "model-value": _ctx.siteHsr.breakpoint_tablet,
+ "onUpdate:modelValue": _cache[8] || (_cache[8] = $event => {
+ _ctx.siteHsr.breakpoint_tablet = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_BreakpointX', _ctx.translate('DevicesDetection_Tablet')),
+ maxlength: 4,
+ "inline-help": _ctx.breakpointGeneralHelp
+ }, null, 8, ["model-value", "title", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_19, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "checkbox",
+ name: "capture_manually",
+ title: _ctx.translate('HeatmapSessionRecording_CaptureDomTitle'),
+ "inline-help": _ctx.captureDomInlineHelp,
+ "model-value": _ctx.siteHsr.capture_manually,
+ "onUpdate:modelValue": _cache[9] || (_cache[9] = $event => {
+ _ctx.siteHsr.capture_manually = $event;
+ _ctx.setValueHasChanged();
+ })
+ }, null, 8, ["title", "inline-help", "model-value"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", {
+ innerHTML: _ctx.$sanitize(_ctx.personalInformationNote)
+ }, null, 8, Editvue_type_template_id_635b8e28_hoisted_20), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_SaveButton, {
+ class: "createButton",
+ onConfirm: _cache[10] || (_cache[10] = $event => _ctx.edit ? _ctx.updateHsr() : _ctx.createHsr()),
+ disabled: _ctx.isUpdating || !_ctx.isDirty,
+ saving: _ctx.isUpdating,
+ value: _ctx.saveButtonText
+ }, null, 8, ["disabled", "saving", "value"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_635b8e28_hoisted_21, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ onClick: _cache[11] || (_cache[11] = $event => _ctx.cancel())
+ }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Cancel')), 1)])])], 32)]),
+ _: 1
+ }, 8, ["content-title"]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Edit.vue?vue&type=template&id=635b8e28
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HsrStore/HsrStore.store.ts
+function HsrStore_store_defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+
+class HsrStore_store_HsrStore {
+ constructor(context) {
+ HsrStore_store_defineProperty(this, "context", void 0);
+ HsrStore_store_defineProperty(this, "privateState", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["reactive"])({
+ allHsrs: [],
+ isLoading: false,
+ isUpdating: false,
+ filterStatus: ''
+ }));
+ HsrStore_store_defineProperty(this, "state", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(() => Object(external_commonjs_vue_commonjs2_vue_root_Vue_["readonly"])(this.privateState)));
+ HsrStore_store_defineProperty(this, "hsrs", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(() => {
+ if (!this.privateState.filterStatus) {
+ return this.state.value.allHsrs;
+ }
+ return this.state.value.allHsrs.filter(hsr => hsr.status === this.privateState.filterStatus);
+ }));
+ // used just for the adapter
+ HsrStore_store_defineProperty(this, "hsrsCloned", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["computed"])(() => Object(external_CoreHome_["clone"])(this.hsrs.value)));
+ HsrStore_store_defineProperty(this, "statusOptions", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["readonly"])([{
+ key: '',
+ value: Object(external_CoreHome_["translate"])('General_All')
+ }, {
+ key: 'active',
+ value: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_StatusActive')
+ }, {
+ key: 'ended',
+ value: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_StatusEnded')
+ }, {
+ key: 'paused',
+ value: Object(external_CoreHome_["translate"])('HeatmapSessionRecording_StatusPaused')
+ }]));
+ HsrStore_store_defineProperty(this, "fetchPromises", {});
+ this.context = context;
+ }
+ setFilterStatus(status) {
+ this.privateState.filterStatus = status;
+ }
+ reload() {
+ this.privateState.allHsrs = [];
+ this.fetchPromises = {};
+ return this.fetchHsrs();
+ }
+ filterRules(rules) {
+ return rules.filter(target => !!target && (target.value || target.type === 'any'));
+ }
+ getApiMethodInContext(apiMethod) {
+ return `${apiMethod}${this.context}`;
+ }
+ fetchHsrs() {
+ let method = 'HeatmapSessionRecording.getHeatmaps';
+ if (this.context === 'SessionRecording') {
+ method = 'HeatmapSessionRecording.getSessionRecordings';
+ }
+ const params = {
+ method,
+ filter_limit: '-1'
+ };
+ if (!this.fetchPromises[method]) {
+ this.fetchPromises[method] = external_CoreHome_["AjaxHelper"].fetch(params);
+ }
+ this.privateState.isLoading = true;
+ this.privateState.allHsrs = [];
+ return this.fetchPromises[method].then(hsrs => {
+ this.privateState.allHsrs = hsrs;
+ return this.state.value.allHsrs;
+ }).finally(() => {
+ this.privateState.isLoading = false;
+ });
+ }
+ findHsr(idSiteHsr) {
+ // before going through an API request we first try to find it in loaded hsrs
+ const found = this.state.value.allHsrs.find(hsr => hsr.idsitehsr === idSiteHsr);
+ if (found) {
+ return Promise.resolve(found);
+ }
+ // otherwise we fetch it via API
+ this.privateState.isLoading = true;
+ return external_CoreHome_["AjaxHelper"].fetch({
+ idSiteHsr,
+ method: this.getApiMethodInContext('HeatmapSessionRecording.get'),
+ filter_limit: '-1'
+ }).finally(() => {
+ this.privateState.isLoading = false;
+ });
+ }
+ deleteHsr(idSiteHsr) {
+ this.privateState.isUpdating = true;
+ this.privateState.allHsrs = [];
+ return external_CoreHome_["AjaxHelper"].fetch({
+ idSiteHsr,
+ method: this.getApiMethodInContext('HeatmapSessionRecording.delete')
+ }, {
+ withTokenInUrl: true
+ }).then(() => ({
+ type: 'success'
+ })).catch(error => ({
+ type: 'error',
+ message: error.message || error
+ })).finally(() => {
+ this.privateState.isUpdating = false;
+ });
+ }
+ completeHsr(idSiteHsr) {
+ this.privateState.isUpdating = true;
+ this.privateState.allHsrs = [];
+ return external_CoreHome_["AjaxHelper"].fetch({
+ idSiteHsr,
+ method: this.getApiMethodInContext('HeatmapSessionRecording.end')
+ }, {
+ withTokenInUrl: true
+ }).then(() => ({
+ type: 'success'
+ })).catch(error => ({
+ type: 'error',
+ message: error.message || error
+ })).finally(() => {
+ this.privateState.isUpdating = false;
+ });
+ }
+ createOrUpdateHsr(hsr, method) {
+ const params = {
+ idSiteHsr: hsr.idsitehsr,
+ sampleLimit: hsr.sample_limit,
+ sampleRate: hsr.sample_rate,
+ excludedElements: hsr.excluded_elements ? hsr.excluded_elements.trim() : undefined,
+ screenshotUrl: hsr.screenshot_url ? hsr.screenshot_url.trim() : undefined,
+ breakpointMobile: hsr.breakpoint_mobile,
+ breakpointTablet: hsr.breakpoint_tablet,
+ minSessionTime: hsr.min_session_time,
+ requiresActivity: hsr.requires_activity ? 1 : 0,
+ captureKeystrokes: hsr.capture_keystrokes ? 1 : 0,
+ captureDomManually: hsr.capture_manually ? 1 : 0,
+ method,
+ name: hsr.name.trim()
+ };
+ const postParams = {
+ matchPageRules: this.filterRules(hsr.match_page_rules)
+ };
+ this.privateState.isUpdating = true;
+ return external_CoreHome_["AjaxHelper"].post(params, postParams, {
+ withTokenInUrl: true
+ }).then(response => ({
+ type: 'success',
+ response
+ })).catch(error => ({
+ type: 'error',
+ message: error.message || error
+ })).finally(() => {
+ this.privateState.isUpdating = false;
+ });
+ }
+}
+const HeatmapStore = new HsrStore_store_HsrStore('Heatmap');
+const SessionRecordingStore = new HsrStore_store_HsrStore('SessionRecording');
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Edit.vue?vue&type=script&lang=ts
+
+
+
+
+
+
+const notificationId = 'hsrmanagement';
+/* harmony default export */ var Editvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ idSiteHsr: Number,
+ breakpointMobile: Number,
+ breakpointTablet: Number
+ },
+ components: {
+ ContentBlock: external_CoreHome_["ContentBlock"],
+ Field: external_CorePluginsAdmin_["Field"],
+ HsrUrlTarget: HsrUrlTarget,
+ HsrTargetTest: HsrTargetTest,
+ SaveButton: external_CorePluginsAdmin_["SaveButton"]
+ },
+ data() {
+ return {
+ isDirty: false,
+ showAdvancedView: false,
+ siteHsr: {}
+ };
+ },
+ created() {
+ this.init();
+ },
+ watch: {
+ idSiteHsr(newValue) {
+ if (newValue === null) {
+ return;
+ }
+ this.init();
+ }
+ },
+ methods: {
+ removeAnyHsrNotification() {
+ external_CoreHome_["NotificationsStore"].remove(notificationId);
+ external_CoreHome_["NotificationsStore"].remove('ajaxHelper');
+ },
+ showNotification(message, context) {
+ const instanceId = external_CoreHome_["NotificationsStore"].show({
+ message,
+ context,
+ id: notificationId,
+ type: 'transient'
+ });
+ setTimeout(() => {
+ external_CoreHome_["NotificationsStore"].scrollToNotification(instanceId);
+ }, 200);
+ },
+ showErrorFieldNotProvidedNotification(title) {
+ const message = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ErrorXNotProvided', [title]);
+ this.showNotification(message, 'error');
+ },
+ init() {
+ const {
+ idSiteHsr
+ } = this;
+ this.siteHsr = {};
+ this.showAdvancedView = false;
+ external_CoreHome_["Matomo"].helper.lazyScrollToContent();
+ if (this.edit && idSiteHsr) {
+ HeatmapStore.findHsr(idSiteHsr).then(siteHsr => {
+ if (!siteHsr) {
+ return;
+ }
+ this.siteHsr = Object(external_CoreHome_["clone"])(siteHsr);
+ this.siteHsr.sample_rate = `${this.siteHsr.sample_rate}`;
+ this.addInitialMatchPageRule();
+ this.isDirty = false;
+ });
+ return;
+ }
+ if (this.create) {
+ this.siteHsr = {
+ idSite: external_CoreHome_["Matomo"].idSite,
+ name: '',
+ sample_rate: '10.0',
+ sample_limit: 1000,
+ breakpoint_mobile: this.breakpointMobile,
+ breakpoint_tablet: this.breakpointTablet,
+ capture_manually: 0
+ };
+ this.isDirty = false;
+ const hashParams = external_CoreHome_["MatomoUrl"].hashParsed.value;
+ if (hashParams.name) {
+ this.siteHsr.name = hashParams.name;
+ this.isDirty = true;
+ }
+ if (hashParams.matchPageRules) {
+ try {
+ this.siteHsr.match_page_rules = JSON.parse(hashParams.matchPageRules);
+ this.isDirty = true;
+ } catch (e) {
+ console.log('warning: could not parse matchPageRules query param, expected JSON');
+ }
+ } else {
+ this.addInitialMatchPageRule();
+ }
+ }
+ },
+ addInitialMatchPageRule() {
+ var _this$siteHsr$match_p;
+ if (!this.siteHsr) {
+ return;
+ }
+ if ((_this$siteHsr$match_p = this.siteHsr.match_page_rules) !== null && _this$siteHsr$match_p !== void 0 && _this$siteHsr$match_p.length) {
+ return;
+ }
+ this.addMatchPageRule();
+ },
+ addMatchPageRule() {
+ var _this$siteHsr$match_p2;
+ if (!this.siteHsr) {
+ return;
+ }
+ if (!((_this$siteHsr$match_p2 = this.siteHsr.match_page_rules) !== null && _this$siteHsr$match_p2 !== void 0 && _this$siteHsr$match_p2.length)) {
+ this.siteHsr.match_page_rules = [];
+ }
+ this.siteHsr.match_page_rules.push({
+ attribute: 'url',
+ type: 'equals_simple',
+ value: '',
+ inverted: 0
+ });
+ this.isDirty = true;
+ },
+ removeMatchPageRule(index) {
+ if (this.siteHsr && index > -1) {
+ this.siteHsr.match_page_rules = [...this.siteHsr.match_page_rules];
+ this.siteHsr.match_page_rules.splice(index, 1);
+ this.isDirty = true;
+ }
+ },
+ cancel() {
+ const newParams = Object.assign({}, external_CoreHome_["MatomoUrl"].hashParsed.value);
+ delete newParams.idSiteHsr;
+ external_CoreHome_["MatomoUrl"].updateHash(newParams);
+ },
+ createHsr() {
+ this.removeAnyHsrNotification();
+ if (!this.checkRequiredFieldsAreSet()) {
+ return;
+ }
+ HeatmapStore.createOrUpdateHsr(this.siteHsr, 'HeatmapSessionRecording.addHeatmap').then(response => {
+ if (!response || response.type === 'error' || !response.response) {
+ return;
+ }
+ this.isDirty = false;
+ const idSiteHsr = response.response.value;
+ HeatmapStore.reload().then(() => {
+ if (external_CoreHome_["Matomo"].helper.isReportingPage()) {
+ external_CoreHome_["Matomo"].postEvent('updateReportingMenu');
+ }
+ external_CoreHome_["MatomoUrl"].updateHash(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].hashParsed.value), {}, {
+ idSiteHsr
+ }));
+ setTimeout(() => {
+ this.showNotification(Object(external_CoreHome_["translate"])('HeatmapSessionRecording_HeatmapCreated'), response.type);
+ }, 200);
+ });
+ });
+ },
+ setValueHasChanged() {
+ this.isDirty = true;
+ },
+ updateHsr() {
+ this.removeAnyHsrNotification();
+ if (!this.checkRequiredFieldsAreSet()) {
+ return;
+ }
+ HeatmapStore.createOrUpdateHsr(this.siteHsr, 'HeatmapSessionRecording.updateHeatmap').then(response => {
+ if (response.type === 'error') {
+ return;
+ }
+ this.isDirty = false;
+ this.siteHsr = {};
+ HeatmapStore.reload().then(() => {
+ this.init();
+ });
+ this.showNotification(Object(external_CoreHome_["translate"])('HeatmapSessionRecording_HeatmapUpdated'), response.type);
+ });
+ },
+ checkRequiredFieldsAreSet() {
+ var _this$siteHsr$match_p3;
+ if (!this.siteHsr.name) {
+ const title = Object(external_CoreHome_["translate"])('General_Name');
+ this.showErrorFieldNotProvidedNotification(title);
+ return false;
+ }
+ if (!((_this$siteHsr$match_p3 = this.siteHsr.match_page_rules) !== null && _this$siteHsr$match_p3 !== void 0 && _this$siteHsr$match_p3.length) || !HeatmapStore.filterRules(this.siteHsr.match_page_rules).length) {
+ const title = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ErrorPageRuleRequired');
+ this.showNotification(title, 'error');
+ return false;
+ }
+ return true;
+ },
+ setMatchPageRule(rule, index) {
+ this.siteHsr.match_page_rules = [...this.siteHsr.match_page_rules];
+ this.siteHsr.match_page_rules[index] = rule;
+ }
+ },
+ computed: {
+ sampleLimits() {
+ return [1000, 2000, 5000].map(v => ({
+ key: `${v}`,
+ value: v
+ }));
+ },
+ sampleRates() {
+ const values = [0.1, 0.5, 1, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100];
+ return values.map(v => ({
+ key: v.toFixed(1),
+ value: `${v}%`
+ }));
+ },
+ create() {
+ return !this.idSiteHsr;
+ },
+ edit() {
+ return !this.create;
+ },
+ editTitle() {
+ const token = this.create ? 'HeatmapSessionRecording_CreateNewHeatmap' : 'HeatmapSessionRecording_EditHeatmapX';
+ return token;
+ },
+ contentTitle() {
+ return Object(external_CoreHome_["translate"])(this.editTitle, this.siteHsr.name ? `"${this.siteHsr.name}"` : '');
+ },
+ isLoading() {
+ return HeatmapStore.state.value.isLoading;
+ },
+ isUpdating() {
+ return HeatmapStore.state.value.isUpdating;
+ },
+ breakpointMobileInlineHelp() {
+ const help1 = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_BreakpointGeneralHelp');
+ const help2 = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_BreakpointGeneralHelpManage');
+ return `${help1} ${help2}`;
+ },
+ breakpointGeneralHelp() {
+ const help1 = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_BreakpointGeneralHelp');
+ const help2 = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_BreakpointGeneralHelpManage');
+ return `${help1} ${help2}`;
+ },
+ captureDomInlineHelp() {
+ const id = this.idSiteHsr ? this.idSiteHsr : '{idHeatmap}';
+ const command = `_paq.push(['HeatmapSessionRecording::captureInitialDom', ${id}]) `;
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_CaptureDomInlineHelp', command, '', ' ');
+ },
+ personalInformationNote() {
+ const url = 'https://developer.matomo.org/guides/heatmap-session-recording/setup#masking-content-on-your-website';
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_PersonalInformationNote', Object(external_CoreHome_["translate"])('HeatmapSessionRecording_Heatmap'), '', '
', ``, ' ');
+ },
+ saveButtonText() {
+ return this.edit ? Object(external_CoreHome_["translate"])('CoreUpdater_UpdateTitle') : Object(external_CoreHome_["translate"])('HeatmapSessionRecording_CreateNewHeatmap');
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Edit.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Edit.vue
+
+
+
+Editvue_type_script_lang_ts.render = Editvue_type_template_id_635b8e28_render
+
+/* harmony default export */ var Edit = (Editvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/List.vue?vue&type=template&id=669edce3
+
+const Listvue_type_template_id_669edce3_hoisted_1 = {
+ class: "heatmapList"
+};
+const Listvue_type_template_id_669edce3_hoisted_2 = {
+ class: "filterStatus"
+};
+const Listvue_type_template_id_669edce3_hoisted_3 = {
+ class: "hsrSearchFilter",
+ style: {
+ "margin-left": "3.5px"
+ }
+};
+const Listvue_type_template_id_669edce3_hoisted_4 = {
+ class: "index"
+};
+const Listvue_type_template_id_669edce3_hoisted_5 = {
+ class: "name"
+};
+const Listvue_type_template_id_669edce3_hoisted_6 = {
+ class: "creationDate"
+};
+const Listvue_type_template_id_669edce3_hoisted_7 = {
+ class: "sampleLimit"
+};
+const Listvue_type_template_id_669edce3_hoisted_8 = {
+ class: "status"
+};
+const Listvue_type_template_id_669edce3_hoisted_9 = {
+ class: "action"
+};
+const Listvue_type_template_id_669edce3_hoisted_10 = {
+ colspan: "7"
+};
+const Listvue_type_template_id_669edce3_hoisted_11 = {
+ class: "loadingPiwik"
+};
+const Listvue_type_template_id_669edce3_hoisted_12 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ src: "plugins/Morpheus/images/loading-blue.gif"
+}, null, -1);
+const Listvue_type_template_id_669edce3_hoisted_13 = {
+ colspan: "7"
+};
+const Listvue_type_template_id_669edce3_hoisted_14 = ["id"];
+const Listvue_type_template_id_669edce3_hoisted_15 = {
+ class: "index"
+};
+const Listvue_type_template_id_669edce3_hoisted_16 = {
+ class: "name"
+};
+const Listvue_type_template_id_669edce3_hoisted_17 = {
+ class: "creationDate"
+};
+const Listvue_type_template_id_669edce3_hoisted_18 = {
+ class: "sampleLimit"
+};
+const Listvue_type_template_id_669edce3_hoisted_19 = {
+ key: 0,
+ class: "status status-paused"
+};
+const Listvue_type_template_id_669edce3_hoisted_20 = ["title"];
+const Listvue_type_template_id_669edce3_hoisted_21 = {
+ key: 1,
+ class: "status"
+};
+const Listvue_type_template_id_669edce3_hoisted_22 = {
+ class: "action"
+};
+const Listvue_type_template_id_669edce3_hoisted_23 = ["title", "onClick"];
+const Listvue_type_template_id_669edce3_hoisted_24 = ["title", "onClick"];
+const Listvue_type_template_id_669edce3_hoisted_25 = ["title", "href"];
+const Listvue_type_template_id_669edce3_hoisted_26 = ["title", "onClick"];
+const Listvue_type_template_id_669edce3_hoisted_27 = {
+ class: "tableActionBar"
+};
+const Listvue_type_template_id_669edce3_hoisted_28 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "icon-add"
+}, null, -1);
+const Listvue_type_template_id_669edce3_hoisted_29 = {
+ class: "ui-confirm",
+ id: "confirmDeleteHeatmap",
+ ref: "confirmDeleteHeatmap"
+};
+const Listvue_type_template_id_669edce3_hoisted_30 = ["value"];
+const Listvue_type_template_id_669edce3_hoisted_31 = ["value"];
+const Listvue_type_template_id_669edce3_hoisted_32 = {
+ class: "ui-confirm",
+ id: "confirmEndHeatmap",
+ ref: "confirmEndHeatmap"
+};
+const Listvue_type_template_id_669edce3_hoisted_33 = ["value"];
+const Listvue_type_template_id_669edce3_hoisted_34 = ["value"];
+function Listvue_type_template_id_669edce3_render(_ctx, _cache, $props, $setup, $data, $options) {
+ const _component_Field = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Field");
+ const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock");
+ const _directive_content_table = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("content-table");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", Listvue_type_template_id_669edce3_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ContentBlock, {
+ "content-title": _ctx.translate('HeatmapSessionRecording_ManageHeatmaps')
+ }, {
+ default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_HeatmapUsageBenefits')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_669edce3_hoisted_2, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "filterStatus",
+ "model-value": _ctx.filterStatus,
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => {
+ _ctx.setFilterStatus($event);
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_Filter'),
+ "full-width": true,
+ options: _ctx.statusOptions
+ }, null, 8, ["model-value", "title", "options"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_669edce3_hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "hsrSearch",
+ title: _ctx.translate('General_Search'),
+ modelValue: _ctx.searchFilter,
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => _ctx.searchFilter = $event),
+ "full-width": true
+ }, null, 8, ["title", "modelValue"]), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.hsrs.length > 0]])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("table", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("thead", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_669edce3_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Id')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_669edce3_hoisted_5, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Name')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_669edce3_hoisted_6, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_CreationDate')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_669edce3_hoisted_7, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_SampleLimit')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_669edce3_hoisted_8, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('CorePluginsAdmin_Status')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_669edce3_hoisted_9, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Actions')), 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tbody", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_669edce3_hoisted_10, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Listvue_type_template_id_669edce3_hoisted_11, [Listvue_type_template_id_669edce3_hoisted_12, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_LoadingData')), 1)])])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isLoading || _ctx.isUpdating]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_669edce3_hoisted_13, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_NoHeatmapsFound')), 1)], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], !_ctx.isLoading && _ctx.hsrs.length === 0]]), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.sortedHsrs, hsr => {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("tr", {
+ id: `hsr${hsr.idsitehsr}`,
+ class: "hsrs",
+ key: hsr.idsitehsr
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_669edce3_hoisted_15, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(hsr.idsitehsr), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_669edce3_hoisted_16, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(hsr.name), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_669edce3_hoisted_17, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(hsr.created_date_pretty), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_669edce3_hoisted_18, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(hsr.sample_limit), 1), hsr.status === 'paused' ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("td", Listvue_type_template_id_669edce3_hoisted_19, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.ucfirst(hsr.status)) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "icon icon-help",
+ title: _ctx.pauseReason
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_20)])) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("td", Listvue_type_template_id_669edce3_hoisted_21, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.ucfirst(hsr.status)), 1)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_669edce3_hoisted_22, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ class: "table-action icon-edit",
+ title: _ctx.translate('HeatmapSessionRecording_EditX', _ctx.translate('HeatmapSessionRecording_Heatmap')),
+ onClick: $event => _ctx.editHsr(hsr.idsitehsr)
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_23), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ a: "",
+ class: "table-action stopRecording icon-drop-crossed",
+ title: _ctx.translate('HeatmapSessionRecording_StopX', _ctx.translate('HeatmapSessionRecording_Heatmap')),
+ onClick: $event => _ctx.completeHsr(hsr)
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_24), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], hsr.status !== 'ended']]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ target: "_blank",
+ class: "table-action icon-show",
+ title: _ctx.translate('HeatmapSessionRecording_ViewReport'),
+ href: _ctx.getViewReportLink(hsr)
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_25), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ class: "table-action icon-delete",
+ title: _ctx.translate('HeatmapSessionRecording_DeleteX', _ctx.translate('HeatmapSessionRecording_Heatmap')),
+ onClick: $event => _ctx.deleteHsr(hsr)
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_26)])], 8, Listvue_type_template_id_669edce3_hoisted_14);
+ }), 128))])])), [[_directive_content_table]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_669edce3_hoisted_27, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ class: "createNewHsr",
+ value: "",
+ onClick: _cache[2] || (_cache[2] = $event => _ctx.createHsr())
+ }, [Listvue_type_template_id_669edce3_hoisted_28, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_CreateNewHeatmap')), 1)])])]),
+ _: 1
+ }, 8, ["content-title"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_669edce3_hoisted_29, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_DeleteHeatmapConfirm')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "yes",
+ type: "button",
+ value: _ctx.translate('General_Yes')
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_30), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "no",
+ type: "button",
+ value: _ctx.translate('General_No')
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_31)], 512), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_669edce3_hoisted_32, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_EndHeatmapConfirm')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "yes",
+ type: "button",
+ value: _ctx.translate('General_Yes')
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_33), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "no",
+ type: "button",
+ value: _ctx.translate('General_No')
+ }, null, 8, Listvue_type_template_id_669edce3_hoisted_34)], 512)]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/List.vue?vue&type=template&id=669edce3
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/List.vue?vue&type=script&lang=ts
+
+
+
+
+/* harmony default export */ var Listvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ pauseReason: String
+ },
+ components: {
+ ContentBlock: external_CoreHome_["ContentBlock"],
+ Field: external_CorePluginsAdmin_["Field"]
+ },
+ directives: {
+ ContentTable: external_CoreHome_["ContentTable"]
+ },
+ data() {
+ return {
+ searchFilter: ''
+ };
+ },
+ created() {
+ HeatmapStore.setFilterStatus('');
+ HeatmapStore.fetchHsrs();
+ },
+ methods: {
+ createHsr() {
+ this.editHsr(0);
+ },
+ editHsr(idSiteHsr) {
+ external_CoreHome_["MatomoUrl"].updateHash(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].hashParsed.value), {}, {
+ idSiteHsr
+ }));
+ },
+ deleteHsr(hsr) {
+ external_CoreHome_["Matomo"].helper.modalConfirm(this.$refs.confirmDeleteHeatmap, {
+ yes: () => {
+ HeatmapStore.deleteHsr(hsr.idsitehsr).then(() => {
+ HeatmapStore.reload();
+ external_CoreHome_["Matomo"].postEvent('updateReportingMenu');
+ });
+ }
+ });
+ },
+ completeHsr(hsr) {
+ external_CoreHome_["Matomo"].helper.modalConfirm(this.$refs.confirmEndHeatmap, {
+ yes: () => {
+ HeatmapStore.completeHsr(hsr.idsitehsr).then(() => {
+ HeatmapStore.reload();
+ });
+ }
+ });
+ },
+ setFilterStatus(filter) {
+ HeatmapStore.setFilterStatus(filter);
+ },
+ ucfirst(s) {
+ return `${s[0].toUpperCase()}${s.substr(1)}`;
+ },
+ getViewReportLink(hsr) {
+ return `?${external_CoreHome_["MatomoUrl"].stringify({
+ module: 'Widgetize',
+ action: 'iframe',
+ moduleToWidgetize: 'HeatmapSessionRecording',
+ actionToWidgetize: 'showHeatmap',
+ idSiteHsr: hsr.idsitehsr,
+ idSite: hsr.idsite,
+ period: 'day',
+ date: 'yesterday'
+ })}`;
+ }
+ },
+ computed: {
+ filterStatus() {
+ return HeatmapStore.state.value.filterStatus;
+ },
+ statusOptions() {
+ return HeatmapStore.statusOptions;
+ },
+ hsrs() {
+ return HeatmapStore.hsrs.value;
+ },
+ isLoading() {
+ return HeatmapStore.state.value.isLoading;
+ },
+ isUpdating() {
+ return HeatmapStore.state.value.isUpdating;
+ },
+ sortedHsrs() {
+ // look through string properties of heatmaps for values that have searchFilter in them
+ // (mimics angularjs filter() filter)
+ const result = [...this.hsrs].filter(h => Object.keys(h).some(propName => {
+ const entity = h;
+ return typeof entity[propName] === 'string' && entity[propName].indexOf(this.searchFilter) !== -1;
+ }));
+ result.sort((lhs, rhs) => rhs.idsitehsr - lhs.idsitehsr);
+ return result;
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/List.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/List.vue
+
+
+
+Listvue_type_script_lang_ts.render = Listvue_type_template_id_669edce3_render
+
+/* harmony default export */ var List = (Listvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Manage.vue?vue&type=template&id=56c7eaa3
+
+const Managevue_type_template_id_56c7eaa3_hoisted_1 = {
+ class: "manageHsr",
+ ref: "root"
+};
+const Managevue_type_template_id_56c7eaa3_hoisted_2 = {
+ key: 0
+};
+const Managevue_type_template_id_56c7eaa3_hoisted_3 = {
+ key: 1
+};
+function Managevue_type_template_id_56c7eaa3_render(_ctx, _cache, $props, $setup, $data, $options) {
+ const _component_MatomoJsNotWritableAlert = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("MatomoJsNotWritableAlert");
+ const _component_HeatmapList = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("HeatmapList");
+ const _component_HeatmapEdit = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("HeatmapEdit");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, [!_ctx.editMode ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_MatomoJsNotWritableAlert, {
+ key: 0,
+ "is-matomo-js-writable": _ctx.isMatomoJsWritable,
+ "recording-type": _ctx.translate('HeatmapSessionRecording_Heatmaps')
+ }, null, 8, ["is-matomo-js-writable", "recording-type"])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Managevue_type_template_id_56c7eaa3_hoisted_1, [!_ctx.editMode ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", Managevue_type_template_id_56c7eaa3_hoisted_2, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_HeatmapList, {
+ "pause-reason": _ctx.pauseReason
+ }, null, 8, ["pause-reason"])])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.editMode ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", Managevue_type_template_id_56c7eaa3_hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_HeatmapEdit, {
+ "breakpoint-mobile": _ctx.breakpointMobile,
+ "breakpoint-tablet": _ctx.breakpointTablet,
+ "id-site-hsr": _ctx.idSiteHsr
+ }, null, 8, ["breakpoint-mobile", "breakpoint-tablet", "id-site-hsr"])])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)], 512)], 64);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Manage.vue?vue&type=template&id=56c7eaa3
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue?vue&type=template&id=3eefb154
+
+const MatomoJsNotWritableAlertvue_type_template_id_3eefb154_hoisted_1 = ["innerHTML"];
+function MatomoJsNotWritableAlertvue_type_template_id_3eefb154_render(_ctx, _cache, $props, $setup, $data, $options) {
+ return !_ctx.isMatomoJsWritable ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", {
+ key: 0,
+ class: "alert alert-warning",
+ innerHTML: _ctx.getJsNotWritableErrorMessage()
+ }, null, 8, MatomoJsNotWritableAlertvue_type_template_id_3eefb154_hoisted_1)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue?vue&type=template&id=3eefb154
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue?vue&type=script&lang=ts
+
+
+/* harmony default export */ var MatomoJsNotWritableAlertvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ recordingType: {
+ type: String,
+ required: true
+ },
+ isMatomoJsWritable: {
+ type: Boolean,
+ required: true
+ }
+ },
+ methods: {
+ getJsNotWritableErrorMessage() {
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_MatomoJSNotWritableErrorMessage', this.recordingType, '', ' ');
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue
+
+
+
+MatomoJsNotWritableAlertvue_type_script_lang_ts.render = MatomoJsNotWritableAlertvue_type_template_id_3eefb154_render
+
+/* harmony default export */ var MatomoJsNotWritableAlert = (MatomoJsNotWritableAlertvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Manage.vue?vue&type=script&lang=ts
+
+
+
+
+
+const {
+ $: Managevue_type_script_lang_ts_$
+} = window;
+/* harmony default export */ var Managevue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ breakpointMobile: Number,
+ breakpointTablet: Number,
+ pauseReason: String,
+ isMatomoJsWritable: {
+ type: Boolean,
+ required: true
+ }
+ },
+ data() {
+ return {
+ editMode: false,
+ idSiteHsr: null
+ };
+ },
+ components: {
+ MatomoJsNotWritableAlert: MatomoJsNotWritableAlert,
+ HeatmapList: List,
+ HeatmapEdit: Edit
+ },
+ watch: {
+ editMode() {
+ // when changing edit modes, the tooltip can sometimes get stuck on the screen
+ Managevue_type_script_lang_ts_$('.ui-tooltip').remove();
+ }
+ },
+ created() {
+ // doing this in a watch because we don't want to post an event in a computed property
+ Object(external_commonjs_vue_commonjs2_vue_root_Vue_["watch"])(() => external_CoreHome_["MatomoUrl"].hashParsed.value.idSiteHsr, idSiteHsr => {
+ this.initState(idSiteHsr);
+ });
+ this.initState(external_CoreHome_["MatomoUrl"].hashParsed.value.idSiteHsr);
+ },
+ methods: {
+ removeAnyHsrNotification() {
+ external_CoreHome_["NotificationsStore"].remove('hsrmanagement');
+ },
+ initState(idSiteHsr) {
+ if (idSiteHsr) {
+ if (idSiteHsr === '0') {
+ const parameters = {
+ isAllowed: true
+ };
+ external_CoreHome_["Matomo"].postEvent('HeatmapSessionRecording.initAddHeatmap', parameters);
+ if (parameters && !parameters.isAllowed) {
+ this.editMode = false;
+ this.idSiteHsr = null;
+ return;
+ }
+ }
+ this.editMode = true;
+ this.idSiteHsr = parseInt(idSiteHsr, 10);
+ } else {
+ this.editMode = false;
+ this.idSiteHsr = null;
+ }
+ this.removeAnyHsrNotification();
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Manage.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageHeatmap/Manage.vue
+
+
+
+Managevue_type_script_lang_ts.render = Managevue_type_template_id_56c7eaa3_render
+
+/* harmony default export */ var Manage = (Managevue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Edit.vue?vue&type=template&id=56c3e386
+
+const Editvue_type_template_id_56c3e386_hoisted_1 = {
+ class: "loadingPiwik"
+};
+const Editvue_type_template_id_56c3e386_hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ src: "plugins/Morpheus/images/loading-blue.gif"
+}, null, -1);
+const Editvue_type_template_id_56c3e386_hoisted_3 = {
+ class: "loadingPiwik"
+};
+const Editvue_type_template_id_56c3e386_hoisted_4 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ src: "plugins/Morpheus/images/loading-blue.gif"
+}, null, -1);
+const Editvue_type_template_id_56c3e386_hoisted_5 = {
+ name: "name"
+};
+const Editvue_type_template_id_56c3e386_hoisted_6 = {
+ name: "sampleLimit"
+};
+const Editvue_type_template_id_56c3e386_hoisted_7 = {
+ class: "form-group row"
+};
+const Editvue_type_template_id_56c3e386_hoisted_8 = {
+ class: "col s12"
+};
+const Editvue_type_template_id_56c3e386_hoisted_9 = {
+ class: "col s12 m6",
+ style: {
+ "padding-left": "0"
+ }
+};
+const Editvue_type_template_id_56c3e386_hoisted_10 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("hr", null, null, -1);
+const Editvue_type_template_id_56c3e386_hoisted_11 = {
+ class: "col s12 m6"
+};
+const Editvue_type_template_id_56c3e386_hoisted_12 = {
+ class: "form-help"
+};
+const Editvue_type_template_id_56c3e386_hoisted_13 = {
+ class: "inline-help"
+};
+const Editvue_type_template_id_56c3e386_hoisted_14 = {
+ name: "sampleRate"
+};
+const Editvue_type_template_id_56c3e386_hoisted_15 = {
+ name: "minSessionTime"
+};
+const Editvue_type_template_id_56c3e386_hoisted_16 = {
+ name: "requiresActivity"
+};
+const Editvue_type_template_id_56c3e386_hoisted_17 = {
+ class: "inline-help-node"
+};
+const Editvue_type_template_id_56c3e386_hoisted_18 = ["innerHTML"];
+const Editvue_type_template_id_56c3e386_hoisted_19 = ["innerHTML"];
+const Editvue_type_template_id_56c3e386_hoisted_20 = {
+ class: "entityCancel"
+};
+function Editvue_type_template_id_56c3e386_render(_ctx, _cache, $props, $setup, $data, $options) {
+ const _component_Field = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Field");
+ const _component_HsrUrlTarget = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("HsrUrlTarget");
+ const _component_HsrTargetTest = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("HsrTargetTest");
+ const _component_SaveButton = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("SaveButton");
+ const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, {
+ class: "editHsr",
+ "content-title": _ctx.contentTitle
+ }, {
+ default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Editvue_type_template_id_56c3e386_hoisted_1, [Editvue_type_template_id_56c3e386_hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_LoadingData')), 1)])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isLoading]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Editvue_type_template_id_56c3e386_hoisted_3, [Editvue_type_template_id_56c3e386_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_UpdatingData')), 1)])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isUpdating]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("form", {
+ onSubmit: _cache[10] || (_cache[10] = $event => _ctx.edit ? _ctx.updateHsr() : _ctx.createHsr())
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_5, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "name",
+ "model-value": _ctx.siteHsr.name,
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => {
+ _ctx.siteHsr.name = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('General_Name'),
+ maxlength: 50,
+ placeholder: _ctx.translate('HeatmapSessionRecording_FieldNamePlaceholder'),
+ "inline-help": _ctx.translate('HeatmapSessionRecording_SessionNameHelp')
+ }, null, 8, ["model-value", "title", "placeholder", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_6, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "sampleLimit",
+ "model-value": _ctx.siteHsr.sample_limit,
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => {
+ _ctx.siteHsr.sample_limit = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_SessionSampleLimit'),
+ options: _ctx.sampleLimits,
+ "inline-help": _ctx.translate('HeatmapSessionRecording_SessionSampleLimitHelp')
+ }, null, 8, ["model-value", "title", "options", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_8, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_TargetPages')) + ":", 1)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_9, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.siteHsr.match_page_rules, (url, index) => {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", {
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(`matchPageRules ${index} multiple`),
+ key: index
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_HsrUrlTarget, {
+ "model-value": url,
+ "onUpdate:modelValue": $event => _ctx.setMatchPageRule($event, index),
+ onAddUrl: _cache[2] || (_cache[2] = $event => _ctx.addMatchPageRule()),
+ onRemoveUrl: $event => _ctx.removeMatchPageRule(index),
+ onAnyChange: _cache[3] || (_cache[3] = $event => _ctx.setValueHasChanged()),
+ "allow-any": true,
+ "disable-if-no-value": index > 0,
+ "can-be-removed": index > 0,
+ "show-add-url": true
+ }, null, 8, ["model-value", "onUpdate:modelValue", "onRemoveUrl", "disable-if-no-value", "can-be-removed"])]), Editvue_type_template_id_56c3e386_hoisted_10], 2);
+ }), 128))]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_11, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_12, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Editvue_type_template_id_56c3e386_hoisted_13, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_FieldIncludedTargetsHelpSessions')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_HsrTargetTest, {
+ "included-targets": _ctx.siteHsr.match_page_rules
+ }, null, 8, ["included-targets"])])])])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_14, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "sampleRate",
+ "model-value": _ctx.siteHsr.sample_rate,
+ "onUpdate:modelValue": _cache[4] || (_cache[4] = $event => {
+ _ctx.siteHsr.sample_rate = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_SampleRate'),
+ options: _ctx.sampleRates,
+ introduction: _ctx.translate('HeatmapSessionRecording_AdvancedOptions'),
+ "inline-help": _ctx.translate('HeatmapSessionRecording_SessionSampleRateHelp')
+ }, null, 8, ["model-value", "title", "options", "introduction", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_15, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "minSessionTime",
+ "model-value": _ctx.siteHsr.min_session_time,
+ "onUpdate:modelValue": _cache[5] || (_cache[5] = $event => {
+ _ctx.siteHsr.min_session_time = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_MinSessionTime'),
+ options: _ctx.minSessionTimes,
+ "inline-help": _ctx.translate('HeatmapSessionRecording_MinSessionTimeHelp')
+ }, null, 8, ["model-value", "title", "options", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_16, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "checkbox",
+ name: "requiresActivity",
+ "model-value": _ctx.siteHsr.requires_activity,
+ "onUpdate:modelValue": _cache[6] || (_cache[6] = $event => {
+ _ctx.siteHsr.requires_activity = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_RequiresActivity'),
+ "inline-help": _ctx.translate('HeatmapSessionRecording_RequiresActivityHelp')
+ }, null, 8, ["model-value", "title", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "checkbox",
+ name: "captureKeystrokes",
+ "model-value": _ctx.siteHsr.capture_keystrokes,
+ "onUpdate:modelValue": _cache[7] || (_cache[7] = $event => {
+ _ctx.siteHsr.capture_keystrokes = $event;
+ _ctx.setValueHasChanged();
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_CaptureKeystrokes')
+ }, {
+ "inline-help": Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_17, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ innerHTML: _ctx.$sanitize(_ctx.captureKeystrokesHelp)
+ }, null, 8, Editvue_type_template_id_56c3e386_hoisted_18)])]),
+ _: 1
+ }, 8, ["model-value", "title"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", {
+ innerHTML: _ctx.$sanitize(_ctx.personalInformationNote)
+ }, null, 8, Editvue_type_template_id_56c3e386_hoisted_19), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_SaveButton, {
+ class: "createButton",
+ onConfirm: _cache[8] || (_cache[8] = $event => _ctx.edit ? _ctx.updateHsr() : _ctx.createHsr()),
+ disabled: _ctx.isUpdating || !_ctx.isDirty,
+ saving: _ctx.isUpdating,
+ value: _ctx.saveButtonText
+ }, null, 8, ["disabled", "saving", "value"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Editvue_type_template_id_56c3e386_hoisted_20, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ onClick: _cache[9] || (_cache[9] = $event => _ctx.cancel())
+ }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Cancel')), 1)])])], 32)]),
+ _: 1
+ }, 8, ["content-title"]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Edit.vue?vue&type=template&id=56c3e386
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Edit.vue?vue&type=script&lang=ts
+
+
+
+
+
+
+const Editvue_type_script_lang_ts_notificationId = 'hsrmanagement';
+/* harmony default export */ var ManageSessionRecording_Editvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ idSiteHsr: Number
+ },
+ components: {
+ ContentBlock: external_CoreHome_["ContentBlock"],
+ Field: external_CorePluginsAdmin_["Field"],
+ HsrUrlTarget: HsrUrlTarget,
+ HsrTargetTest: HsrTargetTest,
+ SaveButton: external_CorePluginsAdmin_["SaveButton"]
+ },
+ data() {
+ return {
+ isDirty: false,
+ showAdvancedView: false,
+ sampleLimits: [],
+ siteHsr: {}
+ };
+ },
+ created() {
+ external_CoreHome_["AjaxHelper"].fetch({
+ method: 'HeatmapSessionRecording.getAvailableSessionRecordingSampleLimits'
+ }).then(sampleLimits => {
+ this.sampleLimits = (sampleLimits || []).map(l => ({
+ key: `${l}`,
+ value: l
+ }));
+ });
+ this.init();
+ },
+ watch: {
+ idSiteHsr(newValue) {
+ if (newValue === null) {
+ return;
+ }
+ this.init();
+ }
+ },
+ methods: {
+ removeAnyHsrNotification() {
+ external_CoreHome_["NotificationsStore"].remove(Editvue_type_script_lang_ts_notificationId);
+ external_CoreHome_["NotificationsStore"].remove('ajaxHelper');
+ },
+ showNotification(message, context) {
+ const instanceId = external_CoreHome_["NotificationsStore"].show({
+ message,
+ context,
+ id: Editvue_type_script_lang_ts_notificationId,
+ type: 'transient'
+ });
+ setTimeout(() => {
+ external_CoreHome_["NotificationsStore"].scrollToNotification(instanceId);
+ }, 200);
+ },
+ showErrorFieldNotProvidedNotification(title) {
+ const message = Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ErrorXNotProvided', [title]);
+ this.showNotification(message, 'error');
+ },
+ init() {
+ const {
+ idSiteHsr
+ } = this;
+ this.siteHsr = {};
+ this.showAdvancedView = false;
+ external_CoreHome_["Matomo"].helper.lazyScrollToContent();
+ if (this.edit && idSiteHsr) {
+ SessionRecordingStore.findHsr(idSiteHsr).then(siteHsr => {
+ if (!siteHsr) {
+ return;
+ }
+ this.siteHsr = Object(external_CoreHome_["clone"])(siteHsr);
+ this.siteHsr.sample_rate = `${this.siteHsr.sample_rate}`;
+ this.addInitialMatchPageRule();
+ this.isDirty = false;
+ });
+ return;
+ }
+ if (this.create) {
+ this.siteHsr = {
+ idSite: external_CoreHome_["Matomo"].idSite,
+ name: '',
+ sample_rate: '10.0',
+ sample_limit: 250,
+ min_session_time: 0,
+ requires_activity: true,
+ capture_keystrokes: false
+ };
+ this.addInitialMatchPageRule();
+ this.isDirty = false;
+ }
+ },
+ addInitialMatchPageRule() {
+ var _this$siteHsr$match_p;
+ if (!this.siteHsr) {
+ return;
+ }
+ if ((_this$siteHsr$match_p = this.siteHsr.match_page_rules) !== null && _this$siteHsr$match_p !== void 0 && _this$siteHsr$match_p.length) {
+ return;
+ }
+ this.siteHsr.match_page_rules = [{
+ attribute: 'url',
+ type: 'any',
+ value: '',
+ inverted: 0
+ }];
+ },
+ addMatchPageRule() {
+ var _this$siteHsr$match_p2;
+ if (!this.siteHsr) {
+ return;
+ }
+ if (!((_this$siteHsr$match_p2 = this.siteHsr.match_page_rules) !== null && _this$siteHsr$match_p2 !== void 0 && _this$siteHsr$match_p2.length)) {
+ this.siteHsr.match_page_rules = [];
+ }
+ this.siteHsr.match_page_rules.push({
+ attribute: 'url',
+ type: 'equals_simple',
+ value: '',
+ inverted: 0
+ });
+ this.isDirty = true;
+ },
+ removeMatchPageRule(index) {
+ if (this.siteHsr && index > -1) {
+ this.siteHsr.match_page_rules = [...this.siteHsr.match_page_rules];
+ this.siteHsr.match_page_rules.splice(index, 1);
+ this.isDirty = true;
+ }
+ },
+ cancel() {
+ const newParams = Object.assign({}, external_CoreHome_["MatomoUrl"].hashParsed.value);
+ delete newParams.idSiteHsr;
+ external_CoreHome_["MatomoUrl"].updateHash(newParams);
+ },
+ createHsr() {
+ this.removeAnyHsrNotification();
+ if (!this.checkRequiredFieldsAreSet()) {
+ return;
+ }
+ SessionRecordingStore.createOrUpdateHsr(this.siteHsr, 'HeatmapSessionRecording.addSessionRecording').then(response => {
+ if (!response || response.type === 'error' || !response.response) {
+ return;
+ }
+ this.isDirty = false;
+ const idSiteHsr = response.response.value;
+ SessionRecordingStore.reload().then(() => {
+ if (external_CoreHome_["Matomo"].helper.isReportingPage()) {
+ external_CoreHome_["Matomo"].postEvent('updateReportingMenu');
+ }
+ external_CoreHome_["MatomoUrl"].updateHash(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].hashParsed.value), {}, {
+ idSiteHsr
+ }));
+ setTimeout(() => {
+ this.showNotification(Object(external_CoreHome_["translate"])('HeatmapSessionRecording_SessionRecordingCreated'), response.type);
+ }, 200);
+ });
+ });
+ },
+ setValueHasChanged() {
+ this.isDirty = true;
+ },
+ updateHsr() {
+ this.removeAnyHsrNotification();
+ if (!this.checkRequiredFieldsAreSet()) {
+ return;
+ }
+ SessionRecordingStore.createOrUpdateHsr(this.siteHsr, 'HeatmapSessionRecording.updateSessionRecording').then(response => {
+ if (response.type === 'error') {
+ return;
+ }
+ this.isDirty = false;
+ this.siteHsr = {};
+ SessionRecordingStore.reload().then(() => {
+ this.init();
+ });
+ this.showNotification(Object(external_CoreHome_["translate"])('HeatmapSessionRecording_SessionRecordingUpdated'), response.type);
+ });
+ },
+ checkRequiredFieldsAreSet() {
+ var _this$siteHsr$match_p3;
+ if (!this.siteHsr.name) {
+ const title = this.translate('General_Name');
+ this.showErrorFieldNotProvidedNotification(title);
+ return false;
+ }
+ if (!((_this$siteHsr$match_p3 = this.siteHsr.match_page_rules) !== null && _this$siteHsr$match_p3 !== void 0 && _this$siteHsr$match_p3.length) || !SessionRecordingStore.filterRules(this.siteHsr.match_page_rules).length) {
+ const title = this.translate('HeatmapSessionRecording_ErrorPageRuleRequired');
+ this.showNotification(title, 'error');
+ return false;
+ }
+ return true;
+ },
+ setMatchPageRule(rule, index) {
+ this.siteHsr.match_page_rules = [...this.siteHsr.match_page_rules];
+ this.siteHsr.match_page_rules[index] = rule;
+ }
+ },
+ computed: {
+ minSessionTimes() {
+ return [0, 5, 10, 15, 20, 30, 45, 60, 90, 120].map(v => ({
+ key: `${v}`,
+ value: `${v} seconds`
+ }));
+ },
+ sampleRates() {
+ const rates = [0.1, 0.5, 1, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100];
+ return rates.map(v => ({
+ key: `${v.toFixed(1)}`,
+ value: `${v}%`
+ }));
+ },
+ create() {
+ return !this.idSiteHsr;
+ },
+ edit() {
+ return !this.create;
+ },
+ editTitle() {
+ const token = this.create ? 'HeatmapSessionRecording_CreateNewSessionRecording' : 'HeatmapSessionRecording_EditSessionRecordingX';
+ return token;
+ },
+ contentTitle() {
+ return Object(external_CoreHome_["translate"])(this.editTitle, this.siteHsr.name ? `"${this.siteHsr.name}"` : '');
+ },
+ isLoading() {
+ return HeatmapStore.state.value.isLoading;
+ },
+ isUpdating() {
+ return HeatmapStore.state.value.isUpdating;
+ },
+ captureKeystrokesHelp() {
+ const link = 'https://developer.matomo.org/guides/heatmap-session-recording/setup#masking-keystrokes-in-form-fields';
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_CaptureKeystrokesHelp', ``, ' ');
+ },
+ personalInformationNote() {
+ const link = 'https://developer.matomo.org/guides/heatmap-session-recording/setup#masking-content-on-your-website';
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_PersonalInformationNote', Object(external_CoreHome_["translate"])('HeatmapSessionRecording_SessionRecording'), '', '
', ``, ' ');
+ },
+ saveButtonText() {
+ return this.edit ? Object(external_CoreHome_["translate"])('CoreUpdater_UpdateTitle') : Object(external_CoreHome_["translate"])('HeatmapSessionRecording_CreateNewSessionRecording');
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Edit.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Edit.vue
+
+
+
+ManageSessionRecording_Editvue_type_script_lang_ts.render = Editvue_type_template_id_56c3e386_render
+
+/* harmony default export */ var ManageSessionRecording_Edit = (ManageSessionRecording_Editvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/List.vue?vue&type=template&id=09d6f8c4
+
+const Listvue_type_template_id_09d6f8c4_hoisted_1 = {
+ class: "sessionRecordingList"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_2 = {
+ class: "filterStatus"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_3 = {
+ class: "hsrSearchFilter",
+ style: {
+ "margin-left": "3.5px"
+ }
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_4 = {
+ class: "index"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_5 = {
+ class: "name"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_6 = {
+ class: "creationDate"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_7 = {
+ class: "sampleLimit"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_8 = {
+ class: "status"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_9 = {
+ class: "action"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_10 = {
+ colspan: "7"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_11 = {
+ class: "loadingPiwik"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_12 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("img", {
+ src: "plugins/Morpheus/images/loading-blue.gif"
+}, null, -1);
+const Listvue_type_template_id_09d6f8c4_hoisted_13 = {
+ colspan: "7"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_14 = ["id"];
+const Listvue_type_template_id_09d6f8c4_hoisted_15 = {
+ class: "index"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_16 = {
+ class: "name"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_17 = {
+ class: "creationDate"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_18 = {
+ class: "sampleLimit"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_19 = {
+ key: 0,
+ class: "status status-paused"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_20 = ["title"];
+const Listvue_type_template_id_09d6f8c4_hoisted_21 = {
+ key: 1,
+ class: "status"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_22 = {
+ class: "action"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_23 = ["title", "onClick"];
+const Listvue_type_template_id_09d6f8c4_hoisted_24 = ["title", "onClick"];
+const Listvue_type_template_id_09d6f8c4_hoisted_25 = ["title", "href"];
+const Listvue_type_template_id_09d6f8c4_hoisted_26 = ["title", "onClick"];
+const Listvue_type_template_id_09d6f8c4_hoisted_27 = {
+ class: "tableActionBar"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_28 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "icon-add"
+}, null, -1);
+const Listvue_type_template_id_09d6f8c4_hoisted_29 = {
+ class: "ui-confirm",
+ ref: "confirmDeleteSessionRecording"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_30 = ["value"];
+const Listvue_type_template_id_09d6f8c4_hoisted_31 = ["value"];
+const Listvue_type_template_id_09d6f8c4_hoisted_32 = {
+ class: "ui-confirm",
+ ref: "confirmEndSessionRecording"
+};
+const Listvue_type_template_id_09d6f8c4_hoisted_33 = ["value"];
+const Listvue_type_template_id_09d6f8c4_hoisted_34 = ["value"];
+function Listvue_type_template_id_09d6f8c4_render(_ctx, _cache, $props, $setup, $data, $options) {
+ const _component_Field = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Field");
+ const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock");
+ const _directive_content_table = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("content-table");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", Listvue_type_template_id_09d6f8c4_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ContentBlock, {
+ "content-title": _ctx.translate('HeatmapSessionRecording_ManageSessionRecordings')
+ }, {
+ default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_SessionRecordingsUsageBenefits')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_09d6f8c4_hoisted_2, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "select",
+ name: "filterStatus",
+ "model-value": _ctx.filterStatus,
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => {
+ _ctx.setFilterStatus($event);
+ }),
+ title: _ctx.translate('HeatmapSessionRecording_Filter'),
+ "full-width": true,
+ options: _ctx.statusOptions
+ }, null, 8, ["model-value", "title", "options"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_09d6f8c4_hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, {
+ uicontrol: "text",
+ name: "hsrSearch",
+ title: _ctx.translate('General_Search'),
+ modelValue: _ctx.searchFilter,
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => _ctx.searchFilter = $event),
+ "full-width": true
+ }, null, 8, ["title", "modelValue"]), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.hsrs.length > 0]])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("table", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("thead", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_09d6f8c4_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Id')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_09d6f8c4_hoisted_5, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Name')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_09d6f8c4_hoisted_6, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_CreationDate')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_09d6f8c4_hoisted_7, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_SampleLimit')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_09d6f8c4_hoisted_8, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('CorePluginsAdmin_Status')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", Listvue_type_template_id_09d6f8c4_hoisted_9, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Actions')), 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tbody", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_09d6f8c4_hoisted_10, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", Listvue_type_template_id_09d6f8c4_hoisted_11, [Listvue_type_template_id_09d6f8c4_hoisted_12, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_LoadingData')), 1)])])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.isLoading || _ctx.isUpdating]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_09d6f8c4_hoisted_13, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_NoSessionRecordingsFound')), 1)], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], !_ctx.isLoading && _ctx.hsrs.length == 0]]), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.sortedHsrs, hsr => {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("tr", {
+ id: `hsr${hsr.idsitehsr}`,
+ class: "hsrs",
+ key: hsr.idsitehsr
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_09d6f8c4_hoisted_15, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(hsr.idsitehsr), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_09d6f8c4_hoisted_16, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(hsr.name), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_09d6f8c4_hoisted_17, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(hsr.created_date_pretty), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_09d6f8c4_hoisted_18, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(hsr.sample_limit), 1), hsr.status === 'paused' ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("td", Listvue_type_template_id_09d6f8c4_hoisted_19, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.ucfirst(hsr.status)) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", {
+ class: "icon icon-help",
+ title: _ctx.pauseReason
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_20)])) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("td", Listvue_type_template_id_09d6f8c4_hoisted_21, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.ucfirst(hsr.status)), 1)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", Listvue_type_template_id_09d6f8c4_hoisted_22, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ class: "table-action icon-edit",
+ title: _ctx.translate('HeatmapSessionRecording_EditX', _ctx.translate('HeatmapSessionRecording_SessionRecording')),
+ onClick: $event => _ctx.editHsr(hsr.idsitehsr)
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_23), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ class: "table-action stopRecording icon-drop-crossed",
+ title: _ctx.translate('HeatmapSessionRecording_StopX', _ctx.translate('HeatmapSessionRecording_SessionRecording')),
+ onClick: $event => _ctx.completeHsr(hsr)
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_24), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], hsr.status !== 'ended']]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ class: "table-action icon-show",
+ title: _ctx.translate('HeatmapSessionRecording_ViewReport'),
+ href: _ctx.getViewReportLink(hsr),
+ target: "_blank"
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_25), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ class: "table-action icon-delete",
+ title: _ctx.translate('HeatmapSessionRecording_DeleteX', _ctx.translate('HeatmapSessionRecording_SessionRecording')),
+ onClick: $event => _ctx.deleteHsr(hsr)
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_26)])], 8, Listvue_type_template_id_09d6f8c4_hoisted_14);
+ }), 128))])])), [[_directive_content_table]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_09d6f8c4_hoisted_27, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", {
+ class: "createNewHsr",
+ value: "",
+ onClick: _cache[2] || (_cache[2] = $event => _ctx.createHsr())
+ }, [Listvue_type_template_id_09d6f8c4_hoisted_28, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_CreateNewSessionRecording')), 1)])])]),
+ _: 1
+ }, 8, ["content-title"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_09d6f8c4_hoisted_29, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_DeleteSessionRecordingConfirm')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "yes",
+ type: "button",
+ value: _ctx.translate('General_Yes')
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_30), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "no",
+ type: "button",
+ value: _ctx.translate('General_No')
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_31)], 512), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Listvue_type_template_id_09d6f8c4_hoisted_32, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_EndSessionRecordingConfirm')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "yes",
+ type: "button",
+ value: _ctx.translate('General_Yes')
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_33), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "no",
+ type: "button",
+ value: _ctx.translate('General_No')
+ }, null, 8, Listvue_type_template_id_09d6f8c4_hoisted_34)], 512)]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/List.vue?vue&type=template&id=09d6f8c4
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/List.vue?vue&type=script&lang=ts
+
+
+
+
+/* harmony default export */ var ManageSessionRecording_Listvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ pauseReason: String
+ },
+ components: {
+ ContentBlock: external_CoreHome_["ContentBlock"],
+ Field: external_CorePluginsAdmin_["Field"]
+ },
+ directives: {
+ ContentTable: external_CoreHome_["ContentTable"]
+ },
+ data() {
+ return {
+ searchFilter: ''
+ };
+ },
+ created() {
+ SessionRecordingStore.setFilterStatus('');
+ SessionRecordingStore.fetchHsrs();
+ },
+ methods: {
+ createHsr() {
+ this.editHsr(0);
+ },
+ editHsr(idSiteHsr) {
+ external_CoreHome_["MatomoUrl"].updateHash(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].hashParsed.value), {}, {
+ idSiteHsr
+ }));
+ },
+ deleteHsr(hsr) {
+ external_CoreHome_["Matomo"].helper.modalConfirm(this.$refs.confirmDeleteSessionRecording, {
+ yes: () => {
+ SessionRecordingStore.deleteHsr(hsr.idsitehsr).then(() => {
+ SessionRecordingStore.reload();
+ external_CoreHome_["Matomo"].postEvent('updateReportingMenu');
+ });
+ }
+ });
+ },
+ completeHsr(hsr) {
+ external_CoreHome_["Matomo"].helper.modalConfirm(this.$refs.confirmEndSessionRecording, {
+ yes: () => {
+ SessionRecordingStore.completeHsr(hsr.idsitehsr).then(() => {
+ SessionRecordingStore.reload();
+ });
+ }
+ });
+ },
+ setFilterStatus(filter) {
+ SessionRecordingStore.setFilterStatus(filter);
+ },
+ ucfirst(s) {
+ return `${s[0].toUpperCase()}${s.substr(1)}`;
+ },
+ getViewReportLink(hsr) {
+ return `?${external_CoreHome_["MatomoUrl"].stringify({
+ module: 'CoreHome',
+ action: 'index',
+ idSite: hsr.idsite,
+ period: 'day',
+ date: 'yesterday'
+ })}#?${external_CoreHome_["MatomoUrl"].stringify({
+ category: 'HeatmapSessionRecording_SessionRecordings',
+ idSite: hsr.idsite,
+ period: 'day',
+ date: 'yesterday',
+ subcategory: hsr.idsitehsr
+ })}`;
+ }
+ },
+ computed: {
+ filterStatus() {
+ return SessionRecordingStore.state.value.filterStatus;
+ },
+ statusOptions() {
+ return SessionRecordingStore.statusOptions;
+ },
+ hsrs() {
+ return SessionRecordingStore.hsrs.value;
+ },
+ isLoading() {
+ return SessionRecordingStore.state.value.isLoading;
+ },
+ isUpdating() {
+ return SessionRecordingStore.state.value.isUpdating;
+ },
+ sortedHsrs() {
+ // look through string properties of heatmaps for values that have searchFilter in them
+ // (mimics angularjs filter() filter)
+ const result = [...this.hsrs].filter(h => Object.keys(h).some(propName => {
+ const entity = h;
+ return typeof entity[propName] === 'string' && entity[propName].indexOf(this.searchFilter) !== -1;
+ }));
+ result.sort((lhs, rhs) => rhs.idsitehsr - lhs.idsitehsr);
+ return result;
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/List.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/List.vue
+
+
+
+ManageSessionRecording_Listvue_type_script_lang_ts.render = Listvue_type_template_id_09d6f8c4_render
+
+/* harmony default export */ var ManageSessionRecording_List = (ManageSessionRecording_Listvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Manage.vue?vue&type=template&id=4a6cf182
+
+const Managevue_type_template_id_4a6cf182_hoisted_1 = {
+ class: "manageHsr"
+};
+function Managevue_type_template_id_4a6cf182_render(_ctx, _cache, $props, $setup, $data, $options) {
+ const _component_MatomoJsNotWritableAlert = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("MatomoJsNotWritableAlert");
+ const _component_SessionRecordingList = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("SessionRecordingList");
+ const _component_SessionRecordingEdit = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("SessionRecordingEdit");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, [!_ctx.editMode ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_MatomoJsNotWritableAlert, {
+ key: 0,
+ "is-matomo-js-writable": _ctx.isMatomoJsWritable,
+ "recording-type": _ctx.translate('HeatmapSessionRecording_SessionRecordings')
+ }, null, 8, ["is-matomo-js-writable", "recording-type"])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", Managevue_type_template_id_4a6cf182_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_SessionRecordingList, {
+ "pause-reason": _ctx.pauseReason
+ }, null, 8, ["pause-reason"])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], !_ctx.editMode]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_SessionRecordingEdit, {
+ "id-site-hsr": _ctx.idSiteHsr
+ }, null, 8, ["id-site-hsr"])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.editMode]])])], 64);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Manage.vue?vue&type=template&id=4a6cf182
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Manage.vue?vue&type=script&lang=ts
+
+
+
+
+
+/* harmony default export */ var ManageSessionRecording_Managevue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ pauseReason: String,
+ isMatomoJsWritable: {
+ type: Boolean,
+ required: true
+ }
+ },
+ data() {
+ return {
+ editMode: false,
+ idSiteHsr: null
+ };
+ },
+ components: {
+ MatomoJsNotWritableAlert: MatomoJsNotWritableAlert,
+ SessionRecordingEdit: ManageSessionRecording_Edit,
+ SessionRecordingList: ManageSessionRecording_List
+ },
+ created() {
+ // doing this in a watch because we don't want to post an event in a computed property
+ Object(external_commonjs_vue_commonjs2_vue_root_Vue_["watch"])(() => external_CoreHome_["MatomoUrl"].hashParsed.value.idSiteHsr, idSiteHsr => {
+ this.initState(idSiteHsr);
+ });
+ this.initState(external_CoreHome_["MatomoUrl"].hashParsed.value.idSiteHsr);
+ },
+ methods: {
+ removeAnyHsrNotification() {
+ external_CoreHome_["NotificationsStore"].remove('hsrmanagement');
+ },
+ initState(idSiteHsr) {
+ if (idSiteHsr) {
+ if (idSiteHsr === '0') {
+ const parameters = {
+ isAllowed: true
+ };
+ external_CoreHome_["Matomo"].postEvent('HeatmapSessionRecording.initAddSessionRecording', parameters);
+ if (parameters && !parameters.isAllowed) {
+ this.editMode = false;
+ this.idSiteHsr = null;
+ return;
+ }
+ }
+ this.editMode = true;
+ this.idSiteHsr = parseInt(idSiteHsr, 10);
+ } else {
+ this.editMode = false;
+ this.idSiteHsr = null;
+ }
+ this.removeAnyHsrNotification();
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Manage.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ManageSessionRecording/Manage.vue
+
+
+
+ManageSessionRecording_Managevue_type_script_lang_ts.render = Managevue_type_template_id_4a6cf182_render
+
+/* harmony default export */ var ManageSessionRecording_Manage = (ManageSessionRecording_Managevue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ListOfPageviews/ListOfPageviews.vue?vue&type=template&id=fe86de22
+
+const ListOfPageviewsvue_type_template_id_fe86de22_hoisted_1 = {
+ class: "ui-confirm",
+ id: "listOfPageviews"
+};
+const ListOfPageviewsvue_type_template_id_fe86de22_hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1);
+const ListOfPageviewsvue_type_template_id_fe86de22_hoisted_3 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1);
+const ListOfPageviewsvue_type_template_id_fe86de22_hoisted_4 = ["onClick"];
+const ListOfPageviewsvue_type_template_id_fe86de22_hoisted_5 = ["title"];
+const ListOfPageviewsvue_type_template_id_fe86de22_hoisted_6 = ["value"];
+function ListOfPageviewsvue_type_template_id_fe86de22_render(_ctx, _cache, $props, $setup, $data, $options) {
+ const _directive_content_table = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("content-table");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", ListOfPageviewsvue_type_template_id_fe86de22_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_PageviewsInVisit')), 1), ListOfPageviewsvue_type_template_id_fe86de22_hoisted_2, ListOfPageviewsvue_type_template_id_fe86de22_hoisted_3, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("table", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("thead", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_ColumnTime')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_TimeOnPage')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('Goals_URL')), 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tbody", null, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.pageviews, pageview => {
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("tr", {
+ key: pageview.idloghsr,
+ class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])({
+ inactive: pageview.idloghsr !== _ctx.idLogHsr
+ }),
+ onClick: $event => _ctx.onClickPageView(pageview)
+ }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(pageview.server_time_pretty), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(pageview.time_on_page_pretty), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", {
+ title: pageview.label
+ }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])((pageview.label || '').substr(0, 50)), 9, ListOfPageviewsvue_type_template_id_fe86de22_hoisted_5)], 10, ListOfPageviewsvue_type_template_id_fe86de22_hoisted_4);
+ }), 128))])])), [[_directive_content_table]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", {
+ role: "close",
+ type: "button",
+ value: _ctx.translate('General_Close')
+ }, null, 8, ListOfPageviewsvue_type_template_id_fe86de22_hoisted_6)]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ListOfPageviews/ListOfPageviews.vue?vue&type=template&id=fe86de22
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/ListOfPageviews/ListOfPageviews.vue?vue&type=script&lang=ts
+
+
+/* harmony default export */ var ListOfPageviewsvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ pageviews: {
+ type: Array,
+ required: true
+ },
+ idLogHsr: {
+ type: Number,
+ required: true
+ }
+ },
+ directives: {
+ ContentTable: external_CoreHome_["ContentTable"]
+ },
+ methods: {
+ onClickPageView(pageview) {
+ if (pageview.idloghsr === this.idLogHsr) {
+ return;
+ }
+ external_CoreHome_["MatomoUrl"].updateUrl(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value), {}, {
+ idLogHsr: pageview.idloghsr
+ }), external_CoreHome_["MatomoUrl"].hashParsed.value.length ? Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].hashParsed.value), {}, {
+ idLogHsr: pageview.idloghsr
+ }) : undefined);
+ }
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ListOfPageviews/ListOfPageviews.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/ListOfPageviews/ListOfPageviews.vue
+
+
+
+ListOfPageviewsvue_type_script_lang_ts.render = ListOfPageviewsvue_type_template_id_fe86de22_render
+
+/* harmony default export */ var ListOfPageviews = (ListOfPageviewsvue_type_script_lang_ts);
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVisPage.vue?vue&type=template&id=7f5f8230
+
+const HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_1 = {
+ class: "heatmap-vis-title"
+};
+const HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_2 = {
+ key: 0,
+ class: "alert alert-info heatmap-country-alert"
+};
+const HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_3 = {
+ key: 1
+};
+const HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_4 = {
+ key: 2
+};
+const HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_5 = ["innerHTML"];
+const HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_6 = {
+ class: "alert alert-info"
+};
+const HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_7 = {
+ key: 3
+};
+const HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_8 = {
+ class: "alert alert-info"
+};
+function HeatmapVisPagevue_type_template_id_7f5f8230_render(_ctx, _cache, $props, $setup, $data, $options) {
+ var _ctx$heatmapMetadata;
+ const _component_EnrichedHeadline = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("EnrichedHeadline");
+ const _component_MatomoJsNotWritableAlert = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("MatomoJsNotWritableAlert");
+ const _component_HeatmapVis = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("HeatmapVis");
+ const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock");
+ return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_EnrichedHeadline, {
+ "edit-url": _ctx.editUrl,
+ "inline-help": _ctx.inlineHelp
+ }, {
+ default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_HeatmapX', `"${_ctx.heatmap.name}"`)), 1)]),
+ _: 1
+ }, 8, ["edit-url", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_MatomoJsNotWritableAlert, {
+ "is-matomo-js-writable": _ctx.isMatomoJsWritable,
+ "recording-type": _ctx.translate('HeatmapSessionRecording_Heatmaps')
+ }, null, 8, ["is-matomo-js-writable", "recording-type"]), _ctx.includedCountries ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('HeatmapSessionRecording_HeatmapInfoTrackVisitsFromCountries', _ctx.includedCountries)), 1)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.heatmap.page_treemirror ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_HeatmapVis, {
+ "created-date": _ctx.createdDate,
+ "excluded-elements": _ctx.heatmap.excluded_elements,
+ "num-samples": _ctx.heatmapMetadata,
+ url: _ctx.heatmap.screenshot_url,
+ "heatmap-date": _ctx.heatmapDate,
+ "heatmap-period": _ctx.heatmapPeriod,
+ "offset-accuracy": _ctx.offsetAccuracy,
+ "breakpoint-tablet": _ctx.heatmap.breakpoint_tablet,
+ "breakpoint-mobile": _ctx.heatmap.breakpoint_mobile,
+ "heatmap-types": _ctx.heatmapTypes,
+ "device-types": _ctx.deviceTypes,
+ "id-site-hsr": _ctx.idSiteHsr,
+ "is-active": _ctx.isActive,
+ "desktop-preview-size": _ctx.desktopPreviewSize,
+ "iframe-resolutions-values": _ctx.iframeResolutions
+ }, null, 8, ["created-date", "excluded-elements", "num-samples", "url", "heatmap-date", "heatmap-period", "offset-accuracy", "breakpoint-tablet", "breakpoint-mobile", "heatmap-types", "device-types", "id-site-hsr", "is-active", "desktop-preview-size", "iframe-resolutions-values"])])) : !((_ctx$heatmapMetadata = _ctx.heatmapMetadata) !== null && _ctx$heatmapMetadata !== void 0 && _ctx$heatmapMetadata.nb_samples_device_all) ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_4, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", {
+ innerHTML: _ctx.$sanitize(_ctx.recordedSamplesTroubleShoot)
+ }, null, 8, HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_5), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ContentBlock, null, {
+ default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_6, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate(_ctx.noDataMessageKey)), 1)]),
+ _: 1
+ })])) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ContentBlock, null, {
+ default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", HeatmapVisPagevue_type_template_id_7f5f8230_hoisted_8, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.noHeatmapScreenshotRecordedYetText), 1)]),
+ _: 1
+ })]))]);
+}
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVisPage.vue?vue&type=template&id=7f5f8230
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVisPage.vue?vue&type=script&lang=ts
+
+
+
+
+/* harmony default export */ var HeatmapVisPagevue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({
+ props: {
+ idSiteHsr: {
+ type: Number,
+ required: true
+ },
+ heatmap: {
+ type: Object,
+ required: true
+ },
+ heatmapMetadata: {
+ type: Object,
+ required: true
+ },
+ deviceTypes: {
+ type: Array,
+ required: true
+ },
+ heatmapTypes: {
+ type: Array,
+ required: true
+ },
+ offsetAccuracy: {
+ type: Number,
+ required: true
+ },
+ heatmapPeriod: {
+ type: String,
+ required: true
+ },
+ heatmapDate: {
+ type: String,
+ required: true
+ },
+ isActive: Boolean,
+ createdDate: {
+ type: String,
+ required: true
+ },
+ editUrl: {
+ type: String,
+ required: true
+ },
+ inlineHelp: {
+ type: String,
+ required: true
+ },
+ includedCountries: {
+ type: String,
+ required: true
+ },
+ desktopPreviewSize: {
+ type: Number,
+ required: true
+ },
+ iframeResolutions: {
+ type: Object,
+ required: true
+ },
+ noDataMessageKey: {
+ type: String,
+ required: true
+ },
+ isMatomoJsWritable: {
+ type: Boolean,
+ required: true
+ }
+ },
+ components: {
+ MatomoJsNotWritableAlert: MatomoJsNotWritableAlert,
+ ContentBlock: external_CoreHome_["ContentBlock"],
+ HeatmapVis: HeatmapVis,
+ EnrichedHeadline: external_CoreHome_["EnrichedHeadline"]
+ },
+ computed: {
+ noHeatmapScreenshotRecordedYetText() {
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_NoHeatmapScreenshotRecordedYet', this.heatmapMetadata.nb_samples_device_all, Object(external_CoreHome_["translate"])('HeatmapSessionRecording_ScreenshotUrl'));
+ },
+ recordedSamplesTroubleShoot() {
+ const linkString = Object(external_CoreHome_["externalLink"])('https://matomo.org/faq/heatmap-session-recording/troubleshooting-heatmaps/');
+ return Object(external_CoreHome_["translate"])('HeatmapSessionRecording_HeatmapTroubleshoot', linkString, '');
+ }
+ },
+ created() {
+ // We want the selector hidden for heatmaps.
+ external_CoreHome_["Matomo"].postEvent('hidePeriodSelector');
+ }
+}));
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVisPage.vue?vue&type=script&lang=ts
+
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/HeatmapVis/HeatmapVisPage.vue
+
+
+
+HeatmapVisPagevue_type_script_lang_ts.render = HeatmapVisPagevue_type_template_id_7f5f8230_render
+
+/* harmony default export */ var HeatmapVisPage = (HeatmapVisPagevue_type_script_lang_ts);
+// CONCATENATED MODULE: ./plugins/HeatmapSessionRecording/vue/src/index.ts
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js
+
+
+
+
+/***/ })
+
+/******/ });
+});
+//# sourceMappingURL=HeatmapSessionRecording.umd.js.map
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/HeatmapSessionRecording.umd.min.js b/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/HeatmapSessionRecording.umd.min.js
new file mode 100644
index 0000000..71fc647
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/HeatmapSessionRecording.umd.min.js
@@ -0,0 +1,73 @@
+(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue"),require("CorePluginsAdmin")):"function"===typeof define&&define.amd?define(["CoreHome",,"CorePluginsAdmin"],t):"object"===typeof exports?exports["HeatmapSessionRecording"]=t(require("CoreHome"),require("vue"),require("CorePluginsAdmin")):e["HeatmapSessionRecording"]=t(e["CoreHome"],e["Vue"],e["CorePluginsAdmin"])})("undefined"!==typeof self?self:this,(function(e,t,a){return function(e){var t={};function a(i){if(t[i])return t[i].exports;var n=t[i]={i:i,l:!1,exports:{}};return e[i].call(n.exports,n,n.exports,a),n.l=!0,n.exports}return a.m=e,a.c=t,a.d=function(e,t,i){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},a.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(a.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)a.d(i,n,function(t){return e[t]}.bind(null,n));return i},a.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="plugins/HeatmapSessionRecording/vue/dist/",a(a.s="fae3")}({"19dc":function(t,a){t.exports=e},"246e":function(e,t,a){var i,n;(function(s,r,o){e.exports?e.exports=o():(i=o,n="function"===typeof i?i.call(t,a,t,e):i,void 0===n||(e.exports=n))})(0,0,(function(){var e={defaultRadius:40,defaultRenderer:"canvas2d",defaultGradient:{.25:"rgb(0,0,255)",.55:"rgb(0,255,0)",.85:"yellow",1:"rgb(255,0,0)"},defaultMaxOpacity:1,defaultMinOpacity:0,defaultBlur:.85,defaultXField:"x",defaultYField:"y",defaultValueField:"value",plugins:{}},t=function(){var t=function(e){this._coordinator={},this._data=[],this._radi=[],this._min=10,this._max=1,this._xField=e["xField"]||e.defaultXField,this._yField=e["yField"]||e.defaultYField,this._valueField=e["valueField"]||e.defaultValueField,e["radius"]&&(this._cfgRadius=e["radius"])},a=e.defaultRadius;return t.prototype={_organiseData:function(e,t){var i=e[this._xField],n=e[this._yField],s=this._radi,r=this._data,o=this._max,l=this._min,c=e[this._valueField]||1,d=e.radius||this._cfgRadius||a;r[i]||(r[i]=[],s[i]=[]),r[i][n]?r[i][n]+=c:(r[i][n]=c,s[i][n]=d);var m=r[i][n];return m>o?(t?this.setDataMax(m):this._max=m,!1):m0){var e=arguments[0],t=e.length;while(t--)this.addData.call(this,e[t])}else{var a=this._organiseData(arguments[0],!0);a&&(0===this._data.length&&(this._min=this._max=a.value),this._coordinator.emit("renderpartial",{min:this._min,max:this._max,data:[a]}))}return this},setData:function(e){var t=e.data,a=t.length;this._data=[],this._radi=[];for(var i=0;i0&&(this._drawAlpha(e),this._colorize())},renderAll:function(e){this._clear(),e.data.length>0&&(this._drawAlpha(a(e)),this._colorize())},_updateGradient:function(t){this._palette=e(t)},updateConfig:function(e){e["gradient"]&&this._updateGradient(e),this._setStyles(e)},setDimensions:function(e,t){this._width=e,this._height=t,this.canvas.width=this.shadowCanvas.width=e,this.canvas.height=this.shadowCanvas.height=t},_clear:function(){this.shadowCtx.clearRect(0,0,this._width,this._height),this.ctx.clearRect(0,0,this._width,this._height)},_setStyles:function(e){this._blur=0==e.blur?0:e.blur||e.defaultBlur,e.backgroundColor&&(this.canvas.style.backgroundColor=e.backgroundColor),this._width=this.canvas.width=this.shadowCanvas.width=e.width||this._width,this._height=this.canvas.height=this.shadowCanvas.height=e.height||this._height,this._opacity=255*(e.opacity||0),this._maxOpacity=255*(e.maxOpacity||e.defaultMaxOpacity),this._minOpacity=255*(e.minOpacity||e.defaultMinOpacity),this._useGradientOpacity=!!e.useGradientOpacity},_drawAlpha:function(e){var a=this._min=e.min,i=this._max=e.max,n=(e=e.data||[],e.length),s=1-this._blur;while(n--){var r,o=e[n],l=o.x,c=o.y,d=o.radius,m=Math.min(o.value,i),p=l-d,h=c-d,u=this.shadowCtx;this._templates[d]?r=this._templates[d]:this._templates[d]=r=t(d,s);var g=(m-a)/(i-a);u.globalAlpha=g<.01?.01:g,u.drawImage(r,p,h),pthis._renderBoundaries[2]&&(this._renderBoundaries[2]=p+2*d),h+2*d>this._renderBoundaries[3]&&(this._renderBoundaries[3]=h+2*d)}},_colorize:function(){var e=this._renderBoundaries[0],t=this._renderBoundaries[1],a=this._renderBoundaries[2]-e,i=this._renderBoundaries[3]-t,n=this._width,s=this._height,r=this._opacity,o=this._maxOpacity,l=this._minOpacity,c=this._useGradientOpacity;e<0&&(e=0),t<0&&(t=0),e+a>n&&(a=n-e),t+i>s&&(i=s-t);for(var d=this.shadowCtx.getImageData(e,t,a,i),m=d.data,p=m.length,h=this._palette,u=3;u0?r:b>0,t},getDataURL:function(){return this.canvas.toDataURL()}},i}(),i=function(){var t=!1;return"canvas2d"===e["defaultRenderer"]&&(t=a),t}(),n={merge:function(){for(var e={},t=arguments.length,a=0;a(Object(s["openBlock"])(),Object(s["createElementBlock"])("span",{class:Object(s["normalizeClass"])(["btn-flat",{visActive:t.key===e.heatmapType,["heatmapType"+t.key]:!0}]),onClick:a=>e.changeHeatmapType(t.key),key:t.key},Object(s["toDisplayString"])(t.name),11,d))),128)),Object(s["createElementVNode"])("h4",m,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_DeviceType")),1),(Object(s["openBlock"])(!0),Object(s["createElementBlock"])(s["Fragment"],null,Object(s["renderList"])(e.deviceTypesWithSamples,t=>(Object(s["openBlock"])(),Object(s["createElementBlock"])("span",{class:Object(s["normalizeClass"])(["btn-flat",{visActive:t.key===e.deviceType,["deviceType"+t.key]:!0}]),title:t.tooltip,onClick:a=>e.changeDeviceType(t.key),key:t.key},[Object(s["createElementVNode"])("img",{height:"15",src:t.logo,alt:`${e.translate("DevicesDetection_Device")} ${t.name}`},null,8,h),Object(s["createTextVNode"])(),Object(s["createElementVNode"])("span",u,Object(s["toDisplayString"])(t.numSamples),1)],10,p))),128)),Object(s["createElementVNode"])("div",g,[Object(s["createElementVNode"])("h4",null,Object(s["toDisplayString"])(e.translate("Installation_Legend")),1),Object(s["createElementVNode"])("div",b,[v,Object(s["createElementVNode"])("img",{class:"gradient",alt:"gradient",src:e.gradientImgData},null,8,O),j])]),Object(s["createElementVNode"])("div",f,[Object(s["createElementVNode"])("span",{style:{"margin-left":"2.5rem","margin-right":"13.5px"},textContent:Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_Width"))},null,8,y),Object(s["createVNode"])(P,{uicontrol:"select",name:"iframewidth","model-value":e.customIframeWidth,"onUpdate:modelValue":t[0]||(t[0]=t=>{e.customIframeWidth=t,e.changeIframeWidth(e.customIframeWidth,!0)}),options:e.iframeWidthOptions},null,8,["model-value","options"])])]),Object(s["createElementVNode"])("div",S,[Object(s["createElementVNode"])("div",H,[Object(s["createElementVNode"])("div",_,null,512),V]),Object(s["withDirectives"])(Object(s["createElementVNode"])("div",{class:"hsrLoadingOuter",style:Object(s["normalizeStyle"])([{height:"400px"},{width:e.iframeWidth+"px"}])},[N,Object(s["createElementVNode"])("div",E,[Object(s["createElementVNode"])("div",R,Object(s["toDisplayString"])(e.translate("General_Loading")),1)])],4),[[s["vShow"],e.isLoading]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("div",{class:"aboveFoldLine",title:e.translate("HeatmapSessionRecording_AvgAboveFoldDescription"),style:Object(s["normalizeStyle"])({width:e.iframeWidth+"px",top:e.avgFold+"px"})},[Object(s["createElementVNode"])("div",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_AvgAboveFoldTitle",e.avgFold)),1)],12,w),[[s["vShow"],e.avgFold]]),e.embedUrl?(Object(s["openBlock"])(),Object(s["createElementBlock"])("iframe",{key:0,id:"recordingPlayer",ref:"recordingPlayer",sandbox:"allow-scripts allow-same-origin",referrerpolicy:"no-referrer",onLoad:t[1]||(t[1]=t=>e.onLoaded()),height:"400",src:e.embedUrl,width:e.iframeWidth},null,40,k)):Object(s["createCommentVNode"])("",!0)],512),Object(s["withDirectives"])(Object(s["createElementVNode"])("div",x,[Object(s["createVNode"])(B,{style:{display:"block !important"},loading:e.isLoading,onClick:t[2]||(t[2]=t=>e.deleteScreenshot()),value:e.translate("HeatmapSessionRecording_DeleteScreenshot")},null,8,["loading","value"])],512),[[s["vShow"],e.showDeleteScreenshot]]),Object(s["createElementVNode"])("div",C,[Object(s["createElementVNode"])("h2",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_DeleteHeatmapScreenshotConfirm")),1),Object(s["createElementVNode"])("input",{role:"yes",type:"button",value:e.translate("General_Yes")},null,8,T),Object(s["createElementVNode"])("input",{role:"no",type:"button",value:e.translate("General_No")},null,8,D)],512),Object(s["createVNode"])(A,{ref:"tooltip","click-count":e.clickCount,"click-rate":e.clickRate,"is-moves":1===e.heatmapType},null,8,["click-count","click-rate","is-moves"])])}var P=a("246e"),B=a.n(P),A=a("19dc"),U=a("a5a2");
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+function L(e){return e&&e.contentWindow?e.contentWindow:e&&e.contentDocument&&e.contentDocument.defaultView?e.contentDocument.defaultView:void 0}
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */function I(e,t){let a=null;return(i,n)=>(a&&(a.abort(),a=null),a=new AbortController,A["AjaxHelper"].post(Object.assign(Object.assign({},i),{},{method:e}),n,Object.assign(Object.assign({},t),{},{abortController:a})).finally(()=>{a=null}))}const F={class:"tooltip-item"},W={class:"tooltip-label"},q={class:"tooltip-value"},z={class:"tooltip-item"},$={class:"tooltip-label"},G={class:"tooltip-value"};function J(e,t,a,i,n,r){return Object(s["withDirectives"])((Object(s["openBlock"])(),Object(s["createElementBlock"])("div",{ref:"tooltipRef",class:"tooltip",style:Object(s["normalizeStyle"])(e.tooltipStyle)},[Object(s["createElementVNode"])("div",F,[Object(s["createElementVNode"])("span",W,Object(s["toDisplayString"])(e.getClickCountTranslation),1),Object(s["createElementVNode"])("span",q,Object(s["toDisplayString"])(e.getClickCount),1)]),Object(s["createElementVNode"])("div",z,[Object(s["createElementVNode"])("span",$,Object(s["toDisplayString"])(e.getClickRateTranslation),1),Object(s["createElementVNode"])("span",G,Object(s["toDisplayString"])(e.getClickRate),1)])],4)),[[s["vShow"],e.visible]])}var X=Object(s["defineComponent"])({props:{clickCount:{type:Number,required:!0},clickRate:{type:Number,required:!0},isMoves:{type:Boolean,required:!1,default:!1}},setup(){const e=Object(s["reactive"])({visible:!1,position:{top:0,left:0}}),t=Object(s["ref"])(null),a=Object(s["computed"])(()=>({top:e.position.top+"px",left:e.position.left+"px",position:"absolute",zIndex:1e3}));function i(a){const i=window.scrollY||document.documentElement.scrollTop,n=window.scrollX||document.documentElement.scrollLeft;e.position.top=a.clientY+i+10,e.position.left=a.clientX+n+10,e.visible=!0,Object(s["nextTick"])(()=>{const s=t.value;if(s){const{innerWidth:t,innerHeight:r}=window,o=s.getBoundingClientRect();o.right>t&&(e.position.left=a.clientX+n-o.width-10),o.bottom>r&&(e.position.top=a.clientY+i-o.height-10);const l=s.getBoundingClientRect();l.left<0&&(e.position.left=n+10),l.top<0&&(e.position.top=i+10)}})}function n(){e.visible=!1}return Object.assign(Object.assign({},Object(s["toRefs"])(e)),{},{tooltipRef:t,show:i,hide:n,tooltipStyle:a,translate:A["translate"]})},computed:{getClickCount(){return A["NumberFormatter"].formatNumber(this.clickCount)},getClickRate(){return A["NumberFormatter"].formatPercent(this.clickRate)},getClickCountTranslation(){const e=this.isMoves?"HeatmapSessionRecording_Moves":"HeatmapSessionRecording_Clicks";return Object(A["translate"])(e)},getClickRateTranslation(){const e=this.isMoves?"HeatmapSessionRecording_MoveRate":"HeatmapSessionRecording_ClickRate";return Object(A["translate"])(e)}}});X.render=J;var Y=X;const{$:K}=window,Q=1,Z=2,ee=3;let te=32e3;const ae=String(window.navigator.userAgent).toLowerCase();function ie(e,t,a){const i=K(e);i.css("height","400px");const n=a.getIframeHeight();i.css("height",n+"px"),K(t).css("height",n+"px").css("width",i.width()+"px").empty();const s=Math.ceil(n/te);for(let r=1;r<=s;r+=1){let e=te;r===s&&(e=n%te),K(t).append(`
`),K(t).find("#heatmap"+r).css({height:e+"px"})}return s}function ne(e,t,a,i){const n=K(t);n.css("height","400px");const s=a.getIframeHeight();n.css("height",s+"px");const r=1e3,o=s/r,l=i.reduce((e,t)=>e+parseInt(t.value,10),0),c=[];let d=0,m=null,p=100,h=0;function u(e,t,a,i,n){return i+(e-t)/(a-t)*(n-i)}function g(e,t,a){if(t===a||!t&&!a)return[255,255,0];const i=u(e,t,a,0,255),n=(a-t)/5;return i>204?[255,u(e,a-n,a,255,0),0]:i>153?[u(e,a-2*n,a-n,0,255),255,0]:i>102?[0,255,u(e,a-3*n,a-2*n,255,0)]:i>51?[0,u(e,a-4*n,a-3*n,0,255),255]:[u(e,t,a-4*n,255,0),0,255]}if(i.forEach(e=>{const t=parseInt(e.value,10),a=parseInt(e.label,10),i=Math.round(a*o);m&&m.position===i?d+=t:(0!==l&&(p=(l-d)/l*100),d+=t,m={percentageValue:10*parseFloat(p.toFixed(1)),position:h,percent:p.toFixed(1)},c.push(m)),h=i}),c.length){const e=c.some(e=>0===e.position);e||c.unshift({percent:"100.0",percentageValue:1e3,position:0})}else c.push({percent:"0",percentageValue:0,position:0});let b=0;const v=1e3;c&&c.length&&c[0]&&(b=c[c.length-1].percentageValue);const O=n.width();let j=null;for(let f=0;f`)}K(".scrollHeatmapLeaf",e).tooltip({track:!0,items:"*",tooltipClass:"heatmapTooltip",show:!1,hide:!1}),K(".legend-area .min").text((b/10).toFixed(1)+"%"),K(".legend-area .max").text((v/10).toFixed(1)+"%")}function se(e,t,a,i){const n=ie(e,t,a),s=document.createElement("canvas");s.width=100,s.height=10;const r=document.querySelector(".legend-area .min"),o=document.querySelector(".legend-area .max"),l=document.querySelector(".legend-area .gradient"),c=s.getContext("2d");let d={};function m(e){if(r.innerHTML=""+e.min,o.innerHTML=""+e.max,e.gradient&&e.gradient!==d){d=e.gradient;const t=c.createLinearGradient(0,0,100,1);Object.keys(d).forEach(e=>{t.addColorStop(parseFloat(e),d[e])}),c.fillStyle=t,c.fillRect(0,0,100,10),l.src=s.toDataURL()}}const p=[];for(let h=1;h<=n;h+=1){const e={min:i.min,max:i.max,data:[]},t={container:document.getElementById("heatmap"+h),radius:10,maxOpacity:.5,minOpacity:0,blur:.75};if(1===h&&(t.onExtremaChange=m),i&&i.data&&i.data.length>=2e4?t.radius=8:i&&i.data&&i.data.length>=2e3&&(t.radius=9),1===n)e.data=i.data;else{const t=(h-1)*te,a=t+te-1;i.data.forEach(i=>{if(i.y>=t&&i.y<=a){const a=Object.assign(Object.assign({},i),{},{y:i.y-t});e.data.push(a)}})}const a=B.a.create(t);a.setData(e),p.push(a)}return p}ae.match(/(iPod|iPhone|iPad|Android|IEMobile|Windows Phone)/i)?te=2e3:(ae.indexOf("msie ")>0||ae.indexOf("trident/")>0||ae.indexOf("edge")>0)&&(te=8e3);var re=Object(s["defineComponent"])({props:{idSiteHsr:{type:Number,required:!0},deviceTypes:{type:Array,required:!0},heatmapTypes:{type:Array,required:!0},breakpointMobile:{type:Number,required:!0},breakpointTablet:{type:Number,required:!0},offsetAccuracy:{type:Number,required:!0},heatmapPeriod:{type:String,required:!0},heatmapDate:{type:String,required:!0},url:{type:String,required:!0},isActive:Boolean,numSamples:{type:Object,required:!0},excludedElements:{type:String,required:!0},createdDate:{type:String,required:!0},desktopPreviewSize:{type:Number,required:!0},iframeResolutionsValues:{type:Object,required:!0}},components:{Field:U["Field"],SaveButton:U["SaveButton"],Tooltip:Y},data(){return{isLoading:!1,iframeWidth:this.desktopPreviewSize,customIframeWidth:this.desktopPreviewSize,avgFold:0,heatmapType:this.heatmapTypes[0].key,deviceType:this.deviceTypes[0].key,iframeResolutions:this.iframeResolutionsValues,actualNumSamples:this.numSamples,dataCoordinates:[],currentElement:null,totalClicks:0,tooltipShowTimeoutId:null,clickCount:0,clickRate:0}},setup(e){const t=Object(s["ref"])(null);let a=null;const i=new Promise(e=>{a=e});let n=null;const r=t=>(n||(n=L(t).recordingFrame,n.excludeElements(e.excludedElements),n.addClass("html","piwikHeatmap"),n.addClass("html","matomoHeatmap"),n.addWorkaroundForSharepointHeatmaps()),n),o=Object(s["ref"])(null),l=(e,t,a,i)=>{o.value=se(e,t,a,i)};return{iframeLoadedPromise:i,onLoaded:a,getRecordedHeatmap:I("HeatmapSessionRecording.getRecordedHeatmap"),getRecordedHeatmapMetadata:I("HeatmapSessionRecording.getRecordedHeatmapMetadata"),getRecordingIframe:r,heatmapInstances:o,renderHeatmap:l,tooltip:t}},created(){-1===this.iframeResolutions.indexOf(this.breakpointMobile)&&this.iframeResolutions.push(this.breakpointMobile),-1===this.iframeResolutions.indexOf(this.breakpointTablet)&&this.iframeResolutions.push(this.breakpointTablet),this.iframeResolutions=this.iframeResolutions.sort((e,t)=>e-t),this.fetchHeatmap(),A["Matomo"].postEvent("hidePeriodSelector")},watch:{isLoading(){if(!0===this.isLoading)return;const e=window.document.getElementById("heatmapContainer");e&&(e.addEventListener("mouseleave",e=>{this.tooltipShowTimeoutId&&(clearTimeout(this.tooltipShowTimeoutId),this.tooltipShowTimeoutId=null),this.currentElement=null,this.handleTooltip(e,0,0,"hide");const t=window.document.getElementById("highlightDiv");t&&(t.hidden=!0)}),e.addEventListener("mousemove",e=>{this.handleMouseMove(e)}))}},beforeUnmount(){this.removeScrollHeatmap()},methods:{removeScrollHeatmap(){const e=this.$refs.iframeRecordingContainer;K(e).find(".scrollHeatmapLeaf").remove()},deleteScreenshot(){A["Matomo"].helper.modalConfirm(this.$refs.confirmDeleteHeatmapScreenshot,{yes:()=>{this.isLoading=!0,A["AjaxHelper"].fetch({method:"HeatmapSessionRecording.deleteHeatmapScreenshot",idSiteHsr:this.idSiteHsr}).then(()=>{this.isLoading=!1,window.location.reload()})}})},fetchHeatmap(){if(this.removeScrollHeatmap(),this.heatmapInstances){const e=this.heatmapInstances;e.forEach(e=>{e.setData({max:1,min:0,data:[]})})}this.isLoading=!0,this.avgFold=0;const e=A["MatomoUrl"].parsed.value.segment?decodeURIComponent(A["MatomoUrl"].parsed.value.segment):void 0,t={idSiteHsr:this.idSiteHsr,heatmapType:this.heatmapType,deviceType:this.deviceType,period:this.heatmapPeriod,date:this.heatmapDate,filter_limit:-1,segment:e},a=this.getRecordedHeatmap(t),i=this.getRecordedHeatmapMetadata(t);Promise.all([a,i,this.iframeLoadedPromise]).then(e=>{const t=this.$refs.recordingPlayer,a=this.getRecordingIframe(t);ie(this.$refs.recordingPlayer,this.$refs.heatmapContainer,a),this.removeScrollHeatmap();const i=e[0],n=e[1];if(Array.isArray(n)&&n[0]?[this.actualNumSamples]=n:this.actualNumSamples=n,this.isLoading=!1,this.isScrollHeatmapType)ne(this.$refs.iframeRecordingContainer,t,a,i);else{var s;const e={min:0,max:0,data:[]};for(let t=0;t{null!==e&&void 0!==e&&e.value&&parseInt(e.value,10)>1&&(t+=1)}),t/e.data.length>=.1&&e.data.length>120?e.max=2:e.max=1}else{const t=10,a={};if(e.data.forEach(i=>{if(!i||!i.value)return;let n=parseInt(i.value,10);n>e.max&&(e.max=n),n>t&&(n=t);const s=""+n;s in a?a[s]+=1:a[s]=0}),e.max>t){let i=0;for(let n=t;n>1;n-=1){const t=""+n;if(t in a&&(i+=a[t]),i/e.data.length>=.2){e.max=n;break}}if(e.max>t){e.max=5;for(let t=5;t>0;t-=1){const i=""+t;if(i in a){e.max=t;break}}}}}if(this.renderHeatmap(this.$refs.recordingPlayer,this.$refs.heatmapContainer,a,e),null!==(s=this.actualNumSamples)&&void 0!==s&&s["avg_fold_device_"+this.deviceType]){const e=this.actualNumSamples["avg_fold_device_"+this.deviceType],t=a.getIframeHeight();t&&(this.avgFold=parseInt(""+e/100*t,10))}}}).finally(()=>{this.isLoading=!1})},changeDeviceType(e){this.deviceType=e,this.deviceType===Q?this.changeIframeWidth(this.desktopPreviewSize,!1):this.deviceType===Z?this.changeIframeWidth(this.breakpointTablet||960,!1):this.deviceType===ee&&this.changeIframeWidth(this.breakpointMobile||600,!1)},changeIframeWidth(e,t){this.iframeWidth=e,this.customIframeWidth=this.iframeWidth,this.totalClicks=0,this.dataCoordinates=[],this.fetchHeatmap(),t&&A["Matomo"].helper.lazyScrollToContent()},changeHeatmapType(e){this.heatmapType=e,this.totalClicks=0,this.clickCount=0,this.clickRate=0,this.dataCoordinates=[],this.fetchHeatmap()},handleMouseMove(e){const t=window.document.getElementById("highlightDiv");if(!t)return;this.tooltipShowTimeoutId&&(clearTimeout(this.tooltipShowTimeoutId),this.tooltipShowTimeoutId=null,this.currentElement=null),t.hidden||this.handleTooltip(e,0,0,"move");const a=this.lookUpRecordedElementAtEventLocation(e);if(!a||a===this.currentElement)return;this.handleTooltip(e,0,0,"hide"),t.hidden=!0;const i=a.getBoundingClientRect();let n=0;this.dataCoordinates.forEach(e=>{e.yi.bottom||e.xi.right||(n+=parseInt(e.value,10))}),this.tooltipShowTimeoutId=setTimeout(()=>{this.currentElement=a,t.hidden=!1;const i=this.totalClicks?Math.round(n/this.totalClicks*1e4)/100:0,s=a.getBoundingClientRect();t.style.top=s.top+"px",t.style.left=s.left+"px",t.style.width=s.width+"px",t.style.height=s.height+"px",this.handleTooltip(e,n,i,"show"),this.tooltipShowTimeoutId=null},100)},lookUpRecordedElementAtEventLocation(e){const t=e.target;if(!t)return null;const a=window.document.getElementById("recordingPlayer");if(!a)return null;const i=a.contentWindow?a.contentWindow.document:a.contentDocument;if(!i)return null;const n=t.getBoundingClientRect();return i.elementFromPoint(e.clientX-n.left,e.clientY-n.top)},handleTooltip(e,t,a,i){this.tooltip&&("show"===i?(this.clickCount=t,this.clickRate=a,this.tooltip.show(e)):"move"===i?this.tooltip.show(e):this.tooltip.hide())}},computed:{isScrollHeatmapType(){return 3===this.heatmapType},tokenAuth(){return A["MatomoUrl"].parsed.value.token_auth},embedUrl(){return"?"+A["MatomoUrl"].stringify({module:"HeatmapSessionRecording",action:"embedPage",idSite:A["Matomo"].idSite,idSiteHsr:this.idSiteHsr,token_auth:this.tokenAuth||void 0})},iframeWidthOptions(){return this.iframeResolutions.map(e=>({key:e,value:e+"px"}))},recordedSamplesSince(){const e=Object(A["translate"])("HeatmapSessionRecording_HeatmapXRecordedSamplesSince",`${this.actualNumSamples.nb_samples_device_all} `,this.createdDate),t=Object(A["externalLink"])("https://matomo.org/faq/heatmap-session-recording/troubleshooting-heatmaps/"),a=Object(A["translate"])("HeatmapSessionRecording_HeatmapTroubleshoot",t,"
");return`${e} ${a}`},deviceTypesWithSamples(){return this.deviceTypes.map(e=>{let t;t=this.actualNumSamples["nb_samples_device_"+e.key]?this.actualNumSamples["nb_samples_device_"+e.key]:0;const a=Object(A["translate"])("HeatmapSessionRecording_XSamples",`${e.name} - ${t}`);return Object.assign(Object.assign({},e),{},{numSamples:t,tooltip:a})})},hasWriteAccess(){return!(null===A["Matomo"]||void 0===A["Matomo"]||!A["Matomo"].heatmapWriteAccess)},showDeleteScreenshot(){return this.isActive&&this.hasWriteAccess},gradientImgData(){return"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAKCAYAAABCHPt+AAAAnklEQVRYR+2WQQqDQBAES5wB/f8/Y05RcMWwSu6JIT0Dm4WlH1DUdHew7/z6WYFhhnGRpnlhAEaQpi/ADbh/np0MiBhGhW+2ymFU+DZfg1EhaoB4jCFuMYYcQKZrXwPEVvm5Og0pcYakBvI35G1jNIZ4jCHexxjSpz9ZFUjAynLbpOvqteaODkm9sloz5JF+ZTVmSAWSu9Qb65AvgDwBQoLgVDlWfAQAAAAASUVORK5CYII="}}});re.render=M;var oe=re;const le={class:"sessionRecordingPlayer"},ce={class:"controls"},de={class:"playerActions"},me=["title"],pe=["title"],he=["title"],ue=["title"],ge=["title"],be=["title"],ve=["title"],Oe=["title"],je={version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 768 768"},fe=Object(s["createElementVNode"])("path",{d:"M480 576.5v-321h-64.5v129h-63v-129h-64.5v192h127.5v129h64.5zM607.5 127.999c34.5 0\n 64.5 30 64.5 64.5v447c0 34.5-30 64.5-64.5 64.5h-447c-34.5\n 0-64.5-30-64.5-64.5v-447c0-34.5 30-64.5 64.5-64.5h447z"},null,-1),ye=[fe],Se={version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 768 768"},He=Object(s["createElementVNode"])("path",{d:"M448.5 576.5v-321h-129v64.5h64.5v256.5h64.5zM607.5 127.999c34.5 0 64.5 30 64.5\n 64.5v447c0 34.5-30 64.5-64.5 64.5h-447c-34.5 0-64.5-30-64.5-64.5v-447c0-34.5\n 30-64.5 64.5-64.5h447z"},null,-1),_e=[He],Ve={version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 768 768"},Ne=Object(s["createElementVNode"])("path",{d:"M480 384.5v-64.5c0-36-30-64.5-64.5-64.5h-127.5v64.5h127.5v64.5h-63c-34.5 0-64.5\n 27-64.5 63v129h192v-64.5h-127.5v-64.5h63c34.5 0 64.5-27 64.5-63zM607.5 127.999c34.5\n 0 64.5 30 64.5 64.5v447c0 34.5-30 64.5-64.5 64.5h-447c-34.5\n 0-64.5-30-64.5-64.5v-447c0-34.5 30-64.5 64.5-64.5h447z"},null,-1),Ee=[Ne],Re={version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 768 768"},we=Object(s["createElementVNode"])("path",{d:"M480 320v-64.5h-127.5c-34.5 0-64.5 28.5-64.5 64.5v192c0 36 30 64.5 64.5\n 64.5h63c34.5 0 64.5-28.5 64.5-64.5v-64.5c0-36-30-63-64.5-63h-63v-64.5h127.5zM607.5\n 127.999c34.5 0 64.5 30 64.5 64.5v447c0 34.5-30 64.5-64.5 64.5h-447c-34.5\n 0-64.5-30-64.5-64.5v-447c0-34.5 30-64.5 64.5-64.5h447zM352.5 512v-64.5h63v64.5h-63z"},null,-1),ke=[we],xe=["title"],Ce=Object(s["createElementVNode"])("svg",{version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 768 768"},[Object(s["createElementVNode"])("path",{d:"M223.5 415.5h111l-64.5-63h-46.5v63zM72 72l624 624-42 40.5-88.5-90c-51 36-114\n 57-181.5 57-177 0-319.5-142.5-319.5-319.5 0-67.5 21-130.5 57-181.5l-90-88.5zM544.5\n 352.5h-111l-231-231c51-36 114-57 181.5-57 177 0 319.5 142.5 319.5 319.5 0 67.5-21\n 130.5-57 181.5l-148.5-150h46.5v-63z"})],-1),Te=[Ce],De=["title"],Me=Object(s["createElementVNode"])("svg",{version:"1.1",xmlns:"http://www.w3.org/2000/svg",width:"22",height:"22",viewBox:"0 0 768 768"},[Object(s["createElementVNode"])("path",{d:"M544.5 609v-129h63v192h-384v96l-127.5-127.5 127.5-127.5v96h321zM223.5\n 288v129h-63v-192h384v-96l127.5 127.5-127.5 127.5v-96h-321z"})],-1),Pe=[Me],Be={class:"duration"},Ae={class:"playerHelp"},Ue=Object(s["createElementVNode"])("span",{class:"clickEvent"},null,-1),Le=Object(s["createElementVNode"])("span",{class:"moveEvent"},null,-1),Ie=Object(s["createElementVNode"])("span",{class:"scrollEvent"},null,-1),Fe=Object(s["createElementVNode"])("span",{class:"resizeEvent"},null,-1),We=Object(s["createElementVNode"])("span",{class:"formChange"},null,-1),qe=Object(s["createElementVNode"])("span",{class:"mutationEvent"},null,-1),ze=Object(s["createElementVNode"])("br",{style:{clear:"right"}},null,-1),$e=["title"],Ge=Object(s["createElementVNode"])("br",null,null,-1),Je=Object(s["createElementVNode"])("div",{class:"loadingUnderlay"},null,-1),Xe={class:"valign-wrapper loadingInner"},Ye={class:"loadingContent"},Ke=["src","width","height"];function Qe(e,t,a,i,n,r){return Object(s["openBlock"])(),Object(s["createElementBlock"])("div",le,[Object(s["createElementVNode"])("div",ce,[Object(s["createElementVNode"])("span",de,[Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"playerAction icon-skip-previous",title:e.skipPreviousButtonTitle,onClick:t[0]||(t[0]=t=>e.loadNewRecording(e.previousRecordingId))},null,8,me),[[s["vShow"],e.previousRecordingId]]),Object(s["createElementVNode"])("span",{class:"playerAction icon-fast-rewind",title:e.translate("HeatmapSessionRecording_PlayerRewindFast",10,"J"),onClick:t[1]||(t[1]=t=>e.jumpRelative(10,!1))},null,8,pe),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"playerAction icon-play",title:e.translate("HeatmapSessionRecording_PlayerPlay","K"),onClick:t[2]||(t[2]=t=>e.play())},null,8,he),[[s["vShow"],!e.isPlaying&&!e.isFinished]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"playerAction icon-replay",title:e.translate("HeatmapSessionRecording_PlayerReplay","K"),onClick:t[3]||(t[3]=t=>e.replay())},null,8,ue),[[s["vShow"],!e.isPlaying&&e.isFinished]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"playerAction icon-pause",title:e.translate("HeatmapSessionRecording_PlayerPause","K"),onClick:t[4]||(t[4]=t=>e.pause())},null,8,ge),[[s["vShow"],e.isPlaying]]),Object(s["createElementVNode"])("span",{class:"playerAction icon-fast-forward",title:e.translate("HeatmapSessionRecording_PlayerForwardFast",10,"L"),onClick:t[5]||(t[5]=t=>e.jumpRelative(10,!0))},null,8,be),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"playerAction icon-skip-next",title:e.translate("HeatmapSessionRecording_PlayerPageViewNext",e.nextRecordingInfo,"N"),onClick:t[6]||(t[6]=t=>e.loadNewRecording(e.nextRecordingId))},null,8,ve),[[s["vShow"],e.nextRecordingId]]),Object(s["createElementVNode"])("span",{class:"changeReplaySpeed",title:e.translate("HeatmapSessionRecording_ChangeReplaySpeed","S"),onClick:t[7]||(t[7]=t=>e.increaseReplaySpeed())},[Object(s["withDirectives"])((Object(s["openBlock"])(),Object(s["createElementBlock"])("svg",je,ye,512)),[[s["vShow"],4===e.actualReplaySpeed]]),Object(s["withDirectives"])((Object(s["openBlock"])(),Object(s["createElementBlock"])("svg",Se,_e,512)),[[s["vShow"],1===e.actualReplaySpeed]]),Object(s["withDirectives"])((Object(s["openBlock"])(),Object(s["createElementBlock"])("svg",Ve,Ee,512)),[[s["vShow"],2===e.actualReplaySpeed]]),Object(s["withDirectives"])((Object(s["openBlock"])(),Object(s["createElementBlock"])("svg",Re,ke,512)),[[s["vShow"],6===e.actualReplaySpeed]])],8,Oe),Object(s["createElementVNode"])("span",{class:Object(s["normalizeClass"])(["toggleSkipPause",{active:e.actualSkipPausesEnabled}]),title:e.translate("HeatmapSessionRecording_ClickToSkipPauses",e.skipPausesEnabledText,"B"),onClick:t[8]||(t[8]=t=>e.toggleSkipPauses())},Te,10,xe),Object(s["createElementVNode"])("span",{class:Object(s["normalizeClass"])(["toggleAutoPlay",{active:e.actualAutoPlayEnabled}]),title:e.translate("HeatmapSessionRecording_AutoPlayNextPageview",e.autoplayEnabledText,"A"),onClick:t[9]||(t[9]=t=>e.toggleAutoPlay())},Pe,10,De),Object(s["createElementVNode"])("span",Be,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_PlayerDurationXofY",e.positionPretty,e.durationPretty)),1)]),Object(s["createElementVNode"])("div",Ae,[Object(s["createElementVNode"])("ul",null,[Object(s["createElementVNode"])("li",null,[Ue,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_ActivityClick")),1)]),Object(s["createElementVNode"])("li",null,[Le,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_ActivityMove")),1)]),Object(s["createElementVNode"])("li",null,[Ie,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_ActivityScroll")),1)]),Object(s["createElementVNode"])("li",null,[Fe,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_ActivityResize")),1)]),Object(s["createElementVNode"])("li",null,[We,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_ActivityFormChange")),1)]),Object(s["createElementVNode"])("li",null,[qe,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_ActivityPageChange")),1)])])]),ze]),Object(s["createElementVNode"])("div",{class:"timelineOuter",onClick:t[10]||(t[10]=t=>e.seekEvent(t)),style:Object(s["normalizeStyle"])({width:e.replayWidth+"px"})},[Object(s["createElementVNode"])("div",{class:"timelineInner",style:Object(s["normalizeStyle"])({width:e.progress+"%"})},null,4),(Object(s["openBlock"])(!0),Object(s["createElementBlock"])(s["Fragment"],null,Object(s["renderList"])(e.clues,(e,t)=>(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",{title:e.title,class:Object(s["normalizeClass"])(e.type),style:Object(s["normalizeStyle"])({left:e.left+"%"}),key:t},null,14,$e))),128))],4),Ge,Object(s["withDirectives"])(Object(s["createElementVNode"])("div",{class:"hsrLoadingOuter",style:Object(s["normalizeStyle"])({width:e.replayWidth+"px",height:e.replayHeight+"px"})},[Je,Object(s["createElementVNode"])("div",Xe,[Object(s["createElementVNode"])("div",Ye,Object(s["toDisplayString"])(e.translate("General_Loading")),1)])],4),[[s["vShow"],e.isLoading]]),Object(s["createElementVNode"])("div",{class:"replayContainerOuter",onClick:t[12]||(t[12]=t=>e.togglePlay()),style:Object(s["normalizeStyle"])({height:e.replayHeight+"px",width:e.replayWidth+"px"})},[Object(s["createElementVNode"])("div",{class:"replayContainerInner",style:Object(s["normalizeStyle"])([{"transform-origin":"0 0"},{transform:`scale(${e.replayScale})`,"margin-left":e.replayMarginLeft+"px"}])},[e.embedUrl?(Object(s["openBlock"])(),Object(s["createElementBlock"])("iframe",{key:0,id:"recordingPlayer",ref:"recordingPlayer",onLoad:t[11]||(t[11]=t=>e.onLoaded()),scrolling:"no",sandbox:"allow-scripts allow-same-origin",referrerpolicy:"no-referrer",src:e.embedUrl,width:e.recording.viewport_w_px,height:e.recording.viewport_h_px},null,40,Ke)):Object(s["createCommentVNode"])("",!0)],4)],4)])}const Ze=20,et=1,tt=2,at=3,it=4,nt=5,st=6,rt=9,ot=10,lt=12,ct={[tt]:"clickEvent",[et]:"moveEvent",[at]:"scrollEvent",[lt]:"scrollEvent",[it]:"resizeEvent",[rt]:"formChange",[ot]:"formChange",[nt]:"mutationEvent",[st]:"mutationEvent"},dt={[tt]:Object(A["translate"])("HeatmapSessionRecording_ActivityClick"),[et]:Object(A["translate"])("HeatmapSessionRecording_ActivityMove"),[at]:Object(A["translate"])("HeatmapSessionRecording_ActivityScroll"),[lt]:Object(A["translate"])("HeatmapSessionRecording_ActivityScroll"),[it]:Object(A["translate"])("HeatmapSessionRecording_ActivityResize"),[rt]:Object(A["translate"])("HeatmapSessionRecording_ActivityFormChange"),[ot]:Object(A["translate"])("HeatmapSessionRecording_ActivityFormChange"),[nt]:Object(A["translate"])("HeatmapSessionRecording_ActivityPageChange"),[st]:Object(A["translate"])("HeatmapSessionRecording_ActivityPageChange")},mt='\n\n',{$:pt,Mousetrap:ht}=window;function ut(e){return"number"===typeof e?e:parseInt(e,10)}function gt(e){if(null!==e&&void 0!==e&&e.event_type)return ut(e.event_type)}function bt(e){const t=Math.floor(e/1e3);let a=Math.floor(t/60),i=t%60;return a<10&&(a="0"+a),i<10&&(i="0"+i),`${a}:${i}`}var vt=Object(s["defineComponent"])({props:{offsetAccuracy:{type:Number,required:!0},scrollAccuracy:{type:Number,required:!0},autoPlayEnabled:Boolean,skipPausesEnabled:Boolean,replaySpeed:{type:Number,default:1}},data(){return{isPlaying:!1,progress:0,isFinished:!1,isLoading:!0,seekTimeout:null,lastFramePainted:0,recording:JSON.parse(JSON.stringify(window.sessionRecordingData)),positionPretty:"00:00",previousRecordingId:null,previousRecordingInfo:null,nextRecordingId:null,nextRecordingInfo:null,frame:0,hasFoundPrevious:!1,hasFoundNext:!1,videoPlayerInterval:null,lastCanvasCoordinates:!1,actualAutoPlayEnabled:!!this.autoPlayEnabled,replayWidth:0,replayHeight:0,replayScale:0,replayMarginLeft:0,seek:e=>e,actualSkipPausesEnabled:!!this.skipPausesEnabled,actualReplaySpeed:this.replaySpeed}},setup(){const e=Object(s["ref"])(!1);let t=null;const a=new Promise(a=>{t=a,e.value=!0}),i=()=>{setTimeout(()=>{t("loaded")},500)};return{iframeLoadedPromise:a,onLoaded:i,iframeLoaded:e}},created(){this.recording.duration=ut(this.recording.duration),this.recording.pageviews.forEach(e=>{e&&e.idloghsr&&(""+e.idloghsr===""+this.recording.idLogHsr?this.hasFoundPrevious=!0:this.hasFoundPrevious?this.hasFoundNext||(this.hasFoundNext=!0,this.nextRecordingId=e.idloghsr,this.nextRecordingInfo=[e.label,e.server_time_pretty,e.time_on_page_pretty].join(" - ")):(this.previousRecordingId=e.idloghsr,this.previousRecordingInfo=[e.label,e.server_time_pretty,e.time_on_page_pretty].join(" - ")))})},mounted(){ht.bind(["space","k"],()=>{this.togglePlay()}),ht.bind("0",()=>{this.isFinished&&this.replay()}),ht.bind("p",()=>{this.loadNewRecording(this.previousRecordingId)}),ht.bind("n",()=>{this.loadNewRecording(this.nextRecordingId)}),ht.bind("s",()=>{this.increaseReplaySpeed()}),ht.bind("a",()=>{this.toggleAutoPlay()}),ht.bind("b",()=>{this.toggleSkipPauses()}),ht.bind("left",()=>{const e=5,t=!1;this.jumpRelative(e,t)}),ht.bind("right",()=>{const e=5,t=!0;this.jumpRelative(e,t)}),ht.bind("j",()=>{const e=10,t=!1;this.jumpRelative(e,t)}),ht.bind("l",()=>{const e=10,t=!0;this.jumpRelative(e,t)}),this.initViewport(),pt(window).on("resize",()=>this.initViewport()),this.iframeLoadedPromise.then(()=>{this.initPlayer()}),window.addEventListener("beforeunload",()=>{this.isPlaying=!1,this.videoPlayerInterval&&(clearInterval(this.videoPlayerInterval),this.videoPlayerInterval=null)})},methods:{initPlayer(){const e=this.$refs.recordingPlayer,t=L(e).recordingFrame;if(!t||!t.isSupportedBrowser())return;t.addClass("html","piwikSessionRecording"),t.addClass("html","matomoSessionRecording");let a=null;const i=(e,i)=>{a&&a.css({left:e.x-8+"px",top:e.y-8+"px"}),this.lastCanvasCoordinates&&(t.drawLine(this.lastCanvasCoordinates.x,this.lastCanvasCoordinates.y,e.x,e.y,i),this.lastCanvasCoordinates=e)},n=(e,n)=>{if(!this.lastCanvasCoordinates||!a)return void t.scrollTo(e,n);const s=t.getScrollTop(),r=t.getScrollLeft();t.scrollTo(e,n);const o=n-s,l=e-r;let c=l+this.lastCanvasCoordinates.x,d=o+this.lastCanvasCoordinates.y;c<=0&&(c=0),d<=0&&(d=0),i({x:c,y:d},"blue")},s=(e,t,a)=>{null!==e&&void 0!==e&&e.scrollTo?e.scrollTo(t,a):(e.scrollLeft=t,e.scrollTop=a)};let r=null;const o=e=>{const{isPlaying:a}=this;this.isPlaying=!1;const i=gt(e);let o=null;if(i===et)e.selector&&(o=t.getCoordinatesInFrame(e.selector,e.x,e.y,this.offsetAccuracy,!1),o&&r(o));else if(i===tt)e.selector&&(o=t.getCoordinatesInFrame(e.selector,e.x,e.y,this.offsetAccuracy,!1),o&&(r(o),t.drawCircle(o.x,o.y,"#ff9407")));else if(i===st)e.text&&t.applyMutation(e.text);else if(i===at){const a=t.getIframeHeight(),i=t.getIframeWidth(),s=parseInt(""+a/this.scrollAccuracy*ut(e.y),10),r=parseInt(""+i/this.scrollAccuracy*ut(e.x),10);n(r,s)}else if(i===lt){if(e.selector){const a=t.findElement(e.selector);if(a&&a.length&&a[0]){const t=Math.max(a[0].scrollHeight,a[0].offsetHeight,a.height(),0),i=Math.max(a[0].scrollWidth,a[0].offsetWidth,a.width(),0);if(t&&i){const n=parseInt(""+t/this.scrollAccuracy*ut(e.y),10),r=parseInt(""+i/this.scrollAccuracy*ut(e.x),10);s(a[0],r,n)}}}}else if(i===it)this.setViewportResolution(e.x,e.y);else if(i===rt){if(e.selector){const a=t.findElement(e.selector);if(a.length){const t=a.attr("type");t&&"file"===(""+t).toLowerCase()||a.val(e.text).change()}}}else if(i===ot&&e.selector){const a=t.findElement(e.selector);a.is("input")?a.prop("checked",1===e.text||"1"===e.text):a.is("select")&&a.val(e.text).change()}this.isPlaying=a};r=e=>{const n=()=>{const e=t.getIframeWidth(),a=t.getIframeHeight();t.makeSvg(e,a);for(let t=0;t<=this.frame;t+=Ze){if(!this.timeFrameBuckets[t])return;this.timeFrameBuckets[t].forEach(e=>{const a=gt(e);a!==et&&a!==at&&a!==lt&&a!==tt||(this.lastFramePainted=t,o(e))})}},s=t.getIframeWindow();if(!this.lastCanvasCoordinates){const i=t.getIframeHeight(),r=t.getIframeWidth();return t.appendContent(mt),a=t.findElement(".mousePointer"),t.makeSvg(r,i),s.removeEventListener("resize",n,!1),s.addEventListener("resize",n,!1),this.lastCanvasCoordinates=e,void a.css({left:e.x-8+"px",top:e.y-8+"px"})}let r=t.getScrollTop();const l=t.getScrollLeft();(e.y>r+ut(this.recording.viewport_h_px)||e.yl+ut(this.recording.viewport_w_px)||e.x{if(!this.iframeLoaded)return;this.isLoading=!0;let a=this.frame;const i=e=>{for(let t=e;t<=this.frame;t+=Ze)(this.timeFrameBuckets[t]||[]).forEach(e=>{this.lastFramePainted=t,o(e)})};this.isFinished=!1,this.frame=e-e%Ze,this.progress=parseFloat(parseFloat(""+this.frame/ut(this.recording.duration)*100).toFixed(2)),this.positionPretty=bt(this.frame),a>this.frame?(a=0,this.lastCanvasCoordinates=!1,this.initialMutation&&t.initialMutation(this.initialMutation.text),t.scrollTo(0,0),this.setViewportResolution(window.sessionRecordingData.viewport_w_px,window.sessionRecordingData.viewport_h_px),this.seekTimeout&&(clearTimeout(this.seekTimeout),this.seekTimeout=null),(e=>{this.seekTimeout=setTimeout(()=>{i(e),this.isLoading=!1},1050)})(a)):(this.seekTimeout&&(clearTimeout(this.seekTimeout),this.seekTimeout=null),i(a),this.isLoading=!1)},this.isLoading=!1,this.isPlaying=!0;let l=0;const c=()=>{if(this.isPlaying&&!this.isLoading){l+=1;const e=ut(this.recording.duration);if(this.frame>=e?(this.isPlaying=!1,this.progress=100,this.isFinished=!0,this.positionPretty=this.durationPretty,this.actualAutoPlayEnabled&&this.nextRecordingId&&this.loadNewRecording(this.nextRecordingId)):(this.progress=parseFloat(parseFloat(""+this.frame/e*100).toFixed(2)),20===l&&(l=0,this.positionPretty=bt(this.frame))),(this.timeFrameBuckets[this.frame]||[]).forEach(e=>{this.lastFramePainted=this.frame,o(e)}),this.actualSkipPausesEnabled&&this.frame-this.lastFramePainted>1800){let t=Object.keys(this.timeFrameBuckets).map(e=>parseInt(e,10));t=t.sort((e,t)=>e-t);const a=t.find(e=>e>this.frame),i=!!a;if(a){const e=a-this.frame>1e3;e&&(this.frame=a-20*Ze)}if(!i){const t=e-this.frame>1e3;t&&(this.frame=e-20*Ze)}}this.frame+=Ze}};this.videoPlayerInterval=setInterval(()=>{for(let e=1;e<=this.actualReplaySpeed;e+=1)c()},Ze)},initViewport(){this.replayHeight=pt(window).height()-48-pt(".sessionRecording .sessionRecordingHead").outerHeight(!0)-pt(".sessionRecordingPlayer .controls").outerHeight(!0),this.replayWidth=pt(window).width()-48;const e=ut(this.recording.viewport_w_px),t=ut(this.recording.viewport_h_px),a=400;this.replayWidtha&&(this.replayWidth=a);const i=400;this.replayHeighti&&(this.replayHeight=i);let n=1,s=1;e>this.replayWidth&&(n=parseFloat(parseFloat(""+this.replayWidth/e).toFixed(4))),t>this.replayHeight&&(s=parseFloat(parseFloat(""+this.replayHeight/t).toFixed(4))),this.replayScale=Math.min(n,s),this.replayMarginLeft=(this.replayWidth-this.replayScale*e)/2},setViewportResolution(e,t){this.recording.viewport_w_px=parseInt(""+e,10),this.recording.viewport_h_px=parseInt(""+t,10),pt(".recordingWidth").text(e),pt(".recordingHeight").text(t),this.initViewport()},increaseReplaySpeed(){1===this.actualReplaySpeed?this.actualReplaySpeed=2:2===this.actualReplaySpeed?this.actualReplaySpeed=4:4===this.actualReplaySpeed?this.actualReplaySpeed=6:this.actualReplaySpeed=1,this.updateSettings()},updateSettings(){A["AjaxHelper"].fetch({module:"HeatmapSessionRecording",action:"saveSessionRecordingSettings",autoplay:this.actualAutoPlayEnabled?1:0,skippauses:this.actualSkipPausesEnabled?1:0,replayspeed:this.actualReplaySpeed},{format:"html"})},toggleAutoPlay(){this.actualAutoPlayEnabled=!this.actualAutoPlayEnabled,this.updateSettings()},toggleSkipPauses(){this.actualSkipPausesEnabled=!this.actualSkipPausesEnabled,this.updateSettings()},loadNewRecording(e){e&&(this.isPlaying=!1,A["MatomoUrl"].updateUrl(Object.assign(Object.assign({},A["MatomoUrl"].urlParsed.value),{},{idLogHsr:parseInt(""+e,10),updated:A["MatomoUrl"].urlParsed.value.updated?parseInt(A["MatomoUrl"].urlParsed.value.updated,10)+1:1})))},jumpRelative(e,t){const a=1e3*e;let i;t?(i=this.frame+a,i>this.recording.duration&&(i=ut(this.recording.duration)-Ze)):(i=this.frame-a,i<0&&(i=0)),this.seek(i)},replay(){this.isFinished=!1,this.lastFramePainted=0,this.seek(0),this.play()},pause(){this.isPlaying=!1},togglePlay(){this.isFinished?this.replay():this.isPlaying?this.pause():this.play()},seekEvent(e){const t=pt(e.currentTarget).offset(),a=e.pageX-t.left,i=this.replayWidth,n=a/i,s=ut(this.recording.duration)*n;this.seek(s)},play(){this.isPlaying=!0}},computed:{durationPretty(){return bt(ut(this.recording.duration))},embedUrl(){return"?"+A["MatomoUrl"].stringify({module:"HeatmapSessionRecording",action:"embedPage",idSite:this.recording.idSite,idLogHsr:this.recording.idLogHsr,idSiteHsr:this.recording.idSiteHsr,token_auth:A["MatomoUrl"].urlParsed.value.token_auth||void 0})},skipPreviousButtonTitle(){return Object(A["translate"])("HeatmapSessionRecording_PlayerPageViewPrevious",this.previousRecordingInfo||"","P")},skipPausesEnabledText(){return this.actualSkipPausesEnabled?Object(A["translate"])("HeatmapSessionRecording_disable"):Object(A["translate"])("HeatmapSessionRecording_enable")},autoplayEnabledText(){return this.actualAutoPlayEnabled?Object(A["translate"])("HeatmapSessionRecording_disable"):Object(A["translate"])("HeatmapSessionRecording_enable")},recordingEvents(){return this.recording?this.recording.events.map(e=>{const t=gt(e);let{text:a}=e;return t!==nt&&t!==st||"string"!==typeof a||(a=JSON.parse(a)),Object.assign(Object.assign({},e),{},{text:a})}):[]},initialMutation(){const e=this.recordingEvents.find(e=>{const t=gt(e),a=t===nt||t===st,i=a&&(t===nt||!e.time_since_load||"0"===e.time_since_load);return i});return e},timeFrameBuckets(){const e={};return this.recordingEvents.forEach(t=>{if(t===this.initialMutation)return;const a=Math.round(ut(t.time_since_load)/Ze)*Ze;e[a]=e[a]||[],e[a].push(t)}),e},clues(){const e=[];return this.recordingEvents.forEach(t=>{if(t===this.initialMutation)return;const a=gt(t),i=ct[a]||"",n=dt[a]||"";if(i){if((0===t.time_since_load||"0"===t.time_since_load)&&"moveEvent"===i)return;e.push({left:parseFloat(""+ut(t.time_since_load)/ut(this.recording.duration)*100).toFixed(2),type:i,title:n})}}),e}}});vt.render=Qe;var Ot=vt;const jt={class:"form-group hsrTargetTest"},ft={class:"loadingPiwik loadingMatchingSteps"},yt=Object(s["createElementVNode"])("img",{src:"plugins/Morpheus/images/loading-blue.gif",alt:""},null,-1),St=Object(s["createElementVNode"])("div",{id:"hsrTargetValidationError"},null,-1);function Ht(e,t,a,i,n,r){return Object(s["openBlock"])(),Object(s["createElementBlock"])("div",jt,[Object(s["createElementVNode"])("label",null,[Object(s["createElementVNode"])("strong",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_TargetPageTestTitle"))+":",1),Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_TargetPageTestLabel")),1)]),Object(s["withDirectives"])(Object(s["createElementVNode"])("input",{type:"text",id:"urltargettest",placeholder:"http://www.example.com/","onUpdate:modelValue":t[0]||(t[0]=t=>e.url=t),class:Object(s["normalizeClass"])({invalid:e.url&&!e.matches&&e.isValid})},null,2),[[s["vModelText"],e.url]]),Object(s["createElementVNode"])("div",null,[Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"testInfo"},Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_TargetPageTestErrorInvalidUrl")),513),[[s["vShow"],e.url&&!e.isValid]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"testInfo matches"},Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_TargetPageTestUrlMatches")),513),[[s["vShow"],e.url&&e.matches&&e.isValid]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"testInfo notMatches"},Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_TargetPageTestUrlNotMatches")),513),[[s["vShow"],e.url&&!e.matches&&e.isValid]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",ft,[yt,Object(s["createTextVNode"])(Object(s["toDisplayString"])(e.translate("General_LoadingData")),1)],512),[[s["vShow"],e.isLoadingTestMatchPage]])]),St])}function _t(e){return e.indexOf("://")>3}var Vt=Object(s["defineComponent"])({props:{includedTargets:Array},data(){return{url:"",matches:!1,isLoadingTestMatchPage:!1}},watch:{isValid(e){e||(this.matches=!1)},includedTargets(){this.runTest()},url(){this.runTest()}},setup(){return{testUrlMatchPages:I("HeatmapSessionRecording.testUrlMatchPages",{errorElement:"#hsrTargetValidationError"})}},created(){this.runTest=Object(A["debounce"])(this.runTest,200)},methods:{checkIsMatchingUrl(){if(!this.isValid)return;const e=this.targetUrl,t=this.filteredIncludedTargets;null!==t&&void 0!==t&&t.length&&(this.isLoadingTestMatchPage=!0,this.testUrlMatchPages({url:e},{matchPageRules:t}).then(e=>{var t;null!==(t=this.filteredIncludedTargets)&&void 0!==t&&t.length&&(null===e||void 0===e?void 0:e.url)===this.targetUrl&&(this.matches=e.matches)}).finally(()=>{this.isLoadingTestMatchPage=!1}))},runTest(){this.isValid&&this.checkIsMatchingUrl()}},computed:{targetUrl(){return(this.url||"").trim()},isValid(){return this.targetUrl&&_t(this.targetUrl)},filteredIncludedTargets(){if(this.includedTargets)return this.includedTargets.filter(e=>(null===e||void 0===e?void 0:e.value)||"any"===(null===e||void 0===e?void 0:e.type)).map(e=>Object.assign(Object.assign({},e),{},{value:e.value?e.value.trim():""}))}}});Vt.render=Ht;var Nt=Vt;const Et={style:{width:"100%"}},Rt={name:"targetAttribute"},wt={name:"targetType"},kt={name:"targetValue"},xt={name:"targetValue2"},Ct=["title"],Tt=["title"];function Dt(e,t,a,i,n,r){const o=Object(s["resolveComponent"])("Field");return Object(s["openBlock"])(),Object(s["createElementBlock"])("div",{class:Object(s["normalizeClass"])(["form-group hsrUrltarget valign-wrapper",{disabled:e.disableIfNoValue&&!e.modelValue.value}])},[Object(s["createElementVNode"])("div",Et,[Object(s["createElementVNode"])("div",Rt,[Object(s["createVNode"])(o,{uicontrol:"select",name:"targetAttribute","model-value":e.modelValue.attribute,"onUpdate:modelValue":t[0]||(t[0]=t=>e.$emit("update:modelValue",Object.assign(Object.assign({},e.modelValue),{},{attribute:t}))),title:e.translate("HeatmapSessionRecording_Rule"),options:e.targetAttributes,"full-width":!0},null,8,["model-value","title","options"])]),Object(s["createElementVNode"])("div",wt,[Object(s["createVNode"])(o,{uicontrol:"select",name:"targetType","model-value":e.pattern_type,"onUpdate:modelValue":t[1]||(t[1]=t=>{e.onTypeChange(t)}),options:e.targetOptions[e.modelValue.attribute],"full-width":!0},null,8,["model-value","options"])]),Object(s["createElementVNode"])("div",kt,[Object(s["withDirectives"])(Object(s["createVNode"])(o,{uicontrol:"text",name:"targetValue",placeholder:"eg. "+e.targetExamples[e.modelValue.attribute],"model-value":e.modelValue.value,"onUpdate:modelValue":t[2]||(t[2]=t=>e.$emit("update:modelValue",Object.assign(Object.assign({},e.modelValue),{},{value:t.trim()}))),maxlength:500,"full-width":!0},null,8,["placeholder","model-value"]),[[s["vShow"],"any"!==e.pattern_type]])]),Object(s["createElementVNode"])("div",xt,[Object(s["withDirectives"])(Object(s["createVNode"])(o,{uicontrol:"text",name:"targetValue2","model-value":e.modelValue.value2,"onUpdate:modelValue":t[3]||(t[3]=t=>e.$emit("update:modelValue",Object.assign(Object.assign({},e.modelValue),{},{value2:t.trim()}))),maxlength:500,"full-width":!0,placeholder:e.translate("HeatmapSessionRecording_UrlParameterValueToMatchPlaceholder")},null,8,["model-value","placeholder"]),[[s["vShow"],"urlparam"===e.modelValue.attribute&&e.pattern_type&&"exists"!==e.pattern_type&&"not_exists"!==e.pattern_type]])])]),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"icon-plus valign",title:e.translate("General_Add"),onClick:t[4]||(t[4]=t=>e.$emit("addUrl"))},null,8,Ct),[[s["vShow"],e.showAddUrl]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("span",{class:"icon-minus valign",title:e.translate("General_Remove"),onClick:t[5]||(t[5]=t=>e.$emit("removeUrl"))},null,8,Tt),[[s["vShow"],e.canBeRemoved]])],2)}function Mt(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */class Pt{constructor(){Mt(this,"privateState",Object(s["reactive"])({rules:[]})),Mt(this,"state",Object(s["computed"])(()=>Object(s["readonly"])(this.privateState))),Mt(this,"rules",Object(s["computed"])(()=>this.state.value.rules)),Mt(this,"initPromise",null)}init(){return this.initPromise||(this.initPromise=A["AjaxHelper"].fetch({method:"HeatmapSessionRecording.getAvailableTargetPageRules",filter_limit:"-1"}).then(e=>(this.privateState.rules=e,this.rules.value))),this.initPromise}}var Bt=new Pt,At=Object(s["defineComponent"])({props:{modelValue:{type:Object,required:!0},canBeRemoved:Boolean,disableIfNoValue:Boolean,allowAny:Boolean,showAddUrl:Boolean},components:{Field:U["Field"]},emits:["addUrl","removeUrl","update:modelValue"],created(){Bt.init()},watch:{modelValue(e){if(!e.attribute)return;const t=this.targetOptions[e.attribute],a=t.find(e=>e.key===this.pattern_type);!a&&t[0]&&this.onTypeChange(t[0].key)}},computed:{pattern_type(){let e=this.modelValue.type;return this.modelValue.inverted&&"0"!==this.modelValue.inverted&&(e="not_"+this.modelValue.type),e},targetAttributes(){return Bt.rules.value.map(e=>({key:e.value,value:e.name}))},targetOptions(){const e={};return Bt.rules.value.forEach(t=>{e[t.value]=[],this.allowAny&&"url"===t.value&&e[t.value].push({value:Object(A["translate"])("HeatmapSessionRecording_TargetTypeIsAny"),key:"any"}),t.types.forEach(a=>{e[t.value].push({value:a.name,key:a.value}),e[t.value].push({value:Object(A["translate"])("HeatmapSessionRecording_TargetTypeIsNot",a.name),key:"not_"+a.value})})}),e},targetExamples(){const e={};return Bt.rules.value.forEach(t=>{e[t.value]=t.example}),e}},methods:{onTypeChange(e){let t=0,a=e;0===e.indexOf("not_")&&(a=e.substring("not_".length),t=1),this.$emit("update:modelValue",Object.assign(Object.assign({},this.modelValue),{},{type:a,inverted:t}))}}});At.render=Dt;var Ut=At;const Lt={class:"loadingPiwik"},It=Object(s["createElementVNode"])("img",{src:"plugins/Morpheus/images/loading-blue.gif"},null,-1),Ft={class:"loadingPiwik"},Wt=Object(s["createElementVNode"])("img",{src:"plugins/Morpheus/images/loading-blue.gif"},null,-1),qt={name:"name"},zt={name:"sampleLimit"},$t={class:"form-group row"},Gt={class:"col s12"},Jt={class:"col s12 m6",style:{"padding-left":"0"}},Xt=Object(s["createElementVNode"])("hr",null,null,-1),Yt={class:"col s12 m6"},Kt={class:"form-help"},Qt={class:"inline-help"},Zt={name:"sampleRate"},ea={name:"excludedElements"},ta={name:"screenshotUrl"},aa={name:"breakpointMobile"},ia={name:"breakpointTablet"},na={name:"trackManually"},sa=["innerHTML"],ra={class:"entityCancel"};function oa(e,t,a,i,n,r){const o=Object(s["resolveComponent"])("Field"),l=Object(s["resolveComponent"])("HsrUrlTarget"),c=Object(s["resolveComponent"])("HsrTargetTest"),d=Object(s["resolveComponent"])("SaveButton"),m=Object(s["resolveComponent"])("ContentBlock");return Object(s["openBlock"])(),Object(s["createBlock"])(m,{class:"editHsr","content-title":e.contentTitle},{default:Object(s["withCtx"])(()=>[Object(s["withDirectives"])(Object(s["createElementVNode"])("p",null,[Object(s["createElementVNode"])("span",Lt,[It,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("General_LoadingData")),1)])],512),[[s["vShow"],e.isLoading]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("p",null,[Object(s["createElementVNode"])("span",Ft,[Wt,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_UpdatingData")),1)])],512),[[s["vShow"],e.isUpdating]]),Object(s["createElementVNode"])("form",{onSubmit:t[12]||(t[12]=t=>e.edit?e.updateHsr():e.createHsr())},[Object(s["createElementVNode"])("div",null,[Object(s["createElementVNode"])("div",qt,[Object(s["createVNode"])(o,{uicontrol:"text",name:"name","model-value":e.siteHsr.name,"onUpdate:modelValue":t[0]||(t[0]=t=>{e.siteHsr.name=t,e.setValueHasChanged()}),title:e.translate("General_Name"),maxlength:50,placeholder:e.translate("HeatmapSessionRecording_FieldNamePlaceholder"),"inline-help":e.translate("HeatmapSessionRecording_HeatmapNameHelp")},null,8,["model-value","title","placeholder","inline-help"])]),Object(s["createElementVNode"])("div",zt,[Object(s["createVNode"])(o,{uicontrol:"select",name:"sampleLimit","model-value":e.siteHsr.sample_limit,"onUpdate:modelValue":t[1]||(t[1]=t=>{e.siteHsr.sample_limit=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_HeatmapSampleLimit"),options:e.sampleLimits,"inline-help":e.translate("HeatmapSessionRecording_HeatmapSampleLimitHelp")},null,8,["model-value","title","options","inline-help"])]),Object(s["createElementVNode"])("div",$t,[Object(s["createElementVNode"])("div",Gt,[Object(s["createElementVNode"])("h3",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_TargetPage"))+":",1)]),Object(s["createElementVNode"])("div",Jt,[(Object(s["openBlock"])(!0),Object(s["createElementBlock"])(s["Fragment"],null,Object(s["renderList"])(e.siteHsr.match_page_rules,(a,i)=>(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",{class:Object(s["normalizeClass"])(`matchPageRules ${i} multiple`),key:i},[Object(s["createElementVNode"])("div",null,[Object(s["createVNode"])(l,{"model-value":a,"onUpdate:modelValue":t=>e.setMatchPageRule(t,i),onAddUrl:t[2]||(t[2]=t=>e.addMatchPageRule()),onRemoveUrl:t=>e.removeMatchPageRule(i),onAnyChange:t[3]||(t[3]=t=>e.setValueHasChanged()),"allow-any":!1,"disable-if-no-value":i>0,"can-be-removed":i>0,"show-add-url":!0},null,8,["model-value","onUpdate:modelValue","onRemoveUrl","disable-if-no-value","can-be-removed"])]),Xt],2))),128))]),Object(s["createElementVNode"])("div",Yt,[Object(s["createElementVNode"])("div",Kt,[Object(s["createElementVNode"])("span",Qt,[Object(s["createTextVNode"])(Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_FieldIncludedTargetsHelp"))+" ",1),Object(s["createElementVNode"])("div",null,[Object(s["createVNode"])(c,{"included-targets":e.siteHsr.match_page_rules},null,8,["included-targets"])])])])])]),Object(s["createElementVNode"])("div",Zt,[Object(s["createVNode"])(o,{uicontrol:"select",name:"sampleRate","model-value":e.siteHsr.sample_rate,"onUpdate:modelValue":t[4]||(t[4]=t=>{e.siteHsr.sample_rate=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_SampleRate"),options:e.sampleRates,introduction:e.translate("HeatmapSessionRecording_AdvancedOptions"),"inline-help":e.translate("HeatmapSessionRecording_HeatmapSampleRateHelp")},null,8,["model-value","title","options","introduction","inline-help"])]),Object(s["createElementVNode"])("div",ea,[Object(s["createVNode"])(o,{uicontrol:"text",name:"excludedElements","model-value":e.siteHsr.excluded_elements,"onUpdate:modelValue":t[5]||(t[5]=t=>{e.siteHsr.excluded_elements=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_ExcludedElements"),maxlength:1e3,"inline-help":e.translate("HeatmapSessionRecording_ExcludedElementsHelp")},null,8,["model-value","title","inline-help"])]),Object(s["createElementVNode"])("div",ta,[Object(s["createVNode"])(o,{uicontrol:"text",name:"screenshotUrl","model-value":e.siteHsr.screenshot_url,"onUpdate:modelValue":t[6]||(t[6]=t=>{e.siteHsr.screenshot_url=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_ScreenshotUrl"),maxlength:300,disabled:!!e.siteHsr.page_treemirror,"inline-help":e.translate("HeatmapSessionRecording_ScreenshotUrlHelp")},null,8,["model-value","title","disabled","inline-help"])]),Object(s["createElementVNode"])("div",aa,[Object(s["createVNode"])(o,{uicontrol:"text",name:"breakpointMobile","model-value":e.siteHsr.breakpoint_mobile,"onUpdate:modelValue":t[7]||(t[7]=t=>{e.siteHsr.breakpoint_mobile=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_BreakpointX",e.translate("General_Mobile")),maxlength:4,"inline-help":e.breakpointMobileInlineHelp},null,8,["model-value","title","inline-help"])]),Object(s["createElementVNode"])("div",ia,[Object(s["createVNode"])(o,{uicontrol:"text",name:"breakpointTablet","model-value":e.siteHsr.breakpoint_tablet,"onUpdate:modelValue":t[8]||(t[8]=t=>{e.siteHsr.breakpoint_tablet=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_BreakpointX",e.translate("DevicesDetection_Tablet")),maxlength:4,"inline-help":e.breakpointGeneralHelp},null,8,["model-value","title","inline-help"])]),Object(s["createElementVNode"])("div",na,[Object(s["createVNode"])(o,{uicontrol:"checkbox",name:"capture_manually",title:e.translate("HeatmapSessionRecording_CaptureDomTitle"),"inline-help":e.captureDomInlineHelp,"model-value":e.siteHsr.capture_manually,"onUpdate:modelValue":t[9]||(t[9]=t=>{e.siteHsr.capture_manually=t,e.setValueHasChanged()})},null,8,["title","inline-help","model-value"])]),Object(s["createElementVNode"])("p",{innerHTML:e.$sanitize(e.personalInformationNote)},null,8,sa),Object(s["createVNode"])(d,{class:"createButton",onConfirm:t[10]||(t[10]=t=>e.edit?e.updateHsr():e.createHsr()),disabled:e.isUpdating||!e.isDirty,saving:e.isUpdating,value:e.saveButtonText},null,8,["disabled","saving","value"]),Object(s["createElementVNode"])("div",ra,[Object(s["createElementVNode"])("a",{onClick:t[11]||(t[11]=t=>e.cancel())},Object(s["toDisplayString"])(e.translate("General_Cancel")),1)])])],32)]),_:1},8,["content-title"])}function la(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */class ca{constructor(e){la(this,"context",void 0),la(this,"privateState",Object(s["reactive"])({allHsrs:[],isLoading:!1,isUpdating:!1,filterStatus:""})),la(this,"state",Object(s["computed"])(()=>Object(s["readonly"])(this.privateState))),la(this,"hsrs",Object(s["computed"])(()=>this.privateState.filterStatus?this.state.value.allHsrs.filter(e=>e.status===this.privateState.filterStatus):this.state.value.allHsrs)),la(this,"hsrsCloned",Object(s["computed"])(()=>Object(A["clone"])(this.hsrs.value))),la(this,"statusOptions",Object(s["readonly"])([{key:"",value:Object(A["translate"])("General_All")},{key:"active",value:Object(A["translate"])("HeatmapSessionRecording_StatusActive")},{key:"ended",value:Object(A["translate"])("HeatmapSessionRecording_StatusEnded")},{key:"paused",value:Object(A["translate"])("HeatmapSessionRecording_StatusPaused")}])),la(this,"fetchPromises",{}),this.context=e}setFilterStatus(e){this.privateState.filterStatus=e}reload(){return this.privateState.allHsrs=[],this.fetchPromises={},this.fetchHsrs()}filterRules(e){return e.filter(e=>!!e&&(e.value||"any"===e.type))}getApiMethodInContext(e){return`${e}${this.context}`}fetchHsrs(){let e="HeatmapSessionRecording.getHeatmaps";"SessionRecording"===this.context&&(e="HeatmapSessionRecording.getSessionRecordings");const t={method:e,filter_limit:"-1"};return this.fetchPromises[e]||(this.fetchPromises[e]=A["AjaxHelper"].fetch(t)),this.privateState.isLoading=!0,this.privateState.allHsrs=[],this.fetchPromises[e].then(e=>(this.privateState.allHsrs=e,this.state.value.allHsrs)).finally(()=>{this.privateState.isLoading=!1})}findHsr(e){const t=this.state.value.allHsrs.find(t=>t.idsitehsr===e);return t?Promise.resolve(t):(this.privateState.isLoading=!0,A["AjaxHelper"].fetch({idSiteHsr:e,method:this.getApiMethodInContext("HeatmapSessionRecording.get"),filter_limit:"-1"}).finally(()=>{this.privateState.isLoading=!1}))}deleteHsr(e){return this.privateState.isUpdating=!0,this.privateState.allHsrs=[],A["AjaxHelper"].fetch({idSiteHsr:e,method:this.getApiMethodInContext("HeatmapSessionRecording.delete")},{withTokenInUrl:!0}).then(()=>({type:"success"})).catch(e=>({type:"error",message:e.message||e})).finally(()=>{this.privateState.isUpdating=!1})}completeHsr(e){return this.privateState.isUpdating=!0,this.privateState.allHsrs=[],A["AjaxHelper"].fetch({idSiteHsr:e,method:this.getApiMethodInContext("HeatmapSessionRecording.end")},{withTokenInUrl:!0}).then(()=>({type:"success"})).catch(e=>({type:"error",message:e.message||e})).finally(()=>{this.privateState.isUpdating=!1})}createOrUpdateHsr(e,t){const a={idSiteHsr:e.idsitehsr,sampleLimit:e.sample_limit,sampleRate:e.sample_rate,excludedElements:e.excluded_elements?e.excluded_elements.trim():void 0,screenshotUrl:e.screenshot_url?e.screenshot_url.trim():void 0,breakpointMobile:e.breakpoint_mobile,breakpointTablet:e.breakpoint_tablet,minSessionTime:e.min_session_time,requiresActivity:e.requires_activity?1:0,captureKeystrokes:e.capture_keystrokes?1:0,captureDomManually:e.capture_manually?1:0,method:t,name:e.name.trim()},i={matchPageRules:this.filterRules(e.match_page_rules)};return this.privateState.isUpdating=!0,A["AjaxHelper"].post(a,i,{withTokenInUrl:!0}).then(e=>({type:"success",response:e})).catch(e=>({type:"error",message:e.message||e})).finally(()=>{this.privateState.isUpdating=!1})}}const da=new ca("Heatmap"),ma=new ca("SessionRecording"),pa="hsrmanagement";var ha=Object(s["defineComponent"])({props:{idSiteHsr:Number,breakpointMobile:Number,breakpointTablet:Number},components:{ContentBlock:A["ContentBlock"],Field:U["Field"],HsrUrlTarget:Ut,HsrTargetTest:Nt,SaveButton:U["SaveButton"]},data(){return{isDirty:!1,showAdvancedView:!1,siteHsr:{}}},created(){this.init()},watch:{idSiteHsr(e){null!==e&&this.init()}},methods:{removeAnyHsrNotification(){A["NotificationsStore"].remove(pa),A["NotificationsStore"].remove("ajaxHelper")},showNotification(e,t){const a=A["NotificationsStore"].show({message:e,context:t,id:pa,type:"transient"});setTimeout(()=>{A["NotificationsStore"].scrollToNotification(a)},200)},showErrorFieldNotProvidedNotification(e){const t=Object(A["translate"])("HeatmapSessionRecording_ErrorXNotProvided",[e]);this.showNotification(t,"error")},init(){const{idSiteHsr:e}=this;if(this.siteHsr={},this.showAdvancedView=!1,A["Matomo"].helper.lazyScrollToContent(),this.edit&&e)da.findHsr(e).then(e=>{e&&(this.siteHsr=Object(A["clone"])(e),this.siteHsr.sample_rate=""+this.siteHsr.sample_rate,this.addInitialMatchPageRule(),this.isDirty=!1)});else if(this.create){this.siteHsr={idSite:A["Matomo"].idSite,name:"",sample_rate:"10.0",sample_limit:1e3,breakpoint_mobile:this.breakpointMobile,breakpoint_tablet:this.breakpointTablet,capture_manually:0},this.isDirty=!1;const e=A["MatomoUrl"].hashParsed.value;if(e.name&&(this.siteHsr.name=e.name,this.isDirty=!0),e.matchPageRules)try{this.siteHsr.match_page_rules=JSON.parse(e.matchPageRules),this.isDirty=!0}catch(t){console.log("warning: could not parse matchPageRules query param, expected JSON")}else this.addInitialMatchPageRule()}},addInitialMatchPageRule(){var e;this.siteHsr&&(null!==(e=this.siteHsr.match_page_rules)&&void 0!==e&&e.length||this.addMatchPageRule())},addMatchPageRule(){var e;this.siteHsr&&(null!==(e=this.siteHsr.match_page_rules)&&void 0!==e&&e.length||(this.siteHsr.match_page_rules=[]),this.siteHsr.match_page_rules.push({attribute:"url",type:"equals_simple",value:"",inverted:0}),this.isDirty=!0)},removeMatchPageRule(e){this.siteHsr&&e>-1&&(this.siteHsr.match_page_rules=[...this.siteHsr.match_page_rules],this.siteHsr.match_page_rules.splice(e,1),this.isDirty=!0)},cancel(){const e=Object.assign({},A["MatomoUrl"].hashParsed.value);delete e.idSiteHsr,A["MatomoUrl"].updateHash(e)},createHsr(){this.removeAnyHsrNotification(),this.checkRequiredFieldsAreSet()&&da.createOrUpdateHsr(this.siteHsr,"HeatmapSessionRecording.addHeatmap").then(e=>{if(!e||"error"===e.type||!e.response)return;this.isDirty=!1;const t=e.response.value;da.reload().then(()=>{A["Matomo"].helper.isReportingPage()&&A["Matomo"].postEvent("updateReportingMenu"),A["MatomoUrl"].updateHash(Object.assign(Object.assign({},A["MatomoUrl"].hashParsed.value),{},{idSiteHsr:t})),setTimeout(()=>{this.showNotification(Object(A["translate"])("HeatmapSessionRecording_HeatmapCreated"),e.type)},200)})})},setValueHasChanged(){this.isDirty=!0},updateHsr(){this.removeAnyHsrNotification(),this.checkRequiredFieldsAreSet()&&da.createOrUpdateHsr(this.siteHsr,"HeatmapSessionRecording.updateHeatmap").then(e=>{"error"!==e.type&&(this.isDirty=!1,this.siteHsr={},da.reload().then(()=>{this.init()}),this.showNotification(Object(A["translate"])("HeatmapSessionRecording_HeatmapUpdated"),e.type))})},checkRequiredFieldsAreSet(){var e;if(!this.siteHsr.name){const e=Object(A["translate"])("General_Name");return this.showErrorFieldNotProvidedNotification(e),!1}if(null===(e=this.siteHsr.match_page_rules)||void 0===e||!e.length||!da.filterRules(this.siteHsr.match_page_rules).length){const e=Object(A["translate"])("HeatmapSessionRecording_ErrorPageRuleRequired");return this.showNotification(e,"error"),!1}return!0},setMatchPageRule(e,t){this.siteHsr.match_page_rules=[...this.siteHsr.match_page_rules],this.siteHsr.match_page_rules[t]=e}},computed:{sampleLimits(){return[1e3,2e3,5e3].map(e=>({key:""+e,value:e}))},sampleRates(){const e=[.1,.5,1,2,3,4,5,6,8,10,15,20,30,40,50,60,70,80,90,100];return e.map(e=>({key:e.toFixed(1),value:e+"%"}))},create(){return!this.idSiteHsr},edit(){return!this.create},editTitle(){const e=this.create?"HeatmapSessionRecording_CreateNewHeatmap":"HeatmapSessionRecording_EditHeatmapX";return e},contentTitle(){return Object(A["translate"])(this.editTitle,this.siteHsr.name?`"${this.siteHsr.name}"`:"")},isLoading(){return da.state.value.isLoading},isUpdating(){return da.state.value.isUpdating},breakpointMobileInlineHelp(){const e=Object(A["translate"])("HeatmapSessionRecording_BreakpointGeneralHelp"),t=Object(A["translate"])("HeatmapSessionRecording_BreakpointGeneralHelpManage");return`${e} ${t}`},breakpointGeneralHelp(){const e=Object(A["translate"])("HeatmapSessionRecording_BreakpointGeneralHelp"),t=Object(A["translate"])("HeatmapSessionRecording_BreakpointGeneralHelpManage");return`${e} ${t}`},captureDomInlineHelp(){const e=this.idSiteHsr?this.idSiteHsr:"{idHeatmap}",t=`_paq.push(['HeatmapSessionRecording::captureInitialDom', ${e}]) `;return Object(A["translate"])("HeatmapSessionRecording_CaptureDomInlineHelp",t,""," ")},personalInformationNote(){const e="https://developer.matomo.org/guides/heatmap-session-recording/setup#masking-content-on-your-website";return Object(A["translate"])("HeatmapSessionRecording_PersonalInformationNote",Object(A["translate"])("HeatmapSessionRecording_Heatmap"),"","
",``," ")},saveButtonText(){return this.edit?Object(A["translate"])("CoreUpdater_UpdateTitle"):Object(A["translate"])("HeatmapSessionRecording_CreateNewHeatmap")}}});ha.render=oa;var ua=ha;const ga={class:"heatmapList"},ba={class:"filterStatus"},va={class:"hsrSearchFilter",style:{"margin-left":"3.5px"}},Oa={class:"index"},ja={class:"name"},fa={class:"creationDate"},ya={class:"sampleLimit"},Sa={class:"status"},Ha={class:"action"},_a={colspan:"7"},Va={class:"loadingPiwik"},Na=Object(s["createElementVNode"])("img",{src:"plugins/Morpheus/images/loading-blue.gif"},null,-1),Ea={colspan:"7"},Ra=["id"],wa={class:"index"},ka={class:"name"},xa={class:"creationDate"},Ca={class:"sampleLimit"},Ta={key:0,class:"status status-paused"},Da=["title"],Ma={key:1,class:"status"},Pa={class:"action"},Ba=["title","onClick"],Aa=["title","onClick"],Ua=["title","href"],La=["title","onClick"],Ia={class:"tableActionBar"},Fa=Object(s["createElementVNode"])("span",{class:"icon-add"},null,-1),Wa={class:"ui-confirm",id:"confirmDeleteHeatmap",ref:"confirmDeleteHeatmap"},qa=["value"],za=["value"],$a={class:"ui-confirm",id:"confirmEndHeatmap",ref:"confirmEndHeatmap"},Ga=["value"],Ja=["value"];function Xa(e,t,a,i,n,r){const o=Object(s["resolveComponent"])("Field"),l=Object(s["resolveComponent"])("ContentBlock"),c=Object(s["resolveDirective"])("content-table");return Object(s["openBlock"])(),Object(s["createElementBlock"])("div",ga,[Object(s["createVNode"])(l,{"content-title":e.translate("HeatmapSessionRecording_ManageHeatmaps")},{default:Object(s["withCtx"])(()=>[Object(s["createElementVNode"])("p",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_HeatmapUsageBenefits")),1),Object(s["createElementVNode"])("div",null,[Object(s["createElementVNode"])("div",ba,[Object(s["createVNode"])(o,{uicontrol:"select",name:"filterStatus","model-value":e.filterStatus,"onUpdate:modelValue":t[0]||(t[0]=t=>{e.setFilterStatus(t)}),title:e.translate("HeatmapSessionRecording_Filter"),"full-width":!0,options:e.statusOptions},null,8,["model-value","title","options"])]),Object(s["createElementVNode"])("div",va,[Object(s["withDirectives"])(Object(s["createVNode"])(o,{uicontrol:"text",name:"hsrSearch",title:e.translate("General_Search"),modelValue:e.searchFilter,"onUpdate:modelValue":t[1]||(t[1]=t=>e.searchFilter=t),"full-width":!0},null,8,["title","modelValue"]),[[s["vShow"],e.hsrs.length>0]])])]),Object(s["withDirectives"])((Object(s["openBlock"])(),Object(s["createElementBlock"])("table",null,[Object(s["createElementVNode"])("thead",null,[Object(s["createElementVNode"])("tr",null,[Object(s["createElementVNode"])("th",Oa,Object(s["toDisplayString"])(e.translate("General_Id")),1),Object(s["createElementVNode"])("th",ja,Object(s["toDisplayString"])(e.translate("General_Name")),1),Object(s["createElementVNode"])("th",fa,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_CreationDate")),1),Object(s["createElementVNode"])("th",ya,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_SampleLimit")),1),Object(s["createElementVNode"])("th",Sa,Object(s["toDisplayString"])(e.translate("CorePluginsAdmin_Status")),1),Object(s["createElementVNode"])("th",Ha,Object(s["toDisplayString"])(e.translate("General_Actions")),1)])]),Object(s["createElementVNode"])("tbody",null,[Object(s["withDirectives"])(Object(s["createElementVNode"])("tr",null,[Object(s["createElementVNode"])("td",_a,[Object(s["createElementVNode"])("span",Va,[Na,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("General_LoadingData")),1)])])],512),[[s["vShow"],e.isLoading||e.isUpdating]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("tr",null,[Object(s["createElementVNode"])("td",Ea,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_NoHeatmapsFound")),1)],512),[[s["vShow"],!e.isLoading&&0===e.hsrs.length]]),(Object(s["openBlock"])(!0),Object(s["createElementBlock"])(s["Fragment"],null,Object(s["renderList"])(e.sortedHsrs,t=>(Object(s["openBlock"])(),Object(s["createElementBlock"])("tr",{id:"hsr"+t.idsitehsr,class:"hsrs",key:t.idsitehsr},[Object(s["createElementVNode"])("td",wa,Object(s["toDisplayString"])(t.idsitehsr),1),Object(s["createElementVNode"])("td",ka,Object(s["toDisplayString"])(t.name),1),Object(s["createElementVNode"])("td",xa,Object(s["toDisplayString"])(t.created_date_pretty),1),Object(s["createElementVNode"])("td",Ca,Object(s["toDisplayString"])(t.sample_limit),1),"paused"===t.status?(Object(s["openBlock"])(),Object(s["createElementBlock"])("td",Ta,[Object(s["createTextVNode"])(Object(s["toDisplayString"])(e.ucfirst(t.status))+" ",1),Object(s["createElementVNode"])("span",{class:"icon icon-help",title:e.pauseReason},null,8,Da)])):(Object(s["openBlock"])(),Object(s["createElementBlock"])("td",Ma,Object(s["toDisplayString"])(e.ucfirst(t.status)),1)),Object(s["createElementVNode"])("td",Pa,[Object(s["createElementVNode"])("a",{class:"table-action icon-edit",title:e.translate("HeatmapSessionRecording_EditX",e.translate("HeatmapSessionRecording_Heatmap")),onClick:a=>e.editHsr(t.idsitehsr)},null,8,Ba),Object(s["withDirectives"])(Object(s["createElementVNode"])("a",{a:"",class:"table-action stopRecording icon-drop-crossed",title:e.translate("HeatmapSessionRecording_StopX",e.translate("HeatmapSessionRecording_Heatmap")),onClick:a=>e.completeHsr(t)},null,8,Aa),[[s["vShow"],"ended"!==t.status]]),Object(s["createElementVNode"])("a",{target:"_blank",class:"table-action icon-show",title:e.translate("HeatmapSessionRecording_ViewReport"),href:e.getViewReportLink(t)},null,8,Ua),Object(s["createElementVNode"])("a",{class:"table-action icon-delete",title:e.translate("HeatmapSessionRecording_DeleteX",e.translate("HeatmapSessionRecording_Heatmap")),onClick:a=>e.deleteHsr(t)},null,8,La)])],8,Ra))),128))])])),[[c]]),Object(s["createElementVNode"])("div",Ia,[Object(s["createElementVNode"])("a",{class:"createNewHsr",value:"",onClick:t[2]||(t[2]=t=>e.createHsr())},[Fa,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_CreateNewHeatmap")),1)])])]),_:1},8,["content-title"]),Object(s["createElementVNode"])("div",Wa,[Object(s["createElementVNode"])("h2",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_DeleteHeatmapConfirm")),1),Object(s["createElementVNode"])("input",{role:"yes",type:"button",value:e.translate("General_Yes")},null,8,qa),Object(s["createElementVNode"])("input",{role:"no",type:"button",value:e.translate("General_No")},null,8,za)],512),Object(s["createElementVNode"])("div",$a,[Object(s["createElementVNode"])("h2",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_EndHeatmapConfirm")),1),Object(s["createElementVNode"])("input",{role:"yes",type:"button",value:e.translate("General_Yes")},null,8,Ga),Object(s["createElementVNode"])("input",{role:"no",type:"button",value:e.translate("General_No")},null,8,Ja)],512)])}var Ya=Object(s["defineComponent"])({props:{pauseReason:String},components:{ContentBlock:A["ContentBlock"],Field:U["Field"]},directives:{ContentTable:A["ContentTable"]},data(){return{searchFilter:""}},created(){da.setFilterStatus(""),da.fetchHsrs()},methods:{createHsr(){this.editHsr(0)},editHsr(e){A["MatomoUrl"].updateHash(Object.assign(Object.assign({},A["MatomoUrl"].hashParsed.value),{},{idSiteHsr:e}))},deleteHsr(e){A["Matomo"].helper.modalConfirm(this.$refs.confirmDeleteHeatmap,{yes:()=>{da.deleteHsr(e.idsitehsr).then(()=>{da.reload(),A["Matomo"].postEvent("updateReportingMenu")})}})},completeHsr(e){A["Matomo"].helper.modalConfirm(this.$refs.confirmEndHeatmap,{yes:()=>{da.completeHsr(e.idsitehsr).then(()=>{da.reload()})}})},setFilterStatus(e){da.setFilterStatus(e)},ucfirst(e){return`${e[0].toUpperCase()}${e.substr(1)}`},getViewReportLink(e){return"?"+A["MatomoUrl"].stringify({module:"Widgetize",action:"iframe",moduleToWidgetize:"HeatmapSessionRecording",actionToWidgetize:"showHeatmap",idSiteHsr:e.idsitehsr,idSite:e.idsite,period:"day",date:"yesterday"})}},computed:{filterStatus(){return da.state.value.filterStatus},statusOptions(){return da.statusOptions},hsrs(){return da.hsrs.value},isLoading(){return da.state.value.isLoading},isUpdating(){return da.state.value.isUpdating},sortedHsrs(){const e=[...this.hsrs].filter(e=>Object.keys(e).some(t=>{const a=e;return"string"===typeof a[t]&&-1!==a[t].indexOf(this.searchFilter)}));return e.sort((e,t)=>t.idsitehsr-e.idsitehsr),e}}});Ya.render=Xa;var Ka=Ya;const Qa={class:"manageHsr",ref:"root"},Za={key:0},ei={key:1};function ti(e,t,a,i,n,r){const o=Object(s["resolveComponent"])("MatomoJsNotWritableAlert"),l=Object(s["resolveComponent"])("HeatmapList"),c=Object(s["resolveComponent"])("HeatmapEdit");return Object(s["openBlock"])(),Object(s["createElementBlock"])(s["Fragment"],null,[e.editMode?Object(s["createCommentVNode"])("",!0):(Object(s["openBlock"])(),Object(s["createBlock"])(o,{key:0,"is-matomo-js-writable":e.isMatomoJsWritable,"recording-type":e.translate("HeatmapSessionRecording_Heatmaps")},null,8,["is-matomo-js-writable","recording-type"])),Object(s["createElementVNode"])("div",Qa,[e.editMode?Object(s["createCommentVNode"])("",!0):(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",Za,[Object(s["createVNode"])(l,{"pause-reason":e.pauseReason},null,8,["pause-reason"])])),e.editMode?(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",ei,[Object(s["createVNode"])(c,{"breakpoint-mobile":e.breakpointMobile,"breakpoint-tablet":e.breakpointTablet,"id-site-hsr":e.idSiteHsr},null,8,["breakpoint-mobile","breakpoint-tablet","id-site-hsr"])])):Object(s["createCommentVNode"])("",!0)],512)],64)}const ai=["innerHTML"];function ii(e,t,a,i,n,r){return e.isMatomoJsWritable?Object(s["createCommentVNode"])("",!0):(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",{key:0,class:"alert alert-warning",innerHTML:e.getJsNotWritableErrorMessage()},null,8,ai))}var ni=Object(s["defineComponent"])({props:{recordingType:{type:String,required:!0},isMatomoJsWritable:{type:Boolean,required:!0}},methods:{getJsNotWritableErrorMessage(){return Object(A["translate"])("HeatmapSessionRecording_MatomoJSNotWritableErrorMessage",this.recordingType,''," ")}}});ni.render=ii;var si=ni;const{$:ri}=window;var oi=Object(s["defineComponent"])({props:{breakpointMobile:Number,breakpointTablet:Number,pauseReason:String,isMatomoJsWritable:{type:Boolean,required:!0}},data(){return{editMode:!1,idSiteHsr:null}},components:{MatomoJsNotWritableAlert:si,HeatmapList:Ka,HeatmapEdit:ua},watch:{editMode(){ri(".ui-tooltip").remove()}},created(){Object(s["watch"])(()=>A["MatomoUrl"].hashParsed.value.idSiteHsr,e=>{this.initState(e)}),this.initState(A["MatomoUrl"].hashParsed.value.idSiteHsr)},methods:{removeAnyHsrNotification(){A["NotificationsStore"].remove("hsrmanagement")},initState(e){if(e){if("0"===e){const e={isAllowed:!0};if(A["Matomo"].postEvent("HeatmapSessionRecording.initAddHeatmap",e),e&&!e.isAllowed)return this.editMode=!1,void(this.idSiteHsr=null)}this.editMode=!0,this.idSiteHsr=parseInt(e,10)}else this.editMode=!1,this.idSiteHsr=null;this.removeAnyHsrNotification()}}});oi.render=ti;var li=oi;const ci={class:"loadingPiwik"},di=Object(s["createElementVNode"])("img",{src:"plugins/Morpheus/images/loading-blue.gif"},null,-1),mi={class:"loadingPiwik"},pi=Object(s["createElementVNode"])("img",{src:"plugins/Morpheus/images/loading-blue.gif"},null,-1),hi={name:"name"},ui={name:"sampleLimit"},gi={class:"form-group row"},bi={class:"col s12"},vi={class:"col s12 m6",style:{"padding-left":"0"}},Oi=Object(s["createElementVNode"])("hr",null,null,-1),ji={class:"col s12 m6"},fi={class:"form-help"},yi={class:"inline-help"},Si={name:"sampleRate"},Hi={name:"minSessionTime"},_i={name:"requiresActivity"},Vi={class:"inline-help-node"},Ni=["innerHTML"],Ei=["innerHTML"],Ri={class:"entityCancel"};function wi(e,t,a,i,n,r){const o=Object(s["resolveComponent"])("Field"),l=Object(s["resolveComponent"])("HsrUrlTarget"),c=Object(s["resolveComponent"])("HsrTargetTest"),d=Object(s["resolveComponent"])("SaveButton"),m=Object(s["resolveComponent"])("ContentBlock");return Object(s["openBlock"])(),Object(s["createBlock"])(m,{class:"editHsr","content-title":e.contentTitle},{default:Object(s["withCtx"])(()=>[Object(s["withDirectives"])(Object(s["createElementVNode"])("p",null,[Object(s["createElementVNode"])("span",ci,[di,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("General_LoadingData")),1)])],512),[[s["vShow"],e.isLoading]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("p",null,[Object(s["createElementVNode"])("span",mi,[pi,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_UpdatingData")),1)])],512),[[s["vShow"],e.isUpdating]]),Object(s["createElementVNode"])("form",{onSubmit:t[10]||(t[10]=t=>e.edit?e.updateHsr():e.createHsr())},[Object(s["createElementVNode"])("div",null,[Object(s["createElementVNode"])("div",hi,[Object(s["createVNode"])(o,{uicontrol:"text",name:"name","model-value":e.siteHsr.name,"onUpdate:modelValue":t[0]||(t[0]=t=>{e.siteHsr.name=t,e.setValueHasChanged()}),title:e.translate("General_Name"),maxlength:50,placeholder:e.translate("HeatmapSessionRecording_FieldNamePlaceholder"),"inline-help":e.translate("HeatmapSessionRecording_SessionNameHelp")},null,8,["model-value","title","placeholder","inline-help"])]),Object(s["createElementVNode"])("div",ui,[Object(s["createVNode"])(o,{uicontrol:"select",name:"sampleLimit","model-value":e.siteHsr.sample_limit,"onUpdate:modelValue":t[1]||(t[1]=t=>{e.siteHsr.sample_limit=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_SessionSampleLimit"),options:e.sampleLimits,"inline-help":e.translate("HeatmapSessionRecording_SessionSampleLimitHelp")},null,8,["model-value","title","options","inline-help"])]),Object(s["createElementVNode"])("div",gi,[Object(s["createElementVNode"])("div",bi,[Object(s["createElementVNode"])("h3",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_TargetPages"))+":",1)]),Object(s["createElementVNode"])("div",vi,[(Object(s["openBlock"])(!0),Object(s["createElementBlock"])(s["Fragment"],null,Object(s["renderList"])(e.siteHsr.match_page_rules,(a,i)=>(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",{class:Object(s["normalizeClass"])(`matchPageRules ${i} multiple`),key:i},[Object(s["createElementVNode"])("div",null,[Object(s["createVNode"])(l,{"model-value":a,"onUpdate:modelValue":t=>e.setMatchPageRule(t,i),onAddUrl:t[2]||(t[2]=t=>e.addMatchPageRule()),onRemoveUrl:t=>e.removeMatchPageRule(i),onAnyChange:t[3]||(t[3]=t=>e.setValueHasChanged()),"allow-any":!0,"disable-if-no-value":i>0,"can-be-removed":i>0,"show-add-url":!0},null,8,["model-value","onUpdate:modelValue","onRemoveUrl","disable-if-no-value","can-be-removed"])]),Oi],2))),128))]),Object(s["createElementVNode"])("div",ji,[Object(s["createElementVNode"])("div",fi,[Object(s["createElementVNode"])("span",yi,[Object(s["createTextVNode"])(Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_FieldIncludedTargetsHelpSessions"))+" ",1),Object(s["createElementVNode"])("div",null,[Object(s["createVNode"])(c,{"included-targets":e.siteHsr.match_page_rules},null,8,["included-targets"])])])])])]),Object(s["createElementVNode"])("div",Si,[Object(s["createVNode"])(o,{uicontrol:"select",name:"sampleRate","model-value":e.siteHsr.sample_rate,"onUpdate:modelValue":t[4]||(t[4]=t=>{e.siteHsr.sample_rate=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_SampleRate"),options:e.sampleRates,introduction:e.translate("HeatmapSessionRecording_AdvancedOptions"),"inline-help":e.translate("HeatmapSessionRecording_SessionSampleRateHelp")},null,8,["model-value","title","options","introduction","inline-help"])]),Object(s["createElementVNode"])("div",Hi,[Object(s["createVNode"])(o,{uicontrol:"select",name:"minSessionTime","model-value":e.siteHsr.min_session_time,"onUpdate:modelValue":t[5]||(t[5]=t=>{e.siteHsr.min_session_time=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_MinSessionTime"),options:e.minSessionTimes,"inline-help":e.translate("HeatmapSessionRecording_MinSessionTimeHelp")},null,8,["model-value","title","options","inline-help"])]),Object(s["createElementVNode"])("div",_i,[Object(s["createVNode"])(o,{uicontrol:"checkbox",name:"requiresActivity","model-value":e.siteHsr.requires_activity,"onUpdate:modelValue":t[6]||(t[6]=t=>{e.siteHsr.requires_activity=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_RequiresActivity"),"inline-help":e.translate("HeatmapSessionRecording_RequiresActivityHelp")},null,8,["model-value","title","inline-help"])]),Object(s["createElementVNode"])("div",null,[Object(s["createVNode"])(o,{uicontrol:"checkbox",name:"captureKeystrokes","model-value":e.siteHsr.capture_keystrokes,"onUpdate:modelValue":t[7]||(t[7]=t=>{e.siteHsr.capture_keystrokes=t,e.setValueHasChanged()}),title:e.translate("HeatmapSessionRecording_CaptureKeystrokes")},{"inline-help":Object(s["withCtx"])(()=>[Object(s["createElementVNode"])("div",Vi,[Object(s["createElementVNode"])("span",{innerHTML:e.$sanitize(e.captureKeystrokesHelp)},null,8,Ni)])]),_:1},8,["model-value","title"])]),Object(s["createElementVNode"])("p",{innerHTML:e.$sanitize(e.personalInformationNote)},null,8,Ei),Object(s["createVNode"])(d,{class:"createButton",onConfirm:t[8]||(t[8]=t=>e.edit?e.updateHsr():e.createHsr()),disabled:e.isUpdating||!e.isDirty,saving:e.isUpdating,value:e.saveButtonText},null,8,["disabled","saving","value"]),Object(s["createElementVNode"])("div",Ri,[Object(s["createElementVNode"])("a",{onClick:t[9]||(t[9]=t=>e.cancel())},Object(s["toDisplayString"])(e.translate("General_Cancel")),1)])])],32)]),_:1},8,["content-title"])}const ki="hsrmanagement";var xi=Object(s["defineComponent"])({props:{idSiteHsr:Number},components:{ContentBlock:A["ContentBlock"],Field:U["Field"],HsrUrlTarget:Ut,HsrTargetTest:Nt,SaveButton:U["SaveButton"]},data(){return{isDirty:!1,showAdvancedView:!1,sampleLimits:[],siteHsr:{}}},created(){A["AjaxHelper"].fetch({method:"HeatmapSessionRecording.getAvailableSessionRecordingSampleLimits"}).then(e=>{this.sampleLimits=(e||[]).map(e=>({key:""+e,value:e}))}),this.init()},watch:{idSiteHsr(e){null!==e&&this.init()}},methods:{removeAnyHsrNotification(){A["NotificationsStore"].remove(ki),A["NotificationsStore"].remove("ajaxHelper")},showNotification(e,t){const a=A["NotificationsStore"].show({message:e,context:t,id:ki,type:"transient"});setTimeout(()=>{A["NotificationsStore"].scrollToNotification(a)},200)},showErrorFieldNotProvidedNotification(e){const t=Object(A["translate"])("HeatmapSessionRecording_ErrorXNotProvided",[e]);this.showNotification(t,"error")},init(){const{idSiteHsr:e}=this;this.siteHsr={},this.showAdvancedView=!1,A["Matomo"].helper.lazyScrollToContent(),this.edit&&e?ma.findHsr(e).then(e=>{e&&(this.siteHsr=Object(A["clone"])(e),this.siteHsr.sample_rate=""+this.siteHsr.sample_rate,this.addInitialMatchPageRule(),this.isDirty=!1)}):this.create&&(this.siteHsr={idSite:A["Matomo"].idSite,name:"",sample_rate:"10.0",sample_limit:250,min_session_time:0,requires_activity:!0,capture_keystrokes:!1},this.addInitialMatchPageRule(),this.isDirty=!1)},addInitialMatchPageRule(){var e;this.siteHsr&&(null!==(e=this.siteHsr.match_page_rules)&&void 0!==e&&e.length||(this.siteHsr.match_page_rules=[{attribute:"url",type:"any",value:"",inverted:0}]))},addMatchPageRule(){var e;this.siteHsr&&(null!==(e=this.siteHsr.match_page_rules)&&void 0!==e&&e.length||(this.siteHsr.match_page_rules=[]),this.siteHsr.match_page_rules.push({attribute:"url",type:"equals_simple",value:"",inverted:0}),this.isDirty=!0)},removeMatchPageRule(e){this.siteHsr&&e>-1&&(this.siteHsr.match_page_rules=[...this.siteHsr.match_page_rules],this.siteHsr.match_page_rules.splice(e,1),this.isDirty=!0)},cancel(){const e=Object.assign({},A["MatomoUrl"].hashParsed.value);delete e.idSiteHsr,A["MatomoUrl"].updateHash(e)},createHsr(){this.removeAnyHsrNotification(),this.checkRequiredFieldsAreSet()&&ma.createOrUpdateHsr(this.siteHsr,"HeatmapSessionRecording.addSessionRecording").then(e=>{if(!e||"error"===e.type||!e.response)return;this.isDirty=!1;const t=e.response.value;ma.reload().then(()=>{A["Matomo"].helper.isReportingPage()&&A["Matomo"].postEvent("updateReportingMenu"),A["MatomoUrl"].updateHash(Object.assign(Object.assign({},A["MatomoUrl"].hashParsed.value),{},{idSiteHsr:t})),setTimeout(()=>{this.showNotification(Object(A["translate"])("HeatmapSessionRecording_SessionRecordingCreated"),e.type)},200)})})},setValueHasChanged(){this.isDirty=!0},updateHsr(){this.removeAnyHsrNotification(),this.checkRequiredFieldsAreSet()&&ma.createOrUpdateHsr(this.siteHsr,"HeatmapSessionRecording.updateSessionRecording").then(e=>{"error"!==e.type&&(this.isDirty=!1,this.siteHsr={},ma.reload().then(()=>{this.init()}),this.showNotification(Object(A["translate"])("HeatmapSessionRecording_SessionRecordingUpdated"),e.type))})},checkRequiredFieldsAreSet(){var e;if(!this.siteHsr.name){const e=this.translate("General_Name");return this.showErrorFieldNotProvidedNotification(e),!1}if(null===(e=this.siteHsr.match_page_rules)||void 0===e||!e.length||!ma.filterRules(this.siteHsr.match_page_rules).length){const e=this.translate("HeatmapSessionRecording_ErrorPageRuleRequired");return this.showNotification(e,"error"),!1}return!0},setMatchPageRule(e,t){this.siteHsr.match_page_rules=[...this.siteHsr.match_page_rules],this.siteHsr.match_page_rules[t]=e}},computed:{minSessionTimes(){return[0,5,10,15,20,30,45,60,90,120].map(e=>({key:""+e,value:e+" seconds"}))},sampleRates(){const e=[.1,.5,1,2,3,4,5,6,8,10,15,20,30,40,50,60,70,80,90,100];return e.map(e=>({key:""+e.toFixed(1),value:e+"%"}))},create(){return!this.idSiteHsr},edit(){return!this.create},editTitle(){const e=this.create?"HeatmapSessionRecording_CreateNewSessionRecording":"HeatmapSessionRecording_EditSessionRecordingX";return e},contentTitle(){return Object(A["translate"])(this.editTitle,this.siteHsr.name?`"${this.siteHsr.name}"`:"")},isLoading(){return da.state.value.isLoading},isUpdating(){return da.state.value.isUpdating},captureKeystrokesHelp(){const e="https://developer.matomo.org/guides/heatmap-session-recording/setup#masking-keystrokes-in-form-fields";return Object(A["translate"])("HeatmapSessionRecording_CaptureKeystrokesHelp",``," ")},personalInformationNote(){const e="https://developer.matomo.org/guides/heatmap-session-recording/setup#masking-content-on-your-website";return Object(A["translate"])("HeatmapSessionRecording_PersonalInformationNote",Object(A["translate"])("HeatmapSessionRecording_SessionRecording"),"","
",``," ")},saveButtonText(){return this.edit?Object(A["translate"])("CoreUpdater_UpdateTitle"):Object(A["translate"])("HeatmapSessionRecording_CreateNewSessionRecording")}}});xi.render=wi;var Ci=xi;const Ti={class:"sessionRecordingList"},Di={class:"filterStatus"},Mi={class:"hsrSearchFilter",style:{"margin-left":"3.5px"}},Pi={class:"index"},Bi={class:"name"},Ai={class:"creationDate"},Ui={class:"sampleLimit"},Li={class:"status"},Ii={class:"action"},Fi={colspan:"7"},Wi={class:"loadingPiwik"},qi=Object(s["createElementVNode"])("img",{src:"plugins/Morpheus/images/loading-blue.gif"},null,-1),zi={colspan:"7"},$i=["id"],Gi={class:"index"},Ji={class:"name"},Xi={class:"creationDate"},Yi={class:"sampleLimit"},Ki={key:0,class:"status status-paused"},Qi=["title"],Zi={key:1,class:"status"},en={class:"action"},tn=["title","onClick"],an=["title","onClick"],nn=["title","href"],sn=["title","onClick"],rn={class:"tableActionBar"},on=Object(s["createElementVNode"])("span",{class:"icon-add"},null,-1),ln={class:"ui-confirm",ref:"confirmDeleteSessionRecording"},cn=["value"],dn=["value"],mn={class:"ui-confirm",ref:"confirmEndSessionRecording"},pn=["value"],hn=["value"];function un(e,t,a,i,n,r){const o=Object(s["resolveComponent"])("Field"),l=Object(s["resolveComponent"])("ContentBlock"),c=Object(s["resolveDirective"])("content-table");return Object(s["openBlock"])(),Object(s["createElementBlock"])("div",Ti,[Object(s["createVNode"])(l,{"content-title":e.translate("HeatmapSessionRecording_ManageSessionRecordings")},{default:Object(s["withCtx"])(()=>[Object(s["createElementVNode"])("p",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_SessionRecordingsUsageBenefits")),1),Object(s["createElementVNode"])("div",null,[Object(s["createElementVNode"])("div",Di,[Object(s["createVNode"])(o,{uicontrol:"select",name:"filterStatus","model-value":e.filterStatus,"onUpdate:modelValue":t[0]||(t[0]=t=>{e.setFilterStatus(t)}),title:e.translate("HeatmapSessionRecording_Filter"),"full-width":!0,options:e.statusOptions},null,8,["model-value","title","options"])]),Object(s["createElementVNode"])("div",Mi,[Object(s["withDirectives"])(Object(s["createVNode"])(o,{uicontrol:"text",name:"hsrSearch",title:e.translate("General_Search"),modelValue:e.searchFilter,"onUpdate:modelValue":t[1]||(t[1]=t=>e.searchFilter=t),"full-width":!0},null,8,["title","modelValue"]),[[s["vShow"],e.hsrs.length>0]])])]),Object(s["withDirectives"])((Object(s["openBlock"])(),Object(s["createElementBlock"])("table",null,[Object(s["createElementVNode"])("thead",null,[Object(s["createElementVNode"])("tr",null,[Object(s["createElementVNode"])("th",Pi,Object(s["toDisplayString"])(e.translate("General_Id")),1),Object(s["createElementVNode"])("th",Bi,Object(s["toDisplayString"])(e.translate("General_Name")),1),Object(s["createElementVNode"])("th",Ai,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_CreationDate")),1),Object(s["createElementVNode"])("th",Ui,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_SampleLimit")),1),Object(s["createElementVNode"])("th",Li,Object(s["toDisplayString"])(e.translate("CorePluginsAdmin_Status")),1),Object(s["createElementVNode"])("th",Ii,Object(s["toDisplayString"])(e.translate("General_Actions")),1)])]),Object(s["createElementVNode"])("tbody",null,[Object(s["withDirectives"])(Object(s["createElementVNode"])("tr",null,[Object(s["createElementVNode"])("td",Fi,[Object(s["createElementVNode"])("span",Wi,[qi,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("General_LoadingData")),1)])])],512),[[s["vShow"],e.isLoading||e.isUpdating]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("tr",null,[Object(s["createElementVNode"])("td",zi,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_NoSessionRecordingsFound")),1)],512),[[s["vShow"],!e.isLoading&&0==e.hsrs.length]]),(Object(s["openBlock"])(!0),Object(s["createElementBlock"])(s["Fragment"],null,Object(s["renderList"])(e.sortedHsrs,t=>(Object(s["openBlock"])(),Object(s["createElementBlock"])("tr",{id:"hsr"+t.idsitehsr,class:"hsrs",key:t.idsitehsr},[Object(s["createElementVNode"])("td",Gi,Object(s["toDisplayString"])(t.idsitehsr),1),Object(s["createElementVNode"])("td",Ji,Object(s["toDisplayString"])(t.name),1),Object(s["createElementVNode"])("td",Xi,Object(s["toDisplayString"])(t.created_date_pretty),1),Object(s["createElementVNode"])("td",Yi,Object(s["toDisplayString"])(t.sample_limit),1),"paused"===t.status?(Object(s["openBlock"])(),Object(s["createElementBlock"])("td",Ki,[Object(s["createTextVNode"])(Object(s["toDisplayString"])(e.ucfirst(t.status))+" ",1),Object(s["createElementVNode"])("span",{class:"icon icon-help",title:e.pauseReason},null,8,Qi)])):(Object(s["openBlock"])(),Object(s["createElementBlock"])("td",Zi,Object(s["toDisplayString"])(e.ucfirst(t.status)),1)),Object(s["createElementVNode"])("td",en,[Object(s["createElementVNode"])("a",{class:"table-action icon-edit",title:e.translate("HeatmapSessionRecording_EditX",e.translate("HeatmapSessionRecording_SessionRecording")),onClick:a=>e.editHsr(t.idsitehsr)},null,8,tn),Object(s["withDirectives"])(Object(s["createElementVNode"])("a",{class:"table-action stopRecording icon-drop-crossed",title:e.translate("HeatmapSessionRecording_StopX",e.translate("HeatmapSessionRecording_SessionRecording")),onClick:a=>e.completeHsr(t)},null,8,an),[[s["vShow"],"ended"!==t.status]]),Object(s["createElementVNode"])("a",{class:"table-action icon-show",title:e.translate("HeatmapSessionRecording_ViewReport"),href:e.getViewReportLink(t),target:"_blank"},null,8,nn),Object(s["createElementVNode"])("a",{class:"table-action icon-delete",title:e.translate("HeatmapSessionRecording_DeleteX",e.translate("HeatmapSessionRecording_SessionRecording")),onClick:a=>e.deleteHsr(t)},null,8,sn)])],8,$i))),128))])])),[[c]]),Object(s["createElementVNode"])("div",rn,[Object(s["createElementVNode"])("a",{class:"createNewHsr",value:"",onClick:t[2]||(t[2]=t=>e.createHsr())},[on,Object(s["createTextVNode"])(" "+Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_CreateNewSessionRecording")),1)])])]),_:1},8,["content-title"]),Object(s["createElementVNode"])("div",ln,[Object(s["createElementVNode"])("h2",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_DeleteSessionRecordingConfirm")),1),Object(s["createElementVNode"])("input",{role:"yes",type:"button",value:e.translate("General_Yes")},null,8,cn),Object(s["createElementVNode"])("input",{role:"no",type:"button",value:e.translate("General_No")},null,8,dn)],512),Object(s["createElementVNode"])("div",mn,[Object(s["createElementVNode"])("h2",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_EndSessionRecordingConfirm")),1),Object(s["createElementVNode"])("input",{role:"yes",type:"button",value:e.translate("General_Yes")},null,8,pn),Object(s["createElementVNode"])("input",{role:"no",type:"button",value:e.translate("General_No")},null,8,hn)],512)])}var gn=Object(s["defineComponent"])({props:{pauseReason:String},components:{ContentBlock:A["ContentBlock"],Field:U["Field"]},directives:{ContentTable:A["ContentTable"]},data(){return{searchFilter:""}},created(){ma.setFilterStatus(""),ma.fetchHsrs()},methods:{createHsr(){this.editHsr(0)},editHsr(e){A["MatomoUrl"].updateHash(Object.assign(Object.assign({},A["MatomoUrl"].hashParsed.value),{},{idSiteHsr:e}))},deleteHsr(e){A["Matomo"].helper.modalConfirm(this.$refs.confirmDeleteSessionRecording,{yes:()=>{ma.deleteHsr(e.idsitehsr).then(()=>{ma.reload(),A["Matomo"].postEvent("updateReportingMenu")})}})},completeHsr(e){A["Matomo"].helper.modalConfirm(this.$refs.confirmEndSessionRecording,{yes:()=>{ma.completeHsr(e.idsitehsr).then(()=>{ma.reload()})}})},setFilterStatus(e){ma.setFilterStatus(e)},ucfirst(e){return`${e[0].toUpperCase()}${e.substr(1)}`},getViewReportLink(e){return`?${A["MatomoUrl"].stringify({module:"CoreHome",action:"index",idSite:e.idsite,period:"day",date:"yesterday"})}#?${A["MatomoUrl"].stringify({category:"HeatmapSessionRecording_SessionRecordings",idSite:e.idsite,period:"day",date:"yesterday",subcategory:e.idsitehsr})}`}},computed:{filterStatus(){return ma.state.value.filterStatus},statusOptions(){return ma.statusOptions},hsrs(){return ma.hsrs.value},isLoading(){return ma.state.value.isLoading},isUpdating(){return ma.state.value.isUpdating},sortedHsrs(){const e=[...this.hsrs].filter(e=>Object.keys(e).some(t=>{const a=e;return"string"===typeof a[t]&&-1!==a[t].indexOf(this.searchFilter)}));return e.sort((e,t)=>t.idsitehsr-e.idsitehsr),e}}});gn.render=un;var bn=gn;const vn={class:"manageHsr"};function On(e,t,a,i,n,r){const o=Object(s["resolveComponent"])("MatomoJsNotWritableAlert"),l=Object(s["resolveComponent"])("SessionRecordingList"),c=Object(s["resolveComponent"])("SessionRecordingEdit");return Object(s["openBlock"])(),Object(s["createElementBlock"])(s["Fragment"],null,[e.editMode?Object(s["createCommentVNode"])("",!0):(Object(s["openBlock"])(),Object(s["createBlock"])(o,{key:0,"is-matomo-js-writable":e.isMatomoJsWritable,"recording-type":e.translate("HeatmapSessionRecording_SessionRecordings")},null,8,["is-matomo-js-writable","recording-type"])),Object(s["createElementVNode"])("div",vn,[Object(s["withDirectives"])(Object(s["createElementVNode"])("div",null,[Object(s["createVNode"])(l,{"pause-reason":e.pauseReason},null,8,["pause-reason"])],512),[[s["vShow"],!e.editMode]]),Object(s["withDirectives"])(Object(s["createElementVNode"])("div",null,[Object(s["createVNode"])(c,{"id-site-hsr":e.idSiteHsr},null,8,["id-site-hsr"])],512),[[s["vShow"],e.editMode]])])],64)}var jn=Object(s["defineComponent"])({props:{pauseReason:String,isMatomoJsWritable:{type:Boolean,required:!0}},data(){return{editMode:!1,idSiteHsr:null}},components:{MatomoJsNotWritableAlert:si,SessionRecordingEdit:Ci,SessionRecordingList:bn},created(){Object(s["watch"])(()=>A["MatomoUrl"].hashParsed.value.idSiteHsr,e=>{this.initState(e)}),this.initState(A["MatomoUrl"].hashParsed.value.idSiteHsr)},methods:{removeAnyHsrNotification(){A["NotificationsStore"].remove("hsrmanagement")},initState(e){if(e){if("0"===e){const e={isAllowed:!0};if(A["Matomo"].postEvent("HeatmapSessionRecording.initAddSessionRecording",e),e&&!e.isAllowed)return this.editMode=!1,void(this.idSiteHsr=null)}this.editMode=!0,this.idSiteHsr=parseInt(e,10)}else this.editMode=!1,this.idSiteHsr=null;this.removeAnyHsrNotification()}}});jn.render=On;var fn=jn;const yn={class:"ui-confirm",id:"listOfPageviews"},Sn=Object(s["createElementVNode"])("br",null,null,-1),Hn=Object(s["createElementVNode"])("br",null,null,-1),_n=["onClick"],Vn=["title"],Nn=["value"];function En(e,t,a,i,n,r){const o=Object(s["resolveDirective"])("content-table");return Object(s["openBlock"])(),Object(s["createElementBlock"])("div",yn,[Object(s["createElementVNode"])("h2",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_PageviewsInVisit")),1),Sn,Hn,Object(s["withDirectives"])((Object(s["openBlock"])(),Object(s["createElementBlock"])("table",null,[Object(s["createElementVNode"])("thead",null,[Object(s["createElementVNode"])("tr",null,[Object(s["createElementVNode"])("th",null,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_ColumnTime")),1),Object(s["createElementVNode"])("th",null,Object(s["toDisplayString"])(e.translate("General_TimeOnPage")),1),Object(s["createElementVNode"])("th",null,Object(s["toDisplayString"])(e.translate("Goals_URL")),1)])]),Object(s["createElementVNode"])("tbody",null,[(Object(s["openBlock"])(!0),Object(s["createElementBlock"])(s["Fragment"],null,Object(s["renderList"])(e.pageviews,t=>(Object(s["openBlock"])(),Object(s["createElementBlock"])("tr",{key:t.idloghsr,class:Object(s["normalizeClass"])({inactive:t.idloghsr!==e.idLogHsr}),onClick:a=>e.onClickPageView(t)},[Object(s["createElementVNode"])("td",null,Object(s["toDisplayString"])(t.server_time_pretty),1),Object(s["createElementVNode"])("td",null,Object(s["toDisplayString"])(t.time_on_page_pretty),1),Object(s["createElementVNode"])("td",{title:t.label},Object(s["toDisplayString"])((t.label||"").substr(0,50)),9,Vn)],10,_n))),128))])])),[[o]]),Object(s["createElementVNode"])("input",{role:"close",type:"button",value:e.translate("General_Close")},null,8,Nn)])}var Rn=Object(s["defineComponent"])({props:{pageviews:{type:Array,required:!0},idLogHsr:{type:Number,required:!0}},directives:{ContentTable:A["ContentTable"]},methods:{onClickPageView(e){e.idloghsr!==this.idLogHsr&&A["MatomoUrl"].updateUrl(Object.assign(Object.assign({},A["MatomoUrl"].urlParsed.value),{},{idLogHsr:e.idloghsr}),A["MatomoUrl"].hashParsed.value.length?Object.assign(Object.assign({},A["MatomoUrl"].hashParsed.value),{},{idLogHsr:e.idloghsr}):void 0)}}});Rn.render=En;var wn=Rn;const kn={class:"heatmap-vis-title"},xn={key:0,class:"alert alert-info heatmap-country-alert"},Cn={key:1},Tn={key:2},Dn=["innerHTML"],Mn={class:"alert alert-info"},Pn={key:3},Bn={class:"alert alert-info"};function An(e,t,a,i,n,r){var o;const l=Object(s["resolveComponent"])("EnrichedHeadline"),c=Object(s["resolveComponent"])("MatomoJsNotWritableAlert"),d=Object(s["resolveComponent"])("HeatmapVis"),m=Object(s["resolveComponent"])("ContentBlock");return Object(s["openBlock"])(),Object(s["createElementBlock"])("div",null,[Object(s["createElementVNode"])("h2",kn,[Object(s["createVNode"])(l,{"edit-url":e.editUrl,"inline-help":e.inlineHelp},{default:Object(s["withCtx"])(()=>[Object(s["createTextVNode"])(Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_HeatmapX",`"${e.heatmap.name}"`)),1)]),_:1},8,["edit-url","inline-help"])]),Object(s["createVNode"])(c,{"is-matomo-js-writable":e.isMatomoJsWritable,"recording-type":e.translate("HeatmapSessionRecording_Heatmaps")},null,8,["is-matomo-js-writable","recording-type"]),e.includedCountries?(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",xn,Object(s["toDisplayString"])(e.translate("HeatmapSessionRecording_HeatmapInfoTrackVisitsFromCountries",e.includedCountries)),1)):Object(s["createCommentVNode"])("",!0),e.heatmap.page_treemirror?(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",Cn,[Object(s["createVNode"])(d,{"created-date":e.createdDate,"excluded-elements":e.heatmap.excluded_elements,"num-samples":e.heatmapMetadata,url:e.heatmap.screenshot_url,"heatmap-date":e.heatmapDate,"heatmap-period":e.heatmapPeriod,"offset-accuracy":e.offsetAccuracy,"breakpoint-tablet":e.heatmap.breakpoint_tablet,"breakpoint-mobile":e.heatmap.breakpoint_mobile,"heatmap-types":e.heatmapTypes,"device-types":e.deviceTypes,"id-site-hsr":e.idSiteHsr,"is-active":e.isActive,"desktop-preview-size":e.desktopPreviewSize,"iframe-resolutions-values":e.iframeResolutions},null,8,["created-date","excluded-elements","num-samples","url","heatmap-date","heatmap-period","offset-accuracy","breakpoint-tablet","breakpoint-mobile","heatmap-types","device-types","id-site-hsr","is-active","desktop-preview-size","iframe-resolutions-values"])])):null!==(o=e.heatmapMetadata)&&void 0!==o&&o.nb_samples_device_all?(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",Pn,[Object(s["createVNode"])(m,null,{default:Object(s["withCtx"])(()=>[Object(s["createElementVNode"])("div",Bn,Object(s["toDisplayString"])(e.noHeatmapScreenshotRecordedYetText),1)]),_:1})])):(Object(s["openBlock"])(),Object(s["createElementBlock"])("div",Tn,[Object(s["createElementVNode"])("p",{innerHTML:e.$sanitize(e.recordedSamplesTroubleShoot)},null,8,Dn),Object(s["createVNode"])(m,null,{default:Object(s["withCtx"])(()=>[Object(s["createElementVNode"])("div",Mn,Object(s["toDisplayString"])(e.translate(e.noDataMessageKey)),1)]),_:1})]))])}var Un=Object(s["defineComponent"])({props:{idSiteHsr:{type:Number,required:!0},heatmap:{type:Object,required:!0},heatmapMetadata:{type:Object,required:!0},deviceTypes:{type:Array,required:!0},heatmapTypes:{type:Array,required:!0},offsetAccuracy:{type:Number,required:!0},heatmapPeriod:{type:String,required:!0},heatmapDate:{type:String,required:!0},isActive:Boolean,createdDate:{type:String,required:!0},editUrl:{type:String,required:!0},inlineHelp:{type:String,required:!0},includedCountries:{type:String,required:!0},desktopPreviewSize:{type:Number,required:!0},iframeResolutions:{type:Object,required:!0},noDataMessageKey:{type:String,required:!0},isMatomoJsWritable:{type:Boolean,required:!0}},components:{MatomoJsNotWritableAlert:si,ContentBlock:A["ContentBlock"],HeatmapVis:oe,EnrichedHeadline:A["EnrichedHeadline"]},computed:{noHeatmapScreenshotRecordedYetText(){return Object(A["translate"])("HeatmapSessionRecording_NoHeatmapScreenshotRecordedYet",this.heatmapMetadata.nb_samples_device_all,Object(A["translate"])("HeatmapSessionRecording_ScreenshotUrl"))},recordedSamplesTroubleShoot(){const e=Object(A["externalLink"])("https://matomo.org/faq/heatmap-session-recording/troubleshooting-heatmaps/");return Object(A["translate"])("HeatmapSessionRecording_HeatmapTroubleshoot",e," ")}},created(){A["Matomo"].postEvent("hidePeriodSelector")}});Un.render=An;var Ln=Un;
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */}})}));
+//# sourceMappingURL=HeatmapSessionRecording.umd.min.js.map
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/umd.metadata.json b/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/umd.metadata.json
new file mode 100644
index 0000000..dce4477
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/umd.metadata.json
@@ -0,0 +1,6 @@
+{
+ "dependsOn": [
+ "CoreHome",
+ "CorePluginsAdmin"
+ ]
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVis.less b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVis.less
new file mode 100644
index 0000000..746f9ab
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVis.less
@@ -0,0 +1,99 @@
+.heatmapVis {
+
+ .aboveFoldLine {
+ height: 4px;
+ background: orange;
+ position: absolute;
+
+ div {
+ color: orange;
+ margin-top: 4px;
+ margin-left: 4px;
+ }
+ }
+
+ .iframeRecordingContainer {
+ position: relative;
+ }
+
+ .numSamples {
+ position: relative;
+ top: -2px;
+ }
+
+ .customIframeWidth {
+ display: inline-block;
+ margin-bottom: -3rem;
+ margin-right: -6rem;
+
+ .matomo-form-field {
+ margin-top: -3.2rem;
+ margin-left: 5rem;
+ }
+ }
+
+ .heatmapTile {
+ width: 100%;
+ }
+
+ .heatmapWrapper {
+ position: absolute;
+
+ #heatmapContainer {
+ position:absolute;
+ width: 1000px;
+ height: 1000px;
+ }
+ }
+
+ .heatmapSelection {
+ margin-bottom: 16px;
+ white-space: nowrap;
+ }
+
+ .legendOuter {
+ white-space: nowrap;
+ display:inline;
+
+ h4 {
+ display: inline;
+ margin-left: 2.5rem;
+ margin-right: 10px;
+ }
+ }
+
+ .legend-area {
+ display: inline;
+ .min {
+ margin-left: 8px;
+ margin-right: 8px;
+ }
+ .max {
+ margin-left: 16px;
+ }
+ }
+
+ .btn-flat {
+ border: 1px solid #ccc;
+ border-radius: 0 !important;
+ margin-left: -1px;
+ }
+
+ .visActive {
+ background-color: #ddd;
+ }
+
+ #highlightDiv {
+ position: absolute;
+ background-color: #424242;
+ opacity: 0.5;
+ z-index: 999;
+ pointer-events: none;
+ }
+}
+
+.heatmap-vis-title {
+ .title {
+ color: @theme-color-headline-alternative !important;
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVis.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVis.vue
new file mode 100644
index 0000000..c184f59
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVis.vue
@@ -0,0 +1,1102 @@
+
+
+
+
+
+
+
+
+
+ {{ translate('HeatmapSessionRecording_Action') }}
+
+
{{ theHeatmapType.name }}
+
+ {{ translate('HeatmapSessionRecording_DeviceType') }}
+
+
+ {{ theDeviceType.numSamples }}
+
+
+
{{ translate('Installation_Legend') }}
+
+
0
+
+
0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ translate('General_Loading') }}
+
+
+
+
{{ translate('HeatmapSessionRecording_AvgAboveFoldTitle', avgFold) }}
+
+
+
+
+
+
+
+
{{ translate('HeatmapSessionRecording_DeleteHeatmapScreenshotConfirm') }}
+
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVisPage.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVisPage.vue
new file mode 100644
index 0000000..b22ddb6
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVisPage.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+ {{ translate('HeatmapSessionRecording_HeatmapX', `"${heatmap.name}"`) }}
+
+
+
+
+
+
+ {{ translate('HeatmapSessionRecording_HeatmapInfoTrackVisitsFromCountries',
+ includedCountries) }}
+
+
+
+
+
+
+
+
+
+
+ {{ translate(noDataMessageKey) }}
+
+
+
+
+
+
+ {{ noHeatmapScreenshotRecordedYetText }}
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrStore/HsrStore.store.ts b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrStore/HsrStore.store.ts
new file mode 100644
index 0000000..0fd7aea
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrStore/HsrStore.store.ts
@@ -0,0 +1,213 @@
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+import {
+ reactive,
+ computed,
+ readonly,
+ DeepReadonly,
+} from 'vue';
+import { AjaxHelper, translate, clone } from 'CoreHome';
+import { Heatmap, MatchPageRule, SessionRecording } from '../types';
+
+interface HsrStoreState {
+ allHsrs: (Heatmap|SessionRecording)[];
+ isLoading: boolean;
+ isUpdating: boolean;
+ filterStatus: string;
+}
+
+class HsrStore {
+ private privateState = reactive({
+ allHsrs: [],
+ isLoading: false,
+ isUpdating: false,
+ filterStatus: '',
+ });
+
+ readonly state = computed(() => readonly(this.privateState));
+
+ readonly hsrs = computed(() => {
+ if (!this.privateState.filterStatus) {
+ return this.state.value.allHsrs;
+ }
+
+ return this.state.value.allHsrs.filter((hsr) => hsr.status === this.privateState.filterStatus);
+ });
+
+ // used just for the adapter
+ readonly hsrsCloned = computed(() => clone(this.hsrs.value) as T[]);
+
+ readonly statusOptions = readonly([
+ { key: '', value: translate('General_All') },
+ { key: 'active', value: translate('HeatmapSessionRecording_StatusActive') },
+ { key: 'ended', value: translate('HeatmapSessionRecording_StatusEnded') },
+ { key: 'paused', value: translate('HeatmapSessionRecording_StatusPaused') },
+ ]);
+
+ private fetchPromises: Record> = {};
+
+ constructor(private context: string) {}
+
+ setFilterStatus(status: string) {
+ this.privateState.filterStatus = status;
+ }
+
+ reload(): ReturnType['fetchHsrs']> {
+ this.privateState.allHsrs = [];
+ this.fetchPromises = {};
+ return this.fetchHsrs();
+ }
+
+ filterRules(rules: MatchPageRule[]): MatchPageRule[] {
+ return rules.filter((target) => !!target && (target.value || target.type === 'any'));
+ }
+
+ private getApiMethodInContext(apiMethod: string) {
+ return `${apiMethod}${this.context}`;
+ }
+
+ fetchHsrs(): Promise> {
+ let method = 'HeatmapSessionRecording.getHeatmaps';
+ if (this.context === 'SessionRecording') {
+ method = 'HeatmapSessionRecording.getSessionRecordings';
+ }
+
+ const params = {
+ method,
+ filter_limit: '-1',
+ };
+
+ if (!this.fetchPromises[method]) {
+ this.fetchPromises[method] = AjaxHelper.fetch(params);
+ }
+
+ this.privateState.isLoading = true;
+ this.privateState.allHsrs = [];
+
+ return this.fetchPromises[method].then((hsrs) => {
+ this.privateState.allHsrs = hsrs;
+ return this.state.value.allHsrs as DeepReadonly;
+ }).finally(() => {
+ this.privateState.isLoading = false;
+ });
+ }
+
+ findHsr(idSiteHsr: number): Promise> {
+ // before going through an API request we first try to find it in loaded hsrs
+ const found = this.state.value.allHsrs.find((hsr) => hsr.idsitehsr === idSiteHsr);
+ if (found) {
+ return Promise.resolve(found as DeepReadonly);
+ }
+
+ // otherwise we fetch it via API
+ this.privateState.isLoading = true;
+ return AjaxHelper.fetch({
+ idSiteHsr,
+ method: this.getApiMethodInContext('HeatmapSessionRecording.get'),
+ filter_limit: '-1',
+ }).finally(() => {
+ this.privateState.isLoading = false;
+ });
+ }
+
+ deleteHsr(idSiteHsr: number) {
+ this.privateState.isUpdating = true;
+ this.privateState.allHsrs = [];
+
+ return AjaxHelper.fetch(
+ {
+ idSiteHsr,
+ method: this.getApiMethodInContext('HeatmapSessionRecording.delete'),
+ },
+ {
+ withTokenInUrl: true,
+ },
+ ).then(() => ({
+ type: 'success',
+ })).catch((error) => ({
+ type: 'error',
+ message: error.message || error,
+ })).finally(() => {
+ this.privateState.isUpdating = false;
+ });
+ }
+
+ completeHsr(idSiteHsr: number): Promise<{ type: string, message?: string }> {
+ this.privateState.isUpdating = true;
+ this.privateState.allHsrs = [];
+
+ return AjaxHelper.fetch(
+ {
+ idSiteHsr,
+ method: this.getApiMethodInContext('HeatmapSessionRecording.end'),
+ },
+ {
+ withTokenInUrl: true,
+ },
+ ).then(() => ({
+ type: 'success',
+ })).catch((error) => ({
+ type: 'error',
+ message: error.message || error as unknown as string,
+ })).finally(() => {
+ this.privateState.isUpdating = false;
+ });
+ }
+
+ createOrUpdateHsr(hsr: Heatmap|SessionRecording, method: string): Promise<{
+ type: string,
+ message?: string,
+ response?: { value: number },
+ }> {
+ const params = {
+ idSiteHsr: hsr.idsitehsr,
+ sampleLimit: hsr.sample_limit,
+ sampleRate: hsr.sample_rate,
+ excludedElements: (hsr as Heatmap).excluded_elements
+ ? (hsr as Heatmap).excluded_elements.trim()
+ : undefined,
+ screenshotUrl: (hsr as Heatmap).screenshot_url
+ ? (hsr as Heatmap).screenshot_url.trim()
+ : undefined,
+ breakpointMobile: (hsr as Heatmap).breakpoint_mobile,
+ breakpointTablet: (hsr as Heatmap).breakpoint_tablet,
+ minSessionTime: (hsr as SessionRecording).min_session_time,
+ requiresActivity: (hsr as SessionRecording).requires_activity ? 1 : 0,
+ captureKeystrokes: (hsr as SessionRecording).capture_keystrokes ? 1 : 0,
+ captureDomManually: (hsr as Heatmap).capture_manually ? 1 : 0,
+ method,
+ name: hsr.name.trim(),
+ };
+
+ const postParams = {
+ matchPageRules: this.filterRules(hsr.match_page_rules),
+ };
+
+ this.privateState.isUpdating = true;
+ return AjaxHelper.post(params, postParams, { withTokenInUrl: true }).then((response) => ({
+ type: 'success',
+ response,
+ })).catch((error) => ({
+ type: 'error',
+ message: error.message || error,
+ })).finally(() => {
+ this.privateState.isUpdating = false;
+ });
+ }
+}
+
+export const HeatmapStore = new HsrStore('Heatmap');
+export const SessionRecordingStore = new HsrStore('SessionRecording');
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrTargetTest/HsrTargetTest.less b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrTargetTest/HsrTargetTest.less
new file mode 100644
index 0000000..c4fea5e
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrTargetTest/HsrTargetTest.less
@@ -0,0 +1,20 @@
+.hsrTargetTest {
+
+ .testInfo {
+ margin-top: 8px;
+ display: inline-block;
+ }
+ .matches {
+ color: #43a047;
+ }
+ .notMatches {
+ color: #D4291F;
+ }
+ input, label {
+ width: 100% !important;
+ }
+
+ label {
+ margin-top: 0;
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrTargetTest/HsrTargetTest.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrTargetTest/HsrTargetTest.vue
new file mode 100644
index 0000000..9c69c9a
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrTargetTest/HsrTargetTest.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/AvailableTargetPageRules.store.ts b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/AvailableTargetPageRules.store.ts
new file mode 100644
index 0000000..88a81e9
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/AvailableTargetPageRules.store.ts
@@ -0,0 +1,57 @@
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+import {
+ computed,
+ reactive,
+ readonly,
+ DeepReadonly,
+} from 'vue';
+import { AjaxHelper } from 'CoreHome';
+import { AvailableTargetPageRule } from '../types';
+
+interface AvailableTargetPageRulesStoreState {
+ rules: AvailableTargetPageRule[];
+}
+
+class AvailableTargetPageRulesStore {
+ private privateState = reactive({
+ rules: [],
+ });
+
+ readonly state = computed(() => readonly(this.privateState));
+
+ readonly rules = computed(() => this.state.value.rules);
+
+ private initPromise: Promise>|null = null;
+
+ init() {
+ if (this.initPromise) {
+ return this.initPromise!;
+ }
+
+ this.initPromise = AjaxHelper.fetch({
+ method: 'HeatmapSessionRecording.getAvailableTargetPageRules',
+ filter_limit: '-1',
+ }).then((response) => {
+ this.privateState.rules = response;
+ return this.rules.value;
+ });
+
+ return this.initPromise!;
+ }
+}
+
+export default new AvailableTargetPageRulesStore();
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/HsrUrlTarget.less b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/HsrUrlTarget.less
new file mode 100644
index 0000000..422276a
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/HsrUrlTarget.less
@@ -0,0 +1,17 @@
+.hsrUrltarget {
+ margin-bottom: 40px;
+
+ .targetValue,
+ .targetValue2,
+ .targetType {
+ margin-top:10px;
+ }
+
+ // set the opacity of individual elements to 0.6 instead of the root element if disabled, otherwise
+ // the select dropdown contents will be slightly transparent
+ &.disabled {
+ input,label,.icon-plus,.icon-minus {
+ opacity: 0.6;
+ }
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/HsrUrlTarget.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/HsrUrlTarget.vue
new file mode 100644
index 0000000..624bbe5
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/HsrUrlTarget.vue
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ListOfPageviews/ListOfPageviews.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ListOfPageviews/ListOfPageviews.vue
new file mode 100644
index 0000000..e05fc1b
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ListOfPageviews/ListOfPageviews.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
{{ translate('HeatmapSessionRecording_PageviewsInVisit') }}
+
+
+
+
+
+ {{ translate('HeatmapSessionRecording_ColumnTime') }}
+ {{ translate('General_TimeOnPage') }}
+ {{ translate('Goals_URL') }}
+
+
+
+
+ {{ pageview.server_time_pretty }}
+ {{ pageview.time_on_page_pretty }}
+ {{ (pageview.label || '').substr(0, 50) }}
+
+
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/Edit.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/Edit.vue
new file mode 100644
index 0000000..fce8811
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/Edit.vue
@@ -0,0 +1,522 @@
+
+
+
+
+
+
+ {{ translate('General_LoadingData') }}
+
+
+
+ {{ translate('HeatmapSessionRecording_UpdatingData') }}
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/List.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/List.vue
new file mode 100644
index 0000000..e7c54ee
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/List.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/Manage.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/Manage.vue
new file mode 100644
index 0000000..8125aa6
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/Manage.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/Edit.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/Edit.vue
new file mode 100644
index 0000000..916bc95
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/Edit.vue
@@ -0,0 +1,488 @@
+
+
+
+
+
+
+ {{ translate('General_LoadingData') }}
+
+
+
+ {{ translate('HeatmapSessionRecording_UpdatingData') }}
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/List.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/List.vue
new file mode 100644
index 0000000..fdcc4cd
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/List.vue
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/Manage.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/Manage.vue
new file mode 100644
index 0000000..8a2a78b
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/Manage.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue
new file mode 100644
index 0000000..3ee10ad
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/SessionRecordingVis/SessionRecordingVis.less b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/SessionRecordingVis/SessionRecordingVis.less
new file mode 100644
index 0000000..2766617
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/SessionRecordingVis/SessionRecordingVis.less
@@ -0,0 +1,131 @@
+.sessionRecordingPlayer {
+ font-size: 14px;
+
+ .toggleSkipPause,
+ .toggleAutoPlay {
+ cursor: pointer;
+ margin-right: 3.5px;
+
+ svg {
+ fill: #666;
+ }
+
+ &.active {
+ svg {
+ fill: #0d0d0d;
+ }
+ }
+ }
+
+ .changeReplaySpeed {
+ display: inline-block;
+ cursor: pointer;
+ margin-left: 2px;
+ margin-right: 5.5px;
+ }
+
+ .playerAction {
+ cursor: pointer;
+ font-size: 13px;
+ margin-right: 3.5px;
+ }
+
+ .controls {
+ margin-top: 16px;
+ position: relative;
+ }
+
+ .duration {
+ font-size: 17px;
+ display: inline-block;
+ position: relative;
+ top: -3px;
+ left: 8px;
+ }
+
+ .playerActions {
+ white-space: nowrap;
+ display: inline-block;
+
+ .playerAction {
+ font-size: 22px;
+ &:hover {
+ color: @theme-color-brand;
+ }
+ }
+ }
+
+ .timelineInner {
+ background-color: red;
+ height: 10px;
+ }
+ .timelineOuter {
+ height: 10px;
+ background-color: #d3d3d3;
+ position: absolute;
+ cursor: pointer;
+ margin-top: 3px;
+ }
+
+ .playerHelp {
+ float: right;
+ margin-top: 4px;
+ margin-right: 16px;
+
+ li {
+ font-size: 14px;
+ margin-left: 11.5px;
+ display: inline-block;
+
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+
+ .formChange, .moveEvent, .scrollEvent, .clickEvent, .mutationEvent, .resizeEvent {
+ position: relative;
+ display: inline-block;
+ width: 8px;
+ top: auto;
+ left: auto;
+ }
+
+ }
+ .formChange, .moveEvent, .scrollEvent, .clickEvent, .mutationEvent, .resizeEvent {
+ position: absolute;
+ height: 8px;
+ width: 3px;
+ top: 1px;
+ left: 10px;
+ }
+
+ .moveEvent {
+ background: orange;
+ }
+ .clickEvent {
+ background: yellow;
+ }
+ .resizeEvent {
+ background: brown;
+ }
+ .mutationEvent {
+ background: black;
+ }
+ .scrollEvent {
+ background: blue;
+ }
+ .formChange {
+ background: green;
+ }
+
+ .replayContainerOuter {
+ background: black;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .replayContainerInner {
+ height: 100%;
+ width: 100%;
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/SessionRecordingVis/SessionRecordingVis.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/SessionRecordingVis/SessionRecordingVis.vue
new file mode 100644
index 0000000..fff1a0e
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/SessionRecordingVis/SessionRecordingVis.vue
@@ -0,0 +1,1222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ translate(
+ 'HeatmapSessionRecording_PlayerDurationXofY', positionPretty, durationPretty) }}
+
+
+
+
+
+ {{ translate('HeatmapSessionRecording_ActivityClick') }}
+
+
+ {{ translate('HeatmapSessionRecording_ActivityMove') }}
+
+
+
+ {{ translate('HeatmapSessionRecording_ActivityScroll') }}
+
+
+
+ {{ translate('HeatmapSessionRecording_ActivityResize') }}
+
+
+
+ {{ translate('HeatmapSessionRecording_ActivityFormChange') }}
+
+
+
+ {{ translate('HeatmapSessionRecording_ActivityPageChange') }}
+
+
+
+
+
+
+
+
+
+
+
{{ translate('General_Loading') }}
+
+
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/Tooltip/Tooltip.less b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/Tooltip/Tooltip.less
new file mode 100644
index 0000000..90c671f
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/Tooltip/Tooltip.less
@@ -0,0 +1,59 @@
+.heatmapVis .tooltip {
+ position: absolute;
+ display: inline-flex;
+ flex-direction: column;
+ align-items: flex-start;
+ min-width: 8rem;
+ padding: 1.2rem 1.2rem;
+ gap: 8px;
+ border-radius: 4px;
+ font-size: 14px;
+ text-align: left;
+ color: #fff;
+ background-color: #000;
+ pointer-events: none;
+
+ .tooltip-item {
+ color: #ccc;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+
+ &.selected {
+ font-weight: bold;
+ color: #fff;
+ }
+
+ &.title {
+ color: #fff;
+ font-size: 16px;
+ font-weight: bold;
+ white-space: nowrap;
+ }
+
+ &.subtitle {
+ font-size: 16px;
+ margin-top: -2px;
+ margin-bottom: 13px;
+ white-space: nowrap;
+ }
+ }
+
+ .tooltip-separator {
+ width: 100%;
+ height: 1px;
+ background-color: #444;
+ margin: 0.4rem 0;
+ display: none;
+ }
+
+ .tooltip-label {
+ margin-right: 1rem;
+ white-space: nowrap;
+ }
+
+ .tooltip-value {
+ text-align: right;
+ white-space: nowrap;
+ }
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/Tooltip/Tooltip.vue b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/Tooltip/Tooltip.vue
new file mode 100644
index 0000000..72ea06e
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/Tooltip/Tooltip.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/getIframeWindow.ts b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/getIframeWindow.ts
new file mode 100644
index 0000000..3dd849c
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/getIframeWindow.ts
@@ -0,0 +1,27 @@
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export default function getIframeWindow(iframeElement: HTMLIFrameElement): any|undefined {
+ if (iframeElement && iframeElement.contentWindow) {
+ return iframeElement.contentWindow;
+ }
+
+ if (iframeElement && iframeElement.contentDocument && iframeElement.contentDocument.defaultView) {
+ return iframeElement.contentDocument.defaultView;
+ }
+
+ return undefined;
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/index.ts b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/index.ts
new file mode 100644
index 0000000..aac404f
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/index.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+export { default as HeatmapVis } from './HeatmapVis/HeatmapVis.vue';
+export { default as SessionRecordingVis } from './SessionRecordingVis/SessionRecordingVis.vue';
+export { default as HsrTargetTest } from './HsrTargetTest/HsrTargetTest.vue';
+export { default as HsrUrlTarget } from './HsrUrlTarget/HsrUrlTarget.vue';
+export { default as HeatmapEdit } from './ManageHeatmap/Edit.vue';
+export { default as HeatmapList } from './ManageHeatmap/List.vue';
+export { default as HeatmapManage } from './ManageHeatmap/Manage.vue';
+export { default as SessionRecordingEdit } from './ManageSessionRecording/Edit.vue';
+export { default as SessionRecordingList } from './ManageSessionRecording/List.vue';
+export { default as SessionRecordingManage } from './ManageSessionRecording/Manage.vue';
+export { default as ListOfPageviews } from './ListOfPageviews/ListOfPageviews.vue';
+export { default as HeatmapVisPage } from './HeatmapVis/HeatmapVisPage.vue';
+export { default as MatomoJsNotWritableAlert } from './MatomoJsNotWritable/MatomoJsNotWritableAlert.vue';
+export { default as Tooltip } from './Tooltip/Tooltip.vue';
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/oneAtATime.ts b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/oneAtATime.ts
new file mode 100644
index 0000000..71b371b
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/oneAtATime.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+import { AjaxHelper, AjaxOptions } from 'CoreHome';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export default function oneAtATime(
+ method: string,
+ options?: AjaxOptions,
+): (params: QueryParameters, postParams?: QueryParameters) => Promise {
+ let abortController: AbortController|null = null;
+
+ return (params: QueryParameters, postParams?: QueryParameters) => {
+ if (abortController) {
+ abortController.abort();
+ abortController = null;
+ }
+
+ abortController = new AbortController();
+ return AjaxHelper.post(
+ {
+ ...params,
+ method,
+ },
+ postParams,
+ { ...options, abortController },
+ ).finally(() => {
+ abortController = null;
+ });
+ };
+}
diff --git a/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/types.ts b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/types.ts
new file mode 100644
index 0000000..59ac353
--- /dev/null
+++ b/files/plugin-HeatmapSessionRecording-5.2.4/vue/src/types.ts
@@ -0,0 +1,131 @@
+/**
+ * Copyright (C) InnoCraft Ltd - All rights reserved.
+ *
+ * NOTICE: All information contained herein is, and remains the property of InnoCraft Ltd.
+ * The intellectual and technical concepts contained herein are protected by trade secret
+ * or copyright law. Redistribution of this information or reproduction of this material is
+ * strictly forbidden unless prior written permission is obtained from InnoCraft Ltd.
+ *
+ * You shall use this code only in accordance with the license agreement obtained from
+ * InnoCraft Ltd.
+ *
+ * @link https://www.innocraft.com/
+ * @license For license details see https://www.innocraft.com/license
+ */
+
+export interface AvailableTargetPageRuleType {
+ name: string;
+ value: string;
+}
+
+export interface AvailableTargetPageRule {
+ example: string;
+ name: string;
+ types: AvailableTargetPageRuleType[];
+ value: string;
+}
+
+export interface Status {
+ value: string;
+ name: string;
+}
+
+export interface MatchPageRule {
+ attribute: string;
+ type: string;
+ value: string;
+ inverted: string|number;
+}
+
+export interface Heatmap {
+ breakpoint_mobile: number;
+ breakpoint_tablet: number;
+ created_date?: string;
+ created_date_pretty?: string;
+ excluded_elements: string;
+ heatmapViewUrl: string;
+ idsite: number;
+ idsitehsr: number;
+ match_page_rules: MatchPageRule[];
+ name: string;
+ page_treemirror: string;
+ sample_limit: number;
+ sample_rate: string;
+ screenshot_url: string;
+ status: string;
+ updated_date?: string;
+ capture_manually: boolean,
+}
+
+export interface SessionRecording {
+ capture_keystrokes: boolean;
+ created_date?: string;
+ created_date_pretty?: string;
+ idsite: number;
+ idsitehsr: number;
+ match_page_rules: MatchPageRule[];
+ min_session_time: number;
+ name: string;
+ requires_activity: boolean;
+ sample_limit: number;
+ sample_rate: string|number;
+ status: string;
+ updated_date?: string;
+}
+
+export interface DeviceType {
+ name: string;
+ key: number;
+ logo: string;
+}
+
+export interface HeatmapType {
+ name: string;
+ key: number;
+}
+
+export type HeatmapMetadata = Record;
+
+export interface SessionRecordingEvent {
+ time_since_load: number|string;
+ event_type: number|string;
+ x: number|string;
+ y: number|string;
+ selector: string;
+ text?: unknown;
+}
+
+export interface PageviewInSession {
+ idloghsr: number|string;
+ fold_y_relative: string|number;
+ idvisitor: string;
+ label: string;
+ resolution: string;
+ scroll_y_max_relative: string|number;
+ server_time: string;
+ server_time_pretty: string;
+ time_on_page: string|number;
+ time_on_page_pretty: string;
+}
+
+export interface SessionRecordingData {
+ events: SessionRecordingEvent[];
+ viewport_w_px: number|string;
+ viewport_h_px: number|string;
+ pageviews: PageviewInSession[];
+ idLogHsr: number|string;
+ idSiteHsr: number|string;
+ idSite: number|string;
+ duration: number|string;
+ url: string;
+}
+
+declare global {
+ interface PiwikGlobal {
+ heatmapWriteAccess?: boolean;
+ }
+
+ interface Window {
+ sessionRecordingData: SessionRecordingData;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/API.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/API.php
new file mode 100644
index 0000000..9c00f9b
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/API.php
@@ -0,0 +1,410 @@
+SearchEngineKeywordsPerformance API lets you download all your SEO search keywords from Google,
+ * Bing & Yahoo and Yandex, as well as getting a detailed overview of how search robots crawl your websites and any
+ * error they may encounter.
+ *
+ * 1) download all your search keywords as they were searched on Google, Bing & Yahoo and Yandex. This includes Google
+ * Images, Google Videos and Google News. This lets you view all keywords normally hidden from view behind "keyword not defined".
+ * With this plugin you can view them all!
+ *
+ * 2) download all crawling overview stats and metrics from Bring and Yahoo and Google. Many metrics are available such
+ * as: Crawled pages, Crawl errors, Connection timeouts, HTTP-Status Code 301 (Permanently moved), HTTP-Status Code
+ * 400-499 (Request errors), All other HTTP-Status Codes, Total pages in index, Robots.txt exclusion, DNS failures,
+ * HTTP-Status Code 200-299, HTTP-Status Code 301 (Temporarily moved), HTTP-Status Code 500-599 (Internal server
+ * errors), Malware infected sites, Total inbound links.
+ *
+ * @package Piwik\Plugins\SearchEngineKeywordsPerformance
+ */
+class API extends \Piwik\Plugin\API
+{
+ /**
+ * Returns Keyword data used on any search
+ * Combines imported search keywords with those returned by Referrers plugin
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywords($idSite, $period, $date)
+ {
+ $keywordsDataTable = $this->getKeywordsImported($idSite, $period, $date);
+ $keywordsDataTable->deleteColumns([\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::POSITION, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_IMPRESSIONS, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::CTR]);
+ $keywordsDataTable->filter('ColumnCallbackDeleteRow', [\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS, function ($clicks) {
+ return $clicks === 0;
+ }]);
+ $keywordsDataTable->filter('ReplaceColumnNames', [[\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS => 'nb_visits']]);
+ $setting = new \Piwik\Plugins\SearchEngineKeywordsPerformance\MeasurableSettings($idSite);
+ $googleEnabled = !empty($setting->googleSearchConsoleUrl) && $setting->googleSearchConsoleUrl->getValue();
+ $bingEnabled = !empty($setting->bingSiteUrl) && $setting->bingSiteUrl->getValue();
+ $referrersApi = new \Piwik\Plugins\Referrers\API();
+ $referrersKeywords = $referrersApi->getSearchEngines($idSite, $period, $date, \false, \true);
+ if ($keywordsDataTable instanceof DataTable\Map) {
+ $referrerTables = $referrersKeywords->getDataTables();
+ foreach ($keywordsDataTable->getDataTables() as $label => $table) {
+ if (!empty($referrerTables[$label])) {
+ $this->combineKeywordReports($table, $referrerTables[$label], $googleEnabled, $bingEnabled, $period);
+ }
+ }
+ } else {
+ $this->combineKeywordReports($keywordsDataTable, $referrersKeywords, $googleEnabled, $bingEnabled, $period);
+ }
+ return $keywordsDataTable;
+ }
+ private function combineKeywordReports(DataTable $keywordsDataTable, DataTable $referrersKeywords, $googleEnabled, $bingEnabled, $period)
+ {
+ foreach ($referrersKeywords->getRowsWithoutSummaryRow() as $searchEngineRow) {
+ $label = $searchEngineRow->getColumn('label');
+ if (strpos($label, 'Google') !== \false && $googleEnabled) {
+ continue;
+ }
+ if (strpos($label, 'Bing') !== \false && $bingEnabled && $period !== 'day') {
+ continue;
+ }
+ if (strpos($label, 'Yahoo') !== \false && $bingEnabled && $period !== 'day') {
+ continue;
+ }
+ $keywordsTable = $searchEngineRow->getSubtable();
+ if (empty($keywordsTable) || !$keywordsTable->getRowsCount()) {
+ continue;
+ }
+ foreach ($keywordsTable->getRowsWithoutSummaryRow() as $keywordRow) {
+ if ($keywordRow->getColumn('label') == \Piwik\Plugins\Referrers\API::LABEL_KEYWORD_NOT_DEFINED) {
+ continue;
+ }
+ $keywordsDataTable->addRow(new DataTable\Row([DataTable\Row::COLUMNS => ['label' => $keywordRow->getColumn('label'), 'nb_visits' => $keywordRow->getColumn(\Piwik\Metrics::INDEX_NB_VISITS)]]));
+ }
+ }
+ $keywordsDataTable->filter('GroupBy', ['label']);
+ $keywordsDataTable->filter('ReplaceSummaryRowLabel');
+ }
+ /**
+ * Returns Keyword data used on any imported search engine
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywordsImported($idSite, $period, $date)
+ {
+ $googleWebKwds = $this->getKeywordsGoogleWeb($idSite, $period, $date);
+ $googleImgKwds = $this->getKeywordsGoogleImage($idSite, $period, $date);
+ $googleVidKwds = $this->getKeywordsGoogleVideo($idSite, $period, $date);
+ $googleNwsKwds = $this->getKeywordsGoogleNews($idSite, $period, $date);
+ $bingKeywords = $this->getKeywordsBing($idSite, $period, $date);
+ $yandexKeywords = $this->getKeywordsYandex($idSite, $period, $date);
+ return $this->combineDataTables([$googleWebKwds, $googleImgKwds, $googleVidKwds, $googleNwsKwds, $bingKeywords, $yandexKeywords]);
+ }
+ /**
+ * Returns Keyword data used on Google
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywordsGoogle($idSite, $period, $date)
+ {
+ $googleWebKwds = $this->getKeywordsGoogleWeb($idSite, $period, $date);
+ $googleImgKwds = $this->getKeywordsGoogleImage($idSite, $period, $date);
+ $googleVidKwds = $this->getKeywordsGoogleVideo($idSite, $period, $date);
+ $googleNwsKwds = $this->getKeywordsGoogleNews($idSite, $period, $date);
+ return $this->combineDataTables([$googleWebKwds, $googleImgKwds, $googleVidKwds, $googleNwsKwds]);
+ }
+ /**
+ * Returns a DataTable with the given datatables combined
+ *
+ * @param DataTable[]|DataTable\Map[] $dataTablesToCombine
+ *
+ * @return DataTable|DataTable\Map
+ */
+ protected function combineDataTables(array $dataTablesToCombine)
+ {
+ if (reset($dataTablesToCombine) instanceof DataTable\Map) {
+ $dataTable = new DataTable\Map();
+ $dataTables = [];
+ foreach ($dataTablesToCombine as $dataTableMap) {
+ /** @var DataTable\Map $dataTableMap */
+ $tables = $dataTableMap->getDataTables();
+ foreach ($tables as $label => $table) {
+ if (empty($dataTables[$label])) {
+ $dataTables[$label] = new DataTable();
+ $dataTables[$label]->setAllTableMetadata($table->getAllTableMetadata());
+ $dataTables[$label]->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::getColumnsAggregationOperations());
+ }
+ $dataTables[$label]->addDataTable($table);
+ }
+ }
+ foreach ($dataTables as $label => $table) {
+ $dataTable->addTable($table, $label);
+ }
+ } else {
+ $dataTable = new DataTable();
+ $dataTable->setAllTableMetadata(reset($dataTablesToCombine)->getAllTableMetadata());
+ $dataTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::getColumnsAggregationOperations());
+ foreach ($dataTablesToCombine as $table) {
+ $dataTable->addDataTable($table);
+ }
+ }
+ return $dataTable;
+ }
+ /**
+ * Returns Bing keyword data used on search
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywordsBing($idSite, $period, $date)
+ {
+ if (!Bing::getInstance()->isSupportedPeriod($date, $period)) {
+ if (Period::isMultiplePeriod($date, $period)) {
+ return new DataTable\Map();
+ }
+ return new DataTable();
+ }
+ if (!SearchEngineKeywordsPerformance::isReportEnabled('Bing')) {
+ return SearchEngineKeywordsPerformance::commonEmptyDataTable($period, $date);
+ }
+ $dataTable = $this->getDataTable(BingRecordBuilder::KEYWORDS_BING_RECORD_NAME, $idSite, $period, $date);
+ return $dataTable;
+ }
+ /**
+ * Returns Yandex keyword data used on search
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywordsYandex($idSite, $period, $date)
+ {
+ if (!SearchEngineKeywordsPerformance::isReportEnabled('Yandex')) {
+ return SearchEngineKeywordsPerformance::commonEmptyDataTable($period, $date);
+ }
+ $dataTable = $this->getDataTable(YandexRecordBuilders::KEYWORDS_YANDEX_RECORD_NAME, $idSite, $period, $date);
+ return $dataTable;
+ }
+ /**
+ * Returns Google keyword data used on Web search
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywordsGoogleWeb($idSite, $period, $date)
+ {
+ if (!SearchEngineKeywordsPerformance::isReportEnabled('Google', 'web')) {
+ return SearchEngineKeywordsPerformance::commonEmptyDataTable($period, $date);
+ }
+ $dataTable = $this->getDataTable(GoogleRecordBuilder::KEYWORDS_GOOGLE_WEB_RECORD_NAME, $idSite, $period, $date);
+ return $dataTable;
+ }
+ /**
+ * Returns Google keyword data used on Image search
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywordsGoogleImage($idSite, $period, $date)
+ {
+ if (!SearchEngineKeywordsPerformance::isReportEnabled('Google', 'image')) {
+ return SearchEngineKeywordsPerformance::commonEmptyDataTable($period, $date);
+ }
+ $dataTable = $this->getDataTable(GoogleRecordBuilder::KEYWORDS_GOOGLE_IMAGE_RECORD_NAME, $idSite, $period, $date);
+ return $dataTable;
+ }
+ /**
+ * Returns Google keyword data used on Video search
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywordsGoogleVideo($idSite, $period, $date)
+ {
+ if (!SearchEngineKeywordsPerformance::isReportEnabled('Google', 'video')) {
+ return SearchEngineKeywordsPerformance::commonEmptyDataTable($period, $date);
+ }
+ $dataTable = $this->getDataTable(GoogleRecordBuilder::KEYWORDS_GOOGLE_VIDEO_RECORD_NAME, $idSite, $period, $date);
+ return $dataTable;
+ }
+ /**
+ * Returns Google keyword data used on News search
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getKeywordsGoogleNews($idSite, $period, $date)
+ {
+ if (!SearchEngineKeywordsPerformance::isReportEnabled('Google', 'news')) {
+ return SearchEngineKeywordsPerformance::commonEmptyDataTable($period, $date);
+ }
+ $dataTable = $this->getDataTable(GoogleRecordBuilder::KEYWORDS_GOOGLE_NEWS_RECORD_NAME, $idSite, $period, $date);
+ return $dataTable;
+ }
+ /**
+ * Returns crawling metrics for Bing
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getCrawlingOverviewBing($idSite, $period, $date)
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+ $archive = Archive::build($idSite, $period, $date);
+ $dataTable = $archive->getDataTableFromNumeric([
+ BingRecordBuilder::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_IN_INDEX_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_IN_LINKS_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_MALWARE_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_BLOCKED_ROBOTS_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_ERRORS_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_DNS_FAILURE_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_TIMEOUT_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_2XX_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_301_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_302_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_4XX_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_5XX_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_OTHER_CODES_RECORD_NAME
+ ]);
+ return $dataTable;
+ }
+ /**
+ * Returns crawling metrics for Yandex
+ *
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ public function getCrawlingOverviewYandex($idSite, $period, $date)
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+ $archive = Archive::build($idSite, $period, $date);
+ $dataTable = $archive->getDataTableFromNumeric([
+ YandexRecordBuilders::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME,
+ YandexRecordBuilders::CRAWLSTATS_APPEARED_PAGES_RECORD_NAME,
+ YandexRecordBuilders::CRAWLSTATS_REMOVED_PAGES_RECORD_NAME,
+ YandexRecordBuilders::CRAWLSTATS_IN_INDEX_RECORD_NAME,
+ YandexRecordBuilders::CRAWLSTATS_CODE_2XX_RECORD_NAME,
+ YandexRecordBuilders::CRAWLSTATS_CODE_3XX_RECORD_NAME,
+ YandexRecordBuilders::CRAWLSTATS_CODE_4XX_RECORD_NAME,
+ YandexRecordBuilders::CRAWLSTATS_CODE_5XX_RECORD_NAME,
+ YandexRecordBuilders::CRAWLSTATS_ERRORS_RECORD_NAME
+ ]);
+ return $dataTable;
+ }
+ /**
+ * Returns list of pages that had an error while crawling for Bing
+ *
+ * Note: This methods returns data imported lately. It does not support any historical reports
+ *
+ * @param $idSite
+ *
+ * @return DataTable
+ */
+ public function getCrawlingErrorExamplesBing($idSite)
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+ $dataTable = new DataTable();
+ $settings = new \Piwik\Plugins\SearchEngineKeywordsPerformance\MeasurableSettings($idSite);
+ $bingSiteSetting = $settings->bingSiteUrl;
+ if (empty($bingSiteSetting) || empty($bingSiteSetting->getValue())) {
+ return $dataTable;
+ }
+ list($apiKey, $bingSiteUrl) = explode('##', $bingSiteSetting->getValue());
+ $model = new \Piwik\Plugins\SearchEngineKeywordsPerformance\Model\Bing();
+ $data = $model->getCrawlErrors($bingSiteUrl);
+ if (empty($data)) {
+ return $dataTable;
+ }
+ $dataTable->addRowsFromSerializedArray($data);
+ $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url'));
+ $dataTable->filter('ColumnCallbackReplace', array('label', function ($val) use ($bingSiteUrl) {
+ return preg_replace('|https?://[^/]*/|i', '', $val);
+ }));
+ return $dataTable;
+ }
+ /**
+ * Returns datatable for the requested archive
+ *
+ * @param string $name name of the archive to use
+ * @param string|int|array $idSite A single ID (eg, `'1'`), multiple IDs (eg, `'1,2,3'` or `array(1, 2, 3)`),
+ * or `'all'`.
+ * @param string $period 'day', `'week'`, `'month'`, `'year'` or `'range'`
+ * @param Date|string $date 'YYYY-MM-DD', magic keywords (ie, 'today'; {@link Date::factory()}
+ * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
+ * @return DataTable|DataTable\Map
+ */
+ private function getDataTable($name, $idSite, $period, $date)
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+ $archive = Archive::build($idSite, $period, $date, $segment = \false);
+ $dataTable = $archive->getDataTable($name);
+ $dataTable->queueFilter('ReplaceSummaryRowLabel');
+ return $dataTable;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Activity/AccountAdded.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Activity/AccountAdded.php
new file mode 100644
index 0000000..245770b
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Activity/AccountAdded.php
@@ -0,0 +1,46 @@
+ All channels > Channel type*
+ * *Acquisition > All channels > Referrers*
+ * *Acquisition > Search Engines*
+* Show related reports for reports showing imported keywords to show originally tracked keywords instead
+* Translations for German and Albanian
+
+__3.3.2__
+* Fix sorting for keyword tables
+* Improved compatibility with Roll-Up Reporting plugin
+* Translation updates
+
+__3.3.1__
+* Ensure at least one keyword type is configured for Google imports
+* Deprecated Property Set and Android App imports
+* Improve sorting of keyword reports by adding a secondary sort column
+* Added proper handling for new Domain properties on Google Search Console
+
+__3.3.0__
+* Fixed bug with incorrect numbers for reports including day stats for Bing
+* Improved validation of uploaded Google client configs
+* Updated dependencies
+* Deprecated Google Crawl Errors reports (due to Google API deprecation).
+ Old reports will still be available, but no new data can be imported after end of March '19.
+ New installs won't show those reports at all.
+* Translation updates
+
+__3.2.7__
+* Fixed notice occurring if search import is force enabled
+
+__3.2.6__
+* Allow force enabling crawling error reports.
+* Improve handling of Google import (avoid importing property set data since it does not exist)
+
+__3.2.5__
+* Security improvements
+* Theme updates
+
+__3.2.4__
+* Improve handling of Bing Crawl Errors (fixes a notice while import)
+* Improve Google import handling of empty results
+* Security improvements
+* UI improvements
+* Translations for Polish
+
+__3.2.3__
+* Various code improvements
+* Translations for Chinese (Taiwan) and Italian
+
+__3.2.0__
+* Changes the _Combined Keywords_ report to also include keywords reported by Referrers.getKeywords
+* Adds new reports _Combined imported keywords_ (which is what the combined keywords was before)
+* Replaces Referrers.getKeywords reports in order to change name and show it as related report
+* Move all reports to the Search Engines & Keywords category (showing Search Engines last)
+
+__3.1.0__
+* New crawl errors reports und Pages > crawl errors showing pages having crawl issues on Google and Bing/Yahoo!
+
+__3.0.10__
+* Improved error handling
+* Row evolution for combined keywords reports
+* Fixed error when generating scheduled reports with evolution charts
+
+__3.0.9__
+* Renamed Piwik to Matomo
+
+__3.0.8__
+* Possibility to show keyword position as float instead of integer
+
+__3.0.7__
+* Added commands to trigger import using console command
+* Various UI/UX improvements
+
+__3.0.6__
+* Now uses Piwik proxy config if defined
+
+__3.0__
+* Possibility to import keyords & crawl stats from Google Search Console
+* Setting per website if web, image and/or video keywords should be imported
+* Possibility to import keywords & crawl stats from Bing/Yahoo! Webmaster API
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Categories/CrawlingOverviewSubcategory.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Categories/CrawlingOverviewSubcategory.php
new file mode 100644
index 0000000..df098d4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Categories/CrawlingOverviewSubcategory.php
@@ -0,0 +1,31 @@
+' . Piwik::translate('SearchEngineKeywordsPerformance_CrawlingOverview1') . '';
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Categories/SearchKeywordsSubcategory.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Categories/SearchKeywordsSubcategory.php
new file mode 100644
index 0000000..a60d2ec
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Categories/SearchKeywordsSubcategory.php
@@ -0,0 +1,26 @@
+configuration = $configuration;
+ }
+ /**
+ * Returns configured client api keys
+ *
+ * @return array
+ */
+ public function getAccounts()
+ {
+ return $this->configuration->getAccounts();
+ }
+ /**
+ * Removes client api key
+ *
+ * @param string $apiKey
+ * @return boolean
+ */
+ public function removeAccount($apiKey)
+ {
+ $this->configuration->removeAccount($apiKey);
+ Piwik::postEvent(
+ 'SearchEngineKeywordsPerformance.AccountRemoved',
+ [['provider' => \Piwik\Plugins\SearchEngineKeywordsPerformance\Provider\Bing::getInstance()->getName(), 'account' => substr($apiKey, 0, 5) . '*****' . substr($apiKey, -5, 5)]]
+ );
+ return \true;
+ }
+ /**
+ * Adds a client api key
+ *
+ * @param $apiKey
+ * @param $username
+ * @return boolean
+ */
+ public function addAccount($apiKey, $username)
+ {
+ $this->configuration->addAccount($apiKey, $username);
+ Piwik::postEvent('SearchEngineKeywordsPerformance.AccountAdded', [['provider' => \Piwik\Plugins\SearchEngineKeywordsPerformance\Provider\Bing::getInstance()->getName(), 'account' => substr($apiKey, 0, 5) . '*****' . substr($apiKey, -5, 5)]]);
+ return \true;
+ }
+ /**
+ * Returns if client is configured
+ *
+ * @return bool
+ */
+ public function isConfigured()
+ {
+ return count($this->getAccounts()) > 0;
+ }
+ /**
+ * Checks if API key can be used to query the API
+ *
+ * @param $apiKey
+ * @return bool
+ * @throws \Exception
+ */
+ public function testConfiguration($apiKey)
+ {
+ $data = $this->sendAPIRequest($apiKey, 'GetUserSites');
+ if (empty($data)) {
+ throw new \Exception('Unknown error');
+ }
+ return \true;
+ }
+ /**
+ * Returns the urls, keyword data is available for (in connected google account)
+ *
+ * @param $apiKey
+ *
+ * @return array
+ */
+ public function getAvailableUrls($apiKey, $removeUrlsWithoutAccess = \true)
+ {
+ try {
+ $data = $this->sendAPIRequest($apiKey, 'GetUserSites');
+ } catch (\Exception $e) {
+ return [];
+ }
+ if (empty($data) || !is_array($data['d'])) {
+ return [];
+ }
+ $urls = [];
+ foreach ($data['d'] as $item) {
+ if (!$removeUrlsWithoutAccess || $item['IsVerified']) {
+ $urls[$item['Url']] = $item['IsVerified'];
+ }
+ }
+ return $urls;
+ }
+ /**
+ * Returns search anyalytics data from Bing API
+ *
+ * @param string $apiKey
+ * @param string $url
+ * @return array
+ */
+ public function getSearchAnalyticsData($apiKey, $url)
+ {
+ $keywordDataSets = $this->sendAPIRequest($apiKey, 'GetQueryStats', ['siteUrl' => $url]);
+ $keywords = [];
+ if (empty($keywordDataSets['d']) || !is_array($keywordDataSets['d'])) {
+ return [];
+ }
+ foreach ($keywordDataSets['d'] as $keywordDataSet) {
+ $timestamp = substr($keywordDataSet['Date'], 6, 10);
+ $date = date('Y-m-d', $timestamp);
+ if (!isset($keywords[$date])) {
+ $keywords[$date] = [];
+ }
+ $keywords[$date][] = ['keyword' => $keywordDataSet['Query'], 'clicks' => $keywordDataSet['Clicks'], 'impressions' => $keywordDataSet['Impressions'], 'position' => $keywordDataSet['AvgImpressionPosition']];
+ }
+ return $keywords;
+ }
+ /**
+ * Returns crawl statistics from Bing API
+ *
+ * @param string $apiKey
+ * @param string $url
+ * @return array
+ */
+ public function getCrawlStats($apiKey, $url)
+ {
+ $crawlStatsDataSets = $this->sendAPIRequest($apiKey, 'GetCrawlStats', ['siteUrl' => $url]);
+ $crawlStats = [];
+ if (empty($crawlStatsDataSets) || !is_array($crawlStatsDataSets['d'])) {
+ return [];
+ }
+ foreach ($crawlStatsDataSets['d'] as $crawlStatsDataSet) {
+ $timestamp = substr($crawlStatsDataSet['Date'], 6, 10);
+ $date = date('Y-m-d', $timestamp);
+ unset($crawlStatsDataSet['Date']);
+ $crawlStats[$date] = $crawlStatsDataSet;
+ }
+ return $crawlStats;
+ }
+ /**
+ * Returns urls with crawl issues from Bing API
+ *
+ * @param string $apiKey
+ * @param string $url
+ * @return array
+ */
+ public function getUrlWithCrawlIssues($apiKey, $url)
+ {
+ $crawlErrorsDataSets = $this->sendAPIRequest($apiKey, 'GetCrawlIssues', ['siteUrl' => $url]);
+ $crawlErrors = [];
+ if (empty($crawlErrorsDataSets) || !is_array($crawlErrorsDataSets['d'])) {
+ return [];
+ }
+ $crawlIssueMapping = [1 => 'Code301', 2 => 'Code302', 4 => 'Code4xx', 8 => 'Code5xx', 16 => 'BlockedByRobotsTxt', 32 => 'ContainsMalware', 64 => 'ImportantUrlBlockedByRobotsTxt'];
+ foreach ($crawlErrorsDataSets['d'] as $crawlStatsDataSet) {
+ $issues = $crawlStatsDataSet['Issues'];
+ $messages = [];
+ foreach ($crawlIssueMapping as $code => $message) {
+ if ($issues & $code) {
+ $messages[] = $message;
+ }
+ }
+ $crawlStatsDataSet['Issues'] = implode(',', $messages);
+ $crawlErrors[] = $crawlStatsDataSet;
+ }
+ return $crawlErrors;
+ }
+ /**
+ * Executes a API-Request to Bing with the given parameters
+ *
+ * @param string $apiKey
+ * @param string $method
+ * @param array $params
+ * @return mixed
+ * @throws InvalidCredentialsException
+ * @throws \Exception
+ */
+ protected function sendAPIRequest($apiKey, $method, $params = [])
+ {
+ $params['apikey'] = $apiKey;
+ $this->throwIfThrottled($apiKey);
+ $url = $this->baseAPIUrl . $method . '?' . Http::buildQuery($params);
+ $retries = 0;
+ while ($retries < 5) {
+ try {
+ $additionalHeaders = ['X-Forwarded-For: ' . (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] . ',' : '') . IP::getIpFromHeader()];
+ $response = Http::sendHttpRequestBy(Http::getTransportMethod(), $url, 2000, null, null, null, 0, false, false, false, false, 'GET', null, null, null, $additionalHeaders);
+ $response = json_decode($response, \true);
+ } catch (\Exception $e) {
+ if ($retries < 4) {
+ $retries++;
+ usleep($retries * 500);
+ continue;
+ }
+ throw $e;
+ }
+ if (!empty($response['ErrorCode'])) {
+ $isThrottledHost = strpos($response['Message'], 'ThrottleHost') !== \false || $response['ErrorCode'] == self::API_ERROR_THROTTLE_HOST;
+ $isThrottledUser = strpos($response['Message'], 'ThrottleUser') !== \false || $response['ErrorCode'] == self::API_ERROR_THROTTLE_USER;
+ $isThrottledIp = strpos($response['Message'], 'ThrottleIP') !== false;
+ $isThrottled = $isThrottledHost || $isThrottledUser || $isThrottledIp;
+ $isUnknownError = $response['ErrorCode'] === self::API_ERROR_UNKNOWN;
+ // we retry each request up to 5 times, if the error is unknown or the connection was throttled
+ if (($isThrottled || $isUnknownError) && $retries < 4) {
+ $retries++;
+ usleep($retries * 500);
+ continue;
+ }
+ // if connection is still throttled after retrying, we block additional requests for some time
+ if ($isThrottled) {
+ Option::set($this->getThrottleOptionKey($apiKey), Date::getNowTimestamp());
+ $this->throwIfThrottled($apiKey, $response['ErrorCode']);
+ }
+ if ($isUnknownError) {
+ throw new UnknownAPIException($response['Message'], $response['ErrorCode']);
+ }
+ $authenticationError = strpos($response['Message'], 'NotAuthorized') !== \false
+ || strpos($response['Message'], 'InvalidApiKey') !== \false
+ || in_array($response['ErrorCode'], [self::API_ERROR_NOT_AUTHORIZED, self::API_ERROR_INVALID_API_KEY]);
+ if ($authenticationError) {
+ throw new InvalidCredentialsException($response['Message'], $response['ErrorCode']);
+ }
+ throw new \Exception($response['Message'], $response['ErrorCode']);
+ }
+ return $response;
+ }
+ return null;
+ }
+ protected function getThrottleOptionKey($apiKey)
+ {
+ return sprintf(self::OPTION_NAME_THROTTLE_TIME, md5($apiKey));
+ }
+ public function throwIfThrottled($apiKey, $errorCode = null)
+ {
+ $throttleTime = Option::get($this->getThrottleOptionKey($apiKey));
+ if (empty($throttleTime)) {
+ return;
+ }
+ try {
+ $throttleDate = Date::factory((int) $throttleTime);
+ } catch (\Exception $e) {
+ return;
+ }
+ if (Date::now()->subHour(self::THROTTLE_BREAK_HOURS)->isEarlier($throttleDate)) {
+ $errorCode = !empty($errorCode) ? $errorCode : self::API_ERROR_THROTTLE_USER;
+ throw new RateLimitApiException('API requests temporarily throttled till ' . $throttleDate->addHour(self::THROTTLE_BREAK_HOURS)->getDatetime(), $errorCode);
+ }
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/BaseConfiguration.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/BaseConfiguration.php
new file mode 100644
index 0000000..af01cc9
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/BaseConfiguration.php
@@ -0,0 +1,49 @@
+accounts)) {
+ $accounts = Option::get(self::CLIENT_CONFIG_OPTION_NAME);
+ $accounts = @json_decode($accounts, \true);
+ if (is_array($accounts)) {
+ $this->accounts = $accounts;
+ }
+ }
+ return $this->accounts;
+ }
+ /**
+ * Adds new account
+ *
+ * @param $apiKey
+ * @param $username
+ */
+ public function addAccount($apiKey, $username)
+ {
+ $currentAccounts = (array) $this->getAccounts();
+ if (array_key_exists($apiKey, $currentAccounts)) {
+ return;
+ }
+ $currentAccounts[$apiKey] = ['apiKey' => $apiKey, 'username' => $username, 'created' => time()];
+ $this->setAccounts($currentAccounts);
+ }
+ /**
+ * Removes account with given API-Key
+ *
+ * @param $apiKey
+ */
+ public function removeAccount($apiKey)
+ {
+ $currentAccounts = (array) $this->getAccounts();
+ $this->checkPermissionToRemoveAccount($apiKey, $currentAccounts);
+ unset($currentAccounts[$apiKey]);
+ $this->setAccounts($currentAccounts);
+ }
+ protected function setAccounts($newAccounts)
+ {
+ $accounts = json_encode($newAccounts);
+ Option::set(self::CLIENT_CONFIG_OPTION_NAME, $accounts);
+ $this->accounts = [];
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Google.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Google.php
new file mode 100644
index 0000000..f98383e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Google.php
@@ -0,0 +1,178 @@
+accounts)) {
+ $accounts = Option::get(self::OAUTH_CONFIG_OPTION_NAME);
+ $accounts = @json_decode($accounts, \true);
+ if (is_array($accounts) && !empty($accounts)) {
+ $this->accounts = $accounts;
+ }
+ }
+ return $this->accounts;
+ }
+ /**
+ * adds new account data
+ *
+ * @param string $id
+ * @param array $accountData
+ * @param string $username
+ */
+ public function addAccount($id, $accountData, $username)
+ {
+ $currentAccounts = (array) $this->getAccounts();
+ $id = (string) $id;
+ if (array_key_exists($id, $currentAccounts)) {
+ return;
+ }
+ $currentAccounts[$id] = ['config' => $accountData, 'username' => $username, 'created' => time()];
+ $this->setAccounts($currentAccounts);
+ }
+ /**
+ * removes account data
+ *
+ * @param string $id
+ */
+ public function removeAccount($id)
+ {
+ $currentAccounts = (array) $this->getAccounts();
+ $id = (string) $id;
+ $this->checkPermissionToRemoveAccount($id, $currentAccounts);
+ unset($currentAccounts[$id]);
+ $this->setAccounts($currentAccounts);
+ }
+ protected function setAccounts($newAccounts)
+ {
+ $accounts = json_encode($newAccounts);
+ Option::set(self::OAUTH_CONFIG_OPTION_NAME, $accounts);
+ $this->accounts = $newAccounts;
+ }
+ /**
+ * Returns the access token for the given account id
+ *
+ * @param string $accountId
+ * @return mixed|null
+ */
+ public function getAccessToken($accountId)
+ {
+ $accounts = $this->getAccounts();
+ if (array_key_exists($accountId, $accounts)) {
+ return $accounts[$accountId]['config']['accessToken'];
+ }
+ return null;
+ }
+ /**
+ * Returns the user info for the given account id
+ *
+ * @param string $accountId
+ * @return mixed|null
+ */
+ public function getUserInfo($accountId)
+ {
+ $accounts = $this->getAccounts();
+ if (array_key_exists($accountId, $accounts)) {
+ return $accounts[$accountId]['config']['userInfo'];
+ }
+ return null;
+ }
+ /**
+ * Returns stored client config
+ *
+ * @return mixed|null
+ */
+ public function getClientConfig()
+ {
+ if (empty($this->clientConfig)) {
+ $config = Common::unsanitizeInputValue(Option::get(self::CLIENT_CONFIG_OPTION_NAME));
+ if (!empty($config) && ($config = @json_decode($config, \true))) {
+ $this->clientConfig = $config;
+ }
+ }
+ return $this->clientConfig;
+ }
+ /**
+ * Sets client config to be used for querying google api
+ *
+ * NOTE: Check for valid config should be done before
+ *
+ * @param string $config json encoded client config
+ */
+ public function setClientConfig($config)
+ {
+ Option::set(self::CLIENT_CONFIG_OPTION_NAME, $config);
+ }
+ /**
+ * Delete the Google client config option so that the customer will be prompted to upload a new one or use the Cloud
+ * config.
+ *
+ * @return void
+ */
+ public function deleteClientConfig(): void
+ {
+ Option::delete(self::CLIENT_CONFIG_OPTION_NAME);
+ }
+ /**
+ * Returns path to client config file that is used for automatic import
+ *
+ * @return string
+ */
+ protected function getConfigurationFilename()
+ {
+ return dirname(dirname(dirname(__FILE__))) . \DIRECTORY_SEPARATOR . 'client_secrets.json';
+ }
+ /**
+ * Imports client config from shipped config file if available
+ *
+ * @return bool
+ */
+ public function importShippedClientConfigIfAvailable()
+ {
+ $configFile = $this->getConfigurationFilename();
+ if (!file_exists($configFile)) {
+ return \false;
+ }
+ $config = file_get_contents($configFile);
+ if ($decoded = @json_decode($config, \true)) {
+ $this->setClientConfig($config);
+ return \true;
+ }
+ return \false;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Yandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Yandex.php
new file mode 100644
index 0000000..c5881ef
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Yandex.php
@@ -0,0 +1,130 @@
+accounts)) {
+ $accounts = Option::get(self::OAUTH_CONFIG_OPTION_NAME);
+ $accounts = @json_decode($accounts, \true);
+ if (is_array($accounts) && !empty($accounts)) {
+ $this->accounts = $accounts;
+ }
+ }
+ return $this->accounts;
+ }
+ /**
+ * adds new account data
+ *
+ * @param string $id
+ * @param array $accountData
+ * @param string $username
+ */
+ public function addAccount($id, $accountData, $username)
+ {
+ $currentAccounts = (array) $this->getAccounts();
+ $currentAccounts[$id] = ['config' => $accountData, 'username' => $username, 'created' => time()];
+ $this->setAccounts($currentAccounts);
+ }
+ /**
+ * removes account data
+ *
+ * @param string $id
+ */
+ public function removeAccount($id)
+ {
+ $currentAccounts = (array) $this->getAccounts();
+ $this->checkPermissionToRemoveAccount($id, $currentAccounts);
+ unset($currentAccounts[$id]);
+ $this->setAccounts($currentAccounts);
+ }
+ protected function setAccounts($newAccounts)
+ {
+ $accounts = json_encode($newAccounts);
+ Option::set(self::OAUTH_CONFIG_OPTION_NAME, $accounts);
+ $this->accounts = [];
+ }
+ /**
+ * Returns the access token for the given account id
+ *
+ * @param string $accountId
+ * @return mixed|null
+ */
+ public function getAccessToken($accountId)
+ {
+ $accounts = $this->getAccounts();
+ if (array_key_exists($accountId, $accounts)) {
+ return $accounts[$accountId]['config']['accessToken'];
+ }
+ return null;
+ }
+ /**
+ * Returns the user info for the given account id
+ *
+ * @param string $accountId
+ * @return mixed|null
+ */
+ public function getUserInfo($accountId)
+ {
+ $accounts = $this->getAccounts();
+ if (array_key_exists($accountId, $accounts)) {
+ return $accounts[$accountId]['config']['userInfo'];
+ }
+ return null;
+ }
+ /**
+ * Returns stored client config
+ *
+ * @return mixed|null
+ */
+ public function getClientConfig()
+ {
+ if (empty($this->clientConfig)) {
+ $config = Common::unsanitizeInputValue(Option::get(self::CLIENT_CONFIG_OPTION_NAME));
+ if (!empty($config) && ($config = @json_decode($config, \true))) {
+ $this->clientConfig = $config;
+ }
+ }
+ return $this->clientConfig;
+ }
+ /**
+ * Sets client config to be used for querying yandex api
+ *
+ * NOTE: Check for valid config should be done before
+ *
+ * @param string $config json encoded client config
+ */
+ public function setClientConfig($clientId, $clientSecret)
+ {
+ $config = ['id' => $clientId, 'secret' => $clientSecret, 'date' => time()];
+ Option::set(self::CLIENT_CONFIG_OPTION_NAME, json_encode($config));
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Google.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Google.php
new file mode 100644
index 0000000..c285ceb
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Google.php
@@ -0,0 +1,489 @@
+configuration = $configuration;
+ }
+ /**
+ * @return \Google\Client
+ */
+ protected function getGoogleClient()
+ {
+ $googleClient = StaticContainer::get('SearchEngineKeywordsPerformance.Google.googleClient');
+ $proxyHost = Config::getInstance()->proxy['host'];
+ if ($proxyHost) {
+ $proxyPort = Config::getInstance()->proxy['port'];
+ $proxyUser = Config::getInstance()->proxy['username'];
+ $proxyPassword = Config::getInstance()->proxy['password'];
+ if ($proxyUser) {
+ $proxy = sprintf('http://%s:%s@%s:%s', $proxyUser, $proxyPassword, $proxyHost, $proxyPort);
+ } else {
+ $proxy = sprintf('http://%s:%s', $proxyHost, $proxyPort);
+ }
+ $httpClient = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\GuzzleHttp\Client(['proxy' => $proxy, 'exceptions' => \false, 'base_uri' => \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Client::API_BASE_PATH]);
+ $googleClient->setHttpClient($httpClient);
+ }
+ return $googleClient;
+ }
+ /**
+ * Passes through a direct call to the \Google\Client class
+ *
+ * @param string $method
+ * @param array $params
+ * @return mixed
+ * @throws MissingClientConfigException
+ * @throws MissingOAuthConfigException
+ */
+ public function __call($method, $params = [])
+ {
+ return call_user_func_array([$this->getConfiguredClient('', \true), $method], $params);
+ }
+ /**
+ * Process the given auth code to gain access and refresh token from google api
+ *
+ * @param string $authCode
+ * @throws MissingClientConfigException
+ */
+ public function processAuthCode($authCode)
+ {
+ try {
+ $client = $this->getConfiguredClient('');
+ } catch (MissingOAuthConfigException $e) {
+ // ignore missing oauth config
+ }
+ $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
+ $userInfo = $this->getUserInfoByAccessToken($accessToken);
+ $id = $userInfo->getId();
+ $this->addAccount($id, $accessToken, Piwik::getCurrentUserLogin());
+ Piwik::postEvent('SearchEngineKeywordsPerformance.AccountAdded', [['provider' => \Piwik\Plugins\SearchEngineKeywordsPerformance\Provider\Google::getInstance()->getName(), 'account' => $userInfo->getName()]]);
+ }
+ /**
+ * Sets the client configuration
+ *
+ * @param $config
+ * @return boolean
+ */
+ public function setClientConfig($config)
+ {
+ try {
+ $client = $this->getGoogleClient();
+ $configArray = @json_decode($config, \true);
+ $this->configureClient($client, $configArray);
+ } catch (\Exception $e) {
+ return \false;
+ }
+ $this->configuration->setClientConfig($config);
+ Piwik::postEvent('SearchEngineKeywordsPerformance.GoogleClientConfigChanged');
+ return \true;
+ }
+ /**
+ * Delete the Google client config option so that the customer will be prompted to upload a new one or use the Cloud
+ * config.
+ *
+ * @return void
+ */
+ public function deleteClientConfig(): void
+ {
+ $this->configuration->deleteClientConfig();
+ Piwik::postEvent('SearchEngineKeywordsPerformance.GoogleClientConfigChanged');
+ }
+ /**
+ * @param \Google\Client $client
+ * @param array $config
+ *
+ * @throws MissingClientConfigException
+ */
+ protected function configureClient($client, $config)
+ {
+ try {
+ @$client->setAuthConfig($config);
+ } catch (\Exception $e) {
+ throw new MissingClientConfigException();
+ }
+ // no client config available
+ if (!$client->getClientId() || !$client->getClientSecret()) {
+ throw new MissingClientConfigException();
+ }
+ }
+ /**
+ * Returns configured client api keys
+ *
+ * @return array
+ */
+ public function getAccounts()
+ {
+ return $this->configuration->getAccounts();
+ }
+ /**
+ * Removes client api key
+ *
+ * @param $id
+ * @return bool
+ */
+ public function removeAccount($id)
+ {
+ $userInfo = $this->getUserInfo($id);
+ $this->configuration->removeAccount($id);
+ Piwik::postEvent('SearchEngineKeywordsPerformance.AccountRemoved', [['provider' => \Piwik\Plugins\SearchEngineKeywordsPerformance\Provider\Google::getInstance()->getName(), 'account' => $userInfo['name']]]);
+ return \true;
+ }
+ /**
+ * Adds a client api key
+ *
+ * @param $id
+ * @param $config
+ * @param $username
+ * @return boolean
+ */
+ public function addAccount($id, $accessToken, $username)
+ {
+ $userInfo = $this->getUserInfoByAccessToken($accessToken);
+ $config = ['userInfo' => ['picture' => $userInfo->picture, 'name' => $userInfo->name], 'accessToken' => $accessToken];
+ $this->configuration->addAccount($id, $config, $username);
+ return \true;
+ }
+ /**
+ * Returns if client is configured
+ *
+ * @return bool
+ */
+ public function isConfigured()
+ {
+ return $this->configuration->getClientConfig() && count($this->configuration->getAccounts()) > 0;
+ }
+ /**
+ * Returns configured \Google\Client object
+ *
+ * @param string $accessToken
+ * @param bool $ignoreMissingConfigs
+ * @return \Google\Client
+ * @throws MissingClientConfigException
+ * @throws MissingOAuthConfigException
+ */
+ public function getConfiguredClient($accessToken, $ignoreMissingConfigs = \false)
+ {
+ $client = $this->getGoogleClient();
+ try {
+ $this->configure($client, $accessToken);
+ } catch (\Exception $e) {
+ if (!$ignoreMissingConfigs) {
+ throw $e;
+ }
+ }
+ return $client;
+ }
+ /**
+ * Returns the Auth Url (including the given state param)
+ *
+ * @param $state
+ * @return string
+ * @throws MissingClientConfigException
+ * @throws MissingOAuthConfigException
+ */
+ public function createAuthUrl($state)
+ {
+ $client = $this->getConfiguredClient('', \true);
+ $client->setState($state);
+ $client->setPrompt('consent');
+ return $client->createAuthUrl();
+ }
+ /**
+ * Loads configuration and sets common configuration for \Google\Client
+ *
+ * @param \Google\Client $client
+ * @param string $accessToken
+ * @throws MissingOAuthConfigException
+ * @throws MissingClientConfigException
+ */
+ protected function configure($client, $accessToken)
+ {
+ // import shipped client config if available
+ if (!$this->configuration->getClientConfig()) {
+ $this->configuration->importShippedClientConfigIfAvailable();
+ }
+ $clientConfig = $this->configuration->getClientConfig();
+ $this->configureClient($client, $clientConfig);
+ // Copied this bit about the redirect_uris from the GA Importer Authorization class
+ //since there ie no host defined when running via console it results in error, but we don't need to set any URI when running console commands so can be ignored
+ $expectedUri = Url::getCurrentUrlWithoutQueryString() . '?module=SearchEngineKeywordsPerformance&action=processAuthCode';
+ if (
+ !empty($clientConfig['web']['redirect_uris']) &&
+ !Common::isRunningConsoleCommand() &&
+ !PluginsArchiver::isArchivingProcessActive() &&
+ !$this->isMiscCron() &&
+ stripos($expectedUri, 'unknown/_/console?') === false // To handle case where we are unable to determine the correct URI
+ ) {
+ $uri = $this->getValidUri($clientConfig['web']['redirect_uris']);
+ if (empty($uri)) {
+ throw new \Exception(Piwik::translate('SearchEngineKeywordsPerformance_InvalidRedirectUriInClientConfiguration', [$expectedUri]));
+ }
+ $client->setRedirectUri($uri);
+ }
+ try {
+ $client->setAccessToken($accessToken);
+ } catch (\Exception $e) {
+ throw new MissingOAuthConfigException($e->getMessage());
+ }
+ }
+ public function getUserInfo($accountId)
+ {
+ return $this->configuration->getUserInfo($accountId);
+ }
+ protected function getUserInfoByAccessToken($accessToken)
+ {
+ $service = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Oauth2($this->getConfiguredClient($accessToken));
+ return $service->userinfo->get();
+ }
+ /**
+ * Checks if account can be used to query the API
+ *
+ * @param string $accountId
+ * @return bool
+ * @throws \Exception
+ */
+ public function testConfiguration($accountId)
+ {
+ $accessToken = $this->configuration->getAccessToken($accountId);
+ try {
+ $service = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\SearchConsole($this->getConfiguredClient($accessToken));
+ $service->sites->listSites();
+ } catch (\Exception $e) {
+ $this->handleServiceException($e);
+ throw $e;
+ }
+ return \true;
+ }
+ /**
+ * Returns the urls keyword data is available for (in connected google account)
+ *
+ * @param string $accountId
+ * @param bool $removeUrlsWithoutAccess wether to return unverified urls
+ * @return array
+ */
+ public function getAvailableUrls($accountId, $removeUrlsWithoutAccess = \true)
+ {
+ $accessToken = $this->configuration->getAccessToken($accountId);
+ $sites = [];
+ try {
+ $service = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\SearchConsole($this->getConfiguredClient($accessToken));
+ $service->getClient()->getCache()->clear();
+ //After Guzzle upgrade this seems to fetch same token for all the accounts, to solve that we are clearing the caache explicitly
+ $response = $service->sites->listSites();
+ } catch (\Exception $e) {
+ return $sites;
+ }
+ foreach ($response as $site) {
+ if (!$removeUrlsWithoutAccess || $site['permissionLevel'] != 'siteUnverifiedUser') {
+ $sites[$site['siteUrl']] = $site['permissionLevel'];
+ }
+ }
+ return $sites;
+ }
+ /**
+ * Returns the search analytics data from google search console for the given parameters
+ *
+ * @param string $accountId
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date day string, eg. 2016-12-24
+ * @param string $type 'web', 'image', 'video' or 'news'
+ * @param int $limit maximum of rows to fetch
+ * @return \Google\Service\SearchConsole\SearchAnalyticsQueryResponse
+ * @throws InvalidClientConfigException
+ * @throws InvalidCredentialsException
+ * @throws MissingOAuthConfigException
+ * @throws MissingClientConfigException
+ * @throws UnknownAPIException
+ */
+ public function getSearchAnalyticsData($accountId, $url, $date, $type = 'web', $limit = 500)
+ {
+ $accessToken = $this->configuration->getAccessToken($accountId);
+ if (empty($accessToken)) {
+ throw new MissingOAuthConfigException();
+ }
+ $limit = min($limit, 5000);
+ // maximum allowed by API is 5.000
+ // Google API is only able to handle dates up to ~490 days old
+ $threeMonthBefore = Date::now()->subDay(500);
+ $archivedDate = Date::factory($date);
+ if ($archivedDate->isEarlier($threeMonthBefore) || $archivedDate->isToday()) {
+ Log::debug("[SearchEngineKeywordsPerformance] Skip fetching keywords from Search Console for today and dates more than 500 days in the past");
+ return null;
+ }
+ $service = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\SearchConsole($this->getConfiguredClient($accessToken));
+ $request = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\SearchConsole\SearchAnalyticsQueryRequest();
+ $request->setStartDate($date);
+ $request->setEndDate($date);
+ $request->setDimensions(['query']);
+ $request->setRowLimit($limit);
+ $request->setSearchType($type);
+ $request->setDataState('all');
+ $retries = 0;
+ while ($retries < 5) {
+ try {
+ $response = $service->searchanalytics->query($url, $request);
+ return $response;
+ } catch (\Exception $e) {
+ $this->handleServiceException($e, $retries < 4);
+ usleep(500 * $retries);
+ $retries++;
+ }
+ }
+ return null;
+ }
+ /**
+ * Returns an array of dates where search analytics data is availabe for on search console
+ *
+ * @param string $accountId
+ * @param string $url url, eg. http://matomo.org
+ * @param boolean $onlyFinalized
+ * @return array
+ * @throws MissingClientConfigException
+ * @throws MissingOAuthConfigException
+ * @throws InvalidClientConfigException
+ * @throws InvalidCredentialsException
+ * @throws UnknownAPIException
+ */
+ public function getDatesWithSearchAnalyticsData($accountId, $url, $onlyFinalized = \true)
+ {
+ $accessToken = $this->configuration->getAccessToken($accountId);
+ $service = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\SearchConsole($this->getConfiguredClient($accessToken));
+ $request = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\SearchConsole\SearchAnalyticsQueryRequest();
+ $request->setStartDate(Date::now()->subDay(StaticContainer::get('SearchEngineKeywordsPerformance.Google.ImportLastDaysMax'))->toString());
+ $request->setEndDate(Date::now()->toString());
+ $request->setDimensions(['date']);
+ if ($onlyFinalized === \false) {
+ $request->setDataState('all');
+ }
+ $retries = 0;
+ while ($retries < 5) {
+ try {
+ $entries = $service->searchanalytics->query($url, $request);
+ if (empty($entries) || !($rows = $entries->getRows())) {
+ return [];
+ }
+ $days = [];
+ foreach ($rows as $row) {
+ /** @var \Google\Service\SearchConsole\ApiDataRow $row */
+ $keys = $keys = $row->getKeys();
+ $days[] = array_shift($keys);
+ }
+ return array_unique($days);
+ } catch (\Exception $e) {
+ $this->handleServiceException($e, $retries < 4);
+ $retries++;
+ usleep(500 * $retries);
+ }
+ }
+ return [];
+ }
+ /**
+ * @param \Exception $e
+ * @param bool $ignoreUnknowns
+ * @throws InvalidClientConfigException
+ * @throws InvalidCredentialsException
+ * @throws UnknownAPIException
+ */
+ protected function handleServiceException($e, $ignoreUnknowns = \false)
+ {
+ if (!$e instanceof \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Exception) {
+ if (!PluginsArchiver::isArchivingProcessActive()) {
+ Log::debug('Exception: ' . $e->getMessage());
+ }
+ return;
+ }
+ $error = json_decode($e->getMessage(), \true);
+ if (!empty($error['error']) && $error['error'] == 'invalid_client') {
+ // invalid credentials
+ throw new InvalidClientConfigException($error['error_description']);
+ } elseif (!empty($error['error']['code']) && $error['error']['code'] == 401) {
+ // invalid credentials
+ throw new InvalidCredentialsException($error['error']['message'], $error['error']['code']);
+ } elseif (!empty($error['error']['code']) && $error['error']['code'] == 403) {
+ // no access for given resource (website / app)
+ throw new InvalidCredentialsException($error['error']['message'], $error['error']['code']);
+ } elseif (!empty($error['error']['code']) && in_array($error['error']['code'], [500, 503]) && !$ignoreUnknowns) {
+ // backend or api server error
+ throw new UnknownAPIException($error['error']['message'], $error['error']['code']);
+ } else {
+ if (!PluginsArchiver::isArchivingProcessActive()) {
+ Log::debug('Exception: ' . $e->getMessage());
+ }
+ }
+ }
+ /**
+ * Returns a valid uri. Copied from GA Importer Authorization class.
+ *
+ * @param array $uris
+ * @return string
+ */
+ private function getValidUri(array $uris): string
+ {
+ $validUri = Url::getCurrentUrlWithoutQueryString() . '?module=SearchEngineKeywordsPerformance&action=processAuthCode';
+ $settingURL = SettingsPiwik::getPiwikUrl();
+ if (stripos($settingURL, 'index.php') === false) {
+ $settingURL .= 'index.php';
+ }
+ // Some MWP installs was not working as expected when using Url::getCurrentUrlWithoutQueryString()
+ $validUriFallback = $settingURL . '?module=SearchEngineKeywordsPerformance&action=processAuthCode';
+ foreach ($uris as $uri) {
+ if (stripos($uri, $validUri) !== \false || stripos($uri, $validUriFallback) !== \false) {
+ return $uri;
+ }
+ }
+ return '';
+ }
+
+ private function isMiscCron()
+ {
+ $currentURL = Url::getCurrentUrlWithoutQueryString();
+
+ return (stripos($currentURL, 'misc/cron/archive.php') !== false);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Yandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Yandex.php
new file mode 100644
index 0000000..5056dea
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Yandex.php
@@ -0,0 +1,545 @@
+configuration = $configuration;
+ }
+ /**
+ * Returns if client is configured
+ *
+ * @return bool
+ */
+ public function isConfigured()
+ {
+ return $this->isClientConfigured() && count($this->configuration->getAccounts()) > 0;
+ }
+ /**
+ * Returns if oauth client config is available
+ */
+ public function isClientConfigured()
+ {
+ return \true && $this->getClientConfig();
+ }
+ /**
+ * Returns the client config
+ *
+ * @return mixed|null
+ */
+ public function getClientConfig()
+ {
+ return $this->configuration->getClientConfig();
+ }
+ /**
+ * Checks if account can be used to query the API
+ *
+ * @param string $accountId
+ * @return bool
+ * @throws \Exception
+ */
+ public function testConfiguration($accountId)
+ {
+ $accessToken = $this->configuration->getAccessToken($accountId);
+ $this->getHosts($accessToken);
+ return \true;
+ }
+ /**
+ * Updates the client config
+ *
+ * @param string $clientId new client id
+ * @param string $clientSecret new client secret
+ */
+ public function setClientConfig($clientId, $clientSecret)
+ {
+ $this->configuration->setClientConfig($clientId, $clientSecret);
+ Piwik::postEvent('SearchEngineKeywordsPerformance.GoogleClientConfigChanged');
+ }
+ /**
+ * Returns the urls keyword data is available for (in connected yandex account)
+ *
+ * @param string $accountId
+ * @param bool $removeUrlsWithoutAccess whether to return unverified urls
+ * @return array
+ */
+ public function getAvailableUrls($accountId, $removeUrlsWithoutAccess = \true)
+ {
+ $accessToken = $this->configuration->getAccessToken($accountId);
+ $sites = [];
+ try {
+ $availableSites = $this->getHosts($accessToken);
+ } catch (\Exception $e) {
+ return $sites;
+ }
+ if (property_exists($availableSites, 'hosts')) {
+ foreach ($availableSites->hosts as $availableSite) {
+ if (!$removeUrlsWithoutAccess || $availableSite->verified) {
+ $sites[$availableSite->unicode_host_url] = ['verified' => $availableSite->verified, 'host_id' => $availableSite->host_id];
+ }
+ }
+ }
+ return $sites;
+ }
+ /**
+ * Returns popular search queries from Yandex Webmaster API
+ *
+ * @param string $accountId
+ * @param string $hostId
+ * @param string $date
+ * @return ?array
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ public function getSearchAnalyticsData($accountId, $hostId, $date)
+ {
+ $accessToken = $this->configuration->getAccessToken($accountId);
+ $archivedDate = Date::factory($date);
+ if ($archivedDate->isToday()) {
+ Log::debug("[SearchEngineKeywordsPerformance] Skip fetching keywords from Yandex Webmaster for today.");
+ return null;
+ }
+ $date = strtotime($date);
+ $searchQueries = $this->retryApiMethod(function () use ($accessToken, $hostId, $date) {
+ return $this->getPopularQueries($accessToken, $hostId, $date);
+ });
+ if (empty($searchQueries) || empty($searchQueries->queries)) {
+ return null;
+ }
+ $keywords = [];
+ foreach ($searchQueries->queries as $query) {
+ $keywords[] = ['keyword' => $query->query_text, 'clicks' => $query->indicators->TOTAL_CLICKS, 'impressions' => $query->indicators->TOTAL_SHOWS, 'position' => $query->indicators->AVG_SHOW_POSITION];
+ }
+ return $keywords;
+ }
+ /**
+ * Returns crawl statistics from Yandex Webmaster API
+ *
+ * @param string $accountId
+ * @param string $hostId
+ * @return array
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ public function getCrawlStats($accountId, $hostId, $date)
+ {
+ $accessToken = $this->configuration->getAccessToken($accountId);
+ $archivedDate = Date::factory($date);
+ if ($archivedDate->isToday()) {
+ Log::debug("[SearchEngineKeywordsPerformance] Skip fetching crawl stats from Yandex Webmaster for today.");
+ return null;
+ }
+ $dateTs = strtotime($date);
+ $crawlStatsByDate = [];
+ $crawlStats = $this->retryApiMethod(function () use ($accessToken, $hostId, $dateTs) {
+ return $this->getIndexingHistory($accessToken, $hostId, $dateTs);
+ });
+ if (!empty($crawlStats) && !empty($crawlStats->indicators)) {
+ $indicators = (array) $crawlStats->indicators;
+ foreach ($indicators as $indicator => $indicatorByDate) {
+ foreach ($indicatorByDate as $dateItem) {
+ if (strpos($dateItem->date, $date) === 0) {
+ $crawlStatsByDate[$indicator] = (int) $dateItem->value;
+ }
+ }
+ }
+ }
+ $pagesInIndex = $this->retryApiMethod(function () use ($accessToken, $hostId, $dateTs) {
+ return $this->getPagesInIndex($accessToken, $hostId, $dateTs);
+ });
+ if (!empty($pagesInIndex) && !empty($pagesInIndex->history)) {
+ $history = (array) $pagesInIndex->history;
+ foreach ($history as $entry) {
+ // Look for matching date
+ if (strpos($entry->date, $date) === 0) {
+ $crawlStatsByDate['SEARCHABLE'] = (int) $entry->value;
+ }
+ }
+ }
+ $pageChanges = $this->retryApiMethod(function () use ($accessToken, $hostId, $dateTs) {
+ return $this->getPageChangesInSearch($accessToken, $hostId, $dateTs);
+ });
+ if (!empty($pageChanges) && !empty($pageChanges->indicators)) {
+ $indicators = (array) $pageChanges->indicators;
+ foreach ($indicators as $indicator => $indicatorByDate) {
+ foreach ($indicatorByDate as $dateItem) {
+ // Look for matching date
+ if (strpos($dateItem->date, $date) === 0) {
+ $crawlStatsByDate[$indicator] = (int) $dateItem->value;
+ }
+ }
+ }
+ }
+ return $crawlStatsByDate;
+ }
+ /**
+ * @param callback $method
+ * @return mixed
+ * @throws \Exception
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ protected function retryApiMethod($method)
+ {
+ $retries = 0;
+ while ($retries < 5) {
+ try {
+ return $method();
+ } catch (InvalidCredentialsException $e) {
+ throw $e;
+ } catch (\Exception $e) {
+ if ($retries >= 4) {
+ throw $e;
+ }
+ usleep(500);
+ $retries++;
+ }
+ }
+ }
+ /**
+ * Process the given auth code to gain access and refresh token from yandex api
+ *
+ * @param string $authCode
+ * @throws InvalidCredentialsException
+ * @throws MissingClientConfigException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ public function processAuthCode($authCode)
+ {
+ if (!$this->isClientConfigured()) {
+ throw new MissingClientConfigException();
+ }
+ $accessToken = $this->fetchAccessTokenWithAuthCode($authCode);
+ $userId = $this->getYandexUserId($accessToken);
+ $this->addAccount($userId, $accessToken, Piwik::getCurrentUserLogin());
+ $userInfo = $this->getUserInfo($userId);
+ Piwik::postEvent('SearchEngineKeywordsPerformance.AccountAdded', [['provider' => \Piwik\Plugins\SearchEngineKeywordsPerformance\Provider\Yandex::getInstance()->getName(), 'account' => $userInfo['name']]]);
+ }
+ /**
+ * Returns user information for given account id
+ *
+ * @param $accountId
+ * @return array|null
+ */
+ public function getUserInfo($accountId)
+ {
+ return $this->configuration->getUserInfo($accountId);
+ }
+ /**
+ * Fetches an access token from Yandex OAuth with the given auth code
+ *
+ * @param string $authCode
+ * @return string
+ * @throws \Exception
+ */
+ protected function fetchAccessTokenWithAuthCode($authCode)
+ {
+ $clientConfig = $this->getClientConfig();
+ $response = Http::sendHttpRequestBy(
+ Http::getTransportMethod(),
+ 'https://oauth.yandex.com/token',
+ 2000,
+ null,
+ null,
+ null,
+ 0,
+ \false,
+ \false,
+ \false,
+ \false,
+ 'POST',
+ $clientConfig['id'],
+ $clientConfig['secret'],
+ 'grant_type=authorization_code&code=' . $authCode
+ );
+ $result = json_decode($response, \true);
+ if (isset($result['error'])) {
+ throw new \Exception($result['error_description']);
+ }
+ if (isset($result['access_token'])) {
+ return $result['access_token'];
+ }
+ throw new \Exception('Unknown Error');
+ }
+ /**
+ * Returns Yandex OAuth url
+ *
+ * @return string
+ */
+ public function createAuthUrl()
+ {
+ $clientConfig = $this->getClientConfig();
+ return 'https://oauth.yandex.com/authorize?response_type=code&client_id=' . $clientConfig['id'];
+ }
+ /**
+ * Returns connected oauth accounts
+ *
+ * @return array
+ */
+ public function getAccounts()
+ {
+ return $this->configuration->getAccounts();
+ }
+ /**
+ * Removes oauth account
+ *
+ * @param string $id
+ * @return boolean
+ */
+ public function removeAccount($id)
+ {
+ $userInfo = $this->getUserInfo($id);
+ $this->configuration->removeAccount($id);
+ Piwik::postEvent('SearchEngineKeywordsPerformance.AccountRemoved', [['provider' => \Piwik\Plugins\SearchEngineKeywordsPerformance\Provider\Yandex::getInstance()->getName(), 'account' => $userInfo['name']]]);
+ return \true;
+ }
+ /**
+ * Adds a oauth account
+ *
+ * @param string $id
+ * @param string $config
+ * @param string $username
+ * @return boolean
+ */
+ public function addAccount($id, $accessToken, $username)
+ {
+ $userInfo = $this->getUserInfoByAccessToken($accessToken);
+ $config = ['userInfo' => ['picture' => 'https://avatars.yandex.net/get-yapic/' . $userInfo['default_avatar_id'] . '/islands-retina-50', 'name' => $userInfo['display_name']], 'accessToken' => $accessToken];
+ $this->configuration->addAccount($id, $config, $username);
+ return \true;
+ }
+ /**
+ * Fetches user info from Yandex Passport API
+ *
+ * @param string $accessToken
+ * @return mixed
+ * @throws InvalidCredentialsException
+ * @throws UnknownAPIException
+ */
+ protected function getUserInfoByAccessToken($accessToken)
+ {
+ $url = 'https://login.yandex.ru/info';
+ $response = Http::sendHttpRequestBy(Http::getTransportMethod(), $url, 2000, null, null, null, 0, \false, \false, \false, \false, 'GET', null, null, null, ['Authorization: OAuth ' . $accessToken]);
+ $result = json_decode($response, \true);
+ if (isset($result['error'])) {
+ throw new InvalidCredentialsException($result['error_description'], $result['error']);
+ }
+ if (empty($result) || !is_array($result) || !isset($result['display_name'])) {
+ throw new UnknownAPIException('Unable to receive user information');
+ }
+ return $result;
+ }
+ /**
+ * @param string $accessToken
+ * @param string $hostId
+ * @param string $date
+ * @return mixed
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ protected function getPopularQueries($accessToken, $hostId, $date)
+ {
+ return $this->sendApiRequest(
+ $accessToken,
+ 'user/' . $this->getYandexUserId($accessToken) . '/hosts/' . $hostId . '/search-queries/popular/',
+ ['date_from' => date(\DATE_ATOM, $date), 'date_to' => date(\DATE_ATOM, $date + 24 * 3600 - 1), 'order_by' => 'TOTAL_CLICKS', 'query_indicator' => ['TOTAL_CLICKS', 'TOTAL_SHOWS', 'AVG_SHOW_POSITION', 'AVG_CLICK_POSITION'], 'limit' => 500]
+ );
+ }
+ /**
+ * @param string $accessToken
+ * @param string $hostId
+ * @param string $date
+ * @return mixed
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ protected function getIndexingHistory($accessToken, $hostId, $date)
+ {
+ // note we query a weeks data as otherwise the results might not contain the date we actually want to look at
+ return $this->sendApiRequest(
+ $accessToken,
+ 'user/' . $this->getYandexUserId($accessToken) . '/hosts/' . $hostId . '/indexing/history/',
+ array('date_from' => date(\DATE_ATOM, $date - 7 * 24 * 3600), 'date_to' => date(\DATE_ATOM, $date + 24 * 3600 - 1))
+ );
+ }
+ /**
+ * @param string $accessToken
+ * @param string $hostId
+ * @param string $date
+ * @return mixed
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ protected function getPagesInIndex($accessToken, $hostId, $date)
+ {
+ // note we query a weeks data as otherwise the results might not contain the date we actually want to look at
+ return $this->sendApiRequest(
+ $accessToken,
+ 'user/' . $this->getYandexUserId($accessToken) . '/hosts/' . $hostId . '/search-urls/in-search/history/',
+ array('date_from' => date(\DATE_ATOM, $date - 7 * 24 * 3600), 'date_to' => date(\DATE_ATOM, $date + 24 * 3600 - 1))
+ );
+ }
+ /**
+ * @param string $accessToken
+ * @param string $hostId
+ * @param string $date
+ * @return mixed
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ protected function getPageChangesInSearch($accessToken, $hostId, $date)
+ {
+ // note we query a weeks data as otherwise the results might not contain the date we actually want to look at
+ return $this->sendApiRequest(
+ $accessToken,
+ 'user/' . $this->getYandexUserId($accessToken) . '/hosts/' . $hostId . '/search-urls/events/history/',
+ array('date_from' => date(\DATE_ATOM, $date - 7 * 24 * 3600), 'date_to' => date(\DATE_ATOM, $date + 24 * 3600 - 1))
+ );
+ }
+ /**
+ * Returns the available hosts for the given access token
+ * @param string $accessToken
+ * @return object
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ protected function getHosts($accessToken)
+ {
+ return $this->sendApiRequest($accessToken, 'user/' . $this->getYandexUserId($accessToken) . '/hosts');
+ }
+ /**
+ * Returns the Yandex User ID for the given access token
+ * @param string $accessToken
+ * @return string
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ protected function getYandexUserId($accessToken)
+ {
+ static $userIdByToken = [];
+ if (!empty($userIdByToken[$accessToken])) {
+ return $userIdByToken[$accessToken];
+ }
+ $result = $this->sendApiRequest($accessToken, 'user');
+ if (!empty($result->user_id)) {
+ $userIdByToken[$accessToken] = $result->user_id;
+ return $userIdByToken[$accessToken];
+ }
+ throw new InvalidCredentialsException('Unable to find user ID');
+ }
+ /**
+ * @param string $accessToken
+ * @param string $method
+ * @param array $params
+ * @return mixed
+ * @throws InvalidCredentialsException
+ * @throws RateLimitApiException
+ * @throws UnknownAPIException
+ */
+ protected function sendApiRequest($accessToken, $method, $params = [])
+ {
+ $urlParams = [];
+ foreach ($params as $name => $value) {
+ if (is_array($value)) {
+ foreach ($value as $val) {
+ $urlParams[] = $name . '=' . urlencode($val);
+ }
+ continue;
+ }
+ $urlParams[] = $name . '=' . urlencode($value);
+ }
+ $url = $this->baseAPIUrl . $method . '?' . implode('&', $urlParams);
+ $additionalHeaders = ['Authorization: OAuth ' . $accessToken, 'Accept: application/json', 'Content-type: application/json'];
+ $response = Http::sendHttpRequestBy(
+ Http::getTransportMethod(),
+ $url,
+ $timeout = 60,
+ $userAgent = null,
+ $destinationPath = null,
+ $file = null,
+ $followDepth = 0,
+ $acceptLanguage = \false,
+ $acceptInvalidSslCertificate = \false,
+ $byteRange = \false,
+ $getExtendedInfo = \true,
+ $httpMethod = 'GET',
+ $httpUsername = '',
+ $httpPassword = '',
+ $requestBody = null,
+ $additionalHeaders
+ );
+ if (empty($response['data'])) {
+ throw new \Exception('Yandex API returned no data: ' . var_export($response, \true));
+ }
+ $data = json_decode($response['data'], \false, 512, \JSON_BIGINT_AS_STRING);
+ if (!empty($data->error_code)) {
+ switch ($data->error_code) {
+ case 'INVALID_OAUTH_TOKEN':
+ case 'INVALID_USER_ID':
+ throw new InvalidCredentialsException($data->error_message, (int) $data->error_code);
+ case 'QUOTA_EXCEEDED':
+ case 'TOO_MANY_REQUESTS_ERROR':
+ throw new RateLimitApiException($data->error_message, (int) $data->error_code);
+ }
+ throw new UnknownAPIException($data->error_message, (int) $data->error_code);
+ }
+ return $data;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Columns/Keyword.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Columns/Keyword.php
new file mode 100644
index 0000000..8745447
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Columns/Keyword.php
@@ -0,0 +1,28 @@
+setName('searchengines:import-bing')->setDescription('Imports Bing Keywords')->addNoValueOption('force', 'f', 'Force reimport for data')->addRequiredValueOption('idsite', '', 'Site id');
+ }
+ /**
+ * @return int
+ */
+ protected function doExecute(): int
+ {
+ $input = $this->getInput();
+ $output = $this->getOutput();
+ $output->writeln("Starting to import Bing Keywords");
+ $start = microtime(\true);
+ $idSite = $input->getOption('idsite');
+ $setting = new MeasurableSettings($idSite);
+ $bingSiteUrl = $setting->bingSiteUrl;
+ if (!$bingSiteUrl || !$bingSiteUrl->getValue()) {
+ $output->writeln("Site with ID {$idSite} not configured for Bing Import");
+ }
+ $importer = new Bing($idSite, $input->hasOption('force'));
+ $importer->importAllAvailableData();
+ $output->writeln("Finished in " . round(microtime(\true) - $start, 3) . "s");
+ return self::SUCCESS;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportGoogle.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportGoogle.php
new file mode 100644
index 0000000..0f59541
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportGoogle.php
@@ -0,0 +1,55 @@
+setName('searchengines:import-google')->setDescription('Imports Google Keywords')
+ ->addNoValueOption('force', 'f', 'Force reimport for data')
+ ->addRequiredValueOption('idsite', '', 'Site id')
+ ->addOptionalValueOption('date', 'd', 'specific date');
+ }
+ /**
+ * @return int
+ */
+ protected function doExecute(): int
+ {
+ $input = $this->getInput();
+ $output = $this->getOutput();
+ $output->writeln("Starting to import Google Keywords");
+ $start = microtime(\true);
+ $idSite = $input->getOption('idsite');
+ $setting = new MeasurableSettings($idSite);
+ $searchConsoleUrl = $setting->googleSearchConsoleUrl;
+ if (!$searchConsoleUrl || !$searchConsoleUrl->getValue()) {
+ $output->writeln("Site with ID {$idSite} not configured for Google Import");
+ }
+ $importer = new Google($idSite, $input->getOption('force'));
+ $date = $input->getOption('date') ? $input->getOption('date') : null;
+ $importer->importAllAvailableData($date);
+ $output->writeln("Finished in " . round(microtime(\true) - $start, 3) . "s");
+ return self::SUCCESS;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportYandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportYandex.php
new file mode 100644
index 0000000..f8d243e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportYandex.php
@@ -0,0 +1,55 @@
+setName('searchengines:import-yandex')->setDescription('Imports Yandex Keywords')
+ ->addNoValueOption('force', 'f', 'Force reimport for data')
+ ->addRequiredValueOption('idsite', '', 'Site id')
+ ->addOptionalValueOption('date', 'd', 'specific date');
+ }
+ /**
+ * @return int
+ */
+ protected function doExecute(): int
+ {
+ $input = $this->getInput();
+ $output = $this->getOutput();
+ $output->writeln("Starting to import Yandex Keywords");
+ $start = microtime(\true);
+ $idSite = $input->getOption('idsite');
+ $setting = new MeasurableSettings($idSite);
+ $yandexSiteUrl = $setting->yandexAccountAndHostId;
+ if (!$yandexSiteUrl || !$yandexSiteUrl->getValue()) {
+ $output->writeln("Site with ID {$idSite} not configured for Yandex Import");
+ }
+ $importer = new Yandex($idSite, $input->hasOption('force'));
+ $date = $input->hasOption('date') ? $input->getOption('date') : 100;
+ $importer->importAllAvailableData($date);
+ $output->writeln("Finished in " . round(microtime(\true) - $start, 3) . "s");
+ return self::SUCCESS;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Controller.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Controller.php
new file mode 100644
index 0000000..ee30156
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Controller.php
@@ -0,0 +1,1125 @@
+showNotificationIfNoWebsiteConfigured($provider);
+ }
+ SearchEngineKeywordsPerformance::displayNotificationIfRecentApiErrorsExist($viewVariables['providers']);
+
+ $viewVariables['providers'] = array_map(function (ProviderAbstract $provider) {
+ return $this->toProviderArray($provider);
+ }, $viewVariables['providers']);
+
+ return $this->renderTemplate('index', $viewVariables);
+ }
+
+ private function toProviderArray(ProviderAbstract $provider)
+ {
+ return [
+ 'id' => $provider->getId(),
+ 'is_configured' => $provider->isConfigured(),
+ 'configured_site_ids' => $provider->getConfiguredSiteIds(),
+ 'problems' => $provider->getConfigurationProblems(),
+ 'is_experimental' => $provider->isExperimental(),
+ 'logos' => $provider->getLogoUrls(),
+ 'name' => $provider->getName(),
+ 'description' => $provider->getDescription(),
+ 'note' => $provider->getNote(),
+ ];
+ }
+
+ private function showNotificationIfNoWebsiteConfigured(ProviderAbstract $provider)
+ {
+ if (!$provider->isConfigured()) {
+ return;
+ }
+
+ if (count($provider->getConfiguredSiteIds()) == 0) {
+ $notification = new Notification(Piwik::translate(
+ 'SearchEngineKeywordsPerformance_NoWebsiteConfiguredWarning',
+ $provider->getName()
+ ));
+ $notification->context = Notification::CONTEXT_WARNING;
+ Notification\Manager::notify($provider->getId() . 'nowebsites', $notification);
+ }
+
+ $errors = $provider->getConfigurationProblems();
+
+ if (count($errors['sites'])) {
+ $notification = new Notification(Piwik::translate(
+ 'SearchEngineKeywordsPerformance_ProviderXSitesWarning',
+ [$provider->getName()]
+ ));
+ $notification->context = Notification::CONTEXT_WARNING;
+ $notification->raw = true;
+ Notification\Manager::notify($provider->getId() . 'siteswarning', $notification);
+ }
+
+ if (count($errors['accounts'])) {
+ $notification = new Notification(Piwik::translate(
+ 'SearchEngineKeywordsPerformance_ProviderXAccountWarning',
+ [$provider->getName()]
+ ));
+ $notification->context = Notification::CONTEXT_WARNING;
+ $notification->raw = true;
+ Notification\Manager::notify($provider->getId() . 'accountwarning', $notification);
+ }
+ }
+
+ private function getCurrentSite()
+ {
+ if ($this->site instanceof Site) {
+ return ['id' => $this->site->getId(), 'name' => $this->site->getName()];
+ }
+
+ $sites = Request::processRequest('SitesManager.getSitesWithAdminAccess', [], []);
+
+ if (!empty($sites[0])) {
+ return ['id' => $sites[0]['idsite'], 'name' => $sites[0]['name']];
+ }
+
+ return [];
+ }
+
+ /*****************************************************************************************
+ * Configuration actions for Google provider
+ */
+
+ /**
+ * Show Google configuration page
+ *
+ * @param bool $hasOAuthError indicates if a oAuth access error occurred
+ * @return string
+ */
+ public function configureGoogle($hasOAuthError = false)
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+
+ $configSaved = $this->configureGoogleClientIfProvided();
+ if (false === $configSaved) {
+ $notification = new Notification(Piwik::translate('SearchEngineKeywordsPerformance_ClientConfigSaveError'));
+ $notification->context = Notification::CONTEXT_ERROR;
+ Notification\Manager::notify('clientConfigSaved', $notification);
+ }
+
+ $errorMessage = Common::getRequestVar('error', '');
+ if (!empty($errorMessage)) {
+ if ($errorMessage === 'access_denied') {
+ $errorMessage = Piwik::translate('SearchEngineKeywordsPerformance_OauthFailedMessage');
+ } elseif ($errorMessage === 'jwt_validation_error') {
+ $errorMessage = Piwik::translate('General_ExceptionSecurityCheckFailed');
+ }
+ $notification = new Notification($errorMessage);
+ $notification->context = Notification::CONTEXT_ERROR;
+ $notification->type = Notification::TYPE_TRANSIENT;
+ Notification\Manager::notify('configureerror', $notification);
+ }
+
+ $googleClient = ProviderGoogle::getInstance()->getClient();
+ $clientConfigured = true;
+
+ try {
+ $googleClient->getConfiguredClient('');
+ } catch (MissingClientConfigException $e) {
+ $clientConfigured = false;
+ } catch (MissingOAuthConfigException $e) {
+ // ignore missing accounts
+ } catch (\Exception $e) {
+ // Catch any general exceptions because they likely won't be recoverable. Delete the config so that they can try again
+ // If we don't delete the config, the customer won't have any way to fix the issue
+ $googleClient->deleteClientConfig();
+
+ // Make sure we cancel the success notification because that could confuse the customer since things failed
+ Notification\Manager::cancel('clientConfigSaved');
+
+ // Mark the client as not configured and notify the user that something is wrong with the configuration
+ $clientConfigured = false;
+ $notification = new Notification($e->getMessage());
+ $notification->context = Notification::CONTEXT_ERROR;
+ $notification->type = Notification::TYPE_TRANSIENT;
+ Notification\Manager::notify('configureerror', $notification);
+ }
+
+ $this->addGoogleSiteConfigIfProvided();
+ $this->removeGoogleSiteConfigIfProvided();
+ $this->removeGoogleAccountIfProvided();
+
+ $urlOptions = [];
+ $accounts = $googleClient->getAccounts();
+ $countOfAccountsWithAccess = 0;
+
+ foreach ($accounts as $id => &$account) {
+ $userInfo = $googleClient->getUserInfo($id);
+ $urls = $googleClient->getAvailableUrls($id, false);
+ $account['picture'] = $userInfo['picture'];
+ $account['name'] = $userInfo['name'];
+ $account['urls'] = $urls;
+ $account['hasAccess'] = Piwik::hasUserSuperUserAccessOrIsTheUser($account['username']);
+ if ($account['hasAccess']) {
+ ++$countOfAccountsWithAccess;
+ }
+ $account['created_formatted'] = Date::factory(date(
+ 'Y-m-d',
+ $account['created']
+ ))->getLocalized(Date::DATE_FORMAT_LONG);
+ try {
+ $googleClient->testConfiguration($id);
+ } catch (\Exception $e) {
+ $account['hasError'] = $e->getMessage();
+ }
+
+ if ($account['hasAccess']) {
+ foreach ($googleClient->getAvailableUrls($id) as $url => $status) {
+ $urlOptions[$id . '##' . $url] = $url . ' (' . $account['name'] . ')';
+ }
+ }
+ }
+
+ $isClientConfigurable = StaticContainer::get('SearchEngineKeywordsPerformance.Google.isClientConfigurable');
+
+ $viewVariables = [];
+ $viewVariables['isConfigured'] = $googleClient->isConfigured();
+ $viewVariables['clientId'] = $googleClient->getClientId();
+ $viewVariables['auth_nonce'] = Nonce::getNonce('SEKP.google.auth');
+ $viewVariables['clientSecret'] = preg_replace('/\w/', '*', $googleClient->getClientSecret() ?? '');
+ $viewVariables['isClientConfigured'] = $clientConfigured;
+ $viewVariables['isClientConfigurable'] = $isClientConfigurable;
+ $viewVariables['isOAuthConfigured'] = count($accounts) > 0;
+ $viewVariables['accounts'] = $accounts;
+ $viewVariables['urlOptions'] = $urlOptions;
+ $viewVariables['hasOAuthError'] = $hasOAuthError;
+ $viewVariables['configuredMeasurables'] = ProviderGoogle::getInstance()->getConfiguredSiteIds();
+ $viewVariables['nonce'] = Nonce::getNonce('SEKP.google.config');
+ $viewVariables['sitesInfos'] = [];
+ $viewVariables['currentSite'] = $this->getCurrentSite();
+ $viewVariables['countOfAccountsWithAccess'] = $countOfAccountsWithAccess;
+ $viewVariables['addGoogleSiteConfigNonce'] = Nonce::getNonce(self::GOOGLE_ADD_SITE_CONFIG_NONCE_KEY);
+ $viewVariables['removeGoogleSiteConfigNonce'] = Nonce::getNonce(self::GOOGLE_REMOVE_SITE_CONFIG_NONCE_KEY);
+ $viewVariables['removeGoogleAccountNonce'] = Nonce::getNonce(self::GOOGLE_REMOVE_ACCOUNT_NONCE_KEY);
+
+ $siteIds = $viewVariables['configuredMeasurables'];
+
+ foreach ($siteIds as $siteId => $config) {
+ $googleSiteUrl = $config['googleSearchConsoleUrl'];
+ $viewVariables['sitesInfos'][$siteId] = Site::getSite($siteId);
+ $lastRun = Option::get('GoogleImporterTask_LastRun_' . $siteId);
+
+ if ($lastRun) {
+ $lastRun = date('Y-m-d H:i', $lastRun) . ' UTC';
+ } else {
+ $lastRun = Piwik::translate('General_Never');
+ }
+
+ $viewVariables['sitesInfos'][$siteId]['lastRun'] = $lastRun;
+
+ [$accountId, $url] = explode('##', $googleSiteUrl);
+
+ try {
+ $viewVariables['sitesInfos'][$siteId]['accountValid'] = $googleClient->testConfiguration($accountId);
+ } catch (\Exception $e) {
+ $viewVariables['sitesInfos'][$siteId]['accountValid'] = false;
+ }
+
+ $urls = $googleClient->getAvailableUrls($accountId);
+
+ $viewVariables['sitesInfos'][$siteId]['urlValid'] = key_exists($url, $urls);
+ }
+
+ if (!empty($this->securityPolicy)) {
+ $this->securityPolicy->addPolicy('img-src', '*.googleusercontent.com');
+ }
+
+ $configureConnectionProps = [];
+ $configureConnectionProps['baseUrl'] = Url::getCurrentUrlWithoutQueryString();
+ $configureConnectionProps['baseDomain'] = Url::getCurrentScheme() . '://' . Url::getCurrentHost();
+ $configureConnectionProps['manualConfigNonce'] = $viewVariables['nonce'];
+ $configureConnectionProps['primaryText'] = Piwik::translate('SearchEngineKeywordsPerformance_ConfigureTheImporterLabel1');
+
+ // There are certain cases where index.php isn't part of the baseUrl when it should be. Append it if missing.
+ if (stripos($configureConnectionProps['baseUrl'], 'index.php') === false) {
+ $configureConnectionProps['baseUrl'] .= 'index.php';
+ }
+
+ $isConnectAccountsActivated = Manager::getInstance()->isPluginActivated('ConnectAccounts');
+ $authBaseUrl = $isConnectAccountsActivated ? "https://" . StaticContainer::get('CloudAccountsInstanceId') . '/index.php?' : '';
+ $jwt = Common::getRequestVar('state', '', 'string');
+ if (empty($jwt) && Piwik::hasUserSuperUserAccess() && $isConnectAccountsActivated) {
+ // verify an existing user by supplying a jwt too
+ $jwt = ConnectHelper::buildOAuthStateJwt(
+ SettingsPiwik::getPiwikInstanceId(),
+ ConnectAccounts::INITIATED_BY_SEK
+ );
+ }
+ $googleAuthUrl = '';
+ if ($isConnectAccountsActivated) {
+ $strategyName = GoogleSearchConnect::getStrategyName();
+ $googleAuthUrl = $authBaseUrl . Http::buildQuery([
+ 'module' => 'ConnectAccounts',
+ 'action' => 'initiateOauth',
+ 'state' => $jwt,
+ 'strategy' => $strategyName
+ ]);
+ $configureConnectionProps['strategy'] = $strategyName;
+ $configureConnectionProps['connectedWith'] = 'Google';
+ $configureConnectionProps['unlinkUrl'] = Url::getCurrentUrlWithoutQueryString() . '?' . Http::buildQuery([
+ 'module' => 'ConnectAccounts',
+ 'action' => 'unlink',
+ 'nonce' => ConnectHelper::getUnlinkNonce(),
+ 'strategy' => $strategyName
+ ]);
+ $configureConnectionProps['authUrl'] = $googleAuthUrl;
+ $configureConnectionProps['connectAccountsUrl'] = $googleAuthUrl;
+ $configureConnectionProps['connectAccountsBtnText'] = Piwik::translate('ConnectAccounts_ConnectWithGoogleText');
+ }
+
+ $configureConnectionProps['isConnectAccountsActivated'] = $isConnectAccountsActivated;
+ if ($isConnectAccountsActivated) {
+ $configureConnectionProps['radioOptions'] = [
+ 'connectAccounts' => Piwik::translate('SearchEngineKeywordsPerformance_OptionQuickConnectWithGoogle'),
+ 'manual' => Piwik::translate('ConnectAccounts_OptionAdvancedConnectWithGa'),
+ ];
+ }
+ $configureConnectionProps['googleAuthUrl'] = $googleAuthUrl;
+ $faqUrl = Url::addCampaignParametersToMatomoLink('https://matomo.org/faq/reports/import-google-search-keywords-in-matomo/#how-to-set-up-google-search-console-and-verify-your-website');
+ $faqAnchorOpen = "";
+ $configureConnectionProps['manualConfigText'] = Piwik::translate('SearchEngineKeywordsPerformance_ConfigureTheImporterLabel2')
+ . ' ' . Piwik::translate('SearchEngineKeywordsPerformance_ConfigureTheImporterLabel3', [
+ $faqAnchorOpen,
+ ' ',
+ ]) . ' ' . Piwik::translate('SearchEngineKeywordsPerformance_OAuthExampleText')
+ . '' . Piwik::translate('SearchEngineKeywordsPerformance_GoogleAuthorizedJavaScriptOrigin')
+ . ": {$configureConnectionProps['baseDomain']}"
+ . Piwik::translate('SearchEngineKeywordsPerformance_GoogleAuthorizedRedirectUri')
+ . ": {$configureConnectionProps['baseUrl']}?module=SearchEngineKeywordsPerformance&action=processAuthCode ";
+
+ $viewVariables['configureConnectionProps'] = $configureConnectionProps;
+ $viewVariables['extensions'] = self::getComponentExtensions();
+ $viewVariables['removeConfigUrl'] = Url::getCurrentQueryStringWithParametersModified([ 'action' => 'removeGoogleClientConfig' ]);
+
+ return $this->renderTemplate('google\configuration', $viewVariables);
+ }
+
+ /**
+ * Save Google client configuration if set in request
+ *
+ * @return bool|null bool on success or failure, null if not data present in request
+ */
+ protected function configureGoogleClientIfProvided()
+ {
+ $googleClient = ProviderGoogle::getInstance()->getClient();
+
+ $config = Common::getRequestVar('client', '');
+
+ if (empty($config) && !empty($_FILES['clientfile'])) {
+ if (!empty($_FILES['clientfile']['error'])) {
+ return false;
+ }
+
+ $file = $_FILES['clientfile']['tmp_name'];
+ if (!file_exists($file)) {
+ return false;
+ }
+
+ $config = file_get_contents($_FILES['clientfile']['tmp_name']);
+ }
+
+ if (!empty($config)) {
+ Nonce::checkNonce('SEKP.google.config', Common::getRequestVar('config_nonce'));
+ try {
+ $config = Common::unsanitizeInputValue($config);
+ $saveResult = $googleClient->setClientConfig($config);
+ if (!$saveResult) {
+ return false;
+ }
+
+ // Show success notification
+ $notification = new Notification(Piwik::translate('SearchEngineKeywordsPerformance_ClientConfigImported'));
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ Notification\Manager::notify('clientConfigSaved', $notification);
+
+ // Redirect so that it's the correct URL and doesn't try to resubmit the form if the customer refreshes
+ Url::redirectToUrl(Url::getCurrentUrlWithoutQueryString() . Url::getCurrentQueryStringWithParametersModified([
+ 'action' => 'configureGoogle',
+ 'code' => null,
+ 'scope' => null,
+ 'state' => null,
+ 'error' => null,
+ ]));
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Save google configuration for a site if given in request
+ */
+ protected function addGoogleSiteConfigIfProvided()
+ {
+ $googleSiteId = Common::getRequestVar('googleSiteId', '');
+ $googleAccountAndUrl = Common::getRequestVar('googleAccountAndUrl', '');
+ $googleTypes = explode(',', Common::getRequestVar('googleTypes', ''));
+
+ if (!empty($googleSiteId) && !empty($googleAccountAndUrl)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::GOOGLE_ADD_SITE_CONFIG_NONCE_KEY, $request->getStringParameter('addSiteConfigNonce', ''));
+ // Do not allow to configure websites with unsupported type or force enabled config
+ if (SearchEngineKeywordsPerformance::isGoogleForceEnabled($googleSiteId) || WebsiteMeasurableType::ID !== Site::getTypeFor($googleSiteId)) {
+ $notification = new Notification(
+ Piwik::translate('SearchEngineKeywordsPerformance_WebsiteTypeUnsupported', [
+ Site::getNameFor($googleSiteId)
+ ])
+ );
+
+ if (class_exists('\Piwik\Plugins\RollUpReporting\Type') && \Piwik\Plugins\RollUpReporting\Type::ID === Site::getTypeFor($googleSiteId)) {
+ $notification->message .= ' ' . Piwik::translate('SearchEngineKeywordsPerformance_WebsiteTypeUnsupportedRollUp');
+ }
+
+ $notification->context = Notification::CONTEXT_ERROR;
+ $notification->raw = true;
+ $notification->flags = Notification::FLAG_CLEAR;
+ Notification\Manager::notify('websiteNotConfigurable', $notification);
+
+ return;
+ }
+
+ $measurableSettings = new MeasurableSettings($googleSiteId);
+ $measurableSettings->googleConfigCreatedBy->setValue(Piwik::getCurrentUserLogin());
+
+ //Need to explicitly setIsWritableByCurrentUser=true, since it can be set as false when we instantiate MeasurableSettings object due to previously added by another user
+ $measurableSettings->googleSearchConsoleUrl->setIsWritableByCurrentUser(true);
+ $measurableSettings->googleWebKeywords->setIsWritableByCurrentUser(true);
+ $measurableSettings->googleImageKeywords->setIsWritableByCurrentUser(true);
+ $measurableSettings->googleNewsKeywords->setIsWritableByCurrentUser(true);
+ $measurableSettings->googleVideoKeywords->setIsWritableByCurrentUser(true);
+
+ $measurableSettings->googleSearchConsoleUrl->setValue($googleAccountAndUrl);
+ $measurableSettings->googleWebKeywords->setValue(in_array('web', $googleTypes));
+ $measurableSettings->googleImageKeywords->setValue(in_array('image', $googleTypes));
+ $measurableSettings->googleNewsKeywords->setValue(in_array('news', $googleTypes));
+ $measurableSettings->googleVideoKeywords->setValue(in_array('video', $googleTypes));
+ $measurableSettings->save();
+
+ $notification = new Notification(
+ Piwik::translate('SearchEngineKeywordsPerformance_WebsiteSuccessfulConfigured', [
+ Site::getNameFor($googleSiteId),
+ '',
+ ' '
+ ])
+ );
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ $notification->raw = true;
+ $notification->flags = Notification::FLAG_CLEAR;
+ Notification\Manager::notify('websiteConfigured', $notification);
+ }
+ }
+
+ /**
+ * Removes a Google account if `remove` param is given in request
+ */
+ protected function removeGoogleAccountIfProvided()
+ {
+ $remove = Common::getRequestVar('remove', '');
+
+ if (!empty($remove)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::GOOGLE_REMOVE_ACCOUNT_NONCE_KEY, $request->getStringParameter('removeAccountNonce', ''));
+ ProviderGoogle::getInstance()->getClient()->removeAccount($remove);
+
+ $sitesWithConfig = ProviderGoogle::getInstance()->getConfiguredSiteIds();
+ foreach ($sitesWithConfig as $siteId => $siteConfig) {
+ $googleSetting = explode('##', $siteConfig['googleSearchConsoleUrl']);
+ if (!empty($googleSetting[0]) && $googleSetting[0] == $remove) {
+ $config = new MeasurableSettings($siteId);
+ $config->googleSearchConsoleUrl->setValue('0');
+ $config->save();
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a Google site config if `removeConfig` param is given in request
+ */
+ protected function removeGoogleSiteConfigIfProvided()
+ {
+ $removeConfig = Common::getRequestVar('removeConfig', '');
+
+ if (!empty($removeConfig)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::GOOGLE_REMOVE_SITE_CONFIG_NONCE_KEY, $request->getStringParameter('removeSiteConfigNonce', ''));
+ $measurableSettings = new MeasurableSettings($removeConfig);
+ $measurableSettings->googleSearchConsoleUrl->setValue('0');
+ $measurableSettings->save();
+ }
+ }
+
+ /**
+ * Delete the Google client config option so that the customer will be prompted to upload a new one or use the Cloud
+ * config. Then refresh the page so show the change.
+ */
+ public function removeGoogleClientConfig()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ Nonce::checkNonce('SEKP.google.config', Common::getRequestVar('config_nonce'));
+
+ ProviderGoogle::getInstance()->getClient()->deleteClientConfig();
+
+ Url::redirectToUrl(Url::getCurrentUrlWithoutQueryString() . Url::getCurrentQueryStringWithParametersModified([
+ 'action' => 'configureGoogle',
+ 'code' => null,
+ 'scope' => null,
+ 'state' => null,
+ 'error' => null,
+ ]));
+ }
+
+ public function forwardToAuth()
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+
+ Nonce::checkNonce('SEKP.google.auth', Common::getRequestVar('auth_nonce'));
+
+ $client = ProviderGoogle::getInstance()->getClient();
+ $state = Nonce::getNonce(self::OAUTH_STATE_NONCE_NAME, 900);
+
+ Url::redirectToUrl($client->createAuthUrl($state));
+ }
+
+ protected function getSession()
+ {
+ return new SessionNamespace('searchperformance');
+ }
+
+ /**
+ * Processes the response from google oauth service
+ *
+ * @return string
+ * @throws \Exception
+ */
+ public function processAuthCode()
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+
+ $error = Common::getRequestVar('error', '');
+ $oauthCode = Common::getRequestVar('code', '');
+
+ if (!$error) {
+ $state = Common::getRequestVar('state');
+ if ($state && !empty($_SERVER['HTTP_REFERER']) && stripos($_SERVER['HTTP_REFERER'], 'https://accounts.google.') === 0) {
+ //We need tp update this, else it will fail for referer like https://accounts.google.co.in
+ $_SERVER['HTTP_REFERER'] = 'https://accounts.google.com';
+ }
+ try {
+ Nonce::checkNonce(static::OAUTH_STATE_NONCE_NAME, $state, defined('PIWIK_TEST_MODE') ? null : 'google.com');
+ } catch (\Exception $ex) {
+ $error = $ex->getMessage();
+ }
+ }
+
+ if ($error) {
+ return $this->configureGoogle(true);
+ }
+
+ try {
+ ProviderGoogle::getInstance()->getClient()->processAuthCode($oauthCode);
+ } catch (\Exception $e) {
+ return $this->configureGoogle($e->getMessage());
+ }
+
+ // we need idSite in the url to display all the menus like Conversion Import after redirect
+ $siteInfo = $this->getCurrentSite();
+ // reload index action to prove everything is configured
+ Url::redirectToUrl(Url::getCurrentUrlWithoutQueryString() . Url::getCurrentQueryStringWithParametersModified([
+ 'action' => 'configureGoogle',
+ 'idSite' => (isset($siteInfo['id']) ? $siteInfo['id'] : 0),
+ 'code' => null,
+ 'scope' => null,
+ 'state' => null
+ ]));
+ }
+ /******************************************************************************************
+ *****************************************************************************************/
+
+ /*****************************************************************************************
+ *****************************************************************************************
+ * Configuration actions for Bing provider
+ */
+
+ /**
+ * Show configuration page for Bing
+ *
+ * @return string
+ */
+ public function configureBing()
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+
+ $viewVariables = [];
+ $viewVariables['apikey'] = '';
+ $bingClient = ProviderBing::getInstance()->getClient();
+
+ $apiKey = Common::getRequestVar('apikey', '');
+
+ if (!empty($apiKey)) {
+ Nonce::checkNonce('SEKP.bing.config', Common::getRequestVar('config_nonce'));
+ try {
+ $bingClient->testConfiguration($apiKey);
+ $bingClient->addAccount($apiKey, Piwik::getCurrentUserLogin());
+ } catch (\Exception $e) {
+ $viewVariables['error'] = $e->getMessage();
+ $viewVariables['apikey'] = $apiKey;
+ }
+ }
+
+ $this->addBingSiteConfigIfProvided();
+ $this->removeBingSiteConfigIfProvided();
+ $this->removeBingAccountIfProvided();
+
+ $urlOptions = [];
+ $accounts = $bingClient->getAccounts();
+ $countOfAccountsWithAccess = 0;
+ foreach ($accounts as &$account) {
+ $account['urls'] = [];
+ $account['created_formatted'] = Date::factory(date(
+ 'Y-m-d',
+ $account['created']
+ ))->getLocalized(Date::DATE_FORMAT_LONG);
+ $account['hasAccess'] = Piwik::hasUserSuperUserAccessOrIsTheUser($account['username']);
+ if ($account['hasAccess']) {
+ ++$countOfAccountsWithAccess;
+ }
+ try {
+ $bingClient->testConfiguration($account['apiKey']);
+ } catch (\Exception $e) {
+ $account['hasError'] = $e->getMessage();
+ continue;
+ }
+
+ $account['urls'] = $bingClient->getAvailableUrls($account['apiKey'], false);
+
+ if ($account['hasAccess']) {
+ foreach ($bingClient->getAvailableUrls($account['apiKey']) as $url => $status) {
+ $urlOptions[$account['apiKey'] . '##' . $url] = $url . ' (' . substr(
+ $account['apiKey'],
+ 0,
+ 5
+ ) . '*****' . substr($account['apiKey'], -5, 5) . ')';
+ }
+ }
+ }
+
+ $viewVariables['nonce'] = Nonce::getNonce('SEKP.bing.config');
+ $viewVariables['accounts'] = $accounts;
+ $viewVariables['urlOptions'] = $urlOptions;
+ $viewVariables['configuredMeasurables'] = ProviderBing::getInstance()->getConfiguredSiteIds();
+ $viewVariables['sitesInfos'] = [];
+ $viewVariables['currentSite'] = $this->getCurrentSite();
+ $viewVariables['countOfAccountsWithAccess'] = $countOfAccountsWithAccess;
+ $viewVariables['addBingSiteConfigNonce'] = Nonce::getNonce(self::BING_ADD_SITE_CONFIG_NONCE_KEY);
+ $viewVariables['removeBingSiteConfigNonce'] = Nonce::getNonce(self::BING_REMOVE_SITE_CONFIG_NONCE_KEY);
+ $viewVariables['removeBingAccountNonce'] = Nonce::getNonce(self::BING_REMOVE_ACCOUNT_NONCE_KEY);
+
+ $siteIds = $viewVariables['configuredMeasurables'];
+
+ foreach ($siteIds as $siteId => $config) {
+ $viewVariables['sitesInfos'][$siteId] = Site::getSite($siteId);
+ $lastRun = Option::get('BingImporterTask_LastRun_' . $siteId);
+
+ if ($lastRun) {
+ $lastRun = date('Y-m-d H:i', $lastRun) . ' UTC';
+ } else {
+ $lastRun = Piwik::translate('General_Never');
+ }
+
+ $viewVariables['sitesInfos'][$siteId]['lastRun'] = $lastRun;
+
+ $bingSiteUrl = $config['bingSiteUrl'];
+ [$apiKey, $url] = explode('##', $bingSiteUrl);
+
+ try {
+ $viewVariables['sitesInfos'][$siteId]['accountValid'] = $bingClient->testConfiguration($apiKey);
+ } catch (\Exception $e) {
+ $viewVariables['sitesInfos'][$siteId]['accountValid'] = false;
+ }
+
+ $urls = $bingClient->getAvailableUrls($apiKey);
+
+ $viewVariables['sitesInfos'][$siteId]['urlValid'] = key_exists($url, $urls);
+ }
+
+ return $this->renderTemplate('bing\configuration', $viewVariables);
+ }
+
+ /**
+ * Save Bing configuration for a site if given in request
+ */
+ protected function addBingSiteConfigIfProvided()
+ {
+ $bingSiteId = Common::getRequestVar('bingSiteId', '');
+ $bingAccountAndUrl = Common::getRequestVar('bingAccountAndUrl', '');
+
+ if (!empty($bingSiteId) && !empty($bingAccountAndUrl)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::BING_ADD_SITE_CONFIG_NONCE_KEY, $request->getStringParameter('addSiteConfigNonce', ''));
+ // Do not allow to configure websites with unsupported type or force enabled config
+ if (SearchEngineKeywordsPerformance::isBingForceEnabled($bingSiteId) || WebsiteMeasurableType::ID !== Site::getTypeFor($bingSiteId)) {
+ $notification = new Notification(
+ Piwik::translate('SearchEngineKeywordsPerformance_WebsiteTypeUnsupported', [
+ Site::getNameFor($bingSiteId)
+ ])
+ );
+
+ if (class_exists('\Piwik\Plugins\RollUpReporting\Type') && \Piwik\Plugins\RollUpReporting\Type::ID === Site::getTypeFor($bingSiteId)) {
+ $notification->message .= ' ' . Piwik::translate('SearchEngineKeywordsPerformance_WebsiteTypeUnsupportedRollUp');
+ }
+
+ $notification->context = Notification::CONTEXT_ERROR;
+ $notification->raw = true;
+ $notification->flags = Notification::FLAG_CLEAR;
+ Notification\Manager::notify('websiteNotConfigurable', $notification);
+
+ return;
+ }
+
+ $measurableSettings = new MeasurableSettings($bingSiteId);
+ $measurableSettings->bingConfigCreatedBy->setValue(Piwik::getCurrentUserLogin());
+
+ //Need to explicitly setIsWritableByCurrentUser=true, since it can be set as false when we instantiate MeasurableSettings object due to previously added by another user
+ $measurableSettings->bingSiteUrl->setIsWritableByCurrentUser(true);
+
+ $measurableSettings->bingSiteUrl->setValue($bingAccountAndUrl);
+ $measurableSettings->save();
+
+ $notification = new Notification(
+ Piwik::translate('SearchEngineKeywordsPerformance_WebsiteSuccessfulConfigured', [
+ Site::getNameFor($bingSiteId),
+ '',
+ ' '
+ ])
+ );
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ $notification->raw = true;
+ $notification->flags = Notification::FLAG_CLEAR;
+ Notification\Manager::notify('websiteConfigured', $notification);
+ }
+ }
+
+ /**
+ * Removes a Bing account if `remove` param is given in request
+ */
+ protected function removeBingAccountIfProvided()
+ {
+ $remove = Common::getRequestVar('remove', '');
+
+ if (!empty($remove)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::BING_REMOVE_ACCOUNT_NONCE_KEY, $request->getStringParameter('removeAccountNonce', ''));
+ ProviderBing::getInstance()->getClient()->removeAccount($remove);
+
+ $sitesWithConfig = ProviderBing::getInstance()->getConfiguredSiteIds();
+ foreach ($sitesWithConfig as $siteId => $siteConfig) {
+ $bingSetting = explode('##', $siteConfig['bingSiteUrl']);
+ if (!empty($bingSetting[0]) && $bingSetting[0] == $remove) {
+ $config = new MeasurableSettings($siteId);
+ $config->bingSiteUrl->setValue('0');
+ $config->save();
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a Bing site config if `removeConfig` param is given in request
+ */
+ protected function removeBingSiteConfigIfProvided()
+ {
+ $removeConfig = Common::getRequestVar('removeConfig', '');
+
+ if (!empty($removeConfig)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::BING_REMOVE_SITE_CONFIG_NONCE_KEY, $request->getStringParameter('removeSiteConfigNonce', ''));
+ $measurableSettings = new MeasurableSettings($removeConfig);
+ $measurableSettings->bingSiteUrl->setValue('0');
+ $measurableSettings->save();
+ }
+ }
+ /******************************************************************************************
+ *****************************************************************************************/
+
+
+ /*****************************************************************************************
+ *****************************************************************************************
+ * Configuration actions for Yandex provider
+ */
+
+ /**
+ * Show Yandex configuration page
+ *
+ * @param bool $hasOAuthError indicates if a oAuth access error occurred
+ * @return string
+ */
+ public function configureYandex($hasOAuthError = false)
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+
+ $configSaved = $this->configureYandexClientIfProvided();
+
+ if (true === $configSaved) {
+ $notification = new Notification(Piwik::translate('SearchEngineKeywordsPerformance_ClientConfigImported'));
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ Notification\Manager::notify('clientConfigSaved', $notification);
+ } elseif (false === $configSaved) {
+ $notification = new Notification(Piwik::translate('SearchEngineKeywordsPerformance_ClientConfigSaveError'));
+ $notification->context = Notification::CONTEXT_ERROR;
+ Notification\Manager::notify('clientConfigSaved', $notification);
+ }
+
+ $yandexClient = ProviderYandex::getInstance()->getClient();
+ $clientConfigured = $yandexClient->isClientConfigured();
+
+ $this->addYandexSiteConfigIfProvided();
+ $this->removeYandexSiteConfigIfProvided();
+ $this->removeYandexAccountIfProvided();
+
+ $urlOptions = [];
+ $accounts = $yandexClient->getAccounts();
+ $countOfAccountsWithAccess = 0;
+
+ foreach ($accounts as $id => &$account) {
+ $userInfo = $yandexClient->getUserInfo($id);
+ $account['urls'] = [];
+ $account['picture'] = $userInfo['picture'];
+ $account['name'] = $userInfo['name'];
+ $account['created_formatted'] = Date::factory(date(
+ 'Y-m-d',
+ $account['created']
+ ))->getLocalized(Date::DATE_FORMAT_LONG);
+ $account['authDaysAgo'] = floor((time() - $account['created']) / (3600 * 24));
+ $account['hasAccess'] = Piwik::hasUserSuperUserAccessOrIsTheUser($account['username']);
+ if ($account['hasAccess']) {
+ ++$countOfAccountsWithAccess;
+ }
+
+ try {
+ $yandexClient->testConfiguration($id);
+ } catch (\Exception $e) {
+ $account['hasError'] = $e->getMessage();
+ continue;
+ }
+
+ $account['urls'] = $yandexClient->getAvailableUrls($id, false);
+
+ if ($account['hasAccess']) {
+ foreach ($yandexClient->getAvailableUrls($id) as $url => $hostData) {
+ $urlOptions[$id . '##' . $hostData['host_id']] = $url . ' (' . $account['name'] . ')';
+ }
+ }
+ }
+
+ $clientConfig = $yandexClient->getClientConfig();
+ $viewVariables = [];
+ $viewVariables['isConfigured'] = $yandexClient->isConfigured();
+ $viewVariables['auth_nonce'] = Nonce::getNonce('SEKP.yandex.auth');
+ $viewVariables['clientId'] = isset($clientConfig['id']) ? $clientConfig['id'] : '';
+ $viewVariables['clientSecret'] = preg_replace('/\w/', '*', isset($clientConfig['secret']) ? $clientConfig['secret'] : '');
+ $viewVariables['isClientConfigured'] = $clientConfigured;
+ $viewVariables['isOAuthConfigured'] = count($accounts) > 0;
+ $viewVariables['accounts'] = $accounts;
+ $viewVariables['urlOptions'] = $urlOptions;
+ $viewVariables['hasOAuthError'] = $hasOAuthError;
+ $viewVariables['configuredMeasurables'] = ProviderYandex::getInstance()->getConfiguredSiteIds();
+ $viewVariables['nonce'] = Nonce::getNonce('SEKP.yandex.config');
+ $viewVariables['addYandexSiteConfigNonce'] = Nonce::getNonce(self::YANDEX_ADD_SITE_CONFIG_NONCE_KEY);
+ $viewVariables['removeYandexSiteConfigNonce'] = Nonce::getNonce(self::YANDEX_REMOVE_SITE_CONFIG_NONCE_KEY);
+ $viewVariables['removeYandexAccountNonce'] = Nonce::getNonce(self::YANDEX_REMOVE_ACCOUNT_NONCE_KEY);
+ $viewVariables['sitesInfos'] = [];
+ $viewVariables['currentSite'] = $this->getCurrentSite();
+ $viewVariables['currentSite'] = $this->getCurrentSite();
+ $viewVariables['countOfAccountsWithAccess'] = $countOfAccountsWithAccess;
+
+ $siteIds = $viewVariables['configuredMeasurables'];
+
+ foreach ($siteIds as $siteId => $config) {
+ $viewVariables['sitesInfos'][$siteId] = Site::getSite($siteId);
+ $lastRun = Option::get('YandexImporterTask_LastRun_' . $siteId);
+
+ if ($lastRun) {
+ $lastRun = date('Y-m-d H:i', $lastRun) . ' UTC';
+ } else {
+ $lastRun = Piwik::translate('General_Never');
+ }
+
+ $viewVariables['sitesInfos'][$siteId]['lastRun'] = $lastRun;
+
+ $yandexAccountAndHostId = $config['yandexAccountAndHostId'];
+ [$accountId, $url] = explode('##', $yandexAccountAndHostId);
+
+ try {
+ $viewVariables['sitesInfos'][$siteId]['accountValid'] = $yandexClient->testConfiguration($accountId);
+ } catch (\Exception $e) {
+ $viewVariables['sitesInfos'][$siteId]['accountValid'] = false;
+ }
+
+ try {
+ $urls = $yandexClient->getAvailableUrls($accountId);
+ } catch (\Exception $e) {
+ $urls = [];
+ }
+
+ $viewVariables['sitesInfos'][$siteId]['urlValid'] = false;
+
+ foreach ($urls as $data) {
+ if ($data['host_id'] == $url) {
+ $viewVariables['sitesInfos'][$siteId]['urlValid'] = true;
+ }
+ }
+ }
+
+ if (!empty($this->securityPolicy)) {
+ $this->securityPolicy->addPolicy('img-src', 'avatars.yandex.net');
+ }
+
+ $viewVariables['baseUrl'] = Url::getCurrentUrlWithoutQueryString();
+ $viewVariables['baseDomain'] = Url::getCurrentScheme() . '://' . Url::getCurrentHost();
+
+ return $this->renderTemplate('yandex\configuration', $viewVariables);
+ }
+
+ /**
+ * Save Yandex configuration if set in request
+ *
+ * @return bool|null bool on success or failure, null if not data present in request
+ */
+ protected function configureYandexClientIfProvided()
+ {
+ $clientId = Common::getRequestVar('clientid', '');
+ $clientSecret = Common::getRequestVar('clientsecret', '');
+
+ if (!empty($clientSecret) || !empty($clientId)) {
+ Nonce::checkNonce('SEKP.yandex.config', Common::getRequestVar('config_nonce'));
+
+ $clientUpdated = false;
+
+ if (!empty($clientSecret) && !empty($clientId)) {
+ $yandexClient = ProviderYandex::getInstance()->getClient();
+ $yandexClient->setClientConfig($clientId, $clientSecret);
+ $clientUpdated = true;
+ }
+
+ return $clientUpdated;
+ }
+
+ return null;
+ }
+
+ /**
+ * Save yandex configuration for a site if given in request
+ */
+ protected function addYandexSiteConfigIfProvided()
+ {
+ $yandexSiteId = Common::getRequestVar('yandexSiteId', '');
+ $yandexAccountAndHostId = Common::getRequestVar('yandexAccountAndHostId', '');
+
+ if (!empty($yandexSiteId) && !empty($yandexAccountAndHostId)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::YANDEX_ADD_SITE_CONFIG_NONCE_KEY, $request->getStringParameter('addSiteConfigNonce', ''));
+ $measurableSettings = new MeasurableSettings($yandexSiteId);
+ $measurableSettings->yandexConfigCreatedBy->setValue(Piwik::getCurrentUserLogin());
+
+ //Need to explicitly setIsWritableByCurrentUser=true, since it can be set as false when we instantiate MeasurableSettings object due to previously added by another user
+ $measurableSettings->yandexAccountAndHostId->setIsWritableByCurrentUser(true);
+
+ $measurableSettings->yandexAccountAndHostId->setValue($yandexAccountAndHostId);
+
+ $measurableSettings->save();
+
+ $notification = new Notification(
+ Piwik::translate('SearchEngineKeywordsPerformance_WebsiteSuccessfulConfigured', [
+ Site::getNameFor($yandexSiteId),
+ '',
+ ' '
+ ])
+ );
+ $notification->context = Notification::CONTEXT_SUCCESS;
+ $notification->raw = true;
+ $notification->flags = Notification::FLAG_CLEAR;
+ Notification\Manager::notify('websiteConfigured', $notification);
+ }
+ }
+
+ /**
+ * Removes a Yandex account if `remove` param is given in request
+ */
+ protected function removeYandexAccountIfProvided()
+ {
+ $remove = Common::getRequestVar('remove', '');
+
+ if (!empty($remove)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::YANDEX_REMOVE_ACCOUNT_NONCE_KEY, $request->getStringParameter('removeAccountNonce', ''));
+ ProviderYandex::getInstance()->getClient()->removeAccount($remove);
+
+ $sitesWithConfig = ProviderYandex::getInstance()->getConfiguredSiteIds();
+ foreach ($sitesWithConfig as $siteId => $siteConfig) {
+ $yandexSetting = explode('##', $siteConfig['yandexAccountAndHostId']);
+ if (!empty($yandexSetting[0]) && $yandexSetting[0] == $remove) {
+ $config = new MeasurableSettings($siteId);
+ $config->yandexAccountAndHostId->setValue('0');
+ $config->save();
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a Yandex site config if `removeConfig` param is given in request
+ */
+ protected function removeYandexSiteConfigIfProvided()
+ {
+ $removeConfig = Common::getRequestVar('removeConfig', '');
+
+ if (!empty($removeConfig)) {
+ $request = \Piwik\Request::fromRequest();
+ Nonce::checkNonce(self::YANDEX_REMOVE_SITE_CONFIG_NONCE_KEY, $request->getStringParameter('removeSiteConfigNonce', ''));
+ $measurableSettings = new MeasurableSettings($removeConfig);
+ $measurableSettings->yandexAccountAndHostId->setValue('0');
+ $measurableSettings->save();
+ }
+ }
+
+
+ public function forwardToYandexAuth()
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+
+ Nonce::checkNonce('SEKP.yandex.auth', Common::getRequestVar('auth_nonce'));
+
+ $session = $this->getSession();
+ $session->yandexauthtime = time() + 60 * 15;
+
+ Url::redirectToUrl(ProviderYandex::getInstance()->getClient()->createAuthUrl());
+ }
+
+ /**
+ * Processes an auth code given by Yandex
+ */
+ public function processYandexAuthCode()
+ {
+ Piwik::checkUserHasSomeAdminAccess();
+
+ $error = Common::getRequestVar('error', '');
+ $oauthCode = Common::getRequestVar('code', '');
+ $timeLimit = $this->getSession()->yandexauthtime;
+
+ // if the auth wasn't triggered within the allowed time frame
+ if (!$timeLimit || time() > $timeLimit) {
+ $error = true;
+ }
+
+ if ($error) {
+ return $this->configureYandex(true);
+ }
+
+ try {
+ ProviderYandex::getInstance()->getClient()->processAuthCode($oauthCode);
+ } catch (\Exception $e) {
+ return $this->configureYandex($e->getMessage());
+ }
+
+ // we need idSite in the url to display all the menus like Conversion Import after redirect
+ $siteInfo = $this->getCurrentSite();
+
+ // reload index action to prove everything is configured
+ Url::redirectToUrl(Url::getCurrentUrlWithoutQueryString() . Url::getCurrentQueryStringWithParametersModified([
+ 'action' => 'configureYandex',
+ 'idSite' => (isset($siteInfo['id']) ? $siteInfo['id'] : 0),
+ 'code' => null
+ ]));
+ }
+
+ /**
+ * Get the map of component extensions to be passed into the Vue template. This allows other plugins to provide
+ * content to display in the template. In this case this plugin will display one component, but that can be
+ * overridden by the ConnectAccounts plugin to display a somewhat different component. This is doing something
+ * similar to what we use {{ postEvent('MyPlugin.MyEventInATemplate) }} for in Twig templates.
+ *
+ * @return array Map of component extensions. Like [ 'plugin' => 'PluginName', 'component' => 'ComponentName' ]
+ * See {@link https://developer.matomo.org/guides/in-depth-vue#allowing-plugins-to-add-content-to-your-vue-components the developer documentation} for more information.
+ */
+ public static function getComponentExtensions(): array
+ {
+ $componentExtensions = [];
+ Piwik::postEvent('SearchEngineKeywordsPerformance.getGoogleConfigComponentExtensions', [
+ &$componentExtensions
+ ]);
+ return $componentExtensions;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/BingAccountDiagnostic.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/BingAccountDiagnostic.php
new file mode 100644
index 0000000..754876a
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/BingAccountDiagnostic.php
@@ -0,0 +1,74 @@
+translator = $translator;
+ }
+ public function execute()
+ {
+ $client = ProviderBing::getInstance()->getClient();
+ $accounts = $client->getAccounts();
+ if (empty($accounts)) {
+ return [];
+ // skip if no accounts configured
+ }
+ $errors = ProviderBing::getInstance()->getConfigurationProblems();
+ $resultAccounts = new DiagnosticResult(Bing::getInstance()->getName() . ' - ' . $this->translator->translate('SearchEngineKeywordsPerformance_ConfiguredAccounts'));
+ foreach ($accounts as $account) {
+ if (array_key_exists($account['apiKey'], $errors['accounts'])) {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, $this->obfuscateApiKey($account['apiKey']) . ': ' . $errors['accounts'][$account['apiKey']]);
+ } else {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_OK, $this->obfuscateApiKey($account['apiKey']) . ': ' . $this->translator->translate('SearchEngineKeywordsPerformance_BingAccountOk'));
+ }
+ $resultAccounts->addItem($item);
+ }
+ $resultMeasurables = new DiagnosticResult(Bing::getInstance()->getName() . ' - ' . $this->translator->translate('SearchEngineKeywordsPerformance_MeasurableConfig'));
+ $configuredSiteIds = ProviderBing::getInstance()->getConfiguredSiteIds();
+ foreach ($configuredSiteIds as $configuredSiteId => $config) {
+ if (array_key_exists($configuredSiteId, $errors['sites'])) {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, Site::getNameFor($configuredSiteId) . ' (' . Site::getMainUrlFor($configuredSiteId) . ')' . ': ' . $errors['sites'][$configuredSiteId]);
+ } else {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_OK, Site::getNameFor($configuredSiteId) . ' (' . Site::getMainUrlFor($configuredSiteId) . ')');
+ }
+ $resultMeasurables->addItem($item);
+ }
+ return [$resultAccounts, $resultMeasurables];
+ }
+ protected function obfuscateApiKey($apiKey)
+ {
+ return substr($apiKey, 0, 5) . '*****' . substr($apiKey, -5, 5);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/GoogleAccountDiagnostic.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/GoogleAccountDiagnostic.php
new file mode 100644
index 0000000..c7dc492
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/GoogleAccountDiagnostic.php
@@ -0,0 +1,71 @@
+translator = $translator;
+ }
+ public function execute()
+ {
+ $client = ProviderGoogle::getInstance()->getClient();
+ $accounts = $client->getAccounts();
+ if (empty($accounts)) {
+ return [];
+ // skip if no accounts configured
+ }
+ $errors = ProviderGoogle::getInstance()->getConfigurationProblems();
+ $resultAccounts = new DiagnosticResult(Google::getInstance()->getName() . ' - ' . $this->translator->translate('SearchEngineKeywordsPerformance_ConfiguredAccounts'));
+ foreach ($accounts as $id => $account) {
+ $userInfo = $client->getUserInfo($id);
+ if (array_key_exists($id, $errors['accounts'])) {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, $userInfo['name'] . ': ' . $errors['accounts'][$id]);
+ } else {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_OK, $userInfo['name'] . ': ' . $this->translator->translate('SearchEngineKeywordsPerformance_GoogleAccountOk'));
+ }
+ $resultAccounts->addItem($item);
+ }
+ $resultMeasurables = new DiagnosticResult(Google::getInstance()->getName() . ' - ' . $this->translator->translate('SearchEngineKeywordsPerformance_MeasurableConfig'));
+ $configuredSiteIds = ProviderGoogle::getInstance()->getConfiguredSiteIds();
+ foreach ($configuredSiteIds as $configuredSiteId => $config) {
+ if (array_key_exists($configuredSiteId, $errors['sites'])) {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, Site::getNameFor($configuredSiteId) . ' (' . Site::getMainUrlFor($configuredSiteId) . ')' . ': ' . $errors['sites'][$configuredSiteId]);
+ } else {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_OK, Site::getNameFor($configuredSiteId) . ' (' . Site::getMainUrlFor($configuredSiteId) . ')');
+ }
+ $resultMeasurables->addItem($item);
+ }
+ return [$resultAccounts, $resultMeasurables];
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/YandexAccountDiagnostic.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/YandexAccountDiagnostic.php
new file mode 100644
index 0000000..a2a2f60
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/YandexAccountDiagnostic.php
@@ -0,0 +1,76 @@
+translator = $translator;
+ }
+ public function execute()
+ {
+ $client = ProviderYandex::getInstance()->getClient();
+ $accounts = $client->getAccounts();
+ if (empty($accounts)) {
+ return [];
+ // skip if no accounts configured
+ }
+ $errors = ProviderYandex::getInstance()->getConfigurationProblems();
+ $resultAccounts = new DiagnosticResult(Yandex::getInstance()->getName() . ' - ' . $this->translator->translate('SearchEngineKeywordsPerformance_ConfiguredAccounts'));
+ foreach ($accounts as $id => $account) {
+ $userInfo = $client->getUserInfo($id);
+ $oauthDaysAgo = floor((time() - $account['created']) / (3600 * 24));
+ if (array_key_exists($id, $errors['accounts'])) {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, $userInfo['name'] . ': ' . $errors['accounts'][$id]);
+ } else {
+ if ($oauthDaysAgo >= 150) {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_WARNING, $userInfo['name'] . ': ' . $this->translator->translate('SearchEngineKeywordsPerformance_OAuthAccessWillTimeOutSoon', 180 - $oauthDaysAgo));
+ } else {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_OK, $userInfo['name'] . ': ' . $this->translator->translate('SearchEngineKeywordsPerformance_YandexAccountOk'));
+ }
+ }
+ $resultAccounts->addItem($item);
+ }
+ $resultMeasurables = new DiagnosticResult(Yandex::getInstance()->getName() . ' - ' . $this->translator->translate('SearchEngineKeywordsPerformance_MeasurableConfig'));
+ $configuredSiteIds = ProviderYandex::getInstance()->getConfiguredSiteIds();
+ foreach ($configuredSiteIds as $configuredSiteId => $config) {
+ if (array_key_exists($configuredSiteId, $errors['sites'])) {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_ERROR, Site::getNameFor($configuredSiteId) . ' (' . Site::getMainUrlFor($configuredSiteId) . ')' . ': ' . $errors['sites'][$configuredSiteId]);
+ } else {
+ $item = new DiagnosticResultItem(DiagnosticResult::STATUS_OK, Site::getNameFor($configuredSiteId) . ' (' . Site::getMainUrlFor($configuredSiteId) . ')');
+ }
+ $resultMeasurables->addItem($item);
+ }
+ return [$resultAccounts, $resultMeasurables];
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/InvalidClientConfigException.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/InvalidClientConfigException.php
new file mode 100644
index 0000000..8da8b3e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/InvalidClientConfigException.php
@@ -0,0 +1,21 @@
+idSite = $idSite;
+ $this->force = $force;
+ $setting = new MeasurableSettings($idSite);
+ $searchConsoleUrl = $setting->bingSiteUrl;
+ $siteConfig = $searchConsoleUrl->getValue();
+ [$this->apiKey, $this->bingSiteUrl] = explode('##', $siteConfig);
+ }
+ protected static function getRowCountToImport()
+ {
+ return Config::getInstance()->General['datatable_archiving_maximum_rows_referrers'];
+ }
+ /**
+ * Run importer for all available data
+ */
+ public function importAllAvailableData()
+ {
+ $dates = self::importAvailablePeriods($this->apiKey, $this->bingSiteUrl, $this->force);
+ if (empty($dates)) {
+ return;
+ }
+ $days = $weeks = $months = $years = [];
+ foreach ($dates as $date) {
+ $date = Date::factory($date);
+ $day = new Day($date);
+ $days[$day->toString()] = $day;
+ $week = new Week($date);
+ $weeks[$week->getRangeString()] = $week;
+ $month = new Month($date);
+ $months[$month->getRangeString()] = $month;
+ $year = new Year($date);
+ $years[$year->getRangeString()] = $year;
+ }
+ $periods = $days + $weeks + $months + $years;
+ foreach ($periods as $period) {
+ $this->completeExistingArchiveIfAny($period);
+ }
+ }
+ /**
+ * Imports available data to model storage if not already done
+ *
+ * @param string $apiKey API key to use
+ * @param string $url url, eg http://matomo.org
+ * @return array
+ */
+ public static function importAvailablePeriods($apiKey, $url, $force = \false)
+ {
+ if (self::$dataImported && !defined('PIWIK_TEST_MODE')) {
+ return [];
+ }
+ $datesImported = [];
+ $logger = StaticContainer::get(LoggerInterface::class);
+ $model = new BingModel();
+ $logger->debug("[SearchEngineKeywordsPerformance] Fetching Bing keywords for {$url}");
+ try {
+ $keywordData = StaticContainer::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Bing')->getSearchAnalyticsData($apiKey, $url);
+ foreach ($keywordData as $date => $keywords) {
+ $availableKeywords = $model->getKeywordData($url, $date);
+ $datesImported[] = $date;
+ if (!empty($availableKeywords) && !$force) {
+ continue;
+ // skip as data was already imported before
+ }
+ $dataTable = self::getKeywordsAsDataTable($keywords);
+ if ($dataTable) {
+ $keywordData = $dataTable->getSerialized(self::getRowCountToImport(), null, Metrics::NB_CLICKS);
+ $logger->debug("[SearchEngineKeywordsPerformance] Importing Bing keywords for {$url} / {$date}");
+ $model->archiveKeywordData($url, $date, $keywordData[0]);
+ }
+ }
+ } catch (InvalidCredentialsException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ } catch (UnknownAPIException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ } catch (RateLimitApiException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing keywords for ' . $url . ' ErrorCode: ' . $e->getCode() . ' ErrorMessage: ' . $e->getMessage());
+ } catch (\Exception $e) {
+ $logger->error('[SearchEngineKeywordsPerformance] Exception while importing Bing keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ }
+ $logger->debug("[SearchEngineKeywordsPerformance] Fetching Bing crawl stats for {$url}");
+ try {
+ $crawlStatsDataSets = StaticContainer::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Bing')->getCrawlStats($apiKey, $url);
+ foreach ($crawlStatsDataSets as $date => $crawlStats) {
+ $availableCrawlStats = $model->getCrawlStatsData($url, $date);
+ $datesImported[] = $date;
+ if (!empty($availableCrawlStats)) {
+ continue;
+ // skip as data was already imported before
+ }
+ $dataTable = self::getCrawlStatsAsDataTable($crawlStats);
+ if ($dataTable) {
+ $keywordData = $dataTable->getSerialized();
+ $logger->debug("[SearchEngineKeywordsPerformance] Importing Bing crawl stats for {$url} / {$date}");
+ $model->archiveCrawlStatsData($url, $date, $keywordData[0]);
+ }
+ }
+ } catch (InvalidCredentialsException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing crawl stats for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ } catch (UnknownAPIException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing crawl stats for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ } catch (RateLimitApiException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing keywords for ' . $url . ' ErrorCode: ' . $e->getCode() . ' ErrorMessage: ' . $e->getMessage());
+ } catch (\Exception $e) {
+ $logger->error('[SearchEngineKeywordsPerformance] Exception while importing Bing crawl stats for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ }
+ try {
+ $crawlErrorsDataSets = StaticContainer::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Bing')->getUrlWithCrawlIssues($apiKey, $url);
+ $logger->debug("[SearchEngineKeywordsPerformance] Importing Bing crawl issues for {$url}");
+ $dataTable = self::getCrawlErrorsAsDataTable($crawlErrorsDataSets);
+ if ($dataTable->getRowsCount()) {
+ $crawlErrorsData = $dataTable->getSerialized();
+ $model->archiveCrawlErrors($url, $crawlErrorsData[0]);
+ }
+ } catch (InvalidCredentialsException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing crawl issues for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ } catch (UnknownAPIException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing crawl issues for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ } catch (RateLimitApiException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Bing crawl issues for ' . $url . ' ErrorCode: ' . $e->getCode() . ' ErrorMessage: ' . $e->getMessage());
+ } catch (\Exception $e) {
+ $logger->error('[SearchEngineKeywordsPerformance] Exception while importing Bing crawl issues for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ }
+ $datesImported = array_unique($datesImported);
+ sort($datesImported);
+ self::$dataImported = \true;
+ return $datesImported;
+ }
+ protected static function getKeywordsAsDataTable($keywords)
+ {
+ $dataTable = new DataTable();
+ foreach ($keywords as $keywordDataSet) {
+ $rowData = [
+ DataTable\Row::COLUMNS => [
+ 'label' => $keywordDataSet['keyword'],
+ Metrics::NB_CLICKS => (int) $keywordDataSet['clicks'],
+ Metrics::NB_IMPRESSIONS => (int) $keywordDataSet['impressions'],
+ Metrics::CTR => (float) round($keywordDataSet['clicks'] / $keywordDataSet['impressions'], 2),
+ Metrics::POSITION => (float) $keywordDataSet['position']
+ ]
+ ];
+ $row = new DataTable\Row($rowData);
+ $dataTable->addRow($row);
+ }
+ return $dataTable;
+ }
+ protected static function getCrawlStatsAsDataTable($crawlStats)
+ {
+ $dataTable = new DataTable();
+ foreach ($crawlStats as $label => $pagesCount) {
+ $rowData = [DataTable\Row::COLUMNS => ['label' => $label, Metrics::NB_PAGES => (int) $pagesCount]];
+ $row = new DataTable\Row($rowData);
+ $dataTable->addRow($row);
+ }
+ return $dataTable;
+ }
+ protected static function getCrawlErrorsAsDataTable($crawlErrors)
+ {
+ $dataTable = new DataTable();
+ foreach ($crawlErrors as $crawlError) {
+ $rowData = [DataTable\Row::COLUMNS => ['label' => $crawlError['Url'], 'category' => $crawlError['Issues'], 'inLinks' => $crawlError['InLinks'], 'responseCode' => $crawlError['HttpCode']]];
+ $row = new DataTable\Row($rowData);
+ $dataTable->addRow($row);
+ }
+ return $dataTable;
+ }
+ /**
+ * Runs the Archiving for SearchEngineKeywordsPerformance plugin if an archive for the given period already exists
+ *
+ * @param \Piwik\Period $period
+ */
+ protected function completeExistingArchiveIfAny($period)
+ {
+ $parameters = new Parameters(new Site($this->idSite), $period, new Segment('', [$this->idSite]));
+ $parameters->setRequestedPlugin('SearchEngineKeywordsPerformance');
+ $parameters->onlyArchiveRequestedPlugin();
+ $result = ArchiveSelector::getArchiveIdAndVisits($parameters, $period->getDateStart()->getDateStartUTC());
+ $idArchive = $result[0][0] ?? null;
+ if (empty($idArchive)) {
+ return;
+ // ignore periods that weren't archived before
+ }
+ $archiveWriter = new ArchiveWriter($parameters);
+ $archiveWriter->idArchive = $idArchive;
+ $archiveProcessor = new ArchiveProcessor($parameters, $archiveWriter, new LogAggregator($parameters));
+ $archiveProcessor->setNumberOfVisits(1, 1);
+ $bingRecordBuilder = BingRecordBuilder::make($this->idSite);
+ if (empty($bingRecordBuilder)) {
+ return;
+ }
+ if ($period instanceof Day) {
+ $bingRecordBuilder->buildFromLogs($archiveProcessor);
+ } else {
+ $bingRecordBuilder->buildForNonDayPeriod($archiveProcessor);
+ }
+ $archiveWriter->flushSpools();
+ DataTableManager::getInstance()->deleteAll();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Google.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Google.php
new file mode 100644
index 0000000..613d72f
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Google.php
@@ -0,0 +1,361 @@
+idSite = $idSite;
+ $this->force = $force;
+ $setting = new MeasurableSettings($idSite);
+ $searchConsoleUrl = $setting->googleSearchConsoleUrl;
+ [$this->accountId, $this->searchConsoleUrl] = explode('##', $searchConsoleUrl->getValue());
+ }
+ protected static function getRowCountToImport()
+ {
+ return Config::getInstance()->General['datatable_archiving_maximum_rows_referrers'];
+ }
+ /**
+ * Triggers keyword import and plugin archiving for all dates search console has data for
+ *
+ * @param string|int|null $limitKeywordDates if integer given: limits the amount of imported dates to the last
+ * available X if string given: only imports keywords for the given
+ * string date
+ * @return void
+ */
+ public function importAllAvailableData($limitKeywordDates = null)
+ {
+ // if specific date given
+ if (is_string($limitKeywordDates) && strlen($limitKeywordDates) == 10) {
+ $availableDates = [$limitKeywordDates];
+ } else {
+ $availableDates = self::getAvailableDates($this->accountId, $this->searchConsoleUrl);
+ sort($availableDates);
+ if ($limitKeywordDates > 0) {
+ $limitKeywordDates += 5;
+ // always import 5 days more in the past, to ensure that non final data is imported again.
+ $availableDates = array_slice($availableDates, -$limitKeywordDates, $limitKeywordDates);
+ }
+ }
+ $this->importKeywordsForListOfDates($availableDates);
+ $this->completeExistingArchivesForListOfDates($availableDates);
+ }
+ protected function importKeywordsForListOfDates($datesToImport)
+ {
+ foreach ($datesToImport as $date) {
+ foreach (self::$typesToImport as $type) {
+ $this->importKeywordsIfNecessary($this->accountId, $this->searchConsoleUrl, $date, $type, $this->force);
+ }
+ }
+ }
+ protected function completeExistingArchivesForListOfDates($datesToComplete)
+ {
+ $days = $weeks = $months = $years = [];
+ sort($datesToComplete);
+ foreach ($datesToComplete as $date) {
+ $date = Date::factory($date);
+ $day = new Day($date);
+ $days[$day->toString()] = $day;
+ $week = new Week($date);
+ $weeks[$week->getRangeString()] = $week;
+ $month = new Month($date);
+ $months[$month->getRangeString()] = $month;
+ $year = new Year($date);
+ $years[$year->getRangeString()] = $year;
+ }
+ $periods = $days + $weeks + $months + $years;
+ foreach ($periods as $period) {
+ $this->completeExistingArchiveIfAny($period);
+ }
+ }
+ /**
+ * Imports keyword to model storage if not already done
+ *
+ * @param string $accountId google account id
+ * @param string $url url, eg http://matomo.org
+ * @param string $date date string, eg 2016-12-24
+ * @param string $type 'web', 'image', 'video' or 'news'
+ * @param bool $force force reimport
+ * @return boolean
+ */
+ public function importKeywordsIfNecessary($accountId, $url, $date, $type, $force = \false)
+ {
+ $model = new GoogleModel();
+ $logger = StaticContainer::get(LoggerInterface::class);
+ $keywordData = $model->getKeywordData($url, $date, $type);
+ // check if available data is temporary and force a reimport in that case
+ if ($keywordData) {
+ $dataTable = new DataTable();
+ $dataTable->addRowsFromSerializedArray($keywordData);
+ $isTemporary = $dataTable->getMetadata(self::DATATABLE_METADATA_TEMPORARY);
+ if ($isTemporary === \true) {
+ $logger->info('[SearchEngineKeywordsPerformance] Forcing reimport Google keywords for ' . $url . ' as imported data was not final.');
+ $force = \true;
+ }
+ }
+ if ($keywordData && !$force) {
+ $logger->info('[SearchEngineKeywordsPerformance] Skipping import of Google keywords for ' . $date . ' and ' . $url . ' as data already imported.');
+ return \false;
+ // skip if data already available and no reimport forced
+ }
+ $dataTable = $this->getKeywordsFromConsoleAsDataTable($accountId, $url, $date, $type);
+ if ($dataTable) {
+ $keywordData = $dataTable->getSerialized(self::getRowCountToImport(), null, Metrics::NB_CLICKS);
+ $model->archiveKeywordData($url, $date, $type, $keywordData[0]);
+ return \true;
+ }
+ return \false;
+ }
+ protected static function getAvailableDates($accountId, $url)
+ {
+ $logger = StaticContainer::get(LoggerInterface::class);
+ try {
+ if (!array_key_exists($accountId . $url, self::$availableDates) || defined('PIWIK_TEST_MODE')) {
+ $finalDates = StaticContainer::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Google')->getDatesWithSearchAnalyticsData($accountId, $url);
+ self::$availableDates[$accountId . $url] = StaticContainer::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Google')->getDatesWithSearchAnalyticsData($accountId, $url, \false);
+ self::$availableDatesNonFinal[$accountId . $url] = array_diff(self::$availableDates[$accountId . $url], $finalDates);
+ }
+ } catch (InvalidCredentialsException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return [];
+ } catch (InvalidClientConfigException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return [];
+ } catch (MissingOAuthConfigException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return [];
+ } catch (MissingClientConfigException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return [];
+ } catch (UnknownAPIException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return [];
+ } catch (\Exception $e) {
+ $logger->error('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return [];
+ }
+ if (array_key_exists($accountId . $url, self::$availableDates)) {
+ return self::$availableDates[$accountId . $url];
+ }
+ return [];
+ }
+ private static function isFinalDate($accountId, $url, $date)
+ {
+ if (array_key_exists($accountId . $url, self::$availableDatesNonFinal)) {
+ return !in_array($date, self::$availableDatesNonFinal[$accountId . $url]);
+ }
+ return \true;
+ }
+ /**
+ * Fetches data from google search console and migrates it to a Matomo Datatable
+ *
+ * @param string $accountId google account id
+ * @param string $url url, eg http://matomo.org
+ * @param string $date date string, eg 2016-12-24
+ * @param string $type 'web', 'image', 'video' or 'news'
+ * @return null|DataTable
+ */
+ protected function getKeywordsFromConsoleAsDataTable($accountId, $url, $date, $type)
+ {
+ $dataTable = new DataTable();
+ $logger = StaticContainer::get(LoggerInterface::class);
+ try {
+ if (!defined('PIWIK_TEST_MODE') && !$this->isImportAllowedForDate($date)) {
+ $logger->debug("[SearchEngineKeywordsPerformance] Skip fetching keywords from Search Console for today and dates more than 500 days in the past: " . $date);
+ return null;
+ }
+ $availableDates = self::getAvailableDates($accountId, $url);
+ if (!in_array($date, $availableDates)) {
+ $logger->debug("[SearchEngineKeywordsPerformance] No {$type} keywords available for {$date} and {$url}");
+ return null;
+ }
+ $logger->debug("[SearchEngineKeywordsPerformance] Fetching {$type} keywords for {$date} and {$url}");
+ $keywordData = StaticContainer::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Google')->getSearchAnalyticsData($accountId, $url, $date, $type, self::getRowCountToImport());
+ } catch (InvalidCredentialsException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return null;
+ } catch (InvalidClientConfigException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return null;
+ } catch (MissingOAuthConfigException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return null;
+ } catch (MissingClientConfigException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return null;
+ } catch (UnknownAPIException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return null;
+ } catch (\Exception $e) {
+ $logger->error('[SearchEngineKeywordsPerformance] Exception while importing Google keywords for ' . $url . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ return null;
+ }
+ if (!self::isFinalDate($accountId, $url, $date)) {
+ $dataTable->setMetadata(self::DATATABLE_METADATA_TEMPORARY, \true);
+ }
+ if (empty($keywordData) || !($rows = $keywordData->getRows())) {
+ return $dataTable;
+ // return empty table so it will be stored
+ }
+ foreach ($rows as $keywordDataSet) {
+ /** @var \Google\Service\SearchConsole\ApiDataRow $keywordDataSet */
+ $keys = $keywordDataSet->getKeys();
+ $rowData = [
+ DataTable\Row::COLUMNS => [
+ 'label' => reset($keys),
+ Metrics::NB_CLICKS => (int) $keywordDataSet->getClicks(),
+ Metrics::NB_IMPRESSIONS => (int) $keywordDataSet->getImpressions(),
+ Metrics::CTR => (float) $keywordDataSet->getCtr(),
+ Metrics::POSITION => (float) $keywordDataSet->getPosition()
+ ]
+ ];
+ $row = new DataTable\Row($rowData);
+ $dataTable->addRow($row);
+ }
+ unset($keywordData);
+ return $dataTable;
+ }
+ protected function isImportAllowedForDate($date): bool
+ {
+ $site = new Site($this->idSite);
+ $siteCreationDate = $site->getCreationDate()->subDay(30);
+ $earliestDate = Date::now()->subDay(500);
+ $earliestImportDate = $siteCreationDate->isEarlier($earliestDate) ? $earliestDate : $siteCreationDate;
+ $archivedDate = Date::factory($date);
+ if ($archivedDate->isEarlier($earliestImportDate) || $archivedDate->isToday()) {
+ return \false;
+ }
+ return \true;
+ }
+ /**
+ * Runs the Archiving for SearchEngineKeywordsPerformance plugin if an archive for the given period already exists
+ *
+ * @param \Piwik\Period $period
+ */
+ protected function completeExistingArchiveIfAny($period)
+ {
+ $parameters = new Parameters(new Site($this->idSite), $period, new Segment('', [$this->idSite]));
+ $parameters->setRequestedPlugin('SearchEngineKeywordsPerformance');
+ $parameters->onlyArchiveRequestedPlugin();
+ $result = ArchiveSelector::getArchiveIdAndVisits($parameters, $period->getDateStart()->getDateStartUTC());
+ $idArchive = $result[0][0] ?? null;
+ if (empty($idArchive)) {
+ return;
+ // ignore periods that weren't archived before
+ }
+ $archiveWriter = new ArchiveWriter($parameters);
+ $archiveWriter->idArchive = $idArchive;
+ $archiveProcessor = new ArchiveProcessor($parameters, $archiveWriter, new LogAggregator($parameters));
+ $archiveProcessor->setNumberOfVisits(1, 1);
+ /** @var GoogleRecordBuilder[] $recordBuilders */
+ $recordBuilders = GoogleRecordBuilder::makeAll($this->idSite);
+ if (empty($recordBuilders)) {
+ return;
+ }
+ foreach ($recordBuilders as $builder) {
+ if ($period instanceof Day) {
+ $builder->buildFromLogs($archiveProcessor);
+ } else {
+ $builder->buildForNonDayPeriod($archiveProcessor);
+ }
+ }
+ $archiveWriter->flushSpools();
+ DataTableManager::getInstance()->deleteAll();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Yandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Yandex.php
new file mode 100644
index 0000000..b2ee231
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Yandex.php
@@ -0,0 +1,292 @@
+idSite = $idSite;
+ $this->force = $force;
+ $setting = new MeasurableSettings($idSite);
+ $yandexConfig = $setting->yandexAccountAndHostId;
+ $siteConfig = $yandexConfig->getValue();
+ [$this->accountId, $this->yandexHostId] = explode('##', $siteConfig);
+ }
+ protected static function getRowCountToImport()
+ {
+ return Config::getInstance()->General['datatable_archiving_maximum_rows_referrers'];
+ }
+ /**
+ * Run importer for all available data
+ */
+ public function importAllAvailableData($limitDays = 100)
+ {
+ if (is_string($limitDays) && strlen($limitDays) == 10) {
+ $dates = [$limitDays];
+ } else {
+ for ($i = 0; $i <= $limitDays; $i++) {
+ $dates[] = date('Y-m-d', strtotime("-{$i} days"));
+ }
+ }
+ foreach ($dates as $date) {
+ self::importAvailableDataForDate($this->accountId, $this->yandexHostId, $date, $this->force);
+ }
+ if (empty($dates)) {
+ return;
+ }
+ $this->completeExistingArchivesForListOfDates($dates);
+ }
+ protected function completeExistingArchivesForListOfDates($datesToComplete)
+ {
+ $days = $weeks = $months = $years = [];
+ sort($datesToComplete);
+ foreach ($datesToComplete as $date) {
+ $date = Date::factory($date);
+ $day = new Day($date);
+ $days[$day->toString()] = $day;
+ $week = new Week($date);
+ $weeks[$week->getRangeString()] = $week;
+ $month = new Month($date);
+ $months[$month->getRangeString()] = $month;
+ $year = new Year($date);
+ $years[$year->getRangeString()] = $year;
+ }
+ $periods = $days + $weeks + $months + $years;
+ foreach ($periods as $period) {
+ $this->completeExistingArchiveIfAny($period);
+ }
+ }
+ /**
+ * Imports available data to model storage if not already done
+ *
+ * @param string $accountId Id oc account to use
+ * @param string $hostId url, eg https:piwik.org:443
+ * @param string $date date, eg 2019-05-20
+ * @return array
+ */
+ public static function importAvailableDataForDate($accountId, $hostId, $date, $force = \false)
+ {
+ $datesImported = [];
+ $timestamp = strtotime($date);
+ if ($timestamp > time()) {
+ return [];
+ // no import for dates in the future
+ }
+ $logger = StaticContainer::get(LoggerInterface::class);
+ if ($timestamp > time() - self::MAX_DAYS_KEYWORD_DATA_DELAY * 24 * 3600) {
+ $force = \true;
+ // always reimport the last few days
+ }
+ $model = new YandexModel();
+ try {
+ $availableKeywordsDataTable = new DataTable();
+ $availableKeywords = $model->getKeywordData($hostId, $date);
+ if (!empty($availableKeywords)) {
+ $availableKeywordsDataTable->addRowsFromSerializedArray($availableKeywords);
+ }
+ // Only assume keywords were imported if there are actually some rows available, otherwise try to import them (again)
+ if ($availableKeywordsDataTable->getRowsCountWithoutSummaryRow() > 0 && !$force) {
+ $logger->debug("[SearchEngineKeywordsPerformance] Yandex keywords already imported for {$hostId} and date {$date}");
+ } else {
+ $logger->debug("[SearchEngineKeywordsPerformance] Fetching Yandex keywords for {$hostId} and date {$date}");
+ $keywords = StaticContainer::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Yandex')->getSearchAnalyticsData($accountId, $hostId, $date);
+ $datesImported[] = $date;
+ $dataTable = self::getKeywordsAsDataTable($keywords);
+ // do not store empty results for the last days
+ if ($dataTable && ($dataTable->getRowsCountWithoutSummaryRow() > 0 || $timestamp < time() - self::MAX_DAYS_KEYWORD_DATA_DELAY * 24 * 3600)) {
+ $keywordData = $dataTable->getSerialized(self::getRowCountToImport(), null, Metrics::NB_CLICKS);
+ $logger->debug("[SearchEngineKeywordsPerformance] Importing Yandex keywords for {$hostId} / {$date}");
+ $model->archiveKeywordData($hostId, $date, $keywordData[0]);
+ }
+ }
+ } catch (InvalidCredentialsException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Yandex keywords for ' . $hostId . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ } catch (RateLimitApiException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Yandex keywords for ' . $hostId . ': ' . $e->getMessage());
+ } catch (\Exception $e) {
+ $logger->error('[SearchEngineKeywordsPerformance] Exception while importing Yandex keywords for ' . $hostId . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ }
+ try {
+ $availableCrawlStats = $model->getCrawlStatsData($hostId, $date);
+ if (!empty($availableCrawlStats) && !$force) {
+ $logger->debug("[SearchEngineKeywordsPerformance] Yandex crawl stats already imported for {$hostId} and date {$date}");
+ } else {
+ $logger->debug("[SearchEngineKeywordsPerformance] Fetching Yandex crawl stats for {$hostId} and date {$date}");
+ $crawlStats = StaticContainer::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Yandex')->getCrawlStats($accountId, $hostId, $date);
+ $datesImported[] = $date;
+ $dataTable = self::getCrawlStatsAsDataTable($crawlStats);
+ if ($dataTable) {
+ $keywordData = $dataTable->getSerialized();
+ $logger->debug("[SearchEngineKeywordsPerformance] Importing Yandex crawl stats for {$hostId} and date {$date}");
+ $model->archiveCrawlStatsData($hostId, $date, $keywordData[0]);
+ }
+ }
+ } catch (InvalidCredentialsException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Yandex crawl stats for ' . $hostId . ': ' . $e->getMessage());
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ } catch (RateLimitApiException $e) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Yandex crawl stats for ' . $hostId . ': ' . $e->getMessage());
+ } catch (\Exception $e) {
+ // ignore empty server reply as they seem temporary only
+ if (strpos($e->getMessage(), 'Empty reply from server')) {
+ $logger->info('[SearchEngineKeywordsPerformance] Exception while importing Yandex crawl stats for ' . $hostId . ': ' . $e->getMessage());
+ } else {
+ $logger->error('[SearchEngineKeywordsPerformance] Exception while importing Yandex crawl stats for ' . $hostId . ': ' . $e->getMessage());
+ }
+ Provider::getInstance()->recordNewApiErrorForProvider();
+ }
+ $datesImported = array_unique($datesImported);
+ sort($datesImported);
+ return $datesImported;
+ }
+ protected static function getKeywordsAsDataTable($keywords)
+ {
+ $dataTable = new DataTable();
+ if (empty($keywords)) {
+ return $dataTable;
+ }
+ foreach ($keywords as $keywordDataSet) {
+ // If the keyword is empty, that will cause an error if we try to add the row. Skip and move on to the next.
+ if (empty($keywordDataSet['keyword'])) {
+ continue;
+ }
+ $rowData = [
+ DataTable\Row::COLUMNS => [
+ 'label' => $keywordDataSet['keyword'],
+ Metrics::NB_CLICKS => (int) $keywordDataSet['clicks'],
+ Metrics::NB_IMPRESSIONS => (int) $keywordDataSet['impressions'],
+ Metrics::CTR => (float) round($keywordDataSet['clicks'] / $keywordDataSet['impressions'], 2),
+ Metrics::POSITION => (float) $keywordDataSet['position']
+ ]
+ ];
+ $row = new DataTable\Row($rowData);
+ $dataTable->addRow($row);
+ }
+ return $dataTable;
+ }
+ protected static function getCrawlStatsAsDataTable($crawlStats)
+ {
+ $dataTable = new DataTable();
+ if (empty($crawlStats) || !is_array($crawlStats)) {
+ return $dataTable;
+ }
+ foreach ($crawlStats as $label => $pagesCount) {
+ if (empty($label)) {
+ continue;
+ }
+ $rowData = [DataTable\Row::COLUMNS => ['label' => $label, Metrics::NB_PAGES => (int) $pagesCount]];
+ $row = new DataTable\Row($rowData);
+ $dataTable->addRow($row);
+ }
+ return $dataTable;
+ }
+ /**
+ * Runs the Archiving for SearchEngineKeywordsPerformance plugin if an archive for the given period already exists
+ *
+ * @param \Piwik\Period $period
+ */
+ protected function completeExistingArchiveIfAny($period)
+ {
+ $parameters = new Parameters(new Site($this->idSite), $period, new Segment('', [$this->idSite]));
+ $parameters->setRequestedPlugin('SearchEngineKeywordsPerformance');
+ $parameters->onlyArchiveRequestedPlugin();
+ $result = ArchiveSelector::getArchiveIdAndVisits($parameters, $period->getDateStart()->getDateStartUTC());
+ $idArchive = $result[0][0] ?? null;
+ if (empty($idArchive)) {
+ return;
+ // ignore periods that weren't archived before
+ }
+ $archiveWriter = new ArchiveWriter($parameters);
+ $archiveWriter->idArchive = $idArchive;
+ $archiveProcessor = new ArchiveProcessor($parameters, $archiveWriter, new LogAggregator($parameters));
+ $archiveProcessor->setNumberOfVisits(1, 1);
+ $builder = YandexRecordBuilder::make($this->idSite);
+ if (empty($builder)) {
+ return;
+ }
+ if ($period instanceof Day) {
+ $builder->buildFromLogs($archiveProcessor);
+ } else {
+ $builder->buildForNonDayPeriod($archiveProcessor);
+ }
+ $archiveWriter->flushSpools();
+ DataTableManager::getInstance()->deleteAll();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/LICENSE b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/LICENSE
new file mode 100644
index 0000000..4686f35
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/LICENSE
@@ -0,0 +1,49 @@
+InnoCraft License
+
+This InnoCraft End User License Agreement (the "InnoCraft EULA") is between you and InnoCraft Ltd (NZBN 6106769) ("InnoCraft"). If you are agreeing to this Agreement not as an individual but on behalf of your company, then "Customer" or "you" means your company, and you are binding your company to this Agreement. InnoCraft may modify this Agreement from time to time, subject to the terms in Section (xii) below.
+
+By clicking on the "I’ve read and accept the terms & conditions (https://shop.matomo.org/terms-conditions/)" (or similar button) that is presented to you at the time of your Order, or by using or accessing InnoCraft products, you indicate your assent to be bound by this Agreement.
+
+
+InnoCraft EULA
+
+(i) InnoCraft is the licensor of the Plugin for Matomo Analytics (the "Software").
+
+(ii) Subject to the terms and conditions of this Agreement, InnoCraft grants you a limited, worldwide, non-exclusive, non-transferable and non-sublicensable license to install and use the Software only on hardware systems owned, leased or controlled by you, during the applicable License Term. The term of each Software license ("License Term") will be specified in your Order. Your License Term will end upon any breach of this Agreement.
+
+(iii) Unless otherwise specified in your Order, for each Software license that you purchase, you may install one production instance of the Software in a Matomo Analytics instance owned or operated by you, and accessible via one URL ("Matomo instance"). Additional licenses must be purchased in order to deploy the Software in multiple Matomo instances, including when these multiple Matomo instances are hosted on a single hardware system.
+
+(iv) Licenses granted by InnoCraft are granted subject to the condition that you must ensure the maximum number of Authorized Users and Authorized Sites that are able to access and use the Software is equal to the number of User and Site Licenses for which the necessary fees have been paid to InnoCraft for the Subscription period. You may upgrade your license at any time on payment of the appropriate fees to InnoCraft in order to increase the maximum number of authorized users or sites. The number of User and Site Licenses granted to you is dependent on the fees paid by you. “User License” means a license granted under this EULA to you to permit an Authorized User to use the Software. “Authorized User” means a person who has an account in the Matomo instance and for which the necessary fees (“Subscription fees”) have been paid to InnoCraft for the current license term. "Site License" means a license granted under this EULA to you to permit an Authorized Site to use the Matomo Marketplace Plugin. “Authorized Sites” means a website or a measurable within Matomo instance and for which the necessary fees (“Subscription fees”) have been paid to InnoCraft for the current license term. These restrictions also apply if you install the Matomo Analytics Platform as part of your WordPress.
+
+(v) Piwik Analytics was renamed to Matomo Analytics in January 2018. The same terms and conditions as well as any restrictions or grants apply if you are using any version of Piwik.
+
+(vi) The Software requires a license key in order to operate, which will be delivered to the email addresses specified in your Order when we have received payment of the applicable fees.
+
+(vii) Any information that InnoCraft may collect from you or your device will be subject to InnoCraft Privacy Policy (https://www.innocraft.com/privacy).
+
+(viii) You are bound by the Matomo Marketplace Terms and Conditions (https://shop.matomo.org/terms-conditions/).
+
+(ix) You may not reverse engineer or disassemble or re-distribute the Software in whole or in part, or create any derivative works from or sublicense any rights in the Software, unless otherwise expressly authorized in writing by InnoCraft.
+
+(x) The Software is protected by copyright and other intellectual property laws and treaties. InnoCraft own all title, copyright and other intellectual property rights in the Software, and the Software is licensed to you directly by InnoCraft, not sold.
+
+(xi) The Software is provided under an "as is" basis and without any support or maintenance. Nothing in this Agreement shall require InnoCraft to provide you with support or fixes to any bug, failure, mis-performance or other defect in The Software. InnoCraft may provide you, from time to time, according to his sole discretion, with updates of the Software. You hereby warrant to keep the Software up-to-date and install all relevant updates. InnoCraft shall provide any update free of charge.
+
+(xii) The Software is provided "as is", and InnoCraft hereby disclaim all warranties, including but not limited to any implied warranties of title, non-infringement, merchantability or fitness for a particular purpose. InnoCraft shall not be liable or responsible in any way for any losses or damage of any kind, including lost profits or other indirect or consequential damages, relating to your use of or reliance upon the Software.
+
+(xiii) We may update or modify this Agreement from time to time, including the referenced Privacy Policy and the Matomo Marketplace Terms and Conditions. If a revision meaningfully reduces your rights, we will use reasonable efforts to notify you (by, for example, sending an email to the billing or technical contact you designate in the applicable Order). If we modify the Agreement during your License Term or Subscription Term, the modified version will be effective upon your next renewal of a License Term.
+
+
+About InnoCraft Ltd
+
+At InnoCraft Ltd, we create innovating quality products to grow your business and to maximize your success.
+
+Our software products are built on top of Matomo Analytics: the leading open digital analytics platform used by more than one million websites worldwide. We are the creators and makers of the Matomo Analytics platform.
+
+
+Contact
+
+Email: contact@innocraft.com
+Contact form: https://www.innocraft.com/#contact
+Website: https://www.innocraft.com/
+Buy our products: Premium Features for Matomo Analytics https://plugins.matomo.org/premium
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/MeasurableSettings.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/MeasurableSettings.php
new file mode 100644
index 0000000..2172fc8
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/MeasurableSettings.php
@@ -0,0 +1,182 @@
+configureGoogleSettings();
+ $this->configureBingSettings();
+ $this->configureYandexSettings();
+ }
+ /**
+ * Configures Settings used for Google Search Console Import
+ */
+ protected function configureGoogleSettings()
+ {
+ $googleClient = ProviderGoogle::getInstance()->getClient();
+ // check if google search console is configured and available for website type
+ if (!\Piwik\Plugins\SearchEngineKeywordsPerformance\SearchEngineKeywordsPerformance::isGoogleForceEnabled($this->idSite) && (!$this->hasMeasurableType(WebsiteMeasurableType::ID) || !$googleClient->isConfigured())) {
+ return;
+ }
+ $this->googleConfigCreatedBy = $this->makeSetting('googleconfigcreatedby', '', FieldConfig::TYPE_STRING, function (FieldConfig $field) {
+ $field->uiControl = FieldConfig::UI_CONTROL_HIDDEN;
+ });
+ $this->googleSearchConsoleUrl = $this->makeSetting('searchconsoleurl', '0', FieldConfig::TYPE_STRING, function (FieldConfig $field) use ($googleClient) {
+ $field->title = Piwik::translate('SearchEngineKeywordsPerformance_GoogleSearchConsoleUrl');
+ $field->description = Piwik::translate('SearchEngineKeywordsPerformance_GoogleSearchConsoleUrlDescription');
+ $field->uiControl = FieldConfig::UI_CONTROL_SINGLE_SELECT;
+ $field->availableValues = ['0' => Piwik::translate('SearchEngineKeywordsPerformance_NotAvailable')];
+ foreach ($googleClient->getAccounts() as $id => $account) {
+ if (Piwik::hasUserSuperUserAccessOrIsTheUser($account['username'])) {
+ $availableSites = $googleClient->getAvailableUrls($id);
+ foreach ($availableSites as $url => $accessLevel) {
+ $value = $id . '##' . $url;
+ $field->availableValues[$value] = $url;
+ }
+ }
+ }
+ });
+ $this->googleWebKeywords = $this->makeSetting('googlewebkeywords', \true, FieldConfig::TYPE_BOOL, function (FieldConfig $field) {
+ $field->title = Piwik::translate('SearchEngineKeywordsPerformance_FetchWebKeyword');
+ $field->description = Piwik::translate('SearchEngineKeywordsPerformance_FetchWebKeywordDesc');
+ $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX;
+ $field->condition = 'searchconsoleurl';
+ });
+ $this->googleImageKeywords = $this->makeSetting('googleimagekeywords', \false, FieldConfig::TYPE_BOOL, function (FieldConfig $field) {
+ $field->title = Piwik::translate('SearchEngineKeywordsPerformance_FetchImageKeyword');
+ $field->description = Piwik::translate('SearchEngineKeywordsPerformance_FetchImageKeywordDesc');
+ $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX;
+ $field->condition = 'searchconsoleurl && searchconsoleurl.indexOf(\'android-app\') == -1';
+ });
+ $this->googleVideoKeywords = $this->makeSetting('googlevideokeywords', \false, FieldConfig::TYPE_BOOL, function (FieldConfig $field) {
+ $field->title = Piwik::translate('SearchEngineKeywordsPerformance_FetchVideoKeyword');
+ $field->description = Piwik::translate('SearchEngineKeywordsPerformance_FetchVideoKeywordDesc');
+ $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX;
+ $field->condition = 'searchconsoleurl && searchconsoleurl.indexOf(\'android-app\') == -1';
+ });
+ $this->googleNewsKeywords = $this->makeSetting('googlenewskeywords', \false, FieldConfig::TYPE_BOOL, function (FieldConfig $field) {
+ $field->title = Piwik::translate('SearchEngineKeywordsPerformance_FetchNewsKeyword');
+ $field->description = Piwik::translate('SearchEngineKeywordsPerformance_FetchNewsKeywordDesc');
+ $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX;
+ $field->condition = 'searchconsoleurl && searchconsoleurl.indexOf(\'android-app\') == -1';
+ });
+ $createdByUser = $this->googleConfigCreatedBy->getValue();
+ if (!empty($createdByUser) && !Piwik::hasUserSuperUserAccessOrIsTheUser($createdByUser)) {
+ $this->googleSearchConsoleUrl->setIsWritableByCurrentUser(\false);
+ $this->googleWebKeywords->setIsWritableByCurrentUser(\false);
+ $this->googleImageKeywords->setIsWritableByCurrentUser(\false);
+ $this->googleVideoKeywords->setIsWritableByCurrentUser(\false);
+ $this->googleNewsKeywords->setIsWritableByCurrentUser(\false);
+ }
+ }
+ /**
+ * Configures Settings used for Bing Webmaster API Import
+ */
+ protected function configureBingSettings()
+ {
+ $bingClient = ProviderBing::getInstance()->getClient();
+ // check if Bing Webmaster API is configured and available for website type
+ if (!\Piwik\Plugins\SearchEngineKeywordsPerformance\SearchEngineKeywordsPerformance::isBingForceEnabled($this->idSite) && (!$this->hasMeasurableType(WebsiteMeasurableType::ID) || !$bingClient->isConfigured())) {
+ return;
+ }
+ $this->bingConfigCreatedBy = $this->makeSetting('bingconfigcreatedby', '', FieldConfig::TYPE_STRING, function (FieldConfig $field) {
+ $field->uiControl = FieldConfig::UI_CONTROL_HIDDEN;
+ });
+ $this->bingSiteUrl = $this->makeSetting('bingsiteurl', '0', FieldConfig::TYPE_STRING, function (FieldConfig $field) use ($bingClient) {
+ $field->title = Piwik::translate('SearchEngineKeywordsPerformance_BingWebmasterApiUrl');
+ $field->description = Piwik::translate('SearchEngineKeywordsPerformance_BingWebmasterApiUrlDescription');
+ $field->uiControl = FieldConfig::UI_CONTROL_SINGLE_SELECT;
+ $field->availableValues = ['0' => Piwik::translate('SearchEngineKeywordsPerformance_NotAvailable')];
+ foreach ($bingClient->getAccounts() as $account) {
+ if (Piwik::hasUserSuperUserAccessOrIsTheUser($account['username'])) {
+ $availableSites = $bingClient->getAvailableUrls($account['apiKey']);
+ foreach ($availableSites as $url => $isVerified) {
+ $value = $account['apiKey'] . '##' . $url;
+ $field->availableValues[$value] = $url;
+ }
+ }
+ }
+ });
+ $createdByUser = $this->bingConfigCreatedBy->getValue();
+ if (!empty($createdByUser) && !Piwik::hasUserSuperUserAccessOrIsTheUser($createdByUser)) {
+ $this->bingSiteUrl->setIsWritableByCurrentUser(\false);
+ }
+ }
+ /**
+ * Configures Settings used for Yandex Webmaster API Import
+ */
+ protected function configureYandexSettings()
+ {
+ $yandexClient = ProviderYandex::getInstance()->getClient();
+ // check if Yandex Webmaster API is configured and available for website type
+ if (!$this->hasMeasurableType(WebsiteMeasurableType::ID) || !$yandexClient->isConfigured()) {
+ return;
+ }
+ $this->yandexConfigCreatedBy = $this->makeSetting('yandexconfigcreatedby', '', FieldConfig::TYPE_STRING, function (FieldConfig $field) {
+ $field->uiControl = FieldConfig::UI_CONTROL_HIDDEN;
+ });
+ $this->yandexAccountAndHostId = $this->makeSetting('yandexAccountAndHostId', '0', FieldConfig::TYPE_STRING, function (FieldConfig $field) use ($yandexClient) {
+ $field->title = Piwik::translate('SearchEngineKeywordsPerformance_YandexWebmasterApiUrl');
+ $field->description = Piwik::translate('SearchEngineKeywordsPerformance_YandexWebmasterApiUrlDescription');
+ $field->uiControl = FieldConfig::UI_CONTROL_SINGLE_SELECT;
+ $field->availableValues = ['0' => Piwik::translate('SearchEngineKeywordsPerformance_NotAvailable')];
+ foreach ($yandexClient->getAccounts() as $id => $account) {
+ if (Piwik::hasUserSuperUserAccessOrIsTheUser($account['username'])) {
+ $availableSites = $yandexClient->getAvailableUrls($id);
+ foreach ($availableSites as $url => $hostData) {
+ $value = $id . '##' . $hostData['host_id'];
+ $field->availableValues[$value] = $url;
+ }
+ }
+ }
+ });
+ $createdByUser = $this->yandexConfigCreatedBy->getValue();
+ if (!empty($createdByUser) && !Piwik::hasUserSuperUserAccessOrIsTheUser($createdByUser)) {
+ $this->yandexAccountAndHostId->setIsWritableByCurrentUser(\false);
+ }
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Menu.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Menu.php
new file mode 100644
index 0000000..d71cd44
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Menu.php
@@ -0,0 +1,30 @@
+addSystemItem('SearchEngineKeywordsPerformance_AdminMenuTitle', $this->urlForAction('index'), $order = 50);
+ }
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Metrics.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Metrics.php
new file mode 100644
index 0000000..f9fbd33
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Metrics.php
@@ -0,0 +1,144 @@
+ Piwik::translate('SearchEngineKeywordsPerformance_Clicks'),
+ self::NB_IMPRESSIONS => Piwik::translate('SearchEngineKeywordsPerformance_Impressions'),
+ self::CTR => Piwik::translate('SearchEngineKeywordsPerformance_Ctr'),
+ self::POSITION => Piwik::translate('SearchEngineKeywordsPerformance_Position')
+ ];
+ }
+ /**
+ * Returns metric semantic types for this plugin's metrics.
+ *
+ * @return array
+ */
+ public static function getMetricSemanticTypes(): array
+ {
+ return [self::NB_CLICKS => Dimension::TYPE_NUMBER, self::NB_IMPRESSIONS => Dimension::TYPE_NUMBER, self::CTR => Dimension::TYPE_NUMBER, self::POSITION => Dimension::TYPE_NUMBER];
+ }
+ /**
+ * Return metric documentations
+ *
+ * @return array
+ */
+ public static function getMetricsDocumentation()
+ {
+ return [
+ self::NB_CLICKS => Piwik::translate('SearchEngineKeywordsPerformance_ClicksDocumentation'),
+ self::NB_IMPRESSIONS => Piwik::translate('SearchEngineKeywordsPerformance_ImpressionsDocumentation'),
+ self::CTR => Piwik::translate('SearchEngineKeywordsPerformance_CtrDocumentation'),
+ self::POSITION => Piwik::translate('SearchEngineKeywordsPerformance_PositionDocumentation'),
+ Bing::CRAWLSTATS_OTHER_CODES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlStatsOtherCodesDesc'),
+ Bing::CRAWLSTATS_BLOCKED_ROBOTS_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlBlockedByRobotsTxtDesc'),
+ Bing::CRAWLSTATS_CODE_2XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus2xxDesc'),
+ Bing::CRAWLSTATS_CODE_301_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus301Desc'),
+ Bing::CRAWLSTATS_CODE_302_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus302Desc'),
+ Bing::CRAWLSTATS_CODE_4XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus4xxDesc'),
+ Bing::CRAWLSTATS_CODE_5XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus5xxDesc'),
+ Bing::CRAWLSTATS_TIMEOUT_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlConnectionTimeoutDesc'),
+ Bing::CRAWLSTATS_MALWARE_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlMalwareInfectedDesc'),
+ Bing::CRAWLSTATS_ERRORS_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlErrorsDesc'),
+ Bing::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlCrawledPagesDesc'),
+ Bing::CRAWLSTATS_DNS_FAILURE_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlDNSFailuresDesc'),
+ Bing::CRAWLSTATS_IN_INDEX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlPagesInIndexDesc'),
+ Bing::CRAWLSTATS_IN_LINKS_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlInboundLinkDesc'),
+ Yandex::CRAWLSTATS_IN_INDEX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlInIndexDesc'),
+ Yandex::CRAWLSTATS_APPEARED_PAGES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlAppearedPagesDesc'),
+ Yandex::CRAWLSTATS_REMOVED_PAGES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlRemovedPagesDesc'),
+ Yandex::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlCrawledPagesDesc'),
+ Yandex::CRAWLSTATS_CODE_2XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlHttpStatus2xxDesc'),
+ Yandex::CRAWLSTATS_CODE_3XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlHttpStatus3xxDesc'),
+ Yandex::CRAWLSTATS_CODE_4XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlHttpStatus4xxDesc'),
+ Yandex::CRAWLSTATS_CODE_5XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlHttpStatus5xxDesc'),
+ Yandex::CRAWLSTATS_ERRORS_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlErrorsDesc')
+ ];
+ }
+ public static function getMetricIdsToProcessReportTotal()
+ {
+ return [self::NB_CLICKS, self::NB_IMPRESSIONS];
+ }
+ /**
+ * Returns operations used to aggregate the metric columns
+ *
+ * @return array
+ */
+ public static function getColumnsAggregationOperations()
+ {
+ /*
+ * Calculate average CTR based on summed impressions and summed clicks
+ */
+ $calcCtr = function ($val1, $val2, $thisRow, $rowToSum) {
+ $sumImpressions = $thisRow->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_IMPRESSIONS) + $rowToSum->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_IMPRESSIONS);
+ $sumClicks = $thisRow->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS) + $rowToSum->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS);
+ if (!$sumImpressions) {
+ return 0.0;
+ }
+ return round($sumClicks / $sumImpressions, 2);
+ };
+ /*
+ * Calculate average position based on impressions and positions
+ */
+ $calcPosition = function ($val1, $val2, $thisRow, $rowToSum) {
+ return round(
+ (
+ $thisRow->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_IMPRESSIONS)
+ * $thisRow->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::POSITION)
+ + $rowToSum->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_IMPRESSIONS)
+ * $rowToSum->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::POSITION)
+ ) / (
+ $thisRow->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_IMPRESSIONS)
+ + $rowToSum->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_IMPRESSIONS)
+ ),
+ 2
+ );
+ };
+ return [\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::CTR => $calcCtr, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::POSITION => $calcPosition];
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Bing.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Bing.php
new file mode 100644
index 0000000..973ccd5
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Bing.php
@@ -0,0 +1,168 @@
+table = Common::prefixTable(self::$rawTableName);
+ }
+ /**
+ * Installs required database table
+ */
+ public static function install()
+ {
+ $table = "`url` VARCHAR( 170 ) NOT NULL ,\n\t\t\t\t\t `date` DATE NOT NULL ,\n\t\t\t\t\t `data` MEDIUMBLOB,\n\t\t\t\t\t `type` VARCHAR( 15 ) NOT NULL,\n\t\t\t\t\t PRIMARY KEY ( `url` , `date` , `type` )";
+ // Key length = 170 + 3 byte (date) + 15 = 188
+ DbHelper::createTable(self::$rawTableName, $table);
+ }
+ /**
+ * Saves keywords for given url and day
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $keywords serialized keyword data
+ * @return bool
+ */
+ public function archiveKeywordData($url, $date, $keywords)
+ {
+ return $this->archiveData($url, $date, $keywords, 'keywords');
+ }
+ /**
+ * Returns the saved keyword data for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @return null|string serialized keyword data
+ */
+ public function getKeywordData($url, $date)
+ {
+ return $this->getData($url, $date, 'keywords');
+ }
+ /**
+ * Returns the latest date keyword data is available for
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @return null|string
+ */
+ public function getLatestDateKeywordDataIsAvailableFor($url)
+ {
+ $date = Db::fetchOne('SELECT `date` FROM ' . $this->table . ' WHERE `url` = ? AND `type` = ? ORDER BY `date` DESC LIMIT 1', [$url, 'keywords']);
+ return $date;
+ }
+ /**
+ * Saves crawl stats for given url and day
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $keywords serialized keyword data
+ * @return bool
+ */
+ public function archiveCrawlStatsData($url, $date, $keywords)
+ {
+ return $this->archiveData($url, $date, $keywords, 'crawlstats');
+ }
+ /**
+ * Returns the saved crawl stats for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @return null|string serialized keyword data
+ */
+ public function getCrawlStatsData($url, $date)
+ {
+ return $this->getData($url, $date, 'crawlstats');
+ }
+ /**
+ * Saves crawl error for given url
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $keywords serialized keyword data
+ * @return bool
+ */
+ public function archiveCrawlErrors($url, $keywords)
+ {
+ return $this->archiveData($url, '0000-00-00', $keywords, 'crawlerrors');
+ }
+ /**
+ * Returns the saved crawl stats for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @return null|string serialized keyword data
+ */
+ public function getCrawlErrors($url)
+ {
+ return $this->getData($url, '0000-00-00', 'crawlerrors');
+ }
+ /**
+ * Returns the saved data for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $type type of data, like keywords, crawlstats,...
+ * @return null|string serialized data
+ */
+ protected function getData($url, $date, $type)
+ {
+ $keywordData = Db::fetchOne('SELECT `data` FROM ' . $this->table . ' WHERE `url` = ? AND `date` = ? AND `type` = ?', [$url, $date, $type]);
+ if ($keywordData) {
+ return $this->uncompress($keywordData);
+ }
+ return null;
+ }
+ /**
+ * Saves data for given type, url and day
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $keywords serialized keyword data
+ * @param string $type type of data, like keywords, crawlstats,...
+ * @return bool
+ */
+ protected function archiveData($url, $date, $data, $type)
+ {
+ $query = "REPLACE INTO " . $this->table . " (`url`, `date`, `data`, `type`) VALUES (?,?,?,?)";
+ $bindSql = [];
+ $bindSql[] = $url;
+ $bindSql[] = $date;
+ $bindSql[] = $this->compress($data);
+ $bindSql[] = $type;
+ Db::query($query, $bindSql);
+ return \true;
+ }
+ protected function compress($data)
+ {
+ if (Db::get()->hasBlobDataType()) {
+ $data = gzcompress($data);
+ }
+ return $data;
+ }
+ protected function uncompress($data)
+ {
+ if (Db::get()->hasBlobDataType()) {
+ $data = gzuncompress($data);
+ }
+ return $data;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Google.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Google.php
new file mode 100644
index 0000000..b815517
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Google.php
@@ -0,0 +1,131 @@
+table = Common::prefixTable(self::$rawTableName);
+ }
+ /**
+ * Installs required database table
+ */
+ public static function install()
+ {
+ $dashboard = "`url` VARCHAR( 170 ) NOT NULL ,\n\t\t\t\t\t `date` DATE NOT NULL ,\n\t\t\t\t\t `data` MEDIUMBLOB,\n\t\t\t\t\t `type` VARCHAR( 15 ),\n\t\t\t\t\t PRIMARY KEY ( `url` , `date`, `type` )";
+ // Key length = 170 + 3 byte (date) + 15 = 188
+ DbHelper::createTable(self::$rawTableName, $dashboard);
+ }
+ /**
+ * Saves keywords for given url and day
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $type 'web', 'image', 'video' or 'news'
+ * @param string $keywords serialized keyword data
+ * @return bool
+ */
+ public function archiveKeywordData($url, $date, $type, $keywords)
+ {
+ return $this->archiveData($url, $date, $keywords, 'keywords' . $type);
+ }
+ /**
+ * Returns the saved keyword data for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $type 'web', 'image', 'video' or 'news'
+ * @return null|string serialized keyword data
+ */
+ public function getKeywordData($url, $date, $type)
+ {
+ return $this->getData($url, $date, 'keywords' . $type);
+ }
+ /**
+ * Returns the latest date keyword data is available for
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string|null $type 'web', 'image', 'video' or 'news'
+ * @return null|string
+ */
+ public function getLatestDateKeywordDataIsAvailableFor($url, $type = null)
+ {
+ if ($type === null) {
+ $date = Db::fetchOne('SELECT `date` FROM ' . $this->table . ' WHERE `url` = ? AND `type` LIKE ? ORDER BY `date` DESC LIMIT 1', [$url, 'keywords%']);
+ } else {
+ $date = Db::fetchOne('SELECT `date` FROM ' . $this->table . ' WHERE `url` = ? AND `type` = ? ORDER BY `date` DESC LIMIT 1', [$url, 'keywords' . $type]);
+ }
+ return $date;
+ }
+ /**
+ * Returns the saved data for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $type type of data, like keywords, crawlstats,...
+ * @return null|string serialized data
+ */
+ protected function getData($url, $date, $type)
+ {
+ $keywordData = Db::fetchOne('SELECT `data` FROM ' . $this->table . ' WHERE `url` = ? AND `date` = ? AND `type` = ?', [$url, $date, $type]);
+ if ($keywordData) {
+ return $this->uncompress($keywordData);
+ }
+ return null;
+ }
+ /**
+ * Saves data for given type, url and day
+ *
+ * @param string $url url, eg. http://matomo.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $data serialized keyword data
+ * @param string $type type of data, like keywords, crawlstats,...
+ * @return bool
+ */
+ protected function archiveData($url, $date, $data, $type)
+ {
+ $query = "REPLACE INTO " . $this->table . " (`url`, `date`, `data`, `type`) VALUES (?,?,?,?)";
+ $bindSql = [];
+ $bindSql[] = $url;
+ $bindSql[] = $date;
+ $bindSql[] = $this->compress($data);
+ $bindSql[] = $type;
+ Db::query($query, $bindSql);
+ return \true;
+ }
+ protected function compress($data)
+ {
+ if (Db::get()->hasBlobDataType()) {
+ $data = gzcompress($data);
+ }
+ return $data;
+ }
+ protected function uncompress($data)
+ {
+ if (Db::get()->hasBlobDataType()) {
+ $data = gzuncompress($data);
+ }
+ return $data;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Yandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Yandex.php
new file mode 100644
index 0000000..5d8d4be
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Yandex.php
@@ -0,0 +1,147 @@
+table = Common::prefixTable(self::$rawTableName);
+ }
+ /**
+ * Installs required database table
+ */
+ public static function install()
+ {
+ $table = "`url` VARCHAR( 170 ) NOT NULL ,\n\t\t\t\t\t `date` DATE NOT NULL ,\n\t\t\t\t\t `data` MEDIUMBLOB,\n\t\t\t\t\t `type` VARCHAR(15) NOT NULL,\n\t\t\t\t\t PRIMARY KEY ( `url` , `date` , `type` )";
+ // Key length = 170 + 3 byte (date) + 15 = 188
+ DbHelper::createTable(self::$rawTableName, $table);
+ }
+ /**
+ * Saves keywords for given url and day
+ *
+ * @param string $url url, eg. http://piwik.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $keywords serialized keyword data
+ * @return bool
+ */
+ public function archiveKeywordData($url, $date, $keywords)
+ {
+ return $this->archiveData($url, $date, $keywords, 'keywords');
+ }
+ /**
+ * Returns the saved keyword data for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://piwik.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @return null|string serialized keyword data
+ */
+ public function getKeywordData($url, $date)
+ {
+ return $this->getData($url, $date, 'keywords');
+ }
+ /**
+ * Returns the latest date keyword data is available for
+ *
+ * @param string $url url, eg. http://piwik.org
+ * @return null|string
+ */
+ public function getLatestDateKeywordDataIsAvailableFor($url)
+ {
+ $date = Db::fetchOne('SELECT `date` FROM ' . $this->table . ' WHERE `url` = ? AND `type` = ? ORDER BY `date` DESC LIMIT 1', array($url, 'keywords'));
+ return $date;
+ }
+ /**
+ * Saves crawl stats for given url and day
+ *
+ * @param string $url url, eg. http://piwik.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $keywords serialized keyword data
+ * @return bool
+ */
+ public function archiveCrawlStatsData($url, $date, $keywords)
+ {
+ return $this->archiveData($url, $date, $keywords, 'crawlstats');
+ }
+ /**
+ * Returns the saved crawl stats for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://piwik.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @return null|string serialized keyword data
+ */
+ public function getCrawlStatsData($url, $date)
+ {
+ return $this->getData($url, $date, 'crawlstats');
+ }
+ /**
+ * Returns the saved data for given parameters (or null if not available)
+ *
+ * @param string $url url, eg. http://piwik.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $type type of data, like keywords, crawlstats,...
+ * @return null|string serialized data
+ */
+ protected function getData($url, $date, $type)
+ {
+ $keywordData = Db::fetchOne('SELECT `data` FROM ' . $this->table . ' WHERE `url` = ? AND `date` = ? AND `type` = ?', [$url, $date, $type]);
+ if ($keywordData) {
+ return $this->uncompress($keywordData);
+ }
+ return null;
+ }
+ /**
+ * Saves data for given type, url and day
+ *
+ * @param string $url url, eg. http://piwik.org
+ * @param string $date a day string, eg. 2016-12-24
+ * @param string $keywords serialized keyword data
+ * @param string $type type of data, like keywords, crawlstats,...
+ * @return bool
+ */
+ protected function archiveData($url, $date, $data, $type)
+ {
+ $query = "REPLACE INTO " . $this->table . " (`url`, `date`, `data`, `type`) VALUES (?,?,?,?)";
+ $bindSql = [];
+ $bindSql[] = $url;
+ $bindSql[] = $date;
+ $bindSql[] = $this->compress($data);
+ $bindSql[] = $type;
+ Db::query($query, $bindSql);
+ return \true;
+ }
+ protected function compress($data)
+ {
+ if (Db::get()->hasBlobDataType()) {
+ return gzcompress($data);
+ }
+ return $data;
+ }
+ protected function uncompress($data)
+ {
+ if (Db::get()->hasBlobDataType()) {
+ return gzuncompress($data);
+ }
+ return $data;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Monolog/Handler/SEKPSystemLogHandler.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Monolog/Handler/SEKPSystemLogHandler.php
new file mode 100644
index 0000000..ab91f05
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Monolog/Handler/SEKPSystemLogHandler.php
@@ -0,0 +1,33 @@
+getClient()->isConfigured();
+ }
+ public function isSupportedPeriod($date, $period)
+ {
+ // disabled for date ranges that need days for processing
+ if ($period == 'range') {
+ $periodObject = new Range($period, $date);
+ $subPeriods = $periodObject->getSubperiods();
+ foreach ($subPeriods as $subPeriod) {
+ if ($subPeriod->getLabel() == 'day') {
+ return \false;
+ }
+ }
+ }
+ // bing statistics are not available for single days
+ if ($period == 'day') {
+ return \false;
+ }
+ return \true;
+ }
+ /**
+ * @inheritdoc
+ */
+ public function getConfiguredSiteIds()
+ {
+ $configuredSites = [];
+ foreach ($this->getMeasurableHelper()->getAllSiteSettings() as $siteId => $settings) {
+ $createdByUser = !is_null($settings->bingConfigCreatedBy) ? $settings->bingConfigCreatedBy->getValue() : '';
+ $siteConfig = [];
+ if ($settings->bingSiteUrl && $settings->bingSiteUrl->getValue()) {
+ $siteConfig['bingSiteUrl'] = $settings->bingSiteUrl->getValue();
+ $siteConfig['createdByUser'] = $createdByUser;
+ $siteConfig['isDeletionAllowed'] = empty($createdByUser) || Piwik::hasUserSuperUserAccessOrIsTheUser($createdByUser);
+ }
+ if (!empty($siteConfig)) {
+ $configuredSites[$siteId] = $siteConfig;
+ }
+ }
+ return $configuredSites;
+ }
+ public function getConfigurationProblems()
+ {
+ return ['sites' => $this->getSiteErrors(), 'accounts' => $this->getAccountErrors()];
+ }
+ protected function getSiteErrors()
+ {
+ $errors = [];
+ $client = $this->getClient();
+ $accounts = $client->getAccounts();
+ $configuredSiteIds = $this->getConfiguredSiteIds();
+ foreach ($configuredSiteIds as $configuredSiteId => $config) {
+ $bingSiteUrl = $config['bingSiteUrl'];
+ list($apiKey, $url) = explode('##', $bingSiteUrl);
+ if (!key_exists($apiKey, $accounts)) {
+ $errors[$configuredSiteId] = Piwik::translate('SearchEngineKeywordsPerformance_AccountDoesNotExist', [$this->obfuscateApiKey($apiKey)]);
+ continue;
+ }
+ $urls = $client->getAvailableUrls($apiKey);
+ if (!key_exists($url, $urls)) {
+ $errors[$configuredSiteId] = Piwik::translate('SearchEngineKeywordsPerformance_ConfiguredUrlNotAvailable');
+ continue;
+ }
+ }
+ return $errors;
+ }
+ protected function getAccountErrors()
+ {
+ $errors = [];
+ $client = $this->getClient();
+ $accounts = $client->getAccounts();
+ if (empty($accounts)) {
+ return [];
+ }
+ foreach ($accounts as $id => $account) {
+ try {
+ $client->testConfiguration($account['apiKey']);
+ } catch (\Exception $e) {
+ $errors[$id] = Piwik::translate('SearchEngineKeywordsPerformance_BingAccountError', $e->getMessage());
+ }
+ }
+ return $errors;
+ }
+ protected function obfuscateApiKey($apiKey)
+ {
+ return substr($apiKey, 0, 5) . '*****' . substr($apiKey, -5, 5);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Google.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Google.php
new file mode 100644
index 0000000..cc2c951
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Google.php
@@ -0,0 +1,147 @@
+getClient()->isConfigured();
+ }
+ /**
+ * @inheritdoc
+ */
+ public function getConfiguredSiteIds()
+ {
+ $configuredSites = [];
+ foreach ($this->getMeasurableHelper()->getAllSiteSettings() as $siteId => $settings) {
+ $createdByUser = !is_null($settings->googleConfigCreatedBy) ? $settings->googleConfigCreatedBy->getValue() : '';
+ $siteConfig = [];
+ if ($settings->googleSearchConsoleUrl && $settings->googleSearchConsoleUrl->getValue()) {
+ $siteConfig['googleSearchConsoleUrl'] = $settings->googleSearchConsoleUrl->getValue();
+ $siteConfig['googleWebKeywords'] = $settings->googleWebKeywords->getValue();
+ $siteConfig['googleImageKeywords'] = $settings->googleImageKeywords->getValue();
+ $siteConfig['googleVideoKeywords'] = $settings->googleVideoKeywords->getValue();
+ $siteConfig['googleNewsKeywords'] = $settings->googleNewsKeywords->getValue();
+ $siteConfig['createdByUser'] = $createdByUser;
+ $siteConfig['isDeletionAllowed'] = empty($createdByUser) || Piwik::hasUserSuperUserAccessOrIsTheUser($createdByUser);
+ }
+ if (!empty($siteConfig)) {
+ $configuredSites[$siteId] = $siteConfig;
+ }
+ }
+ return $configuredSites;
+ }
+ public function getConfigurationProblems()
+ {
+ $errors = ['sites' => $this->getSiteErrors(), 'accounts' => $this->getAccountErrors()];
+ return $errors;
+ }
+ protected function getSiteErrors()
+ {
+ $errors = [];
+ $client = $this->getClient();
+ $accounts = $client->getAccounts();
+ $configuredSiteIds = $this->getConfiguredSiteIds();
+ foreach ($configuredSiteIds as $configuredSiteId => $config) {
+ $googleSiteUrl = $config['googleSearchConsoleUrl'];
+ list($accountId, $url) = explode('##', $googleSiteUrl);
+ if (!key_exists($accountId, $accounts)) {
+ $errors[$configuredSiteId] = Piwik::translate('SearchEngineKeywordsPerformance_AccountDoesNotExist', ['']);
+ continue;
+ }
+ // Property Sets and Apps are deprecated and will be removed, so warn users why it doesn't work anymore
+ // @todo can be removed in august 2019
+ if (strpos($url, 'sc-set:') === 0) {
+ $errors[$configuredSiteId] = 'You are using a property set for importing. Property sets have been deprecated/removed by Google. To ensure no error occurs, please choose another site for import';
+ continue;
+ }
+ if (strpos($url, 'android-app:') === 0) {
+ $errors[$configuredSiteId] = 'You are using a Android App for importing. Importing Android Apps has been deprecated/removed by Google. To ensure no error occurs, please choose another site for import';
+ continue;
+ }
+ $urls = $client->getAvailableUrls($accountId);
+ if (!key_exists($url, $urls)) {
+ $errors[$configuredSiteId] = Piwik::translate('SearchEngineKeywordsPerformance_ConfiguredUrlNotAvailable');
+ continue;
+ }
+ }
+ return $errors;
+ }
+ protected function getAccountErrors()
+ {
+ $errors = [];
+ $client = $this->getClient();
+ $accounts = $client->getAccounts();
+ if (empty($accounts)) {
+ return [];
+ }
+ foreach ($accounts as $id => $account) {
+ try {
+ $client->testConfiguration($id);
+ } catch (\Exception $e) {
+ $errors[$id] = $e->getMessage();
+ }
+ }
+ return $errors;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Helper/MeasurableHelper.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Helper/MeasurableHelper.php
new file mode 100644
index 0000000..995fab8
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Helper/MeasurableHelper.php
@@ -0,0 +1,53 @@
+allSiteSettings)) {
+ return $this->allSiteSettings;
+ }
+
+ $siteManagerModel = StaticContainer::get(SitesManagerModel::class);
+ $allSiteIds = $siteManagerModel->getSitesId();
+ $this->allSiteSettings = [];
+ foreach ($allSiteIds as $siteId) {
+ if (!Piwik::isUserHasAdminAccess($siteId)) {
+ continue;
+ // skip sites without access
+ }
+ $this->allSiteSettings[$siteId] = $this->getMeasurableSettings($siteId);
+ }
+
+ return $this->allSiteSettings;
+ }
+
+ protected function getMeasurableSettings(int $idSite): MeasurableSettings
+ {
+ return new MeasurableSettings($idSite);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/ProviderAbstract.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/ProviderAbstract.php
new file mode 100644
index 0000000..4755515
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/ProviderAbstract.php
@@ -0,0 +1,151 @@
+ [], accounts => [] ]
+ */
+ abstract public function getConfigurationProblems();
+ /**
+ * Record a new timestamp for the current provider indicating an API error occurred.
+ *
+ * @return void
+ */
+ public function recordNewApiErrorForProvider(): void
+ {
+ Option::set(self::OPTION_PREFIX_LAST_ERROR_TIME . $this->getId(), time());
+ }
+ /**
+ * Get the timestamp of the most recent API error for the current provider. If none is found, 0 is returned.
+ *
+ * @return int Unix timestamp or 0;
+ */
+ public function getLastApiErrorTimestamp(): int
+ {
+ $option = Option::get(self::OPTION_PREFIX_LAST_ERROR_TIME . $this->getId());
+ if (empty($option)) {
+ return 0;
+ }
+ return (int) $option;
+ }
+ /**
+ * Checks if there has been an API error for the current provider within the past week.
+ *
+ * @return bool Indicates whether the most recent API error was less than a week ago.
+ */
+ public function hasApiErrorWithinWeek(): bool
+ {
+ $timestamp = $this->getLastApiErrorTimestamp();
+ if ($timestamp === 0) {
+ return \false;
+ }
+ return $timestamp > strtotime('-1 week');
+ }
+ /**
+ * If there's been an API error within the past week a message string is provided. Otherwise, the string is empty.
+ *
+ * @return string Either the message or empty string depending on whether there's been a recent error.
+ */
+ public function getRecentApiErrorMessage(): string
+ {
+ $message = '';
+ if ($this->hasApiErrorWithinWeek()) {
+ $message = '' . $this->getName() . ' - Most recent error: ' . (new Formatter())->getPrettyTimeFromSeconds(time() - $this->getLastApiErrorTimestamp()) . ' ago';
+ }
+ return $message;
+ }
+
+ protected function getMeasurableHelper(): MeasurableHelper
+ {
+ return MeasurableHelper::getInstance();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Yandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Yandex.php
new file mode 100644
index 0000000..ce70f3d
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Yandex.php
@@ -0,0 +1,147 @@
+getClient()->isConfigured();
+ }
+ /**
+ * @inheritdoc
+ */
+ public function getConfiguredSiteIds()
+ {
+ $configuredSites = [];
+ foreach ($this->getMeasurableHelper()->getAllSiteSettings() as $siteId => $settings) {
+ $siteConfig = [];
+ $createdByUser = !is_null($settings->yandexConfigCreatedBy) ? $settings->yandexConfigCreatedBy->getValue() : '';
+ if ($settings->yandexAccountAndHostId && $settings->yandexAccountAndHostId->getValue()) {
+ $siteConfig['yandexAccountAndHostId'] = $settings->yandexAccountAndHostId->getValue();
+ $siteConfig['createdByUser'] = $createdByUser;
+ $siteConfig['isDeletionAllowed'] = empty($createdByUser) || Piwik::hasUserSuperUserAccessOrIsTheUser($createdByUser);
+ }
+ if (!empty($siteConfig)) {
+ $configuredSites[$siteId] = $siteConfig;
+ }
+ }
+ return $configuredSites;
+ }
+ public function getConfigurationProblems()
+ {
+ $errors = ['sites' => $this->getSiteErrors(), 'accounts' => $this->getAccountErrors()];
+ return $errors;
+ }
+ protected function getSiteErrors()
+ {
+ $errors = [];
+ $client = $this->getClient();
+ $accounts = $client->getAccounts();
+ $configuredSiteIds = $this->getConfiguredSiteIds();
+ foreach ($configuredSiteIds as $configuredSiteId => $config) {
+ $yandexSiteUrl = $config['yandexAccountAndHostId'];
+ list($accountId, $url) = explode('##', $yandexSiteUrl);
+ if (!key_exists($accountId, $accounts)) {
+ $errors[$configuredSiteId] = Piwik::translate('SearchEngineKeywordsPerformance_AccountDoesNotExist', ['']);
+ continue;
+ }
+ $urls = [];
+ try {
+ $urlArray = $client->getAvailableUrls($accountId);
+ foreach ($urlArray as $item) {
+ $urls[] = $item['host_id'];
+ }
+ } catch (\Exception $e) {
+ }
+ if (!in_array($url, $urls)) {
+ $errors[$configuredSiteId] = Piwik::translate('SearchEngineKeywordsPerformance_ConfiguredUrlNotAvailable');
+ continue;
+ }
+ }
+ return $errors;
+ }
+ protected function getAccountErrors()
+ {
+ $errors = [];
+ $client = $this->getClient();
+ $accounts = $client->getAccounts();
+ if (empty($accounts)) {
+ return [];
+ }
+ foreach ($accounts as $id => $account) {
+ try {
+ $client->testConfiguration($id);
+ } catch (\Exception $e) {
+ $errors[$id] = $e->getMessage();
+ }
+ }
+ return $errors;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/README.md b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/README.md
new file mode 100644
index 0000000..9e56904
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/README.md
@@ -0,0 +1,87 @@
+# Search Engine Keywords Performance Plugin for Matomo
+
+## Description
+
+Uncover the keywords people use to find your site in the search engines. Learn which keywords are driving the most traffic, leads, and sales.
+
+There was a time when Google Analytics let you see what keywords people searched for to find your site. But one day, Google curtailed this critical information behind two fateful, cryptic words: "(not provided)."
+
+Since then, there was no way you could access this data. Unless you used Matomo's Search Engine Keywords Performance plugin, that is.
+
+With Search Engine Keywords Performance, the keywords people use to find your site become a dimension in your "Referrers" reports.
+
+Monitor your keywords' positions and boost your SEO performance like in the old days.
+
+### How Search Engine Keywords Performance Works
+
+#### All the Keywords Search Engines Don't Want You to See In One Report
+
+
+
+
Google, Yahoo, and Bing may not want you to see what keywords get you traffic, but we do. How? By leveraging their APIs.
+
Slice the keywords data with one of the 90+ dimensions and mix them with metrics like impressions, clicks, CTR, and the average position in the SERPs.
+
+
+
+
+
+
+#### Get An In-Depth Look at Your Crawling Performance
+
+
+
+
No matter how well you optimise your site, without proper crawling, your SEO efforts will be in vain.
+
Discover the number of pages crawled and indexed, 404 pages found, and other issues that could affect your crawling performance in Yahoo and Bing.
+
The page crawling error reports will show you what pages could not be crawled by a search engine with a detailed reason, so you can fix them right away.
+
+
+
+
+
+
+#### Identify What Keywords Your Images and Videos Bring You Traffic
+
+
+
+
Considering that YouTube and Google Images are the second and third largest search engines, your videos and images can drive significant organic traffic to your site.
+
With the Search Engine Keywords Performance plugin, you can uncover every keyword they rank for and how many visitors they attract, among other metrics.
+
+
+
+
+
+
+#### See How Your Keyword Performance Evolves Over Time
+
+
+
+
Track your top keywords and see how your metrics and KPIs unfold. Monitor, identify, and optimise your SEO strategy for opportunities to get the highest return from your efforts.
+
+
+
+
+
+
+### Try Search Engine Keywords Performance Today
+
+Unveil the true picture of your SEO performance with Matomo's Search Engine Keywords Performance plugin. See once again what keywords you rank for and take your organic traffic to the next level.
+
+It's time you enjoy an unparalleled data-driven SEO strategy with Matomo. Start your 30-day free trial today.
+
+## Dependencies
+This plugin had its vendored dependencies scoped using [matomo scoper](https://github.com/matomo-org/matomo-scoper). This means that composer packages are prefixed so that they won't conflict with the same libraries used by other plugins. If you need to update a dependency, you should be able to run `composer install` to populate the vendor directory, make sure that you have the [DevPluginCommands plugin](https://github.com/innocraft/dev-plugin-commands) installed, and run the following command `./console devplugincommands:process-dependencies --plugin="SearchEngineKeywordsPerformance" --downgrade-php` to scope and transpile the dependencies.
+
+### Features
+* New Search Keywords report in Matomo Referrers section.
+* View Keywords analytics by search type (web VS image VS video).
+* View combined Keywords across all search engines (Google + Bing + Yahoo + Yandex).
+* Monitor Keyword rankings and Search Engine Optimisation performance for each keyword with [Row Evolution](https://matomo.org/docs/row-evolution/).
+* New Crawling overview report show how Search engines bots crawl your websites (Bing + Yahoo and Yandex).
+* View crawling overview key metrics (for Bing + Yahoo and Yandex): crawled pages, total pages in index, total inboud links, robots.txt exclusion page count, crawl errors, DNS failures, connection timeouts, page redirects (301, 302 http status), error pages (4xx http status), internet error pages (5xx http status).
+* Import the detailed list of search keywords for Google search, Google images and Google Videos directly from Google Search Console.
+* Import the detailed list of search keywords from Bing and Yahoo! search directly from Bing Webmaster Tools.
+* Import the detailed list of search keywords from Yandex search directly from Yandex Webmaster API.
+* View all crawling errors with detailed reasons like server errors, robots.txt exclusions, not found pages, ... (Bing + Yahoo)
+* Possibility to add support for other search engines that provide their data through an API (contact us).
+* Get your Keyword analytics SEO reports by [email](https://matomo.org/docs/email-reports/) to you, your colleagues or customers.
+* Export your Keyword analytics report using the [Search Keywords Performance Monitor API](http://developer.matomo.org/api-reference/reporting-api#SearchEngineKeywordsPerformance).
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Base.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Base.php
new file mode 100644
index 0000000..dbf86c2
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Base.php
@@ -0,0 +1,32 @@
+maxRowsInTable = PiwikConfig::getInstance()->General['datatable_archiving_maximum_rows_referrers'];
+ $this->columnToSortByBeforeTruncation = Metrics::NB_CLICKS;
+ $this->columnAggregationOps = Metrics::getColumnsAggregationOperations();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Bing.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Bing.php
new file mode 100644
index 0000000..f376b1c
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Bing.php
@@ -0,0 +1,204 @@
+apiKey = $apiKey;
+ $this->apiUrl = $apiUrl;
+ $this->logger = $logger;
+ $this->columnAggregationOps = array_merge(Metrics::getColumnsAggregationOperations(), [
+ self::CRAWLSTATS_OTHER_CODES_RECORD_NAME => 'max',
+ self::CRAWLSTATS_BLOCKED_ROBOTS_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_2XX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_301_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_302_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_4XX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_5XX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_TIMEOUT_RECORD_NAME => 'max',
+ self::CRAWLSTATS_MALWARE_RECORD_NAME => 'max',
+ self::CRAWLSTATS_ERRORS_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME => 'max',
+ self::CRAWLSTATS_DNS_FAILURE_RECORD_NAME => 'max',
+ self::CRAWLSTATS_IN_INDEX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_IN_LINKS_RECORD_NAME => 'max'
+ ]);
+ }
+ public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array
+ {
+ return [
+ Record::make(Record::TYPE_BLOB, self::KEYWORDS_BING_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_OTHER_CODES_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_BLOCKED_ROBOTS_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_2XX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_301_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_302_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_4XX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_5XX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_TIMEOUT_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_MALWARE_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_ERRORS_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_DNS_FAILURE_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_IN_INDEX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_IN_LINKS_RECORD_NAME)
+ ];
+ }
+ protected function aggregate(ArchiveProcessor $archiveProcessor): array
+ {
+ $records = [];
+ $parameters = $archiveProcessor->getParams();
+ $date = $parameters->getDateStart()->setTimezone('UTC')->toString('Y-m-d');
+ $this->logger->debug("[SearchEngineKeywordsPerformance] Archiving bing records for {$date} and {$this->apiUrl}");
+ $dataTable = $this->getKeywordsAsDataTable($date);
+ if (empty($dataTable)) {
+ // ensure data is present (if available)
+ BingImporter::importAvailablePeriods($this->apiKey, $this->apiUrl);
+ $dataTable = $this->getKeywordsAsDataTable($date);
+ }
+ if (!empty($dataTable)) {
+ $this->logger->debug("[SearchEngineKeywordsPerformance] Archiving bing keywords for {$date} and {$this->apiUrl}");
+ $records[self::KEYWORDS_BING_RECORD_NAME] = $dataTable;
+ }
+ $this->archiveDayCrawlStatNumerics($records, $date);
+ return $records;
+ }
+ /**
+ * Returns keyword data for given parameters as DataTable
+ */
+ protected function getKeywordsAsDataTable(string $date): ?DataTable
+ {
+ $model = new BingModel();
+ $keywordData = $model->getKeywordData($this->apiUrl, $date);
+ if (!empty($keywordData)) {
+ $dataTable = new DataTable();
+ $dataTable->addRowsFromSerializedArray($keywordData);
+ return $dataTable;
+ }
+ return null;
+ }
+ /**
+ * Inserts various numeric records for crawl stats
+ */
+ protected function archiveDayCrawlStatNumerics(array &$records, string $date): void
+ {
+ $dataTable = $this->getCrawlStatsAsDataTable($date);
+ if (!empty($dataTable)) {
+ Log::debug("[SearchEngineKeywordsPerformance] Archiving bing crawl stats for {$date} and {$this->apiUrl}");
+ $getValue = function ($label) use ($dataTable) {
+ $row = $dataTable->getRowFromLabel($label);
+ if ($row) {
+ return (int) $row->getColumn(Metrics::NB_PAGES);
+ }
+ return 0;
+ };
+ $records = array_merge($records, [
+ self::CRAWLSTATS_OTHER_CODES_RECORD_NAME => $getValue('AllOtherCodes'),
+ self::CRAWLSTATS_BLOCKED_ROBOTS_RECORD_NAME => $getValue('BlockedByRobotsTxt'),
+ self::CRAWLSTATS_CODE_2XX_RECORD_NAME => $getValue('Code2xx'),
+ self::CRAWLSTATS_CODE_301_RECORD_NAME => $getValue('Code301'),
+ self::CRAWLSTATS_CODE_302_RECORD_NAME => $getValue('Code302'),
+ self::CRAWLSTATS_CODE_4XX_RECORD_NAME => $getValue('Code4xx'),
+ self::CRAWLSTATS_CODE_5XX_RECORD_NAME => $getValue('Code5xx'),
+ self::CRAWLSTATS_TIMEOUT_RECORD_NAME => $getValue('ConnectionTimeout'),
+ self::CRAWLSTATS_MALWARE_RECORD_NAME => $getValue('ContainsMalware'),
+ self::CRAWLSTATS_ERRORS_RECORD_NAME => $getValue('CrawlErrors'),
+ self::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME => $getValue('CrawledPages'),
+ self::CRAWLSTATS_DNS_FAILURE_RECORD_NAME => $getValue('DnsFailures'),
+ self::CRAWLSTATS_IN_INDEX_RECORD_NAME => $getValue('InIndex'),
+ self::CRAWLSTATS_IN_LINKS_RECORD_NAME => $getValue('InLinks')
+ ]);
+ Common::destroy($dataTable);
+ unset($dataTable);
+ }
+ }
+ /**
+ * Returns crawl stats for given parameters as DataTable
+ */
+ protected function getCrawlStatsAsDataTable(string $date): ?DataTable
+ {
+ $model = new BingModel();
+ $keywordData = $model->getCrawlStatsData($this->apiUrl, $date);
+ if (!empty($keywordData)) {
+ $dataTable = new DataTable();
+ $dataTable->addRowsFromSerializedArray($keywordData);
+ return $dataTable;
+ }
+ return null;
+ }
+ public static function make(int $idSite): ?self
+ {
+ $site = new Site($idSite);
+ $setting = new MeasurableSettings($site->getId(), $site->getType());
+ $bingSiteUrl = $setting->bingSiteUrl;
+ $doesNotHaveBing = empty($bingSiteUrl) || !$bingSiteUrl->getValue() || \false === strpos($bingSiteUrl->getValue(), '##');
+ if ($doesNotHaveBing) {
+ // bing api not activated for that site
+ return null;
+ }
+ list($apiKey, $url) = explode('##', $bingSiteUrl->getValue());
+ return StaticContainer::getContainer()->make(\Piwik\Plugins\SearchEngineKeywordsPerformance\RecordBuilders\Bing::class, ['apiKey' => $apiKey, 'apiUrl' => $url]);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Google.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Google.php
new file mode 100644
index 0000000..796f408
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Google.php
@@ -0,0 +1,155 @@
+accountId = $accountId;
+ $this->searchConsoleUrl = $searchConsoleUrl;
+ $this->recordName = $recordName;
+ $this->logger = $logger;
+ }
+ public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array
+ {
+ return [ArchiveProcessor\Record::make(Record::TYPE_BLOB, $this->recordName)];
+ }
+ protected function aggregate(ArchiveProcessor $archiveProcessor): array
+ {
+ $parameters = $archiveProcessor->getParams();
+ $date = $parameters->getDateStart()->setTimezone('UTC')->toString('Y-m-d');
+ $record = $this->aggregateDayBySearchType($archiveProcessor, $this->recordName, $date);
+ if (empty($record)) {
+ return [];
+ }
+ return [$this->recordName => $record];
+ }
+ public function isEnabled(ArchiveProcessor $archiveProcessor): bool
+ {
+ $segment = $archiveProcessor->getParams()->getSegment();
+ if (!$segment->isEmpty()) {
+ $this->logger->debug("Skip Archiving for SearchEngineKeywordsPerformance plugin for segments");
+ return \false;
+ // do not archive data for segments
+ }
+ return \true;
+ }
+ /**
+ * Aggregates data for a given day by type of search
+ */
+ protected function aggregateDayBySearchType(ArchiveProcessor $archiveProcessor, string $recordName, string $date): ?DataTable
+ {
+ $types = [self::KEYWORDS_GOOGLE_WEB_RECORD_NAME => 'web', self::KEYWORDS_GOOGLE_IMAGE_RECORD_NAME => 'image', self::KEYWORDS_GOOGLE_VIDEO_RECORD_NAME => 'video', self::KEYWORDS_GOOGLE_NEWS_RECORD_NAME => 'news'];
+ $this->logger->debug("[SearchEngineKeywordsPerformance] Archiving {$types[$recordName]} keywords for {$date} and {$this->searchConsoleUrl}");
+ $dataTable = $this->getKeywordsAsDataTable($archiveProcessor, $date, $types[$recordName]);
+ if (empty($dataTable)) {
+ return null;
+ }
+ return $dataTable;
+ }
+ /**
+ * Returns keyword data for given parameters as DataTable
+ */
+ protected function getKeywordsAsDataTable(ArchiveProcessor $archiveProcessor, string $date, string $type): ?DataTable
+ {
+ // ensure keywords are present (if available)
+ $googleImporter = new GoogleImporter($archiveProcessor->getParams()->getSite()->getId());
+ $googleImporter->importKeywordsIfNecessary($this->accountId, $this->searchConsoleUrl, $date, $type);
+ $model = new GoogleModel();
+ $keywordData = $model->getKeywordData($this->searchConsoleUrl, $date, $type);
+ if (!empty($keywordData)) {
+ $dataTable = new DataTable();
+ $dataTable->addRowsFromSerializedArray($keywordData);
+ return $dataTable;
+ }
+ return null;
+ }
+ public static function makeAll(int $idSite): array
+ {
+ $site = new Site($idSite);
+ $settings = new MeasurableSettings($site->getId(), $site->getType());
+ $searchConsoleUrl = $settings->googleSearchConsoleUrl;
+ if (empty($searchConsoleUrl) || !$searchConsoleUrl->getValue() || \false === strpos($searchConsoleUrl->getValue(), '##')) {
+ return [];
+ // search console not activated for that site
+ }
+ $searchConsoleSetting = $settings->googleSearchConsoleUrl->getValue();
+ list($accountId, $searchConsoleUrl) = explode('##', $searchConsoleSetting);
+ $archives = [];
+ if ($settings->googleWebKeywords->getValue()) {
+ $archives[] = self::KEYWORDS_GOOGLE_WEB_RECORD_NAME;
+ }
+ if ($settings->googleImageKeywords->getValue()) {
+ $archives[] = self::KEYWORDS_GOOGLE_IMAGE_RECORD_NAME;
+ }
+ if ($settings->googleVideoKeywords->getValue()) {
+ $archives[] = self::KEYWORDS_GOOGLE_VIDEO_RECORD_NAME;
+ }
+ if ($settings->googleNewsKeywords->getValue()) {
+ $archives[] = self::KEYWORDS_GOOGLE_NEWS_RECORD_NAME;
+ }
+ $logger = StaticContainer::get(LoggerInterface::class);
+ $builders = array_map(function ($recordName) use ($accountId, $searchConsoleUrl, $logger) {
+ return new self($accountId, $searchConsoleUrl, $recordName, $logger);
+ }, $archives);
+ return $builders;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Yandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Yandex.php
new file mode 100644
index 0000000..140a5b8
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Yandex.php
@@ -0,0 +1,196 @@
+accountId = $accountId;
+ $this->hostId = $hostId;
+ $this->logger = $logger;
+ $this->columnAggregationOps = array_merge(Metrics::getColumnsAggregationOperations(), [
+ self::CRAWLSTATS_IN_INDEX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_APPEARED_PAGES_RECORD_NAME => 'max',
+ self::CRAWLSTATS_REMOVED_PAGES_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_2XX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_3XX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_4XX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_CODE_5XX_RECORD_NAME => 'max',
+ self::CRAWLSTATS_ERRORS_RECORD_NAME => 'max'
+ ]);
+ }
+ public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array
+ {
+ return [
+ Record::make(Record::TYPE_BLOB, self::KEYWORDS_YANDEX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_IN_INDEX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_APPEARED_PAGES_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_REMOVED_PAGES_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_2XX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_3XX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_4XX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_CODE_5XX_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, self::CRAWLSTATS_ERRORS_RECORD_NAME)
+ ];
+ }
+ protected function aggregate(ArchiveProcessor $archiveProcessor): array
+ {
+ $records = [];
+ $parameters = $archiveProcessor->getParams();
+ $date = $parameters->getDateStart()->setTimezone('UTC')->toString('Y-m-d');
+ $this->logger->debug("[SearchEngineKeywordsPerformance] Archiving yandex records for {$date} and {$this->hostId}");
+ $dataTable = $this->getKeywordsAsDataTable($date);
+ if (empty($dataTable)) {
+ // ensure data is present (if available)
+ YandexImporter::importAvailableDataForDate($this->accountId, $this->hostId, $date);
+ $dataTable = $this->getKeywordsAsDataTable($date);
+ }
+ if (!empty($dataTable)) {
+ Log::debug("[SearchEngineKeywordsPerformance] Archiving yandex keywords for {$date} and {$this->hostId}");
+ $records[self::KEYWORDS_YANDEX_RECORD_NAME] = $dataTable;
+ }
+ $records = array_merge($records, $this->archiveDayCrawlStatNumerics($date));
+ return $records;
+ }
+ public function isEnabled(ArchiveProcessor $archiveProcessor): bool
+ {
+ $segment = $archiveProcessor->getParams()->getSegment();
+ if (!$segment->isEmpty()) {
+ $this->logger->debug("Skip Archiving for SearchEngineKeywordsPerformance plugin for segments");
+ return \false;
+ // do not archive data for segments
+ }
+ return \true;
+ }
+ /**
+ * Returns keyword data for given parameters as DataTable
+ */
+ protected function getKeywordsAsDataTable(string $date): ?DataTable
+ {
+ $model = new YandexModel();
+ $keywordData = $model->getKeywordData($this->hostId, $date);
+ if (!empty($keywordData)) {
+ $dataTable = new DataTable();
+ $dataTable->addRowsFromSerializedArray($keywordData);
+ return $dataTable;
+ }
+ return null;
+ }
+ /**
+ * Returns keyword data for given parameters as DataTable
+ */
+ protected function archiveDayCrawlStatNumerics(string $date): array
+ {
+ $dataTable = $this->getCrawlStatsAsDataTable($date);
+ if (!empty($dataTable)) {
+ $this->logger->debug("[SearchEngineKeywordsPerformance] Archiving yandex crawl stats for {$date} and {$this->hostId}");
+ $getValue = function ($label) use ($dataTable) {
+ $row = $dataTable->getRowFromLabel($label);
+ if ($row) {
+ return (int) $row->getColumn(Metrics::NB_PAGES);
+ }
+ return 0;
+ };
+ $numericRecords = [
+ self::CRAWLSTATS_IN_INDEX_RECORD_NAME => $getValue('SEARCHABLE'),
+ self::CRAWLSTATS_APPEARED_PAGES_RECORD_NAME => $getValue('APPEARED_IN_SEARCH'),
+ self::CRAWLSTATS_REMOVED_PAGES_RECORD_NAME => $getValue('REMOVED_FROM_SEARCH'),
+ self::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME => $getValue('HTTP_2XX') + $getValue('HTTP_3XX') + $getValue('HTTP_4XX') + $getValue('HTTP_5XX') + $getValue('OTHER'),
+ self::CRAWLSTATS_CODE_2XX_RECORD_NAME => $getValue('HTTP_2XX'),
+ self::CRAWLSTATS_CODE_3XX_RECORD_NAME => $getValue('HTTP_3XX'),
+ self::CRAWLSTATS_CODE_4XX_RECORD_NAME => $getValue('HTTP_4XX'),
+ self::CRAWLSTATS_CODE_5XX_RECORD_NAME => $getValue('HTTP_5XX'),
+ self::CRAWLSTATS_ERRORS_RECORD_NAME => $getValue('OTHER')
+ ];
+ Common::destroy($dataTable);
+ unset($dataTable);
+ return $numericRecords;
+ }
+ return [];
+ }
+ /**
+ * Returns crawl stats for given parameters as DataTable
+ */
+ protected function getCrawlStatsAsDataTable(string $date): ?DataTable
+ {
+ $model = new YandexModel();
+ $keywordData = $model->getCrawlStatsData($this->hostId, $date);
+ if (!empty($keywordData)) {
+ $dataTable = new DataTable();
+ $dataTable->addRowsFromSerializedArray($keywordData);
+ return $dataTable;
+ }
+ return null;
+ }
+ public static function make(int $idSite): ?self
+ {
+ $site = new Site($idSite);
+ $setting = new MeasurableSettings($site->getId(), $site->getType());
+ $yandexConfig = $setting->yandexAccountAndHostId;
+ $doesNotHaveYandex = empty($yandexConfig) || !$yandexConfig->getValue() || \false === strpos($yandexConfig->getValue(), '##');
+ if ($doesNotHaveYandex) {
+ // yandex api not activated for that site
+ return null;
+ }
+ list($accountId, $hostId) = explode('##', $yandexConfig->getValue());
+ return StaticContainer::getContainer()->make(self::class, ['accountId' => $accountId, 'hostId' => $hostId]);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/Base.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/Base.php
new file mode 100644
index 0000000..2006c39
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/Base.php
@@ -0,0 +1,205 @@
+categoryId = 'Referrers_Referrers';
+ $this->subcategoryId = 'Referrers_SubmenuSearchEngines';
+ $this->defaultSortColumn = Metrics::NB_CLICKS;
+ $this->metrics = Metrics::getKeywordMetrics();
+ $this->processedMetrics = [];
+ if (property_exists($this, 'onlineGuideUrl')) {
+ $this->onlineGuideUrl = Url::addCampaignParametersToMatomoLink('https://matomo.org/guide/installation-maintenance/import-search-keywords/');
+ }
+ }
+ public function getMetricsDocumentation()
+ {
+ return Metrics::getMetricsDocumentation();
+ }
+ public function configureReportMetadata(&$availableReports, $infos)
+ {
+ $this->idSite = $infos['idSite'];
+ parent::configureReportMetadata($availableReports, $infos);
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ $view->config->addTranslations(['label' => $this->dimension->getName()]);
+ $view->config->show_limit_control = \true;
+ $view->config->show_all_views_icons = \false;
+ $view->config->show_table_all_columns = \false;
+ $view->config->columns_to_display = ['label', Metrics::NB_CLICKS, Metrics::NB_IMPRESSIONS, Metrics::CTR, Metrics::POSITION];
+ $view->requestConfig->filter_limit = 10;
+ $this->configureSegmentNotSupported($view);
+ }
+ public function getSecondarySortColumnCallback()
+ {
+ return function ($firstSortColumn, $table) {
+ return $firstSortColumn === Metrics::NB_CLICKS ? Metrics::NB_IMPRESSIONS : Metrics::NB_CLICKS;
+ };
+ }
+ protected function configureSegmentNotSupported(ViewDataTable $view)
+ {
+ // show 'not supported' message if segment is chosen
+ if (Common::getRequestVar('segment', '')) {
+ $view->config->show_footer_message .= '' . Piwik::translate('SearchEngineKeywordsPerformance_NoSegmentation') . '
';
+ }
+ }
+ public function isGoogleEnabledForType($type)
+ {
+ $idSite = Common::getRequestVar('idSite', $this->idSite, 'int');
+ if (empty($idSite)) {
+ return \false;
+ }
+ if (SearchEngineKeywordsPerformance::isGoogleForceEnabled($idSite)) {
+ return \true;
+ }
+ $setting = new MeasurableSettings($idSite);
+ $searchConsoleSetting = $setting->googleSearchConsoleUrl;
+ $typeSetting = $setting->getSetting('google' . $type . 'keywords');
+ return $searchConsoleSetting && $searchConsoleSetting->getValue() && $typeSetting && $typeSetting->getValue() && (strpos($searchConsoleSetting->getValue(), 'android-app') === \false || $type == 'web');
+ }
+ public function isAnyGoogleTypeEnabled()
+ {
+ return $this->isGoogleEnabledForType('web') || $this->isGoogleEnabledForType('image') || $this->isGoogleEnabledForType('video') || $this->isGoogleEnabledForType('news');
+ }
+ public function isBingEnabled()
+ {
+ $idSite = Common::getRequestVar('idSite', $this->idSite, 'int');
+ if (empty($idSite)) {
+ return \false;
+ }
+ if (SearchEngineKeywordsPerformance::isBingForceEnabled($idSite)) {
+ return \true;
+ }
+ $setting = new MeasurableSettings($idSite);
+ return !empty($setting->bingSiteUrl) && $setting->bingSiteUrl->getValue();
+ }
+ public function isYandexEnabled()
+ {
+ $idSite = Common::getRequestVar('idSite', \false, 'int');
+ if (empty($idSite)) {
+ return \false;
+ }
+ if (SearchEngineKeywordsPerformance::isYandexForceEnabled($idSite)) {
+ return \true;
+ }
+ $setting = new MeasurableSettings($idSite);
+ return !empty($setting->yandexAccountAndHostId) && $setting->yandexAccountAndHostId->getValue();
+ }
+ public function getMetricNamesToProcessReportTotals()
+ {
+ return Metrics::getMetricIdsToProcessReportTotal();
+ }
+ /**
+ * @param ViewDataTable $view
+ * @param $type
+ * @throws \Exception
+ */
+ public function configureViewMessagesGoogle($view, $type)
+ {
+ $period = Common::getRequestVar('period', \false, 'string');
+ $date = Common::getRequestVar('date', \false, 'string');
+ $idSite = Common::getRequestVar('idSite', \false, 'string');
+ // Append a footer message if data was not yet reported as final
+ $view->config->filters[] = function ($table) use ($view) {
+ if ($table->getMetadata(Google::DATATABLE_METADATA_TEMPORARY) === \true && \false === strpos($view->config->show_footer_message, Piwik::translate('SearchEngineKeywordsPerformance_GoogleDataNotFinal'))) {
+ $view->config->show_footer_message .= '' . Piwik::translate('SearchEngineKeywordsPerformance_GoogleDataNotFinal') . '
';
+ }
+ };
+ if (SearchEngineKeywordsPerformance::isGoogleForceEnabled($idSite)) {
+ return;
+ }
+ $measurableSetting = new MeasurableSettings($idSite);
+ [$account, $url] = explode('##', $measurableSetting->googleSearchConsoleUrl->getValue());
+ $model = new ModelGoogle();
+ $message = '';
+ $periodObj = Period\Factory::build($period, $date);
+ $lastDate = $model->getLatestDateKeywordDataIsAvailableFor($url);
+ $lastDateForType = $model->getLatestDateKeywordDataIsAvailableFor($url, $type);
+ if ($lastDate && !Date::factory($lastDate)->isEarlier($periodObj->getDateStart())) {
+ return;
+ }
+ $lastDateMessage = '';
+ if ($lastDateForType && $period != 'range') {
+ $periodObjType = Period\Factory::build($period, Date::factory($lastDateForType));
+ $lastDateMessage = Piwik::translate('SearchEngineKeywordsPerformance_LatestAvailableDate', '' . $periodObjType->getLocalizedShortString() . ' ');
+ }
+ if ($periodObj->getDateEnd()->isLater(Date::now()->subDay(5))) {
+ $message .= ''
+ . Piwik::translate('CoreHome_ThereIsNoDataForThisReport')
+ . ' ' . Piwik::translate('SearchEngineKeywordsPerformance_GoogleDataProvidedWithDelay')
+ . ' ' . $lastDateMessage . '
';
+ $view->config->no_data_message = $message;
+ }
+ if (empty($message) && $lastDateMessage) {
+ $view->config->show_footer_message .= '' . $lastDateMessage . '
';
+ }
+ }
+ protected function formatColumnsAsNumbers($view, $columns)
+ {
+ $numberFormatter = NumberFormatter::getInstance();
+ $view->config->filters[] = function (DataTable $table) use ($columns, $numberFormatter) {
+ $firstRow = $table->getFirstRow();
+ if (empty($firstRow)) {
+ return;
+ }
+ foreach ($columns as $metric) {
+ $value = $firstRow->getColumn($metric);
+ if (\false !== $value) {
+ $firstRow->setColumn($metric, $numberFormatter->formatNumber($value));
+ }
+ }
+ };
+ }
+ /**
+ * @param ViewDataTable $view
+ */
+ protected function formatCtrAndPositionColumns($view)
+ {
+ $settings = new SystemSettings();
+ $numberFormatter = NumberFormatter::getInstance();
+ $view->config->filters[] = ['ColumnCallbackReplace', [Metrics::CTR, function ($value) use ($numberFormatter) {
+ return $numberFormatter->formatPercent($value * 100, 0, 0);
+ }]];
+ $precision = $settings->roundKeywordPosition->getValue() ? 0 : 1;
+ $view->config->filters[] = ['ColumnCallbackReplace', [Metrics::POSITION, function ($value) use ($precision, $numberFormatter) {
+ if ($precision) {
+ return $numberFormatter->formatNumber($value, $precision, $precision);
+ }
+ return round($value, $precision);
+ }]];
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingErrorExamplesBing.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingErrorExamplesBing.php
new file mode 100644
index 0000000..427007d
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingErrorExamplesBing.php
@@ -0,0 +1,100 @@
+getLocalized(Date::DATETIME_FORMAT_SHORT) : Piwik::translate('General_Never');
+ $this->categoryId = 'General_Actions';
+ $this->subcategoryId = 'SearchEngineKeywordsPerformance_CrawlingErrors';
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlErrors');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlErrorsFromDateX', $dateOfLastImport);
+ $this->defaultSortColumn = 'label';
+ $this->metrics = [];
+ $this->order = 2;
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ $view->config->show_all_views_icons = \false;
+ $view->config->show_table_all_columns = \false;
+ $view->config->disable_row_evolution = \true;
+ $view->config->addTranslations([
+ 'label' => Piwik::translate('Actions_ColumnPageURL'),
+ 'category' => Piwik::translate('SearchEngineKeywordsPerformance_Category'),
+ 'inLinks' => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlInboundLink'),
+ 'responseCode' => Piwik::translate('SearchEngineKeywordsPerformance_ResponseCode')
+ ]);
+ $translations = [
+ 'Code301' => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus301'),
+ 'Code302' => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus302'),
+ 'Code4xx' => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus4xx'),
+ 'Code5xx' => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus5xx'),
+ 'BlockedByRobotsTxt' => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlBlockedByRobotsTxt'),
+ 'ContainsMalware' => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlMalwareInfected'),
+ 'ImportantUrlBlockedByRobotsTxt' => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlImportantBlockedByRobotsTxt')
+ ];
+ $view->config->filters[] = ['ColumnCallbackReplace', ['category', function ($val) use ($translations) {
+ $codes = explode(',', $val);
+ $result = [];
+ foreach ($codes as $code) {
+ $result[] = array_key_exists($code, $translations) ? $translations[$code] : $code;
+ }
+ return implode(', ', $result);
+ }]];
+ $this->configureSegmentNotSupported($view);
+ }
+ public function getSecondarySortColumnCallback()
+ {
+ return null;
+ }
+ public function configureReportMetadata(&$availableReports, $infos)
+ {
+ }
+ public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)
+ {
+ $widget = $factory->createWidget();
+ $widget->setIsWide();
+ $widgetsList->addWidgetConfig($widget);
+ }
+ public function isEnabled()
+ {
+ $idSite = Common::getRequestVar('idSite', \false, 'int');
+ if (empty($idSite)) {
+ return \false;
+ }
+ if (SearchEngineKeywordsPerformance::isBingForceEnabled($idSite)) {
+ return \true;
+ }
+ $setting = new MeasurableSettings($idSite);
+ return !empty($setting->bingSiteUrl) && $setting->bingSiteUrl->getValue();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingOverviewBing.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingOverviewBing.php
new file mode 100644
index 0000000..9f61693
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingOverviewBing.php
@@ -0,0 +1,132 @@
+subcategoryId = 'SearchEngineKeywordsPerformance_CrawlingStats';
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlingStats');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlingStatsDocumentation');
+ $this->defaultSortColumn = null;
+ $this->metrics = [];
+ $this->order = 10;
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ $period = Common::getRequestVar('period', \false, 'string');
+ $viewDataTable = Common::getRequestVar('viewDataTable', \false, 'string');
+ if ($period != 'day' && $viewDataTable != 'graphEvolution') {
+ $view->config->show_footer_message .= '' . Piwik::translate('SearchEngineKeywordsPerformance_ReportShowMaximumValues') . '
';
+ }
+ $view->config->show_limit_control = \false;
+ $view->config->show_all_views_icons = \false;
+ $view->config->show_table_all_columns = \false;
+ $view->config->setDefaultColumnsToDisplay([BingRecordBuilder::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME], \false, \false);
+ $view->config->addTranslations([
+ BingRecordBuilder::CRAWLSTATS_OTHER_CODES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlStatsOtherCodes'),
+ BingRecordBuilder::CRAWLSTATS_BLOCKED_ROBOTS_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlBlockedByRobotsTxt'),
+ BingRecordBuilder::CRAWLSTATS_CODE_2XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus2xx'),
+ BingRecordBuilder::CRAWLSTATS_CODE_301_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus301'),
+ BingRecordBuilder::CRAWLSTATS_CODE_302_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus302'),
+ BingRecordBuilder::CRAWLSTATS_CODE_4XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus4xx'),
+ BingRecordBuilder::CRAWLSTATS_CODE_5XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlHttpStatus5xx'),
+ BingRecordBuilder::CRAWLSTATS_TIMEOUT_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlConnectionTimeout'),
+ BingRecordBuilder::CRAWLSTATS_MALWARE_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlMalwareInfected'),
+ BingRecordBuilder::CRAWLSTATS_ERRORS_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_CrawlingErrors'),
+ BingRecordBuilder::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlCrawledPages'),
+ BingRecordBuilder::CRAWLSTATS_DNS_FAILURE_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlDNSFailures'),
+ BingRecordBuilder::CRAWLSTATS_IN_INDEX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlPagesInIndex'),
+ BingRecordBuilder::CRAWLSTATS_IN_LINKS_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_BingCrawlInboundLink')
+ ]);
+ $view->config->selectable_columns = [
+ BingRecordBuilder::CRAWLSTATS_OTHER_CODES_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_BLOCKED_ROBOTS_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_2XX_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_301_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_302_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_4XX_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CODE_5XX_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_TIMEOUT_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_MALWARE_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_ERRORS_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_DNS_FAILURE_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_IN_INDEX_RECORD_NAME,
+ BingRecordBuilder::CRAWLSTATS_IN_LINKS_RECORD_NAME
+ ];
+ $this->configureSegmentNotSupported($view);
+ $this->formatColumnsAsNumbers($view, $view->config->selectable_columns);
+ }
+ public function getSecondarySortColumnCallback()
+ {
+ return null;
+ }
+ public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)
+ {
+ $idSite = Common::getRequestVar('idSite', 0, 'int');
+ if (empty($idSite)) {
+ return;
+ }
+ $subcategory = 'SearchEngineKeywordsPerformance_CrawlingStats';
+ $widgets = [];
+ $config = $factory->createWidget();
+ $config->forceViewDataTable(Evolution::ID);
+ $config->setSubcategoryId($subcategory);
+ $config->setIsNotWidgetizable();
+ $widgets[] = $config;
+ $config = $factory->createWidget();
+ $config->forceViewDataTable(Sparklines::ID);
+ $config->setSubcategoryId($subcategory);
+ $config->setName('');
+ $config->setIsNotWidgetizable();
+ $widgets[] = $config;
+ $config = $factory->createContainerWidget('CrawlingStatsBing');
+ $config->setCategoryId($widgets[0]->getCategoryId());
+ $config->setSubcategoryId($subcategory);
+ $config->setIsWidgetizable();
+ foreach ($widgets as $widget) {
+ $config->addWidgetConfig($widget);
+ }
+ $widgetsList->addWidgetConfigs([$config]);
+ }
+ public function isEnabled()
+ {
+ $idSite = Common::getRequestVar('idSite', \false, 'int');
+ if (empty($idSite)) {
+ return \false;
+ }
+ if (SearchEngineKeywordsPerformance::isBingForceEnabled($idSite)) {
+ return \true;
+ }
+ $setting = new MeasurableSettings($idSite);
+ return !empty($setting->bingSiteUrl) && $setting->bingSiteUrl->getValue();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingOverviewYandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingOverviewYandex.php
new file mode 100644
index 0000000..058aabe
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingOverviewYandex.php
@@ -0,0 +1,107 @@
+subcategoryId = 'SearchEngineKeywordsPerformance_CrawlingStats';
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlingStats');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlingStatsDocumentation');
+ $this->defaultSortColumn = null;
+ $this->metrics = [];
+ $this->order = 10;
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ $period = Common::getRequestVar('period', \false, 'string');
+ $viewDataTable = Common::getRequestVar('viewDataTable', \false, 'string');
+ if ($period != 'day' && $viewDataTable != 'graphEvolution') {
+ $view->config->show_footer_message .= '' . Piwik::translate('SearchEngineKeywordsPerformance_ReportShowMaximumValues') . '
';
+ }
+ $view->config->show_limit_control = \false;
+ $view->config->show_all_views_icons = \false;
+ $view->config->show_table_all_columns = \false;
+ $view->config->setDefaultColumnsToDisplay([YandexRecordBuilder::CRAWLSTATS_IN_INDEX_RECORD_NAME], \false, \false);
+ $view->config->addTranslations([
+ YandexRecordBuilder::CRAWLSTATS_IN_INDEX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlInIndex'),
+ YandexRecordBuilder::CRAWLSTATS_APPEARED_PAGES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlAppearedPages'),
+ YandexRecordBuilder::CRAWLSTATS_REMOVED_PAGES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlRemovedPages'),
+ YandexRecordBuilder::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlCrawledPages'),
+ YandexRecordBuilder::CRAWLSTATS_CODE_2XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlHttpStatus2xx'),
+ YandexRecordBuilder::CRAWLSTATS_CODE_3XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlHttpStatus3xx'),
+ YandexRecordBuilder::CRAWLSTATS_CODE_4XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlHttpStatus4xx'),
+ YandexRecordBuilder::CRAWLSTATS_CODE_5XX_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlHttpStatus5xx'),
+ YandexRecordBuilder::CRAWLSTATS_ERRORS_RECORD_NAME => Piwik::translate('SearchEngineKeywordsPerformance_YandexCrawlErrors')
+ ]);
+ $view->config->selectable_columns = [
+ YandexRecordBuilder::CRAWLSTATS_IN_INDEX_RECORD_NAME,
+ YandexRecordBuilder::CRAWLSTATS_APPEARED_PAGES_RECORD_NAME,
+ YandexRecordBuilder::CRAWLSTATS_REMOVED_PAGES_RECORD_NAME,
+ YandexRecordBuilder::CRAWLSTATS_CRAWLED_PAGES_RECORD_NAME,
+ YandexRecordBuilder::CRAWLSTATS_CODE_2XX_RECORD_NAME,
+ YandexRecordBuilder::CRAWLSTATS_CODE_3XX_RECORD_NAME,
+ YandexRecordBuilder::CRAWLSTATS_CODE_4XX_RECORD_NAME,
+ YandexRecordBuilder::CRAWLSTATS_CODE_5XX_RECORD_NAME,
+ YandexRecordBuilder::CRAWLSTATS_ERRORS_RECORD_NAME
+ ];
+ $this->configureSegmentNotSupported($view);
+ }
+ public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)
+ {
+ $idSite = Common::getRequestVar('idSite', 0, 'int');
+ if (empty($idSite)) {
+ return;
+ }
+ $subcategory = 'SearchEngineKeywordsPerformance_CrawlingStats';
+ $widgets = [];
+ $config = $factory->createWidget();
+ $config->forceViewDataTable(Evolution::ID);
+ $config->setSubcategoryId($subcategory);
+ $config->setIsNotWidgetizable();
+ $widgets[] = $config;
+ $config = $factory->createWidget();
+ $config->forceViewDataTable(Sparklines::ID);
+ $config->setSubcategoryId($subcategory);
+ $config->setName('');
+ $config->setIsNotWidgetizable();
+ $widgets[] = $config;
+ $config = $factory->createContainerWidget('CrawlingStatsYandex');
+ $config->setCategoryId($widgets[0]->getCategoryId());
+ $config->setSubcategoryId($subcategory);
+ $config->setIsWidgetizable();
+ foreach ($widgets as $widget) {
+ $config->addWidgetConfig($widget);
+ }
+ $widgetsList->addWidgetConfigs([$config]);
+ }
+ public function isEnabled()
+ {
+ return parent::isYandexEnabled();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywords.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywords.php
new file mode 100644
index 0000000..c5be0de
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywords.php
@@ -0,0 +1,53 @@
+dimension = new Keyword();
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_KeywordsCombined');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_KeywordsCombinedDocumentation');
+ $this->metrics = ['nb_visits'];
+ $this->order = 1;
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ parent::configureView($view);
+ $view->config->columns_to_display = ['label', 'nb_visits'];
+ }
+ public function getRelatedReports()
+ {
+ $getKeywordsImported = new GetKeywordsImported();
+ if ($getKeywordsImported->isEnabled()) {
+ return [ReportsProvider::factory('SearchEngineKeywordsPerformance', 'getKeywordsImported'), ReportsProvider::factory('Referrers', 'getKeywords')];
+ }
+
+ return [ReportsProvider::factory('Referrers', 'getKeywords')];
+ }
+ public function alwaysUseDefaultViewDataTable()
+ {
+ return \true;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsBing.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsBing.php
new file mode 100644
index 0000000..520852a
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsBing.php
@@ -0,0 +1,90 @@
+dimension = new Keyword();
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_BingKeywords');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_BingKeywordsDocumentation');
+ $this->order = 15;
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ parent::configureView($view);
+ $period = Common::getRequestVar('period', \false, 'string');
+ $idSite = Common::getRequestVar('idSite', \false, 'string');
+ $model = new ModelBing();
+ $measurableSetting = new MeasurableSettings($idSite);
+ $dateLastData = null;
+ if (!SearchEngineKeywordsPerformance::isBingForceEnabled($idSite)) {
+ list($apiKey, $url) = explode('##', $measurableSetting->bingSiteUrl->getValue());
+ $dateLastData = $model->getLatestDateKeywordDataIsAvailableFor($url);
+ }
+ $lastDateMessage = '';
+ if ($dateLastData && $period != 'range') {
+ $reportPeriod = $period != 'day' ? $period : 'week';
+ $periodObj = Period\Factory::build($reportPeriod, Date::factory($dateLastData));
+ $lastDateMessage = Piwik::translate(
+ 'SearchEngineKeywordsPerformance_LatestAvailableDate',
+ '' . $periodObj->getLocalizedShortString() . ' '
+ );
+ }
+ if ($period == 'day') {
+ $message = ''
+ . Piwik::translate('CoreHome_ThereIsNoDataForThisReport') . ' '
+ . Piwik::translate('SearchEngineKeywordsPerformance_BingKeywordsNotDaily')
+ . ' ' . $lastDateMessage . '
';
+ } else {
+ $message = '' . Piwik::translate('CoreHome_ThereIsNoDataForThisReport') . ' ' . $lastDateMessage . '
';
+ }
+ if ($period == 'range') {
+ $date = Common::getRequestVar('date', \false, 'string');
+ $periodObject = new Range($period, $date, Site::getTimezoneFor($idSite));
+ $subPeriods = $periodObject->getSubperiods();
+ foreach ($subPeriods as $subPeriod) {
+ if ($subPeriod->getLabel() == 'day') {
+ $message = '' . Piwik::translate('SearchEngineKeywordsPerformance_BingKeywordsNoRangeReports') . '
';
+ break;
+ }
+ }
+ }
+ if (!empty($message)) {
+ $view->config->no_data_message = $message;
+ }
+ $this->formatCtrAndPositionColumns($view);
+ }
+ public function isEnabled()
+ {
+ return parent::isBingEnabled();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleImage.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleImage.php
new file mode 100644
index 0000000..90c9a00
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleImage.php
@@ -0,0 +1,44 @@
+dimension = new Keyword();
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_ImageKeywords');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_ImageKeywordsDocumentation');
+ $this->order = 20;
+ }
+ public function isEnabled()
+ {
+ return parent::isGoogleEnabledForType('image');
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ parent::configureView($view);
+ $this->configureViewMessagesGoogle($view, 'image');
+ $this->formatCtrAndPositionColumns($view);
+ $view->requestConfig->filter_limit = 5;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleNews.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleNews.php
new file mode 100644
index 0000000..0187f46
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleNews.php
@@ -0,0 +1,44 @@
+dimension = new Keyword();
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_NewsKeywords');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_NewsKeywordsDocumentation');
+ $this->order = 25;
+ }
+ public function isEnabled()
+ {
+ return parent::isGoogleEnabledForType('news');
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ parent::configureView($view);
+ $this->configureViewMessagesGoogle($view, 'news');
+ $this->formatCtrAndPositionColumns($view);
+ $view->requestConfig->filter_limit = 5;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleVideo.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleVideo.php
new file mode 100644
index 0000000..6422ce7
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleVideo.php
@@ -0,0 +1,44 @@
+dimension = new Keyword();
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_VideoKeywords');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_VideoKeywordsDocumentation');
+ $this->order = 25;
+ }
+ public function isEnabled()
+ {
+ return parent::isGoogleEnabledForType('video');
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ parent::configureView($view);
+ $this->configureViewMessagesGoogle($view, 'video');
+ $this->formatCtrAndPositionColumns($view);
+ $view->requestConfig->filter_limit = 5;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleWeb.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleWeb.php
new file mode 100644
index 0000000..3234ed6
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleWeb.php
@@ -0,0 +1,43 @@
+dimension = new Keyword();
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_WebKeywords');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_WebKeywordsDocumentation');
+ $this->order = 10;
+ }
+ public function isEnabled()
+ {
+ return parent::isGoogleEnabledForType('web');
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ parent::configureView($view);
+ $this->configureViewMessagesGoogle($view, 'web');
+ $this->formatCtrAndPositionColumns($view);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsImported.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsImported.php
new file mode 100644
index 0000000..d11996f
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsImported.php
@@ -0,0 +1,58 @@
+dimension = new Keyword();
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_KeywordsCombinedImported');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_KeywordsCombinedImportedDocumentation');
+ $this->subcategoryId = null;
+ // hide report
+ }
+ public function isEnabled()
+ {
+ $reportsEnabled = 0;
+ $reportsEnabled += (int) parent::isGoogleEnabledForType('image');
+ $reportsEnabled += (int) parent::isGoogleEnabledForType('web');
+ $reportsEnabled += (int) parent::isGoogleEnabledForType('video');
+ $reportsEnabled += (int) parent::isGoogleEnabledForType('news');
+ $reportsEnabled += (int) parent::isBingEnabled();
+ return $reportsEnabled > 1;
+ }
+ public function getRelatedReports()
+ {
+ return [ReportsProvider::factory('SearchEngineKeywordsPerformance', 'getKeywords'), ReportsProvider::factory('Referrers', 'getKeywords')];
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ parent::configureView($view);
+ $this->formatCtrAndPositionColumns($view);
+ }
+ public function alwaysUseDefaultViewDataTable()
+ {
+ return \true;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsReferrers.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsReferrers.php
new file mode 100644
index 0000000..27472f0
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsReferrers.php
@@ -0,0 +1,46 @@
+name = Piwik::translate('SearchEngineKeywordsPerformance_KeywordsReferrers');
+ $this->module = 'Referrers';
+ $this->action = 'getKeywords';
+ $this->subcategoryId = 'Referrers_SubmenuSearchEngines';
+ $this->order = 10;
+ }
+ public function getRelatedReports()
+ {
+ // don't show related reports when viewing the goals page, as related reports don't contain goal data
+ if (Common::getRequestVar('viewDataTable', '') === 'tableGoals' && Common::getRequestVar('idGoal', '') !== '') {
+ return [];
+ }
+ $getKeywordsImported = new GetKeywordsImported();
+ if ($getKeywordsImported->isEnabled()) {
+ return [ReportsProvider::factory('SearchEngineKeywordsPerformance', 'getKeywordsImported'), ReportsProvider::factory('SearchEngineKeywordsPerformance', 'getKeywords')];
+ }
+ return [ReportsProvider::factory('SearchEngineKeywordsPerformance', 'getKeywords')];
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsYandex.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsYandex.php
new file mode 100644
index 0000000..de52173
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsYandex.php
@@ -0,0 +1,62 @@
+dimension = new Keyword();
+ $this->name = Piwik::translate('SearchEngineKeywordsPerformance_YandexKeywords');
+ $this->documentation = Piwik::translate('SearchEngineKeywordsPerformance_YandexKeywordsDocumentation');
+ $this->order = 17;
+ }
+ public function configureView(ViewDataTable $view)
+ {
+ parent::configureView($view);
+ $period = Common::getRequestVar('period', \false, 'string');
+ $idSite = Common::getRequestVar('idSite', \false, 'string');
+ $model = new ModelYandex();
+ $measurableSetting = new MeasurableSettings($idSite);
+ if (!SearchEngineKeywordsPerformance::isYandexForceEnabled($idSite)) {
+ [$account, $url] = explode('##', $measurableSetting->yandexAccountAndHostId->getValue());
+ $dateLastData = $model->getLatestDateKeywordDataIsAvailableFor($url);
+ }
+ if ($dateLastData && $period != 'range') {
+ $periodObjType = Period\Factory::build($period, Date::factory($dateLastData));
+ $lastDateMessage = Piwik::translate('SearchEngineKeywordsPerformance_LatestAvailableDate', '' . $periodObjType->getLocalizedShortString() . ' ');
+ $message = '' . Piwik::translate('CoreHome_ThereIsNoDataForThisReport') . ' ' . $lastDateMessage . '
';
+ $view->config->no_data_message = $message;
+ }
+ $this->formatCtrAndPositionColumns($view);
+ }
+ public function isEnabled()
+ {
+ return parent::isYandexEnabled();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/SearchEngineKeywordsPerformance.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/SearchEngineKeywordsPerformance.php
new file mode 100644
index 0000000..009768d
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/SearchEngineKeywordsPerformance.php
@@ -0,0 +1,962 @@
+ 'getStylesheetFiles',
+ 'Metrics.getDefaultMetricDocumentationTranslations' => 'addMetricDocumentationTranslations',
+ 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations',
+ 'Metrics.getDefaultMetricSemanticTypes' => 'addDefaultMetricSemanticTypes',
+ 'Metrics.isLowerValueBetter' => 'checkIsLowerMetricValueBetter',
+ 'ViewDataTable.configure.end' => 'configureViewDataTable',
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ 'Report.filterReports' => 'manipulateReports',
+ 'API.Request.intercept' => 'manipulateApiRequests',
+ 'API.Referrers.getAll' => 'manipulateAllReferrersReport',
+ 'API.Referrers.getSearchEngines' => 'manipulateSearchEnginesReportParameters',
+ 'API.Referrers.getSearchEngines.end' => 'manipulateSearchEnginesReport',
+ 'API.Referrers.getKeywordsFromSearchEngineId.end' => 'manipulateSearchEnginesKeywordsReport',
+ 'Request.dispatch' => 'manipulateRequests',
+ 'Archiving.getIdSitesToArchiveWhenNoVisits' => 'getIdSitesToArchiveWhenNoVisits',
+ 'Db.getTablesInstalled' => 'getTablesInstalled',
+ 'SearchEngineKeywordsPerformance.getGoogleConfigComponentExtensions' => 'getGoogleConfigComponent',
+ 'Archiver.addRecordBuilders' => 'addRecordBuilders'
+ ];
+ }
+ public function addRecordBuilders(array &$recordBuilders): void
+ {
+ $idSite = \Piwik\Request::fromRequest()->getIntegerParameter('idSite', 0);
+ if (!$idSite) {
+ return;
+ }
+ $bingBuilder = Bing::make($idSite);
+ if (!empty($bingBuilder)) {
+ $recordBuilders[] = $bingBuilder;
+ }
+ $googleBuilders = Google::makeAll($idSite);
+ $recordBuilders = array_merge($recordBuilders, $googleBuilders);
+ $yandexBuilder = Yandex::make($idSite);
+ if (!empty($yandexBuilder)) {
+ $recordBuilders[] = $yandexBuilder;
+ }
+ }
+ /**
+ * Register the new tables, so Matomo knows about them.
+ *
+ * @param array $allTablesInstalled
+ */
+ public function getTablesInstalled(&$allTablesInstalled)
+ {
+ $allTablesInstalled[] = Common::prefixTable('bing_stats');
+ $allTablesInstalled[] = Common::prefixTable('google_stats');
+ }
+ public function getIdSitesToArchiveWhenNoVisits(&$idSites)
+ {
+ $idSitesGoogle = ProviderGoogle::getInstance()->getConfiguredSiteIds();
+ $idSitesBing = ProviderBing::getInstance()->getConfiguredSiteIds();
+ $idSitesYandex = ProviderYandex::getInstance()->getConfiguredSiteIds();
+ $idSites = array_unique(array_merge($idSites, array_keys($idSitesBing), array_keys($idSitesGoogle), array_keys($idSitesYandex)));
+ }
+ /**
+ * Remove `GetKeywords` report of `Referrers` plugin, as it will be replaced with an inherited one
+ *
+ * @see \Piwik\Plugins\SearchEngineKeywordsPerformance\Reports\GetKeywordsReferrers
+ *
+ * @param $reports
+ */
+ public function manipulateReports(&$reports)
+ {
+ $reportsToUnset = ['\\Piwik\\Plugins\\Referrers\\Reports\\GetKeywords'];
+ $report = new \Piwik\Plugins\SearchEngineKeywordsPerformance\Reports\GetKeywords();
+ if (!$report->isBingEnabled() && !$report->isAnyGoogleTypeEnabled() && !$report->isYandexEnabled()) {
+ $reportsToUnset = [
+ '\\Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Reports\\GetKeywords',
+ '\\Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Reports\\GetKeywordsReferrers'
+ ];
+ }
+ foreach ($reportsToUnset as $unset) {
+ foreach ($reports as $key => $report) {
+ if ($report instanceof $unset) {
+ unset($reports[$key]);
+ break;
+ }
+ }
+ }
+ }
+ /**
+ * Manipulate some request so replaced reports look nice
+ *
+ * @param $module
+ * @param $action
+ * @param $parameters
+ */
+ public function manipulateRequests(&$module, &$action, &$parameters)
+ {
+ # Search Engines subtable of Channel Type report is replaced with combined keywords report
+ # as combined keywords report only has visits column, ensure to always use simple table view
+ if (
+ $module === 'Referrers' && $action === 'getReferrerType'
+ && Common::REFERRER_TYPE_SEARCH_ENGINE == Common::getRequestVar('idSubtable', '')
+ && 'tableAllColumns' == Common::getRequestVar('viewDataTable', '')
+ && !$this->shouldShowOriginalReports()
+ ) {
+ $_GET['viewDataTable'] = 'table';
+ }
+ # Keywords subtable of Search Engines report for configured providers are replaced
+ # as those reports only has visits column, ensure to always use simple table view
+ # also disable row evolution as it can't work
+ if ('Referrers' === $module && 'getKeywordsFromSearchEngineId' === $action && !$this->shouldShowOriginalReports()) {
+ /** @var DataTable $dataTable */
+ $dataTable = Request::processRequest('Referrers.getSearchEngines', ['idSubtable' => null, 'filter_limit' => -1, 'filter_offset' => 0]);
+ $row = $dataTable->getRowFromIdSubDataTable(Common::getRequestVar('idSubtable', ''));
+ if ($row) {
+ $label = $row->getColumn('label');
+ $report = new \Piwik\Plugins\SearchEngineKeywordsPerformance\Reports\GetKeywords();
+ if (strpos($label, 'Google') !== \false && $report->isAnyGoogleTypeEnabled()) {
+ $_GET['viewDataTable'] = 'table';
+ $_GET['disable_row_evolution'] = 1;
+ } elseif ((strpos($label, 'Bing') !== \false || strpos($label, 'Yahoo') !== \false) && $report->isBingEnabled()) {
+ $_GET['viewDataTable'] = 'table';
+ $_GET['disable_row_evolution'] = 1;
+ } elseif (strpos($label, 'Yandex') !== \false && $report->isYandexEnabled()) {
+ $_GET['viewDataTable'] = 'table';
+ $_GET['disable_row_evolution'] = 1;
+ }
+ }
+ }
+ }
+ /**
+ * Manipulate some api requests to replace the result
+ *
+ * @param $returnedValue
+ * @param $finalParameters
+ * @param $pluginName
+ * @param $methodName
+ * @param $parametersRequest
+ */
+ public function manipulateApiRequests(&$returnedValue, $finalParameters, $pluginName, $methodName, $parametersRequest = [])
+ {
+ $report = new \Piwik\Plugins\SearchEngineKeywordsPerformance\Reports\GetKeywords();
+ # Replace Search Engines subtable of Channel Type report combined keywords report if any import is configured
+ if ('Referrers' === $pluginName && 'getReferrerType' === $methodName && !empty($finalParameters['idSubtable']) && Common::REFERRER_TYPE_SEARCH_ENGINE == $finalParameters['idSubtable']) {
+ if (!$report->isAnyGoogleTypeEnabled() && !$report->isBingEnabled() && !$report->isYandexEnabled()) {
+ return;
+ }
+ # leave report untouched if original report should be shown
+ if ($this->shouldShowOriginalReports()) {
+ return;
+ }
+ $returnedValue = Request::processRequest('SearchEngineKeywordsPerformance.getKeywords', ['idSubtable' => null, 'segment' => null], $finalParameters);
+ $returnedValue->filter('ColumnCallbackAddMetadata', ['label', 'imported', function () {
+ return \true;
+ }]);
+ return;
+ }
+ #
+ # If a import is configured manipulate subtable requests for aggregated engine rows to show imported keywords
+ # (@see manipulateSearchEnginesReport())
+ #
+ if ('Referrers' === $pluginName && 'getKeywordsFromSearchEngineId' === $methodName) {
+ /** @var DataTable $dataTable */
+ $dataTable = Request::processRequest('Referrers.getSearchEngines', [
+ 'idSubtable' => null,
+ self::REQUEST_PARAM_ORIGINAL_REPORT => 1,
+ // needs to be loaded unexpanded as otherwise the row can't be found using the subtableid
+ 'expanded' => 0,
+ ], $finalParameters);
+ $row = $dataTable->getRowFromIdSubDataTable($finalParameters['idSubtable']);
+ if ($row) {
+ $label = $row->getColumn('label');
+ if ($this->shouldShowOriginalReports()) {
+ // load report with subtables
+ $dataTable = Request::processRequest('Referrers.getSearchEngines', [
+ 'idSubtable' => null,
+ self::REQUEST_PARAM_ORIGINAL_REPORT => 1,
+ // needs to be loaded expanded so we can merge the subtables to get them aggregated
+ 'expanded' => 1,
+ ], $finalParameters);
+ }
+ # If requesting a Google subtable and import is configured
+ if (strpos($label, 'Google') !== \false && $report->isAnyGoogleTypeEnabled()) {
+ # To show proper 'original' data for the aggregated row we need to combine all subtables of the aggregated rows
+ if ($this->shouldShowOriginalReports()) {
+ # remove all rows where label does not contain Google
+ $dataTable->disableRecursiveFilters();
+ $dataTable->filter('ColumnCallbackDeleteRow', ['label', function ($label) {
+ return \false === strpos($label, 'Google');
+ }]);
+ # combine subtables and group labels to get correct result
+ $returnedValue = $dataTable->mergeSubtables();
+ $returnedValue->filter('GroupBy', ['label']);
+ $returnedValue->filter('Piwik\\Plugins\\Referrers\\DataTable\\Filter\\KeywordNotDefined');
+ return;
+ }
+ # Return combined Google keywords
+ $returnedValue = Request::processRequest('SearchEngineKeywordsPerformance.getKeywordsGoogle', ['idSubtable' => null, 'segment' => null, 'filter_sort_column' => 'nb_clicks', 'filter_limit' => null]);
+ } elseif ((strpos($label, 'Bing') !== \false || strpos($label, 'Yahoo') !== \false) && $report->isBingEnabled()) {
+ # To show proper 'original' data for the aggregated row we need to combine all subtables of the aggregated rows
+ if ($this->shouldShowOriginalReports()) {
+ # remove all rows where label does not contain Bing or Yahoo
+ $dataTable->disableRecursiveFilters();
+ $dataTable->filter('ColumnCallbackDeleteRow', ['label', function ($label) {
+ return \false === strpos($label, 'Bing') && \false === strpos($label, 'Yahoo');
+ }]);
+ # combine subtables and group labels to get correct result
+ $returnedValue = $dataTable->mergeSubtables();
+ $returnedValue->filter('GroupBy', ['label']);
+ $returnedValue->filter('Piwik\\Plugins\\Referrers\\DataTable\\Filter\\KeywordNotDefined');
+ return;
+ }
+ # Return combined Bing & Yahoo! keywords
+ $returnedValue = Request::processRequest('SearchEngineKeywordsPerformance.getKeywordsBing', ['idSubtable' => null, 'segment' => null, 'filter_sort_column' => 'nb_clicks', 'filter_limit' => null]);
+ } elseif (strpos($label, 'Yandex') !== \false && $report->isYandexEnabled()) {
+ # To show proper 'original' data for the aggregated row we need to combine all subtables of the aggregated rows
+ if ($this->shouldShowOriginalReports()) {
+ # remove all rows where label does not contain Bing or Yahoo
+ $dataTable->disableRecursiveFilters();
+ $dataTable->filter('ColumnCallbackDeleteRow', ['label', function ($label) {
+ return \false === strpos($label, 'Yandex');
+ }]);
+ # combine subtables and group labels to get correct result
+ $returnedValue = $dataTable->mergeSubtables();
+ $returnedValue->filter('GroupBy', ['label']);
+ $returnedValue->filter('Piwik\\Plugins\\Referrers\\DataTable\\Filter\\KeywordNotDefined');
+ return;
+ }
+ # Return Yandex keywords
+ $returnedValue = Request::processRequest('SearchEngineKeywordsPerformance.getKeywordsYandex', ['idSubtable' => null, 'segment' => null, 'filter_sort_column' => 'nb_clicks', 'filter_limit' => null]);
+ }
+ }
+ # Adjust table so it only shows visits
+ $this->convertDataTableColumns($returnedValue, $dataTable);
+ }
+ }
+ public function manipulateAllReferrersReport(&$finalParameters)
+ {
+ # unset segment if default report is shown, as default `Referrers` report does not support segmentation
+ # as the imported keywords are hooked into the search engines subtable of `getReferrerType` report
+ if (!empty($finalParameters['segment']) && !$this->shouldShowOriginalReports()) {
+ $finalParameters['segment'] = \false;
+ }
+ }
+ public function manipulateSearchEnginesReportParameters(&$finalParameters)
+ {
+ # unset segment if default report is shown flattened, as `Search Engines` subtable reports do not support segmentation
+ # as the imported keywords are hooked into the search engines subtable of `getReferrerType` report
+ if (!empty($finalParameters['segment']) && !empty($finalParameters['flat']) && !$this->shouldShowOriginalReports()) {
+ $finalParameters['segment'] = \false;
+ }
+ }
+ public function manipulateSearchEnginesReport(&$returnedValue, $finalParameters)
+ {
+ # prevent replace for internal proposes
+ if ($this->shouldShowOriginalReports()) {
+ return;
+ }
+ $report = new \Piwik\Plugins\SearchEngineKeywordsPerformance\Reports\GetKeywords();
+ $bingEnabled = $report->isBingEnabled();
+ $googleEnabled = $report->isAnyGoogleTypeEnabled();
+ $yandexEnabled = $report->isYandexEnabled();
+ if (!$returnedValue instanceof DataTable\DataTableInterface || !$bingEnabled && !$googleEnabled && !$yandexEnabled) {
+ return;
+ }
+ #
+ # If any import is configured, rows of the `getSearchEngines` report will be aggregated into one row per imported engine
+ # e.g. `Google`, `Google Images`, and so on will be aggregated into a `Google` row. Same for Bing & Yahoo
+ #
+ $returnedValue->filter('GroupBy', ['label', function ($label) use ($bingEnabled, $googleEnabled, $yandexEnabled) {
+ if ($bingEnabled && (strpos($label, 'Yahoo') !== \false || strpos($label, 'Bing') !== \false)) {
+ return 'Bing & Yahoo!';
+ } else {
+ if ($googleEnabled && strpos($label, 'Google') !== \false) {
+ return 'Google';
+ } else {
+ if ($yandexEnabled && strpos($label, 'Yandex') !== \false) {
+ return 'Yandex';
+ }
+ }
+ }
+ return $label;
+ }]);
+ if ($returnedValue instanceof DataTable\Map) {
+ $tablesToFilter = $returnedValue->getDataTables();
+ } else {
+ $tablesToFilter = [$finalParameters['parameters']['date'] => $returnedValue];
+ }
+ // replace numbers for aggregated rows if no segment was applied
+ if (empty($finalParameters['parameters']['segment']) && Common::getRequestVar('viewDataTable', '') !== 'tableGoals') {
+ foreach ($tablesToFilter as $label => $table) {
+ $table->filter(function (DataTable $dataTable) use ($googleEnabled, $bingEnabled, $yandexEnabled, $label, $finalParameters) {
+ // label should hold the period representation
+ if ($finalParameters['parameters']['period'] != 'range') {
+ try {
+ // date representation might be year or year-month only, so fill it up to a full date and
+ // cut after 10 chars, as it might be too log now, or was a period representation before
+ $date = substr($label . '-01-01', 0, 10);
+ // if date is valid use it as label, otherwise original label will be used
+ // which is the case for e.g. `yesterday`
+ if (Date::factory($date)->toString() == $date) {
+ $label = $date;
+ }
+ } catch (\Exception $e) {
+ }
+ }
+ if ($googleEnabled) {
+ $row = $dataTable->getRowFromLabel('Google');
+ /** @var DataTable $subTable */
+ $subTable = Request::processRequest('SearchEngineKeywordsPerformance.getKeywordsGoogle', ['idSite' => $finalParameters['parameters']['idSite'], 'date' => $label, 'period' => $finalParameters['parameters']['period']], []);
+ $totalVisits = 0;
+ foreach ($subTable->getRowsWithoutSummaryRow() as $tableRow) {
+ $totalVisits += $tableRow->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS);
+ }
+ // replace subtable with processed data
+ if ($row && $row->getIdSubDataTable()) {
+ if ($row->isSubtableLoaded()) {
+ $row->setSubtable($this->convertDataTableColumns($subTable, $dataTable));
+ }
+ }
+ // add a new row if non exists yet and some data was imported
+ if ($totalVisits && !$row) {
+ $columns = ['label' => 'Google', \Piwik\Metrics::INDEX_NB_VISITS => $totalVisits];
+ $row = new DataTable\Row([DataTable\Row::COLUMNS => $columns]);
+ $row->setMetadata('imported', \true);
+ $dataTable->addRow($row);
+ }
+ $dataTable->deleteColumn(\Piwik\Metrics::INDEX_NB_UNIQ_VISITORS);
+ }
+ if ($bingEnabled) {
+ $row = $dataTable->getRowFromLabel('Bing & Yahoo!');
+ /** @var DataTable $subTable */
+ $subTable = Request::processRequest('SearchEngineKeywordsPerformance.getKeywordsBing', ['idSite' => $finalParameters['parameters']['idSite'], 'date' => $label, 'period' => $finalParameters['parameters']['period']], []);
+ $totalVisits = 0;
+ foreach ($subTable->getRowsWithoutSummaryRow() as $tableRow) {
+ $totalVisits += $tableRow->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS);
+ }
+ // replace subtable with processed data
+ if ($row && $row->getIdSubDataTable()) {
+ if ($row->isSubtableLoaded()) {
+ $row->setSubtable($this->convertDataTableColumns($subTable, $dataTable));
+ }
+ }
+ // add a new row if non exists yet and some data was imported
+ if ($totalVisits && !$row) {
+ $columns = ['label' => 'Bing & Yahoo!', \Piwik\Metrics::INDEX_NB_VISITS => $totalVisits];
+ $row = new DataTable\Row([DataTable\Row::COLUMNS => $columns]);
+ $row->setMetadata('imported', \true);
+ $dataTable->addRow($row);
+ }
+ $dataTable->deleteColumn(\Piwik\Metrics::INDEX_NB_UNIQ_VISITORS);
+ }
+ if ($yandexEnabled) {
+ $row = $dataTable->getRowFromLabel('Yandex');
+ /** @var DataTable $subTable */
+ $subTable = Request::processRequest('SearchEngineKeywordsPerformance.getKeywordsYandex', ['idSite' => $finalParameters['parameters']['idSite'], 'date' => $label, 'period' => $finalParameters['parameters']['period']], []);
+ $totalVisits = 0;
+ foreach ($subTable->getRowsWithoutSummaryRow() as $tableRow) {
+ $totalVisits += $tableRow->getColumn(\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS);
+ }
+ // replace subtable with processed data
+ if ($row && $row->getIdSubDataTable()) {
+ if ($row->isSubtableLoaded()) {
+ $row->setSubtable($this->convertDataTableColumns($subTable, $dataTable));
+ }
+ }
+ // add a new row if non exists yet and some data was imported
+ if ($totalVisits && !$row) {
+ $columns = ['label' => 'Yandex', \Piwik\Metrics::INDEX_NB_VISITS => $totalVisits];
+ $row = new DataTable\Row([DataTable\Row::COLUMNS => $columns]);
+ $row->setMetadata('imported', \true);
+ $dataTable->addRow($row);
+ }
+ $dataTable->deleteColumn(\Piwik\Metrics::INDEX_NB_UNIQ_VISITORS);
+ }
+ });
+ }
+ }
+ if ($bingEnabled || $googleEnabled || $yandexEnabled) {
+ $returnedValue->queueFilter('Sort', [reset($tablesToFilter)->getSortedByColumnName() ?: 'nb_visits']);
+ }
+ // needs to be done as queued filter as metadata will fail otherwise
+ if ($bingEnabled) {
+ $returnedValue->queueFilter(function (DataTable $datatable) {
+ $row = $datatable->getRowFromLabel('Bing & Yahoo!');
+ if ($row) {
+ // rename column and fix metadata
+ $url = SearchEngine::getInstance()->getUrlFromName('Bing');
+ $row->setMetadata('url', $url);
+ $row->setMetadata('logo', SearchEngine::getInstance()->getLogoFromUrl($url));
+ $row->setMetadata('segment', 'referrerType==search;referrerName=@Bing,referrerName=@Yahoo');
+ }
+ });
+ }
+ if ($googleEnabled) {
+ $returnedValue->queueFilter(function (DataTable $datatable) {
+ $row = $datatable->getRowFromLabel('Google');
+ if ($row) {
+ // rename column and fix metadata
+ $url = SearchEngine::getInstance()->getUrlFromName('Google');
+ $row->setMetadata('url', $url);
+ $row->setMetadata('logo', SearchEngine::getInstance()->getLogoFromUrl($url));
+ $row->setMetadata('segment', 'referrerType==search;referrerName=@Google');
+ }
+ });
+ }
+ if ($yandexEnabled) {
+ $returnedValue->queueFilter(function (DataTable $datatable) {
+ $row = $datatable->getRowFromLabel('Yandex');
+ if ($row) {
+ // rename column and fix metadata
+ $url = SearchEngine::getInstance()->getUrlFromName('Yandex');
+ $row->setMetadata('url', $url);
+ $row->setMetadata('logo', SearchEngine::getInstance()->getLogoFromUrl($url));
+ $row->setMetadata('segment', 'referrerType==search;referrerName=@Yandex');
+ }
+ });
+ }
+ }
+ /**
+ * Manipulates the segment metadata of the aggregated columns in search engines report
+ * This needs to be done in `API.Referrers.getKeywordsFromSearchEngineId.end` event as the queued filters would
+ * otherwise be applied to early and segment metadata would be overwritten again
+ *
+ * @param mixed $returnedValue
+ * @param array $finalParameters
+ */
+ public function manipulateSearchEnginesKeywordsReport(&$returnedValue, $finalParameters)
+ {
+ # only manipulate the original report with aggregated columns
+ if (!$this->shouldShowOriginalReports()) {
+ return;
+ }
+ $report = new \Piwik\Plugins\SearchEngineKeywordsPerformance\Reports\GetKeywords();
+ /** @var DataTable $dataTable */
+ $dataTable = Request::processRequest('Referrers.getSearchEngines', [
+ 'idSubtable' => null,
+ self::REQUEST_PARAM_ORIGINAL_REPORT => 1,
+ // needs to be loaded unexpanded as otherwise the row can't be found using the subtableid
+ 'expanded' => 0,
+ ]);
+ $row = $dataTable->getRowFromIdSubDataTable($finalParameters['parameters']['idSubtable']);
+ if ($row) {
+ $label = $row->getColumn('label');
+ # If requesting a Google subtable and import is configured
+ if (strpos($label, 'Google') !== \false && $report->isAnyGoogleTypeEnabled()) {
+ $returnedValue->queueFilter('ColumnCallbackDeleteMetadata', ['segment']);
+ $returnedValue->queueFilter('ColumnCallbackAddMetadata', ['label', 'segment', function ($label) {
+ if ($label == Referrers\API::getKeywordNotDefinedString()) {
+ return 'referrerKeyword==;referrerType==search;referrerName=@Google';
+ }
+ return 'referrerKeyword==' . urlencode($label) . ';referrerType==search;referrerName=@Google';
+ }]);
+ } elseif ((strpos($label, 'Bing') !== \false || strpos($label, 'Yahoo') !== \false) && $report->isBingEnabled()) {
+ $returnedValue->queueFilter('ColumnCallbackDeleteMetadata', ['segment']);
+ $returnedValue->queueFilter('ColumnCallbackAddMetadata', ['label', 'segment', function ($label) {
+ if ($label == Referrers\API::getKeywordNotDefinedString()) {
+ return 'referrerKeyword==;referrerType==search;referrerName=@Bing,referrerName=@Yahoo';
+ }
+ return 'referrerKeyword==' . urlencode($label) . ';referrerType==search;referrerName=@Bing,referrerName=@Yahoo';
+ }]);
+ } elseif (strpos($label, 'Yandex') !== \false && $report->isYandexEnabled()) {
+ $returnedValue->queueFilter('ColumnCallbackDeleteMetadata', ['segment']);
+ $returnedValue->queueFilter('ColumnCallbackAddMetadata', ['label', 'segment', function ($label) {
+ if ($label == Referrers\API::getKeywordNotDefinedString()) {
+ return 'referrerKeyword==;referrerType==search;referrerName=@Yandex';
+ }
+ return 'referrerKeyword==' . urlencode($label) . ';referrerType==search;referrerName=@Yandex';
+ }]);
+ }
+ }
+ }
+ public function configureViewDataTable(ViewDataTable $viewDataTable)
+ {
+ $report = new \Piwik\Plugins\SearchEngineKeywordsPerformance\Reports\GetKeywords();
+ $bingEnabled = $report->isBingEnabled();
+ $yandexEnabled = $report->isYandexEnabled();
+ $googleEnabled = $report->isAnyGoogleTypeEnabled();
+ #
+ # Add a header message to original referrer keywords reports if plugin is active but no import configured
+ #
+ if ($viewDataTable->requestConfig->apiMethodToRequestDataTable == 'Referrers.getKeywords') {
+ // Check if there have been any recent API errors and notify the customer accordingly.
+ self::displayNotificationIfRecentApiErrorsExist([ProviderGoogle::getInstance(), ProviderBing::getInstance(), ProviderYandex::getInstance()]);
+ if (Common::getRequestVar('widget', 0, 'int')) {
+ return;
+ }
+ if ($bingEnabled || $googleEnabled || $yandexEnabled) {
+ return;
+ }
+ $idSite = Common::getRequestVar('idSite', 0, 'int');
+ if (WebsiteMeasurableType::ID !== Site::getTypeFor($idSite)) {
+ return;
+ }
+ $view = new View('@SearchEngineKeywordsPerformance/messageReferrerKeywordsReport');
+ $view->hasAdminPriviliges = Piwik::isUserHasSomeAdminAccess();
+ $message = $view->render();
+ if (property_exists($viewDataTable->config, 'show_header_message')) {
+ $viewDataTable->config->show_header_message = $message;
+ } else {
+ $viewDataTable->config->show_footer_message .= $message;
+ }
+ }
+ #
+ # Add related reports and segment info to search engines subtable in `All Channels` report
+ #
+ if ('Referrers.getReferrerType' === $viewDataTable->requestConfig->apiMethodToRequestDataTable && !empty($viewDataTable->requestConfig->idSubtable) && Common::REFERRER_TYPE_SEARCH_ENGINE == $viewDataTable->requestConfig->idSubtable) {
+ if (!$bingEnabled && !$googleEnabled && !$yandexEnabled) {
+ return;
+ }
+ if ($this->shouldShowOriginalReports()) {
+ $viewDataTable->config->addRelatedReport('Referrers.getReferrerType', Piwik::translate('SearchEngineKeywordsPerformance_KeywordsSubtableImported'), [self::REQUEST_PARAM_ORIGINAL_REPORT => 0]);
+ } else {
+ $viewDataTable->config->addRelatedReport('Referrers.getReferrerType', Piwik::translate('SearchEngineKeywordsPerformance_KeywordsSubtableOriginal'), [self::REQUEST_PARAM_ORIGINAL_REPORT => 1]);
+ if ('' != Common::getRequestVar('segment', '')) {
+ $this->addSegmentNotSupportedMessage($viewDataTable);
+ }
+ }
+ }
+ #
+ # Add related reports and segment info to `Referrers` report
+ #
+ if ('Referrers.getAll' === $viewDataTable->requestConfig->apiMethodToRequestDataTable) {
+ if (!$bingEnabled && !$googleEnabled && !$yandexEnabled) {
+ return;
+ }
+ if ($this->shouldShowOriginalReports()) {
+ $viewDataTable->config->addRelatedReport('Referrers.getAll', Piwik::translate('SearchEngineKeywordsPerformance_AllReferrersImported'), [self::REQUEST_PARAM_ORIGINAL_REPORT => 0]);
+ } else {
+ $viewDataTable->config->addRelatedReport('Referrers.getAll', Piwik::translate('SearchEngineKeywordsPerformance_AllReferrersOriginal'), [self::REQUEST_PARAM_ORIGINAL_REPORT => 1]);
+ if ('' != Common::getRequestVar('segment', '')) {
+ $this->addSegmentNotSupportedMessage($viewDataTable);
+ }
+ }
+ }
+ #
+ # Add segment info to `Search Engines` report if it is flattened
+ #
+ if ('Referrers.getSearchEngines' === $viewDataTable->requestConfig->apiMethodToRequestDataTable) {
+ if ($viewDataTable->requestConfig->flat) {
+ if ($this->shouldShowOriginalReports()) {
+ $viewDataTable->config->addRelatedReport('Referrers.getSearchEngines', Piwik::translate('SearchEngineKeywordsPerformance_SearchEnginesImported'), [self::REQUEST_PARAM_ORIGINAL_REPORT => 0]);
+ } else {
+ $viewDataTable->config->addRelatedReport('Referrers.getSearchEngines', Piwik::translate('SearchEngineKeywordsPerformance_SearchEnginesOriginal'), [self::REQUEST_PARAM_ORIGINAL_REPORT => 1]);
+ if ('' != Common::getRequestVar('segment', '')) {
+ $this->addSegmentNotSupportedMessage($viewDataTable);
+ }
+ }
+ }
+ }
+ #
+ # Add related reports and segment info to keywords subtable in `Search Engines` report
+ #
+ if ('Referrers.getKeywordsFromSearchEngineId' === $viewDataTable->requestConfig->apiMethodToRequestDataTable) {
+ /** @var DataTable $dataTable */
+ $dataTable = Request::processRequest('Referrers.getSearchEngines', ['idSubtable' => null, self::REQUEST_PARAM_ORIGINAL_REPORT => 1]);
+ $row = $dataTable->getRowFromIdSubDataTable($viewDataTable->requestConfig->idSubtable);
+ if ($row) {
+ $label = $row->getColumn('label');
+ if (
+ strpos($label, 'Google') !== \false && $report->isAnyGoogleTypeEnabled()
+ || strpos($label, 'Yandex') !== \false && $report->isYandexEnabled()
+ || (strpos($label, 'Bing') !== \false
+ || strpos($label, 'Yahoo') !== \false) && $report->isBingEnabled()
+ ) {
+ if ($this->shouldShowOriginalReports()) {
+ $viewDataTable->config->addRelatedReport('Referrers.getKeywordsFromSearchEngineId', Piwik::translate('SearchEngineKeywordsPerformance_KeywordsSubtableImported'), [self::REQUEST_PARAM_ORIGINAL_REPORT => 0]);
+ } else {
+ $viewDataTable->config->addRelatedReport('Referrers.getKeywordsFromSearchEngineId', Piwik::translate('SearchEngineKeywordsPerformance_KeywordsSubtableOriginal'), [self::REQUEST_PARAM_ORIGINAL_REPORT => 1]);
+ if ('' != Common::getRequestVar('segment', '')) {
+ $this->addSegmentNotSupportedMessage($viewDataTable);
+ }
+ }
+ }
+ }
+ }
+ }
+ /**
+ * Removes all rows that does not have a single click, removes all other metrics than clicks, and renames clicks to visits
+ *
+ * @param $dataTable
+ * @param $parentTable
+ * @return mixed
+ */
+ protected function convertDataTableColumns($dataTable, $parentTable = null)
+ {
+ if ($dataTable instanceof DataTable\DataTableInterface) {
+ $dataTable->deleteColumns([\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::POSITION, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_IMPRESSIONS, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::CTR]);
+ $dataTable->filter('ColumnCallbackDeleteRow', [\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS, function ($clicks) {
+ return $clicks === 0;
+ }]);
+ $dataTable->filter('ReplaceColumnNames', [[\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS => 'nb_visits']]);
+ if ($dataTable instanceof DataTable && $parentTable instanceof DataTable) {
+ $totals = $dataTable->getMetadata('totals');
+ $parentTotals = $parentTable->getMetadata('totals');
+ if (!empty($parentTotals['nb_visits']) && !empty($totals[\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS])) {
+ $totals['nb_visits'] = $parentTotals['nb_visits'];
+ unset($totals[\Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::NB_CLICKS]);
+ $dataTable->setMetadata('totals', $totals);
+ }
+ }
+ }
+ return $dataTable;
+ }
+ /**
+ * Returns whether the internal request parameter to prevent api manipulations was set
+ *
+ * @return bool
+ */
+ protected function shouldShowOriginalReports()
+ {
+ return !!Common::getRequestVar(self::REQUEST_PARAM_ORIGINAL_REPORT, \false);
+ }
+ /**
+ * Adds a header (or footer) note to the view, that report does not support segmentation
+ *
+ * @param ViewDataTable $view
+ */
+ protected function addSegmentNotSupportedMessage(ViewDataTable $view)
+ {
+ $message = '' . Piwik::translate('SearchEngineKeywordsPerformance_NoSegmentation') . '
';
+ if (property_exists($view->config, 'show_header_message')) {
+ $view->config->show_header_message = $message;
+ } else {
+ $view->config->show_footer_message .= $message;
+ }
+ }
+ public function getStylesheetFiles(&$stylesheets)
+ {
+ $stylesheets[] = "plugins/SearchEngineKeywordsPerformance/stylesheets/styles.less";
+ $stylesheets[] = "plugins/SearchEngineKeywordsPerformance/vue/src/Configure/ConfigureConnection.less";
+ }
+ public function addDefaultMetricSemanticTypes(&$types): void
+ {
+ $types = array_merge($types, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::getMetricSemanticTypes());
+ }
+ public function addMetricTranslations(&$translations)
+ {
+ $translations = array_merge($translations, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::getMetricsTranslations());
+ }
+ public function addMetricDocumentationTranslations(&$translations)
+ {
+ $translations = array_merge($translations, \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::getMetricsDocumentation());
+ }
+ public function checkIsLowerMetricValueBetter(&$isLowerBetter, $metric)
+ {
+ if ($metric === \Piwik\Plugins\SearchEngineKeywordsPerformance\Metrics::POSITION) {
+ $isLowerBetter = \true;
+ }
+ }
+ public function getClientSideTranslationKeys(&$translationKeys)
+ {
+ $translationKeys[] = "SearchEngineKeywordsPerformance_LinksToUrl";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_SitemapsContainingUrl";
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_SearchEngineKeywordsPerformance';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConfigurationDescription';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ProviderListDescription';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ChangeConfiguration';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_SetupConfiguration';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_BingConfigurationTitle';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_BingConfigurationDescription';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConfigureMeasurables';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConfigureMeasurableBelow';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConfigRemovalConfirm';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_DomainProperty';
+ $translationKeys[] = 'General_Measurable';
+ $translationKeys[] = 'Mobile_Account';
+ $translationKeys[] = 'Goals_URL';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_LastImport';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_NoWebsiteConfigured';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_AddConfiguration';
+ $translationKeys[] = 'CoreHome_ChooseX';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_UrlOfAccount';
+ $translationKeys[] = 'General_Save';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ManageAPIKeys';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_AccountRemovalConfirm';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_AccountAddedBy';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_BingAccountError';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_AccountNoAccess';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_AvailableSites';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_UnverifiedSites';
+ $translationKeys[] = 'General_Remove';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_AddAPIKey';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_BingAPIKeyInstruction';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_APIKey';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_GoogleConfigurationTitle';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_GoogleConfigurationDescription';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_EnabledSearchTypes';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_KeywordTypeWeb';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_KeywordTypeImage';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_KeywordTypeVideo';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_KeywordTypeNews';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_DomainPropertyInfo';
+ $translationKeys[] = 'General_Delete';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConnectGoogleAccounts';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_CurrentlyConnectedAccounts';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConnectFirstAccount';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_OAuthError';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_AccountConnectionValidationError';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ReAddAccountIfPermanentError';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConnectAccount';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConnectAccountDescription';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_RequiredAccessTypes';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_GoogleAccountAccessTypeSearchConsoleData';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_GoogleAccountAccessTypeProfileInfo';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_GoogleAccountAccessTypeOfflineAccess';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_StartOAuth';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_OAuthClientConfig';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ClientId';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ClientSecret';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_HowToGetOAuthClientConfig';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_VisitOAuthHowTo';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_UploadOAuthClientConfig';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_GoogleUploadOrPasteClientConfig';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConfigurationFile';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_Configuration';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_CreatedBy';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_YandexConfigurationTitle';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_YandexConfigurationDescription';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConnectYandexAccounts';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ReAuthenticateIfPermanentError';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_OAuthAccessTimedOut';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_OAuthAccessWillTimeOutSoon';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_OAuthAccessWillTimeOut';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_Reauthenticate';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ConnectAccountYandex';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_SetUpOAuthClientConfig';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_ProvideYandexClientConfig';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_URLPrefixProperty';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_URLPrefixPropertyInfo';
+ $translationKeys[] = "SearchEngineKeywordsPerformance_OAuthExampleText";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_GoogleAuthorizedJavaScriptOrigin";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_GoogleAuthorizedRedirectUri";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_YandexFieldUrlToAppSite";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_YandexFieldCallbackUri";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_RecentApiErrorsWarning";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_ConfigureTheImporterLabel1";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_ConfigureTheImporterLabel2";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_ConfigureTheImporterLabel3";
+ $translationKeys[] = "SearchEngineKeywordsPerformance_OauthFailedMessage";
+ $translationKeys[] = "General_Upload";
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_Uploading';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_DeleteUploadedClientConfig';
+ $translationKeys[] = 'SearchEngineKeywordsPerformance_GooglePendingConfigurationErrorMessage';
+ if (Manager::getInstance()->isPluginActivated('ConnectAccounts')) {
+ $translationKeys[] = "ConnectAccounts_ConfigureGoogleAuthHelp1";
+ $translationKeys[] = "ConnectAccounts_ConfigureGoogleAuthHelp2";
+ $translationKeys[] = "ConnectAccounts_OptionQuickConnectWithGa";
+ $translationKeys[] = "ConnectAccounts_OptionAdvancedConnectWithGa";
+ $translationKeys[] = "ConnectAccounts_ConnectedWithBody";
+ $translationKeys[] = "ConnectAccounts_ReAuthorizeBody";
+ $translationKeys[] = "ConnectAccounts_ReAuthorizeBtnText";
+ $translationKeys[] = "ConnectAccounts_ConnectedWithHeader";
+ $translationKeys[] = "ConnectAccounts_ConnectedWithBody[beforeLink]";
+ $translationKeys[] = "ConnectAccounts_ConnectedWithBody[linkText]";
+ $translationKeys[] = "ConnectAccounts_GaImportBtn";
+ }
+ }
+ /**
+ * Installation
+ */
+ public function install()
+ {
+ GoogleModel::install();
+ BingModel::install();
+ YandexModel::install();
+ }
+ public static function isGoogleForceEnabled($idSite)
+ {
+ return self::isSearchEngineForceEnabled('google', $idSite);
+ }
+ public static function isBingForceEnabled($idSite)
+ {
+ return self::isSearchEngineForceEnabled('bing', $idSite);
+ }
+ public static function isYandexForceEnabled($idSite)
+ {
+ return self::isSearchEngineForceEnabled('yandex', $idSite);
+ }
+ public static function isSearchEngineForceEnabled($searchEngine, $idSite)
+ {
+ if (!is_numeric($idSite) || $idSite <= 0) {
+ return \false;
+ }
+ $cache = Cache::getTransientCache();
+ $cacheKey = 'SearchEngineKeywordsPerformance.isSearchEngineForceEnabled.' . $searchEngine . '.' . $idSite;
+ if ($cache->contains($cacheKey)) {
+ return $cache->fetch($cacheKey);
+ }
+ $result = \false;
+ /**
+ * @ignore
+ */
+ Piwik::postEvent('SearchEngineKeywordsPerformance.isSearchEngineForceEnabled', [&$result, $searchEngine, $idSite]);
+ // force enable reports for rollups where child pages are configured
+ if (class_exists('\\Piwik\\Plugins\\RollUpReporting\\Type') && \Piwik\Plugins\RollUpReporting\Type::ID === Site::getTypeFor($idSite)) {
+ //Need to execute this as a superuser, since a user can have access to a rollup site but not all the child sites
+ Access::doAsSuperUser(function () use ($searchEngine, $idSite, &$result) {
+ $rollUpModel = new \Piwik\Plugins\RollUpReporting\Model();
+ $childIdSites = $rollUpModel->getChildIdSites($idSite);
+ foreach ($childIdSites as $childIdSite) {
+ $measurableSettings = new \Piwik\Plugins\SearchEngineKeywordsPerformance\MeasurableSettings($childIdSite);
+ if ($searchEngine === 'google' && $measurableSettings->googleSearchConsoleUrl && $measurableSettings->googleSearchConsoleUrl->getValue()) {
+ $result = \true;
+ break;
+ }
+ if ($searchEngine === 'yandex' && $measurableSettings->yandexAccountAndHostId && $measurableSettings->yandexAccountAndHostId->getValue()) {
+ $result = \true;
+ break;
+ }
+ if ($searchEngine === 'bing' && $measurableSettings->bingSiteUrl && $measurableSettings->bingSiteUrl->getValue()) {
+ $result = \true;
+ break;
+ }
+ }
+ });
+ }
+ $cache->save($cacheKey, $result);
+ return $result;
+ }
+ /**
+ * Take the list of providers and determine if any of them have had any recent API errors that need to be surfaced
+ * to the customer as a notification. If there are any, it generates the notification message and displays it.
+ *
+ * @param array $providers Collection of providers (like Google, Bing, ...) that extend the ProviderAbstract class.
+ * @return void
+ * @throws \Exception
+ */
+ public static function displayNotificationIfRecentApiErrorsExist(array $providers): void
+ {
+ $recentErrorMessages = [];
+ foreach ($providers as $provider) {
+ $message = '';
+ if ($provider->hasApiErrorWithinWeek()) {
+ $message = $provider->getRecentApiErrorMessage();
+ }
+ if (!empty($message)) {
+ $recentErrorMessages[] = $message;
+ }
+ }
+ // Show notification if there have been errors
+ if (count($recentErrorMessages) > 0) {
+ $providerNameString = implode(' ', $recentErrorMessages);
+ $notification = new Notification(Piwik::translate('SearchEngineKeywordsPerformance_RecentApiErrorsWarning', [$providerNameString]));
+ $notification->context = Notification::CONTEXT_WARNING;
+ $notification->raw = \true;
+ Notification\Manager::notify('sekp_api_errors', $notification);
+ }
+ }
+ public function getGoogleConfigComponent(&$componentExtensions)
+ {
+ $googleClient = ProviderGoogle::getInstance()->getClient();
+ $clientConfigured = \true;
+ // try to configure client (which imports provided client configs)
+ try {
+ $googleClient->getConfiguredClient('');
+ } catch (\Exception $e) {
+ // ignore errors
+ $clientConfigured = \false;
+ }
+ if (!$clientConfigured) {
+ // Only set index 0 if it hasn't already been set, since we want ConnectAccounts to take precedence
+ $componentExtensions[0] = $componentExtensions[0] ?? ['plugin' => 'SearchEngineKeywordsPerformance', 'component' => 'ConfigureConnection'];
+ }
+ return $componentExtensions;
+ }
+
+
+ public static function isReportEnabled($reportType, $googleType = '')
+ {
+ $report = new \Piwik\Plugins\SearchEngineKeywordsPerformance\Reports\GetKeywords();
+ if ($reportType === 'Yandex') {
+ return $report->isYandexEnabled();
+ } elseif ($reportType === 'Bing') {
+ return $report->isBingEnabled();
+ } elseif ($reportType === 'Google' && !empty($googleType)) {
+ return $report->isGoogleEnabledForType($googleType);
+ }
+
+ return false;
+ }
+
+ public static function commonEmptyDataTable($period, $date)
+ {
+ if (Period::isMultiplePeriod($date, $period)) {
+ return new DataTable\Map();
+ }
+ return new DataTable();
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/SystemSettings.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/SystemSettings.php
new file mode 100644
index 0000000..b38d3c5
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/SystemSettings.php
@@ -0,0 +1,41 @@
+roundKeywordPosition = $this->createRoundKeywordPosition();
+ }
+ private function createRoundKeywordPosition()
+ {
+ return $this->makeSetting('roundKeywordPosition', $default = \true, FieldConfig::TYPE_BOOL, function (FieldConfig $field) {
+ $field->title = Piwik::translate('SearchEngineKeywordsPerformance_RoundKeywordPosition');
+ $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX;
+ });
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Tasks.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Tasks.php
new file mode 100644
index 0000000..b826484
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Tasks.php
@@ -0,0 +1,101 @@
+getAllSitesId();
+ foreach ($siteIds as $idSite) {
+ $setting = new \Piwik\Plugins\SearchEngineKeywordsPerformance\MeasurableSettings($idSite);
+ $searchConsoleUrl = $setting->googleSearchConsoleUrl;
+ if ($searchConsoleUrl && $searchConsoleUrl->getValue()) {
+ $this->daily('runImportsGoogle', $idSite);
+ }
+ $bingSiteUrl = $setting->bingSiteUrl;
+ if ($bingSiteUrl && $bingSiteUrl->getValue()) {
+ $this->daily('runImportsBing', $idSite);
+ }
+ $yandexConfig = $setting->yandexAccountAndHostId;
+ if ($yandexConfig && $yandexConfig->getValue()) {
+ $this->daily('runImportsYandex', $idSite);
+ }
+ }
+ }
+ /**
+ * Run Google importer for the last X available dates
+ * To calculate the amount of imported days a timestamp of the last run will be saved
+ * and checked how many days it was ago. This ensures dates will be imported even if
+ * the tasks doesn't run some days. And it also ensure that all available dates will be
+ * imported on the first run, as no last run has been saved before
+ *
+ * @param int $idSite
+ */
+ public function runImportsGoogle($idSite)
+ {
+ $lastRun = Option::get('GoogleImporterTask_LastRun_' . $idSite);
+ $now = time();
+ $limitDays = 0;
+ if ($lastRun) {
+ $difference = $now - $lastRun;
+ $limitDays = ceil($difference / (3600 * 24));
+ }
+ $importer = new GoogleImporter($idSite);
+ $importer->importAllAvailableData($limitDays);
+ Option::set('GoogleImporterTask_LastRun_' . $idSite, $now);
+ }
+ /**
+ * Run Bing importer
+ *
+ * @param int $idSite
+ */
+ public function runImportsBing($idSite)
+ {
+ $importer = new BingImporter($idSite);
+ $importer->importAllAvailableData();
+ Option::set('BingImporterTask_LastRun_' . $idSite, time());
+ }
+ /**
+ * Run Yandex importer
+ *
+ * @param int $idSite
+ */
+ public function runImportsYandex($idSite)
+ {
+ $lastRun = Option::get('YandexImporterTask_LastRun_' . $idSite);
+ $now = time();
+ $limitDays = 100;
+ // import 100 days initially
+ if ($lastRun) {
+ $difference = $now - $lastRun;
+ $limitDays = ceil($difference / (3600 * 24)) + 7;
+ }
+ $importer = new YandexImporter($idSite);
+ $importer->importAllAvailableData($limitDays);
+ Option::set('YandexImporterTask_LastRun_' . $idSite, time());
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/3.5.0.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/3.5.0.php
new file mode 100644
index 0000000..99b14f7
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/3.5.0.php
@@ -0,0 +1,45 @@
+migration = $factory;
+ }
+ public function getMigrations(Updater $updater)
+ {
+ $migrations = [];
+ $migrations[] = $this->migration->db->changeColumnType('bing_stats', 'type', 'VARCHAR(15)');
+ $migrations[] = $this->migration->db->changeColumnType('bing_stats', 'url', 'VARCHAR(170)');
+ $migrations[] = $this->migration->db->changeColumnType('google_stats', 'url', 'VARCHAR(170)');
+ return $migrations;
+ }
+ public function doUpdate(Updater $updater)
+ {
+ $updater->executeMigrations(__FILE__, $this->getMigrations($updater));
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/4.1.0.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/4.1.0.php
new file mode 100644
index 0000000..4294903
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/4.1.0.php
@@ -0,0 +1,30 @@
+ \true,
+ 'Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Client\\Google' => \Piwik\DI::autowire(),
+ 'diagnostics.optional' => \Piwik\DI::add([
+ \Piwik\DI::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Diagnostic\\BingAccountDiagnostic'),
+ \Piwik\DI::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Diagnostic\\GoogleAccountDiagnostic'),
+ \Piwik\DI::get('Piwik\\Plugins\\SearchEngineKeywordsPerformance\\Diagnostic\\YandexAccountDiagnostic')
+ ]),
+ // defines the number of days the plugin will try to import Google keywords for
+ // Google API itself currently supports up to 500 days in the past
+ 'SearchEngineKeywordsPerformance.Google.ImportLastDaysMax' => 365,
+ 'SearchEngineKeywordsPerformance.Google.googleClient' => function (\Piwik\Container\Container $c) {
+ $googleClient = new \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Client();
+ $googleClient->addScope(\Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\SearchConsole::WEBMASTERS_READONLY);
+ $googleClient->addScope(\Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Oauth2::USERINFO_PROFILE);
+ $googleClient->setAccessType('offline');
+ $googleClient->setApprovalPrompt('force');
+ $redirectUrl = Url::getCurrentUrlWithoutQueryString() . '?module=SearchEngineKeywordsPerformance&action=processAuthCode';
+ $googleClient->setRedirectUri($redirectUrl);
+ return $googleClient;
+ },
+];
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/docs/index.md b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/docs/index.md
new file mode 100644
index 0000000..f8e8d71
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/docs/index.md
@@ -0,0 +1,3 @@
+## Documentation
+
+The [Search Engine Keywords Performance User Guide](https://matomo.org/docs/search-engine-keywords-performance/) and the [Search Engine Keywords Performance FAQ](https://matomo.org/faq/search-engine-keywords-performance/) cover how to get the most out of this plugin.
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Bing.png b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Bing.png
new file mode 100644
index 0000000..3436b29
Binary files /dev/null and b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Bing.png differ
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Google.png b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Google.png
new file mode 100644
index 0000000..b0699bf
Binary files /dev/null and b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Google.png differ
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Yahoo.png b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Yahoo.png
new file mode 100644
index 0000000..ce31701
Binary files /dev/null and b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Yahoo.png differ
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Yandex.png b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Yandex.png
new file mode 100644
index 0000000..4271c86
Binary files /dev/null and b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Yandex.png differ
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/bg.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/bg.json
new file mode 100644
index 0000000..75425f8
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/bg.json
@@ -0,0 +1,6 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "AccountAddedBy": "Добавено от %1$s на %2$s ",
+ "AccountConnectionValidationError": "Възникна грешка при валидиране на връзката с акаунта:"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/cs.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/cs.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/cs.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/da.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/da.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/da.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/de.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/de.json
new file mode 100644
index 0000000..ed91db3
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/de.json
@@ -0,0 +1,224 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "APIKey": "API-Key",
+ "AccountAddedBy": "Hinzugefügt von %1$s am %2$s ",
+ "AccountConnectionValidationError": "Beim Überprüfen der Konto-Verknüpfung ist ein Fehler aufgetreten:",
+ "AccountDoesNotExist": "Das konfigurierte Konto %1$s existiert nicht mehr",
+ "AccountNoAccess": "Dieses Konto hat derzeit keinen Zugriff auf irgendeine Website.",
+ "AccountRemovalConfirm": "Sie sind im Begriff das Konto %1$s zu löschen. Dies deaktiviert den Import für all noch verknüpften Websites. Trotzdem fortfahren?",
+ "ActivityAccountAdded": "hat ein neues Konto für die Suchbegriff-Anbindung von %1$s hinzugefügt: %2$s",
+ "ActivityAccountRemoved": "hat ein Konto für die Suchbegriff-Anbindung von %1$s entfernt: %2$s",
+ "ActivityGoogleClientConfigChanged": "Hat die Google-Client Konfiguration geändert.",
+ "ActivityYandexClientConfigChanged": "Hat die Yandex-Client Konfiguration geändert.",
+ "AddAPIKey": "API-Key hinzufügen",
+ "AddConfiguration": "Konfiguration hinzufügen",
+ "AllReferrersImported": "Verweise (mit importierten Suchbegriffen)",
+ "AllReferrersOriginal": "Verweise (mit getrackten Suchbegriffen)",
+ "AvailableSites": "Für den Import verfügbare Websites:",
+ "BingAPIKeyInstruction": "Melden Sie sich bei den %1$sBing Webmaster Tools%2$s an, dann fügen Sie ihre Website zu Bing Webmaster hinzu. Sobald diese bestätigt wurde, können Sie %3$sihren API-Key kopieren%4$s.",
+ "BingAccountError": "Bei der Überprüfung des API-Keys ist ein Fehler aufgetreten: %1$s. Falls Sie diesen API-Key gerade erst in den Bing Webmaster Tools erzeugt haben, versuchen Sie es in einigen Minuten nochmal (API-Keys für die Bing Webmaster Tools benötigen eine Weile bis sie aktiviert sind).",
+ "BingAccountOk": "API-Key erfolgreich überprüft",
+ "BingConfigurationDescription": "Der Zugriff auf die Bing Webmaster Tools benötigt einen API-Key. Hier können Sie API-Keys hinzufügen, um auf die Daten Ihrer Website zuzugreifen.",
+ "BingConfigurationTitle": "Import über Bing Webmaster Tools konfigurieren",
+ "BingCrawlBlockedByRobotsTxt": "Robots.txt Ausnahmen",
+ "BingCrawlBlockedByRobotsTxtDesc": "URLs, deren Zugriff aktuell durch die robots.txt Ihrer Website verhindert wird.",
+ "BingCrawlConnectionTimeout": "Verbindungs-Timeouts ",
+ "BingCrawlConnectionTimeoutDesc": "Diese Zahl steht für aktuelle Ereignisse, bei denen Bing aufgrund von Verbindungsfehlern nicht auf Ihre Website zugreifen konnte. Dies könnte ein vorübergehendes Problem sein, aber Sie sollten Ihre Serverprotokolle überprüfen, um zu sehen, ob Anfragen unbeabsichtigt verloren gehen.",
+ "BingCrawlCrawledPages": "Gecrawlte Seiten",
+ "BingCrawlCrawledPagesDesc": "Anzahl der Seiten die der Bing Crawler abgerufen hat.",
+ "BingCrawlDNSFailures": "DNS Fehler",
+ "BingCrawlDNSFailuresDesc": "Dieser Fehlertyp katalogisiert aktuelle Fehler, die bei der Kommunikation mit dem DNS-Server aufgetreten sind, als der Bot versuchte, auf Ihre Seiten zuzugreifen. Möglicherweise war Ihr Server ausgefallen, oder es gab eine Fehlkonfiguration, die ein DNS-Routing verhinderte, z.B. wurde TTL auf 0 gesetzt.",
+ "BingCrawlErrors": "Crawlerfehler bei Bing",
+ "BingCrawlErrorsDesc": "Anzahl der aufgetretenen Fehler für den Bing-Crawler.",
+ "BingCrawlErrorsFromDateX": "Der Bericht zeigt Crawling-Fehler, die kürzlich von Bing gemeldet wurden. Er liefert keine historischen Daten. Zuletzt aktualisiert: %s",
+ "BingCrawlHttpStatus2xx": "HTTP-Status 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Diese Codes treten auf, wenn der Server eine Seite erfolgreich bereitstellt",
+ "BingCrawlHttpStatus301": "HTTP-Status 301 (Dauerhaft verschoben)",
+ "BingCrawlHttpStatus301Desc": "Diese Codes treten auf, wenn Inhalte dauerhaft von einem Ort (URL) zu einem anderen verschoben wurden.",
+ "BingCrawlHttpStatus302": "HTTP-Status 302 (Temporär verschoben)",
+ "BingCrawlHttpStatus302Desc": "Diese Codes treten auf, wenn Sie Inhalte vorübergehend von einem Ort (URL) zu einem anderen verschoben haben.",
+ "BingCrawlHttpStatus4xx": "HTTP-Status 400-499 (Anfragefehler)",
+ "BingCrawlHttpStatus4xxDesc": "Diese Codes treten auf, wenn wahrscheinlich ein Fehler in der Anfrage vorlag, der den Server daran hinderte, sie zu verarbeiten.",
+ "BingCrawlHttpStatus5xx": "HTTP Code 500-599 (Interne Server-Fehler)",
+ "BingCrawlHttpStatus5xxDesc": "Diese Codes treten auf, wenn der Server eine scheinbar gültige Anforderung nicht beantworten konnte.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Ausschluss einer wichtigen Seite durch robots.txt",
+ "BingCrawlInboundLink": "Eingehende Link insgesamt",
+ "BingCrawlInboundLinkDesc": "Eingehende Links, die Bing bekannt sind und auf URLs auf Ihrer Website verweisen. Dies sind Links von Websites außerhalb Ihrer eigenen, die auf Ihre Inhalte verweisen.",
+ "BingCrawlMalwareInfected": "Mit Malware infizierte Seiten",
+ "BingCrawlMalwareInfectedDesc": "Alle Seiten-URLs, die Bing gefunden hat und die infiziert sind oder mit Malware in Verbindung gebracht werden, werden in diesem Abschnitt zusammengefasst.",
+ "BingCrawlPagesInIndex": "Seiten insg. im Index",
+ "BingCrawlPagesInIndexDesc": "Gesamtzahl der Seiten die im Bing Index vorhanden sind",
+ "BingCrawlStatsOtherCodes": "Alle übrigen HTTP-Status",
+ "BingCrawlStatsOtherCodesDesc": "Gruppiert alle anderen Codes, die keinem anderen Wert entsprechen (z.B. 1xx oder Informationscodes).",
+ "BingCrawlingStats": "Crawl-Übersicht für Bing und Yahoo!",
+ "BingCrawlingStatsDocumentation": "Die Crawl-Übersicht ermöglicht es Ihnen, Informationen zum Crawlen anzuzeigen, wie z.B. Fehler, die der Suchbot beim Besuch einer Seite feststellt, Elemente, die durch Ihre robots.txt-Datei blockiert werden und URLs, die möglicherweise von Malware befallen sind.",
+ "BingKeywordImport": "Import von Suchbegriffen auf Bing",
+ "BingKeywords": "Suchbegriffe (auf Bing und Yahoo!)",
+ "BingKeywordsDocumentation": "Suchbegriffe, die in der Suche auf Bing oder Yahoo! verwendet wurden und Links zu Ihrer Website in der Suchergebnisliste generiert haben.",
+ "BingKeywordsNoRangeReports": "Keywords auf Bing und Yahoo! können nur für benutzerdefinierte Datumsbereiche mit vollständigen Wochen oder Monaten verarbeitet werden, da sie nicht als Tagesberichte verfügbar sind.",
+ "BingKeywordsNotDaily": "Keywords zu Bing und Yahoo! sind nur als Wochenberichte verfügbar. Es gibt keine Keyword-Daten für einzelne Tage.",
+ "BingWebmasterApiUrl": "Url für Bing Webmaster Tools",
+ "BingWebmasterApiUrlDescription": "Geben Sie die URL an, mit der diese Website in Ihren Bing Webmaster Tools verfügbar ist",
+ "Category": "Kategorie",
+ "ChangeConfiguration": "Konfiguration ändern",
+ "Clicks": "Klicks",
+ "ClicksDocumentation": "Hier wird jeder Klick auf einen Link auf einer Suchergebnisseite gezählt, der zu Ihrer Website zeigt.",
+ "ClientConfigImported": "Client-Konfiguration wurde erfolgreich importiert!",
+ "ClientConfigSaveError": "Beim Speichern der Client-Konfiguration ist ein Fehler aufgetreten. Bitte überprüfen Sie, ob die angegebene Konfiguration gültig ist, und versuchen Sie es erneut.",
+ "ClientId": "Client-ID",
+ "ClientSecret": "Clientschlüssel",
+ "ConfigAvailableNoWebsiteConfigured": "Anbindung erfolgreich konfiguriert, allerdings sind derzeit keine Websites für den Import konfiguriert.",
+ "ConfigRemovalConfirm": "Sie sind dabei, die Konfiguration für %1$s zu entfernen. Der Import von Keywords für diese Website wird deaktiviert. Trotzdem fortfahren?",
+ "Configuration": "Konfiguration",
+ "ConfigurationDescription": "Dieses Plugin ermöglicht es Ihnen alle Suchbegriffe, nach denen Ihre Benutzer auf Suchmaschinen gesucht haben, direkt in Matomo zu importieren.",
+ "ConfigurationFile": "Konfigurationsdatei",
+ "ConfigurationValid": "Ihre OAuth Konfiguration ist gültig.",
+ "ConfigureMeasurableBelow": "Um eine Website zu konfigurieren, klicken Sie einfach den Button unterhalb oder konfigurieren Sie dies direkt in den Website-Einstellungen.",
+ "ConfigureMeasurables": "Websites konfigurieren",
+ "ConfiguredAccounts": "Konfigurierte Accounts",
+ "ConfiguredUrlNotAvailable": "Die konfigurierte URL steht für dieses Konto nicht zur Verfügung",
+ "ConnectAccount": "Konto verknüpfen",
+ "ConnectAccountDescription": "Bitte klicken Sie auf den folgenden Button. Sie werden dann zu %1$s weitergeleitet, um dem Zugriff zuzustimmen.",
+ "ConnectAccountYandex": "Die Authentifizierung für Yandex-Konten ist nur %1$s Tage lang gültig. Jedes Konto muss innerhalb dieser Zeit neu authentifiziert werden, damit Importe korrekt funktionieren.",
+ "ConnectFirstAccount": "Beginnen Sie, indem Sie Ihr erstes Konto verknüpfen.",
+ "ConnectGoogleAccounts": "Google Konten verknüpfen",
+ "ConnectYandexAccounts": "Yandex Konten verknüpfen",
+ "ContainingSitemaps": "Enthaltende Sitemaps",
+ "CrawlingErrors": "Crawling-Fehler",
+ "CrawlingOverview1": "Die Crawling-Übersicht enthält die wichtigsten Informationen darüber, wie die Suchmaschinen Ihre Websites crawlen. Diese Metriken werden etwa einmal pro Tag mit Daten aktualisiert, die von den Suchmaschinen bereitgestellt werden.",
+ "CrawlingStats": "Crawling Übersicht",
+ "Ctr": "CTR",
+ "CtrDocumentation": "Klickrate: Gibt das Verhältnis an wie häufig Benutzer, die einen Link auf Ihre Website auf einer Suchergebnisseite gesehen haben auch darauf geklickt haben.",
+ "CurrentlyConnectedAccounts": "Aktuell sind %1$s Konten verknüpft.",
+ "Domain": "Domain",
+ "DomainProperty": "Domain-Property",
+ "DomainPropertyInfo": "Enthält alle Subdomains (m, www etc.) und beide Protokolle (http, https).",
+ "EnabledSearchTypes": "Abzurufende Keyword-Arten",
+ "FetchImageKeyword": "Bilder-Keywords abrufen",
+ "FetchImageKeywordDesc": "Abrufen von Keywords welche in der Google Bildsuche verwendet wurden",
+ "FetchNewsKeyword": "News-Keywords abrufen",
+ "FetchNewsKeywordDesc": "Abrufen von Keywords welche in auf Google News verwendet wurden",
+ "FetchVideoKeyword": "Video-Keywords abrufen",
+ "FetchVideoKeywordDesc": "Abrufen von Keywords welche in der Google Videosuche verwendet wurden",
+ "FetchWebKeyword": "Web-Keywords abrufen",
+ "FetchWebKeywordDesc": "Abrufen von Keywords welche in der Google Websuche verwendet wurden",
+ "FirstDetected": "Zuerst erkannt",
+ "GoogleAccountAccessTypeOfflineAccess": "Offline Zugriff wird benötigt damit die Suchbegriffe auf dann im Hintergrund importiert werden können, wenn Sie gerade nicht aktiv eingeloggt sind.",
+ "GoogleAccountAccessTypeProfileInfo": "Personenbezogene Daten aufrufen wird verwendet, um den Namen der aktuell verbundenen Konten anzuzeigen.",
+ "GoogleAccountAccessTypeSearchConsoleData": "Search Console-Daten ist erforderlich, um Zugriff auf Ihre Google-Suchbegriffe zu erhalten.",
+ "GoogleAccountError": "Bei der Überprüfung des OAuth-Zugriffs ist ein Fehler aufgetreten: %1$s",
+ "GoogleAccountOk": "OAuth-Zugriff erfolgreich überprüft.",
+ "GoogleConfigurationDescription": "Google Search Console verwendet OAuth zur Authentifizierung und Autorisierung.",
+ "GoogleConfigurationTitle": "Import von Google Search Console konfigurieren",
+ "GoogleDataNotFinal": "Die Suchbegriffe in diesem Bericht enthalten möglicherweise noch nicht die endgültigen Daten. Google stellt endgültige Suchbegriffe mit einer Verzögerung von 2 Tagen bereit. Suchbegriffe für neuere Tage werden erneut importiert, bis sie als endgültig gemeldet werden.",
+ "GoogleDataProvidedWithDelay": "Google stellt Suchbegriffe mit einer Verzögerung zur Verfügung. Die Suchbegriffe für dieses Datum werden etwas später importiert.",
+ "GoogleKeywordImport": "Google Keyword Import",
+ "GoogleSearchConsoleUrl": "Url für Google Search Console",
+ "GoogleSearchConsoleUrlDescription": "Geben Sie die URL an, mit der diese Website in Ihrer Google Search Console verfügbar ist",
+ "GoogleUploadOrPasteClientConfig": "Bitte laden Sie Ihre Google OAuth Client-Konfiguration hoch oder fügen Sie sie in das untenstehende Feld ein.",
+ "HowToGetOAuthClientConfig": "Woher bekomme ich meine OAuth-Client Konfiguration",
+ "ImageKeywords": "Bilder Suchbegriffe auf Google",
+ "ImageKeywordsDocumentation": "Suchbegriffe, die in der Google-Bilder suche verwendet wurden und Links zu Ihrer Website in der Suchergebnisliste generiert haben.",
+ "Impressions": "Impressionen",
+ "ImpressionsDocumentation": "Als Impression zählt die Anzeige Ihrer Website auf einer Suchergebnisseite.",
+ "IntegrationConfigured": "Anbindung erfolgreich konfiguriert",
+ "IntegrationNotConfigured": "Anbindung noch nicht konfiguriert",
+ "KeywordStatistics": "Suchbegriffe",
+ "KeywordTypeImage": "Bild",
+ "KeywordTypeNews": "News",
+ "KeywordTypeVideo": "Video",
+ "KeywordTypeWeb": "Web",
+ "KeywordsCombined": "Suchbegriffe (kombiniert)",
+ "KeywordsCombinedDocumentation": "Bericht, der alle von Matomo erkannten und von Suchmaschinen importierten Keywords kombiniert. Dieser Bericht enthält nur die Besuchsmetrik. Sie können zu einem der zugehörigen Berichte wechseln, um detaillierte Kennzahlen zu erhalten.",
+ "KeywordsCombinedImported": "Importierte Suchbegriffe kombiniert",
+ "KeywordsCombinedImportedDocumentation": "Dieser Bericht zeigt alle Suchbegriffe die von allen konfigurierten Suchmaschinen importiert wurden.",
+ "KeywordsReferrers": "Suchbegriffe (inklusive nicht definierter)",
+ "KeywordsSubtableImported": "Importierte Keywords",
+ "KeywordsSubtableOriginal": "Tracked Keywords (inkl. nicht definierte)",
+ "LastCrawled": "Zuletzt gecrawlt",
+ "LastDetected": "Zuletzt erkannt",
+ "LastImport": "Letzter Import",
+ "LatestAvailableDate": "Die neuesten verfügbaren Keyword-Daten sind für %1$s",
+ "LinksToUrl": "Links auf %s",
+ "ManageAPIKeys": "API-Schlüssel verwalten",
+ "MeasurableConfig": "Konfigurierte Websites",
+ "NewsKeywords": "Suchbegriffe auf Google News",
+ "NewsKeywordsDocumentation": "Suchbegriffe, die in der Google-News suche verwendet wurden und Links zu Ihrer Website in der Suchergebnisliste generiert haben.",
+ "NoSegmentation": "Der Bericht unterstützt keine Segmentierung. Bei den angezeigten Daten handelt es sich um Ihre standardmäßigen, unsegmentierten Berichtsdaten.",
+ "NoWebsiteConfigured": "Es ist derzeit keine Website konfiguriert. Um den Import für eine Website zu aktivieren, konfigurieren Sie dies bitte hier.",
+ "NoWebsiteConfiguredWarning": "Import von %s nicht vollständig konfiguriert. Sie müssen eine Website konfigurieren, um den Import zu aktivieren.",
+ "NotAvailable": "Nicht verfügbar",
+ "OAuthAccessTimedOut": "Der OAuth-Zugang für dieses Konto hat möglicherweise sein Zeitlimit überschritten. Sie müssen sich neu authentifizieren, damit die Importe für dieses Konto wieder funktionieren.",
+ "OAuthAccessWillTimeOut": "Der OAuth-Zugang für dieses Konto läuft nach %1$s Tagen ab. Noch %2$s Tage verbleibend ",
+ "OAuthAccessWillTimeOutSoon": "Der OAuth-Zugang für dieses Konto läuft in etwa %1$s Tagen ab. Bitte authentifizieren Sie sich erneut, um zu vermeiden, dass Importe für dieses Konto nicht mehr funktionieren.",
+ "OAuthClientConfig": "OAuth-Client Konfiguration",
+ "OAuthError": "Innerhalb des OAuth-Prozesses ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut und stellen Sie sicher, dass Sie die angeforderten Berechtigungen akzeptieren.",
+ "Platform": "Plattform",
+ "Position": "durchschn. Position",
+ "PositionDocumentation": "Durchschnittliche Position Ihrer Website in der Suchergebnisliste (für diesen Suchbegriff).",
+ "ProvideYandexClientConfig": "Bitte fügen Sie Ihre Yandex OAuth-Client-Konfiguration ein.",
+ "ProviderBingDescription": "Importieren Sie alle Keywords, die verwendet werden, um Ihre Website bei Bing und Yahoo! zu finden.",
+ "ProviderBingNote": "Anmerkung: Microsoft stellt Suchbegriffe nur jeden Samstag für ganze Wochen zur Verfügung. Daher kann es ein paar Tage dauern bis die ersten Suchbegriffe in den Berichten angezeigt werden. Zudem stehen diese nur für Wochen-, Monats- sowie Jahres-Berichte zur Verfügung.",
+ "ProviderGoogleDescription": "Importieren Sie alle Keywords, mit denen Ihre Website in der Google -Suche gefunden wird. Berichte zeigen Ihre Keywords für jeden Suchtyp separat an (Web, Bilder und Videos).",
+ "ProviderGoogleNote": "Anmerkung: Google stellt finale Suchbegriffe mit einer Verzögerung von 2 Tagen zur Verfügung. Nicht endgültige Daten für neuere Tage werden bereits angezeigt, werden aber erneut importiert, bis sie endgültig sind. Der erste Import kann möglicherweise Ihre historischen Suchbegriffe der letzten bis zu 486 Tagen importieren.",
+ "ProviderListDescription": "Wenn Sie unten eine (oder mehrere) Suchmaschinen erfolgreich eingerichtet haben, können Sie konfigurieren, in welche Website(s) Matomo Ihre Suchbegriffe importieren soll.",
+ "ProviderXAccountWarning": "Probleme bei der Kontokonfiguration erkannt Bitte überprüfen Sie die konfigurierten Konten für%s .",
+ "ProviderXSitesWarning": "Probleme bei der Website-Konfiguration erkannt Bitte überprüfen Sie die konfigurierten Webseiten für %s .",
+ "ProviderYandexDescription": "Importieren Sie alle Suchbegriffe, mit denen Ihre Website in der Yandex -Suche gefunden wird.",
+ "ProviderYandexNote": "Anmerkung: Yandex stellt Suchbegriffe mit einem Versatz von bis zu 5 Tagen zur Verfügung. Beim ersten Import wird versucht, Ihre historischen Suchbegriffe der letzten bis zu 100 Tagen zu importieren.",
+ "ReAddAccountIfPermanentError": "Sollte dieser Fehler dauerhaft auftreten, entfernen Sie bitte das Konto und verknüpfen Sie es erneut.",
+ "ReAuthenticateIfPermanentError": "Sollte dieser Fehler dauerhaft auftreten, versuchen Sie, das Konto erneut zu authentifizieren oder es zu entfernen und erneut zu verbinden.",
+ "Reauthenticate": "Erneut authentifizieren",
+ "ReportShowMaximumValues": "Bei den angezeigten Werten handelt es sich um die Maximalwerte, die im gewählten Zeitraum auftraten.",
+ "RequiredAccessTypes": "Folgende Zugriffsrechte werden benötigt:",
+ "ResponseCode": "Response-Code",
+ "RoundKeywordPosition": "Runden der Keyword-Position",
+ "SearchEngineKeywordsPerformance": "Leistung von Suchmaschinen-Keywords",
+ "SearchEnginesImported": "Suchmaschinen (mit importierten Suchbegriffen)",
+ "SearchEnginesOriginal": "Suchmaschinen (mit getrackten Suchbegriffen)",
+ "SetUpOAuthClientConfig": "Einrichten Ihrer OAuth-Client-Konfiguration",
+ "SetupConfiguration": "Einrichten der Konfiguration",
+ "SitemapsContainingUrl": "Sitemaps die %s beinhalten",
+ "StartOAuth": "OAuth-Prozess starten",
+ "URLPrefix": "URL-Präfix",
+ "URLPrefixProperty": "URL-Präfix Property",
+ "URLPrefixPropertyInfo": "Enthält nur URLs mit exakt angegebenem Präfix, einschließlich Protokoll (http oder https). Wenn Ihre Property alle Protokolle oder Subdomains (http, https, www, m usw.) umfassen soll, können Sie stattdessen eine Domain-Property hinzufügen.",
+ "UnverifiedSites": "Unbestätigte Websites:",
+ "UploadOAuthClientConfig": "Laden Sie Ihre OAuth-Client Konfiguration hoch",
+ "UrlOfAccount": "URL (Konto)",
+ "VideoKeywords": "Video Suchbegriffe auf Google",
+ "VideoKeywordsDocumentation": "Suchbegriffe, die in der GoogleVideo suche verwendet wurden und Links zu Ihrer Website in der Suchergebnisliste generiert haben.",
+ "VisitOAuthHowTo": "Bitte besuchen Sie unseren %1$sOnline-Guide%2$s, um zu erfahren, wie Sie Ihre %3$s OAuth-Client-Konfiguration einrichten.",
+ "WebKeywords": "Web Suchbegriffe auf Google",
+ "WebKeywordsDocumentation": "Suchbegriffe, die in der Google-Web suche verwendet wurden und Links zu Ihrer Website in der Suchergebnisliste generiert haben.",
+ "WebsiteSuccessfulConfigured": "Herzlichen Glückwunsch! Sie haben den Keywordimport für die Website %1$s erfolgreich konfiguriert. Es kann einige Tage dauern, bis Ihre ersten Suchbegriffe importiert und in Referrers > Search Keywords angezeigt werden. Weitere Informationen über Verzögerungen und Einschränkungen beim Import von Keywords finden Sie in unseren %2$sFAQs%3$s.",
+ "WebsiteTypeUnsupported": "Die ausgewählte Messgröße %1$s kann nicht konfiguriert werden, da sie einen nicht unterstützten Typ hat. Es werden nur Messgrößen vom Typ 'Website' unterstützt.",
+ "WebsiteTypeUnsupportedRollUp": "Hinweis: Roll-Up-Sites kombinieren automatisch die importierten Daten aller ihrer Unterseiten",
+ "YandexConfigurationDescription": "Yandex verwendet OAuth zur Authentifizierung und Autorisierung.",
+ "YandexConfigurationTitle": "Import von Yandex Webmaster API konfigurieren",
+ "YandexCrawlAppearedPages": "In der Suche erschienene Seiten",
+ "YandexCrawlAppearedPagesDesc": "Seiten die neu in den Suchindex von Yandex aufgenommen wurden",
+ "YandexCrawlCrawledPages": "Gecrawlte Seiten",
+ "YandexCrawlCrawledPagesDesc": "Anzahl der Seiten die der Yandex Crawler abgerufen hat.",
+ "YandexCrawlErrors": "Andere Abfragefehler",
+ "YandexCrawlErrorsDesc": "Gecrawlte Seiten, die aus einem anderen Grund fehlgeschlagen sind",
+ "YandexCrawlHttpStatus2xx": "HTTP-Status 200-299",
+ "YandexCrawlHttpStatus2xxDesc": "Gecrawlte Seiten mit einem 2xx-Code",
+ "YandexCrawlHttpStatus3xx": "HTTP-Code 300-399 (verschobene Seiten)",
+ "YandexCrawlHttpStatus3xxDesc": "Gecrawlte Seiten mit einem 3xx-Code",
+ "YandexCrawlHttpStatus4xx": "HTTP-Status 400-499 (Anfragefehler)",
+ "YandexCrawlHttpStatus4xxDesc": "Gecrawlte Seiten mit einem 4xx-Code",
+ "YandexCrawlHttpStatus5xx": "HTTP Code 500-599 (Interne Server-Fehler)",
+ "YandexCrawlHttpStatus5xxDesc": "Gecrawlte Seiten mit einem 5xx-Code",
+ "YandexCrawlInIndex": "Seiten insg. im Index",
+ "YandexCrawlInIndexDesc": "Gesamtzahl der Seiten die im Yandex Suchindex vorhanden sind",
+ "YandexCrawlRemovedPages": "Aus der Suche entfernte Seiten",
+ "YandexCrawlRemovedPagesDesc": "Seiten die aus dem Suchindex von Yandex entfernt wurden",
+ "YandexCrawlingStats": "Crawl-Übersicht für Yandex!",
+ "YandexCrawlingStatsDocumentation": "In der Crawl-Übersicht können Sie Crawl-bezogene Informationen anzeigen, z. B. Fehler, auf die der Suchbot beim Besuch einer Seite stößt, durch Ihre robots.txt-Datei blockierte Elemente oder die Gesamtzahl der Seiten im Index.",
+ "YandexKeywords": "Suchbegriffe auf Yandex",
+ "YandexKeywordsDocumentation": "Suchbegriffe, die in der Suche auf Yandex verwendet wurden und Links zu Ihrer Website in der Suchergebnisliste generiert haben.",
+ "YandexWebmasterApiUrl": "Url für Yandex Webmaster Tools",
+ "YandexWebmasterApiUrlDescription": "Geben Sie die URL an, mit der diese Website in Ihren Yandex Webmaster Tools verfügbar ist"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/en.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/en.json
new file mode 100644
index 0000000..06085fd
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/en.json
@@ -0,0 +1,240 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "AccountAddedBy": "Added by %1$s<\/em> on %2$s<\/em>",
+ "AccountConnectionValidationError": "An error occured while validating the account connection:",
+ "AccountDoesNotExist": "Configured account %1$s does not exist anymore",
+ "AccountNoAccess": "This account does not currently have access to any website.",
+ "AccountRemovalConfirm": "You are about to remove the account %1$s. This might disable the keywords import for any connected website(s). Proceed anyway?",
+ "ActivityAccountAdded": "added a new account for keyword provider %1$s: %2$s",
+ "ActivityAccountRemoved": "removed an account for keyword provider %1$s: %2$s",
+ "ActivityGoogleClientConfigChanged": "changed the Google client configuration.",
+ "ActivityYandexClientConfigChanged": "changed the Yandex client configuration.",
+ "AddAPIKey": "Add API Key",
+ "AddConfiguration": "Add Configuration",
+ "AdminMenuTitle": "Search Performance",
+ "APIKey": "API Key",
+ "AvailableSites": "Websites available for import:",
+ "Domain": "Domain",
+ "DomainProperty": "Domain property",
+ "DomainPropertyInfo": "Includes all subdomains (m, www, and so on) and both protocols (http, https).",
+ "URLPrefix": "URL-prefix",
+ "URLPrefixProperty": "URL-prefix property",
+ "URLPrefixPropertyInfo": "Includes only URLs with the specified exact prefix, including the protocol (http\/https). If you want your property to match any protocol or subdomain (http\/https\/www.\/m. and so on), consider adding a Domain property instead.",
+ "BingAccountError": "An error occurred while validating the API Key: %1$s. If you have just generated this API key in Bing Webmaster Tools please try again in one or two minutes (API keys in Bing Webmaster Tools take a little while to be activated).",
+ "BingAccountOk": "API Key successfully checked",
+ "BingAPIKeyInstruction": "Sign in at %1$sBing Webmaster Tools%2$s, then add your website in Bing Webmaster. After you have validated it, you can %3$scopy your API key%4$s.",
+ "BingConfigurationDescription": "Bing Webmaster Tools needs an API key to be accessed. Here you can add API keys to access your websites data.",
+ "BingConfigurationTitle": "Configure import from Bing Webmaster Tools",
+ "BingCrawlBlockedByRobotsTxt": "Robots.txt exclusion",
+ "BingCrawlBlockedByRobotsTxtDesc": "URLs currently blocked by your site’s robots.txt.",
+ "BingCrawlConnectionTimeout": "Connection timeouts",
+ "BingCrawlConnectionTimeoutDesc": "This number represents recent occurences when Bing could not access your site due to connection errors. This could be a temporary issue but you should check your server logs to see if you are accidentally dropping requests.",
+ "BingCrawlCrawledPages": "Crawled pages",
+ "BingCrawlCrawledPagesDesc": "Number of pages the Bing crawler requested.",
+ "BingCrawlDNSFailures": "DNS failures",
+ "BingCrawlDNSFailuresDesc": "This issue type catalogs recent errors encountered trying to communicate with the DNS server when the bot tried to access your pages. Possibly your server was down, or there was a misconfiguration that prevented DNS routing, for example TTL was set to 0.",
+ "BingCrawlErrors": "Crawl errors on Bing",
+ "BingCrawlErrorsDesc": "Number of errors occured for the Bing crawler.",
+ "BingCrawlErrorsFromDateX": "The report show crawling errors recently reported by Bing. It does not provide any historical data. Last updated %s",
+ "BingCrawlHttpStatus2xx": "HTTP Code 200-299",
+ "BingCrawlHttpStatus2xxDesc": "These codes appear when the server serves a page successfully",
+ "BingCrawlHttpStatus301": "HTTP Code 301 (Permanently moved)",
+ "BingCrawlHttpStatus301Desc": "These codes appear when you have permanently moved content from one location (URL) to another.",
+ "BingCrawlHttpStatus302": "HTTP Code 302 (Temporarily moved)",
+ "BingCrawlHttpStatus302Desc": "These codes appear when you have temporarily moved content from one location (URL) to another.",
+ "BingCrawlHttpStatus4xx": "HTTP Code 400-499 (Request errors)",
+ "BingCrawlHttpStatus4xxDesc": "These codes appear when there was a likely an error in the request which prevented the server from being able to process it.",
+ "BingCrawlHttpStatus5xx": "HTTP Code 500-599 (Internal server errors)",
+ "BingCrawlHttpStatus5xxDesc": "These codes appear when the server failed to fulfill an apparently valid request.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Robots.txt exclusion of important page",
+ "BingCrawlInboundLink": "Total inbound links",
+ "BingCrawlInboundLinkDesc": "Inbound links Bing is aware of, pointed at URLs on your website. These are links, from websites external of your own, that have been pointed towards your content.",
+ "BingCrawlingStats": "Crawl overview for Bing and Yahoo!",
+ "BingCrawlingStatsDocumentation": "The Crawl overview allows you to view crawl related information such as errors encountered by the search bot when visiting a page, items blocked by your robots.txt file and URLs potentially affected by malware.",
+ "BingCrawlMalwareInfected": "Malware infected websites",
+ "BingCrawlMalwareInfectedDesc": "Any page URLs that Bing found that are infected or associated with malware will be grouped in this section.",
+ "BingCrawlPagesInIndex": "Total pages in index",
+ "BingCrawlPagesInIndexDesc": "Total number of pages available in Bing index",
+ "BingCrawlStatsOtherCodes": "All other HTTP status codes",
+ "BingCrawlStatsOtherCodesDesc": "Groups all other codes that are not matched by any other value (such as 1xx or informational codes).",
+ "BingKeywordImport": "Bing keyword import",
+ "BingKeywords": "Keywords (on Bing and Yahoo!)",
+ "BingKeywordsDocumentation": "Keywords used in Bing or Yahoo! search that generated links to your website in the search results list.",
+ "BingKeywordsNoRangeReports": "Keywords on Bing and Yahoo! can only be processed for custom date ranges including full weeks or months as they are not available as daily reports.",
+ "BingKeywordsNotDaily": "Keywords on Bing and Yahoo! are only available as weekly reports. There is no keyword data for days periods.",
+ "BingWebmasterApiUrl": "Url for Bing Webmaster Tools",
+ "BingWebmasterApiUrlDescription": "Provide the url this website is available in your Bing Webmaster Tools",
+ "Category": "Category",
+ "ChangeConfiguration": "Change configuration",
+ "Clicks": "Clicks",
+ "ClicksDocumentation": "A click is counted each time someone clicks on a link pointing to your website on a search engine results page.",
+ "ClientConfigImported": "Client config has been successfully imported!",
+ "ClientConfigSaveError": "An error occured while saving the client config. Please check if the config provided is valid, and try again.",
+ "ClientId": "Client id",
+ "ClientSecret": "Client secret",
+ "ConfigAvailableNoWebsiteConfigured": "Integration successfully configured, but currently no website configured for import.",
+ "ConfigRemovalConfirm": "You are about to remove the configuration for %1$s. Import of Keywords for that website will be disabled. Proceed anyway?",
+ "Configuration": "Configuration",
+ "ConfigurationDescription": "This plugin allows you to import directly into Matomo all keywords searched by your users on search engines.",
+ "ConfigurationFile": "Configuration file",
+ "ConfigurationValid": "Your OAuth configuration is valid.",
+ "ConfiguredAccounts": "configured accounts",
+ "ConfiguredUrlNotAvailable": "Configured URL is not available for this account",
+ "ConfigureMeasurableBelow": "To configure a website, simply click the button below or configure it directly in the website settings.",
+ "ConfigureMeasurables": "Configure websites",
+ "ConfigureTheImporterLabel1": "Import your Google Search Console keywords and analyse them using Matomo’s powerful analytics tools. Once you connect the importer, select which websites to import and Matomo will start importing keywords for them as part of the scheduled archiving process.",
+ "ConfigureTheImporterLabel2": "In order to import your data from Google Search Console, Matomo need access to this data.",
+ "ConfigureTheImporterLabel3": "To start, %1$sfollow our instructions to retrieve your OAuth Client configuration%2$s. Then upload the client configuration file using the button below.",
+ "ConnectAccount": "Connect Account",
+ "ConnectAccountDescription": "Please click on the button below to be redirected to %1$s where you need to grant access.",
+ "ConnectAccountYandex": "Authentication for Yandex accounts is only valid for %1$s days. Each account needs to be reauthenticated within that time to ensure imports work correctly.",
+ "ConnectFirstAccount": "Start by connecting your first account below.",
+ "ConnectGoogleAccounts": "Connect Google Account(s)",
+ "ContainingSitemaps": "Containing Sitemaps",
+ "CrawlingErrors": "Crawling errors",
+ "ConnectYandexAccounts": "Connect Yandex Account(s)",
+ "CrawlingStats": "Crawling overview",
+ "Ctr": "CTR",
+ "CtrDocumentation": "Clickthrough rate: A ratio showing how often people who see a search engine results page with a link to your website, end up clicking it.",
+ "CurrentlyConnectedAccounts": "There are currently %1$s accounts connected.",
+ "CreatedBy" : "Created By",
+ "DeleteUploadedClientConfig": "If you'd like to remove the uploaded client configuration, click below",
+ "EnabledSearchTypes": "Keyword types to fetch",
+ "FetchImageKeyword": "Fetch image keywords",
+ "FetchImageKeywordDesc": "Fetch keywords used in Google image search",
+ "FetchNewsKeyword": "Fetch news keywords",
+ "FetchNewsKeywordDesc": "Fetch keywords used in Google News",
+ "FetchVideoKeyword": "Fetch video keywords",
+ "FetchVideoKeywordDesc": "Fetch keywords used in Google video search",
+ "FetchWebKeyword": "Fetch web keywords",
+ "FetchWebKeywordDesc": "Fetch keywords used in Google web search",
+ "FirstDetected": "First detected",
+ "GoogleAccountAccessTypeOfflineAccess": "Offline access<\/strong> is required to be able to import your search keywords even when you are not currently logged in.",
+ "GoogleAccountAccessTypeProfileInfo": "Profile info<\/strong> is used to show the name of the account(s) currently connected.",
+ "GoogleAccountAccessTypeSearchConsoleData": "Search Console data<\/strong> is required to get access to your Google search keywords.",
+ "GoogleAccountError": "An error occurred while validating the OAuth access: %1$s",
+ "GoogleAccountOk": "OAuth access successfully checked.",
+ "GoogleAuthorizedJavaScriptOrigin": "Authorized JavaScript origin",
+ "GoogleAuthorizedRedirectUri": "Authorized redirect URI",
+ "GoogleConfigurationDescription": "Google Search Console uses OAuth for authentication and authorization.",
+ "GoogleConfigurationTitle": "Configure import from Google Search Console",
+ "GoogleDataProvidedWithDelay": "Google provides keywords data with a delay. Keywords for this date will be imported a bit later.",
+ "GoogleDataNotFinal": "The keywords data in this report may not yet contain the final data. Google provides final keywords data with a delay of 2 days. Keywords for more recent days will be re-imported until they are reported as final.",
+ "GoogleKeywordImport": "Google keyword import",
+ "GooglePendingConfigurationErrorMessage": "Configuration is pending. Please ask a super user to complete the configuration.",
+ "GoogleSearchConsoleUrl": "Url for Google Search Console",
+ "GoogleSearchConsoleUrlDescription": "Provide the url this website is available in your Google Search Console",
+ "GoogleUploadOrPasteClientConfig": "Please upload your Google OAuth client configuration, or paste it into the field below.",
+ "HowToGetOAuthClientConfig": "How to get your OAuth Client configuration",
+ "ImageKeywords": "Image keywords on Google",
+ "ImageKeywordsDocumentation": "Keywords used in Google image<\/b> search that generated links to your website in the search result list.",
+ "Impressions": "Impressions",
+ "ImpressionsDocumentation": "An impression is counted each time your website is displayed in a search engine results page.",
+ "IntegrationConfigured": "Integration successfully configured",
+ "IntegrationNotConfigured": "Integration not yet configured",
+ "InvalidRedirectUriInClientConfiguration": "Invalid redirect_uris, at least 1 uri should match the uri \"%1$s\" in the uploaded configuration file",
+ "KeywordsCombined": "Combined keywords",
+ "KeywordsCombinedDocumentation": "Report combining all keywords detected by Matomo and imported from search engines. This report only includes the visit metric. You can switch to one of the related report to get detailed metrics.",
+ "KeywordsCombinedImported": "Combined imported keywords",
+ "KeywordsCombinedImportedDocumentation": "Report showing all keywords imported from all configured search engines.",
+ "KeywordsReferrers": "Keywords (including not defined)",
+ "KeywordStatistics": "Search Keywords",
+ "KeywordTypeImage": "image",
+ "KeywordTypeVideo": "video",
+ "KeywordTypeWeb": "web",
+ "KeywordTypeNews": "news",
+ "LastCrawled": "Last crawled",
+ "LastDetected": "Last detected",
+ "LastImport": "Last Import",
+ "LatestAvailableDate": "Most recent keyword data available is for %1$s",
+ "LinksToUrl": "Links to %s",
+ "ManageAPIKeys": "Manage API Keys",
+ "MeasurableConfig": "configured websites",
+ "NoSegmentation": "Report does not support segmentation. The data displayed is your standard, unsegmented report data.",
+ "NotAvailable": "Not Available",
+ "NoWebsiteConfigured": "There is currently no website configured. To enable the import for a specific website, please set up the configuration here.",
+ "NoWebsiteConfiguredWarning": "Import for %s not fully configured. You need to configure some websites to enable the import.",
+ "OAuthAccessTimedOut": "The OAuth access for this account may have timed out. You will need to reauthenticate to get the imports working again for this account.",
+ "OAuthAccessWillTimeOutSoon": "The OAuth access for this account will time out in around %1$s days. Please reauthenticate to avoid any imports for this account to stop working.",
+ "OAuthAccessWillTimeOut": "The OAuth access for this account will time out after %1$s days. %2$s days left<\/strong>",
+ "OAuthClientConfig": "OAuth Client Configuration",
+ "OAuthError": "An error occurred within the OAuth process. Please try again and ensure you accept the requested permissions.",
+ "OAuthExampleText": "The configuration requires the fields listed below. Please use the values provided:",
+ "OauthFailedMessage": "We encountered an issue during the authorization process for your Google Search Console. To try again, please click the button below. If the problem persists, please contact our support team for assistance. They will assist you in resolving the issue and getting your Google Search Console keywords imported.", "Platform": "Platform",
+ "Position": "Avg. position",
+ "PositionDocumentation": "Average position of your website in the search engine results list (for this keyword).",
+ "ProviderBingDescription": "Import all keywords used to find your website on Bing<\/strong> and Yahoo!<\/strong> search.",
+ "ProviderBingNote": "Note:<\/u> Microsoft provides keywords data every saturday and only for whole weeks. As a result your keywords for Bing and Yahoo will take a few days to appear in your reports and will only be available when viewing weeks, months or years.",
+ "ProviderGoogleDescription": "Import all keywords used to find your website on Google<\/strong> search. Reports will show your keywords for each search type separately (Web, Images and Videos).",
+ "ProviderGoogleNote": "Note:<\/u> Google provides final keywords data with a delay of 2 days. Non final data for more recent days will already be shown, but will be re-imported until they are final. Your first import may be able to import your historical keyword data for up to the last 486 days.",
+ "ProviderListDescription": "When you have successfully setup one (or more) search engine below, you can configure into which website(s) should Matomo import your search keywords.",
+ "ProviderXAccountWarning": "Account configuration problems detected<\/strong> Please check the configured accounts for %s<\/strong>.",
+ "ProviderXSitesWarning": "Website configuration problems detected<\/strong> Please check the configured websites for %s<\/strong>.",
+ "ProviderYandexDescription": "Import all keywords used to find your website on Yandex<\/strong> search.",
+ "ProviderYandexNote": "Note:<\/u> Yandex provides the keywords with a delay of up to 5 days. The first import will try to import your historical keywords for up to the last 100 days.",
+ "ProvideYandexClientConfig": "Please insert your Yandex OAuth client configuration.",
+ "Reauthenticate": "Reauthenticate",
+ "ReAddAccountIfPermanentError": "If this is a permanent error, try removing the account and connect it again.",
+ "ReAuthenticateIfPermanentError": "If this is a permanent error, try to reauthenticate the account or remove and connect it again.",
+ "RecentApiErrorsWarning": "Keyword import errors detected<\/strong> Please check the configurations for: %s If your configuration is correct and the errors persist, please contact support.",
+ "ReportShowMaximumValues": "Values displayed are the maximum values which occurred during this period.",
+ "RequiredAccessTypes": "These access types are required:",
+ "ResponseCode": "Response code",
+ "RoundKeywordPosition": "Round keyword position",
+ "SearchEngineKeywordsPerformance": "Search Engine Keywords Performance",
+ "SetupConfiguration": "Setup configuration",
+ "SitemapsContainingUrl": "Sitemaps containing %s",
+ "SetUpOAuthClientConfig": "Setup your OAuth client configuration",
+ "KeywordsSubtableOriginal": "Tracked keywords (including not defined)",
+ "KeywordsSubtableImported": "Imported keywords",
+ "AllReferrersOriginal": "Referrers (with tracked keywords)",
+ "AllReferrersImported": "Referrers (with imported keywords)",
+ "SearchEnginesOriginal": "Search Engines (with tracked keywords)",
+ "SearchEnginesImported": "Search Engines (with imported keywords)",
+ "StartOAuth": "Start OAuth Process",
+ "UnverifiedSites": "Unverified websites:",
+ "UploadOAuthClientConfig": "Upload your OAuth client configuration",
+ "Uploading": "Uploading...",
+ "UrlOfAccount": "URL (Account)",
+ "VideoKeywords": "Video keywords on Google",
+ "VideoKeywordsDocumentation": "Keywords used in Google video<\/b> search that generated links to your website in the search result list.",
+ "NewsKeywords": "News keywords on Google",
+ "NewsKeywordsDocumentation": "Keywords used in Google News<\/b> search that generated links to your website in the search result list.",
+ "VisitOAuthHowTo": "Please visit our %1$sonline guide%2$s to learn how to set up your %3$s OAuth client configuration.",
+ "WebKeywords": "Web keywords on Google",
+ "WebKeywordsDocumentation": "Keywords used in Google web<\/b> search that generated links to your website in the search result list.",
+ "WebsiteSuccessfulConfigured": "Congratulations! You have successfully configured keyword import for the website %1$s. It might take a few days until your first search keywords will be imported and displayed in Referrers > Search Keywords. You can find more information about keyword import delays and limitations in our %2$sFAQ%3$s",
+ "WebsiteTypeUnsupported": "The selected measurable %1$s can't be configured as it has an unsupported type. Only measurables of type 'website' are supported.",
+ "WebsiteTypeUnsupportedRollUp": "Note: Roll-Up sites will automatically combine the imported data of all their child sites",
+ "YandexConfigurationDescription": "Yandex Webmaster API uses OAuth for authentication and authorization.",
+ "YandexConfigurationTitle": "Configure import from Yandex Webmaster API",
+ "YandexCrawlingStats": "Crawl overview for Yandex!",
+ "YandexCrawlingStatsDocumentation": "The Crawl overview allows you to view crawl related information such as errors encountered by the search bot when visiting a page, items blocked by your robots.txt file and the total number of pages in index.",
+ "YandexCrawlHttpStatus2xx": "HTTP Code 200-299",
+ "YandexCrawlHttpStatus2xxDesc": "Crawled Pages with a 2xx code",
+ "YandexCrawlHttpStatus3xx": "HTTP Code 300-399 (Moved pages)",
+ "YandexCrawlHttpStatus3xxDesc": "Crawled Pages with a 3xx code",
+ "YandexCrawlHttpStatus4xx": "HTTP Code 400-499 (Request errors)",
+ "YandexCrawlHttpStatus4xxDesc": "Crawled Pages with a 4xx code",
+ "YandexCrawlHttpStatus5xx": "HTTP Code 500-599 (Internal server errors)",
+ "YandexCrawlHttpStatus5xxDesc": "Crawled Pages with a 5xx code",
+ "YandexCrawlErrors": "Other request errors",
+ "YandexCrawlErrorsDesc": "Crawled paged that failed for any other reason",
+ "YandexCrawlCrawledPages": "Crawled Pages",
+ "YandexCrawlCrawledPagesDesc": "Number of pages the Yandex crawler requested.",
+ "YandexCrawlInIndex": "Total pages in index",
+ "YandexCrawlInIndexDesc": "Total number of pages available in Yandex search index",
+ "YandexCrawlAppearedPages": "Pages appeared in search",
+ "YandexCrawlAppearedPagesDesc": "Pages that were newly added to Yandex search index",
+ "YandexCrawlRemovedPages": "Pages removed from search",
+ "YandexCrawlRemovedPagesDesc": "Pages that were removed from Yandex search index",
+ "YandexFieldCallbackUri": "Callback URI",
+ "YandexFieldUrlToAppSite": "URL to app site",
+ "YandexKeywords": "Keywords on Yandex",
+ "YandexKeywordsDocumentation": "Keywords used in Yandex search that generated links to your website in the search results list.",
+ "YandexWebmasterApiUrl": "Url for Yandex Webmaster Tools",
+ "YandexWebmasterApiUrlDescription": "Provide the url this website is available in your Yandex Webmaster Tools",
+ "CrawlingOverview1": "The Crawling overview reports all the most critical information about how Search Engines robots crawl your websites. These metrics are updated approximately once per day with data provided by the search engines.",
+ "OptionQuickConnectWithGoogle": "Quick connect with Google (recommended)"
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/es.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/es.json
new file mode 100644
index 0000000..8da2b24
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/es.json
@@ -0,0 +1,170 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "APIKey": "Clave API",
+ "AccountAddedBy": "Agregado por %1$s en %2$s ",
+ "AccountConnectionValidationError": "Ocurrió un error al validar la conexión de la cuenta:",
+ "AccountDoesNotExist": "La cuenta configurada %1$s ya no existe más",
+ "AccountNoAccess": "Esta cuenta no tiene acceso a ningún sitio web.",
+ "AccountRemovalConfirm": "Estás a punto de eliminar la cuenta %1$s. Esto podría deshabilitar la importación de palabras clave para cualquiera de los sitio(s) web conectado(s). ¿Proceder de todas maneras?",
+ "ActivityAccountAdded": "agregó una nueva cuenta para el proveedor de palabras clave %1$s: %2$s",
+ "ActivityAccountRemoved": "eliminó una cuenta del proveedor de palabras clave %1$s:%2$s",
+ "ActivityGoogleClientConfigChanged": "cambió la configuración del cliente Google.",
+ "AddAPIKey": "Agregar clave API",
+ "AddConfiguration": "Agregar configuración",
+ "AdminMenuTitle": "Performance de la búsqueda",
+ "AvailableSites": "Sitios de internet disponibles para importar:",
+ "BingAccountError": "Se produjo un error al validar la clave API: %1$s. Si acaba de generar esta clave API en Bing Webmaster Tools, inténtelo nuevamente en uno o dos minutos (las claves API en Bing Webmaster Tools tardan un poco en ser activadas).",
+ "BingAccountOk": "Clave API verificada exitosamente",
+ "BingConfigurationDescription": "Bing Webmaster Tools necesita una clave API para poder acceder. Aquí puede agregar claves API para acceder a los datos de sus sitios web.",
+ "BingConfigurationTitle": "Configurar importación desde Bing Webmasters Tools",
+ "BingCrawlBlockedByRobotsTxt": "Exclusión Robots.txt",
+ "BingCrawlBlockedByRobotsTxtDesc": "URLs bloqueados actualmente por el archivo robots.txt de su sitio.",
+ "BingCrawlConnectionTimeout": "Tiempos de espera de conexión",
+ "BingCrawlConnectionTimeoutDesc": "Este número representa las incidencias recientes cuando Bing no pudo acceder a su sitio debido a errores de conexión. Esto podría ser un problema temporal, pero debe revisar los registros de su servidor para ver si está eliminando accidentalmente solicitudes.",
+ "BingCrawlCrawledPages": "Páginas rastreadas",
+ "BingCrawlCrawledPagesDesc": "Número de páginas solicitadas por el rastreador de Bing.",
+ "BingCrawlDNSFailures": "Fallos DNS",
+ "BingCrawlDNSFailuresDesc": "Este tipo de problema cataloga errores recientes encontrados al comunicarse con el servidor DNS cuando el bot intentaba acceder a sus páginas. Es posible que su servidor no funcionara o que hubiera una mala configuración que impidiera el enrutamiento de DNS, por ejemplo, TTL se estableció en 0.",
+ "BingCrawlErrors": "Errores de rastreo en Bing",
+ "BingCrawlErrorsDesc": "Número de errores ocurridos del rastreador Bing.",
+ "BingCrawlErrorsFromDateX": "El informe muestra errores de seguimiento recientemente reportados por Bing. No proporciona ningún dato histórico. Última actualización %s",
+ "BingCrawlHttpStatus2xx": "HTTP Código 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Estos códigos aparecen cuando el servidor presenta una página exitosamente",
+ "BingCrawlHttpStatus301": "HTTP Código 301 (Movido temporariamente)",
+ "BingCrawlHttpStatus301Desc": "Estos códigos aparecen cuando ha mudado permanentemente el contenido de una ubicación (URL) a otra.",
+ "BingCrawlHttpStatus302": "HTTP Código 302 (Movido temporariamente)",
+ "BingCrawlHttpStatus302Desc": "Estos códigos aparecen cuando ha movido temporalmente el contenido de una ubicación (URL) a otra.",
+ "BingCrawlHttpStatus4xx": "HTTP Código 400-499 (Errores de solicitud)",
+ "BingCrawlHttpStatus4xxDesc": "Estos códigos aparecen probablemente cuando hubo un error en la solicitud que impidió que el servidor pudiera procesarlo.",
+ "BingCrawlHttpStatus5xx": "HTTP Código 500-599 (Errores internos del servidor)",
+ "BingCrawlHttpStatus5xxDesc": "Estos códigos aparecen cuando el servidor no cumple con una solicitud aparentemente válida.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Exclusión Robots.txt de página destacada",
+ "BingCrawlInboundLink": "Total de enlaces entrantes",
+ "BingCrawlInboundLinkDesc": "Los enlaces entrantes que Bing conoce, apuntan a las URL de su sitio web. Estos son enlaces, desde sitios web externos a los suyos, que han sido apuntados hacia su contenido.",
+ "BingCrawlMalwareInfected": "Sitios de internet infectados con malware",
+ "BingCrawlMalwareInfectedDesc": "Cualquier dirección URL de página que Bing haya encontrado que esté infectada o asociada con malware se agrupará en esta sección.",
+ "BingCrawlPagesInIndex": "Páginas totales en el índice",
+ "BingCrawlPagesInIndexDesc": "Número total de páginas disponibles en el índice Bing",
+ "BingCrawlStatsOtherCodes": "Todos los códigos de estados HTTP",
+ "BingCrawlStatsOtherCodesDesc": "Agrupa todos los demás códigos que no coinciden con ningún otro valor (como 1xx o códigos informativos).",
+ "BingCrawlingStats": "Resumen de rastreo para Bing y Yahoo!",
+ "BingCrawlingStatsDocumentation": "El resumen de rastreo le permite ver información relacionada con el rastreo, tales como los errores encontrados por el robot de búsqueda al visitar una página, los elementos bloqueados por su archivo robots.txt y las URL potencialmente afectadas por malware.",
+ "BingKeywordImport": "Importar palabras clave Bing",
+ "BingKeywords": "Palabras claves (en Bing y Yahoo!)",
+ "BingKeywordsDocumentation": "Palabras clave utilizadas en las búsquedas de Bing o Yahoo! que generaron los enlaces a su sitio web en la lista de los resultados de la búsqueda",
+ "BingKeywordsNoRangeReports": "Palabras clave en Bing y Yahoo! solo se pueden utilizar para intervalos de fechas personalizados que incluyen semanas completas o meses, ya que no están disponibles como informes diarios.",
+ "BingKeywordsNotDaily": "Palabras clave en Bing y Yahoo! sólo están disponibles como informes semanales. No hay datos de palabras clave para períodos de días.",
+ "BingWebmasterApiUrl": "Url de Bing Webmaster Tools",
+ "BingWebmasterApiUrlDescription": "Proporcione la dirección URL de este sitio web así está disponible en su Bing Webmaster Tools",
+ "Category": "Categoría",
+ "ChangeConfiguration": "Cambiar configuración",
+ "Clicks": "Clics",
+ "ClicksDocumentation": "Un clic se cuenta cada vez que alguien hace clic en un enlace que apunta a su sitio web en una página de resultados del motor de búsqueda.",
+ "ClientConfigImported": "La configuración del cliente ha sido importada con éxito!",
+ "ClientConfigSaveError": "Se produjo un error al guardar la configuración del cliente. Verifique si la configuración proporcionada es válida e intente nuevamente.",
+ "ClientId": "id de cliente",
+ "ClientSecret": "Cliente secreto",
+ "ConfigAvailableNoWebsiteConfigured": "Integración exitosamente configurada, pero actualmente no hay sitio web configurado para importar.",
+ "ConfigRemovalConfirm": "Está a punto de eliminar la configuración de %1$s. La importación de palabras clave para ese sitio web será deshabilitada. ¿Proceder de todas maneras?",
+ "Configuration": "Configuración",
+ "ConfigurationDescription": "Este complemento le permite directamente importar en Matomo todas las palabras claves buscadas por sus usuarios en los motores de búsqueda.",
+ "ConfigurationFile": "Archivo de configuración",
+ "ConfigurationValid": "Su configuración OAuth es válida.",
+ "ConfigureMeasurableBelow": "Para configurar un sitio web, simplemente haga clic en el botón a continuación o configúrelo directamente en la configuración del sitio web.",
+ "ConfigureMeasurables": "Configurar sitios de internet",
+ "ConfiguredAccounts": "cuentas configuradas",
+ "ConfiguredUrlNotAvailable": "La URL configurada no está disponible para esta cuenta",
+ "ConnectAccount": "Conectar cuenta",
+ "ConnectFirstAccount": "Comience por conectar su primera cuenta a continuación.",
+ "ConnectGoogleAccounts": "Conectar cuenta(s) Google",
+ "ContainingSitemaps": "Conteniendo mapas de sitio",
+ "CrawlingErrors": "Errores de rastreo",
+ "CrawlingStats": "Descripción de los rastreos",
+ "Ctr": "CTR",
+ "CtrDocumentation": "Porcentaje de clics: una proporción que muestra la frecuencia con la que las personas que ven una página de resultados vía motor de búsqueda con un enlace a su sitio web terminan haciendo clic en el mismo.",
+ "CurrentlyConnectedAccounts": "Actualmente hay %1$s cuentas conectadas.",
+ "Domain": "Dominio",
+ "DomainProperty": "Propiedades de dominio",
+ "DomainPropertyInfo": "Incluye todos los subdominios (m, www, etc.) y ambos protocolos (http y https).",
+ "EnabledSearchTypes": "Tipos de palabras clave a recolectar",
+ "FetchImageKeyword": "Recolectar palabras claves de las imágenes",
+ "FetchImageKeywordDesc": "Recolectar palabras claves usadas en la búsqueda de imágenes de Google",
+ "FetchVideoKeyword": "Recolectar palabras claves de los videos",
+ "FetchVideoKeywordDesc": "Recolectar palabras claves usadas en la búsqueda de videos de Google",
+ "FetchWebKeyword": "Recolectar palabras claves de la web",
+ "FetchWebKeywordDesc": "Recolectar palabras claves usadas en las búsquedas web de Google",
+ "FirstDetected": "Primera detectada",
+ "GoogleAccountAccessTypeOfflineAccess": "Acceso sin conexión es necesario para poder importar sus palabras clave de búsqueda incluso cuando no esté actualmente conectado.",
+ "GoogleAccountAccessTypeProfileInfo": "Información de perfil es usada para mostrar el nombre de las actuales cuenta(s) conectadas.",
+ "GoogleAccountAccessTypeSearchConsoleData": "Datos de la consola de búsqueda es necesaria para obtener acceso a sus palabras clave de búsqueda en Google",
+ "GoogleAccountError": "Se produjo un error al validar el acceso OAuth: %1$s",
+ "GoogleAccountOk": "Acceso OAuth verificado exitosamente.",
+ "GoogleConfigurationDescription": "Google Search Console usa OAuth para autenticación y autorización.",
+ "GoogleConfigurationTitle": "Configurar importación desde Google Search Console",
+ "GoogleKeywordImport": "Importar palabras clave Google",
+ "GoogleSearchConsoleUrl": "URL para la Google Search Console",
+ "GoogleSearchConsoleUrlDescription": "Proporcione la URL, este sitio web está disponible en su Consola de búsqueda de Google",
+ "GoogleUploadOrPasteClientConfig": "Por favor, suba la configuración de su cliente de Google OAuth o péguela a continuación en el campo.",
+ "HowToGetOAuthClientConfig": "Cómo obtener la configuración de su cliente OAuth",
+ "ImageKeywords": "Palabras clave de la imagen en Google",
+ "ImageKeywordsDocumentation": "Palabras clave en búsquedas de Google Imágenes que generaron enlace a su sitio web en la lista resultante de la búsqueda.",
+ "Impressions": "Impresiones",
+ "ImpressionsDocumentation": "Se contabiliza una impresión cada vez que se muestra su sitio web en una página de resultados del motor de búsqueda.",
+ "IntegrationConfigured": "Integración configurada exitosamente",
+ "IntegrationNotConfigured": "Integración aun no configurada",
+ "KeywordStatistics": "Buscar palabras claves",
+ "KeywordTypeImage": "imagen",
+ "KeywordTypeVideo": "video",
+ "KeywordTypeWeb": "web",
+ "KeywordsCombined": "Palabras claves combinadas",
+ "KeywordsCombinedDocumentation": "Informe que combina todas las palabras clave detectadas por Matomo e importadas desde los motores de búsqueda. Este informe solo incluye la métrica de la visita. Puede cambiar a uno de los informes relacionados para obtener métricas detalladas.",
+ "KeywordsCombinedImported": "Palabras clave importadas combinadas",
+ "KeywordsCombinedImportedDocumentation": "Informe que muestra todas las palabras clave importadas de todos los motores de búsqueda configurados.",
+ "KeywordsReferrers": "Palabras clave (incluyendo las no definidas)",
+ "LastCrawled": "Ultimo rastreo",
+ "LastDetected": "Ultima detectada",
+ "LastImport": "Ultima importación",
+ "LatestAvailableDate": "Los datos de palabras clave más recientes disponibles son para %1$s",
+ "LinksToUrl": "Enlaces a %s",
+ "ManageAPIKeys": "Administrar claves API",
+ "MeasurableConfig": "Sitios de internet configurados",
+ "NoSegmentation": "El informe no admite la segmentación. Los datos que se muestran son sus datos de informe estándar, no segmentados.",
+ "NoWebsiteConfigured": "Actualmente no hay ningún sitio web configurado. Para habilitar la importación de un sitio web específico, ajuste la configuración aquí.",
+ "NoWebsiteConfiguredWarning": "Importación para %s no está totalmente configurado. Es necesario ajustar algunos sitios web para habilitar la importación.",
+ "NotAvailable": "no disponible",
+ "OAuthClientConfig": "Configuración del cliente OAuth",
+ "OAuthError": "Un error ocurrió durante el proceso OAuth. Por favor inténtelo nuevamente y asegúrese de aceptar los permisos solicitados.",
+ "Platform": "Plataforma",
+ "Position": "Posición promedio",
+ "PositionDocumentation": "Posición promedio de su sitio web en la lista de resultados del motor de búsqueda (para esta palabra clave).",
+ "ProviderBingDescription": "Importar todas las palabras claves usadas para encontrar su sitio web en las búsquedas hechas por Bing y Yahoo .",
+ "ProviderBingNote": "Nota: Microsoft proporciona datos de palabras clave todos los sábados y solo durante semanas enteras. Como resultado, sus palabras clave para Bing y Yahoo tardarán unos días en aparecer en sus informes y solo estarán disponibles cuando las vea en semanas, meses o años.",
+ "ProviderGoogleDescription": "Importe todas las palabras clave utilizadas para encontrar su sitio web en la búsqueda de Google . Los informes mostrarán sus palabras clave para cada tipo de búsqueda por separado (Web, imágenes y videos).",
+ "ProviderListDescription": "Cuando haya configurado correctamente uno (o más) motores de búsqueda, puede configurar en qué sitios web(s) debe Matomo importar sus palabras clave de búsqueda.",
+ "ProviderXAccountWarning": "Problemas de configuración de la cuenta detectados Por favor, compruebe las cuentas configuradas %s .",
+ "ProviderXSitesWarning": "Problemas de configuración del sitio web detectados Por favor verifique los sitios web configurados %s .",
+ "ReAddAccountIfPermanentError": "Si este es un error permanente, intente eliminar la cuenta y conéctela nuevamente.",
+ "RequiredAccessTypes": "Estos tipos de acceso son necesarios:",
+ "ResponseCode": "Código de respuesta",
+ "RoundKeywordPosition": "Ronda de posición de palabras claves",
+ "SearchEngineKeywordsPerformance": "Rendimiento de las palabras claves en los motores de búsqueda",
+ "SetupConfiguration": "Ajustes de configuración",
+ "SitemapsContainingUrl": "Mapas de sitio conteniendo %s",
+ "StartOAuth": "Iniciar proceso OAuth",
+ "URLPrefix": "Prefijo de la URL",
+ "URLPrefixProperty": "Propiedades de prefijo de URL",
+ "URLPrefixPropertyInfo": "Incluye solo las URL con el prefijo exacto especificado, incluido el protocolo (http/https). Si quieres que tu propiedad abarque más protocolos o subdominios (http/https/www./m., etc.), quizás te interese más añadir una propiedad de dominio.",
+ "UnverifiedSites": "Sitios de internet sin verificar:",
+ "UploadOAuthClientConfig": "Cargue su configuración de cliente OAuth",
+ "UrlOfAccount": "URL (Cuenta)",
+ "VideoKeywords": "Palabras clave de videos en Google",
+ "VideoKeywordsDocumentation": "Palabras clave utilizadas en la búsqueda de videos de Google que generaron enlaces a su sitio web en la lista de resultados de búsqueda.",
+ "WebKeywords": "Palabras clave de la web en Google",
+ "WebKeywordsDocumentation": "Palabras clave utilizadas en la búsqueda web de Google que generaron enlaces a su sitio web en la lista de resultados de búsqueda.",
+ "WebsiteSuccessfulConfigured": "Felicidades! Ha configurado correctamente la importación de palabras clave para el sitio web %1$s. Es posible que pasen unos días hasta que sus primeras palabras clave de búsqueda se importen y se muestren en Referencias > Palabras clave de búsqueda. Puede encontrar más información sobre los retrasos y limitaciones de importación de palabras clave en nuestras %2$sPreguntas frecuentes%3$s",
+ "YandexCrawlHttpStatus2xx": "HTTP Código 200-299",
+ "YandexCrawlHttpStatus4xx": "HTTP Código 400-499 (Errores de solicitud)",
+ "YandexCrawlHttpStatus5xx": "HTTP Código 500-599 (Errores internos del servidor)",
+ "YandexCrawlInIndex": "Páginas totales en el índice"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/fi.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/fi.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/fi.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/fr.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/fr.json
new file mode 100644
index 0000000..a33569f
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/fr.json
@@ -0,0 +1,241 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "APIKey": "Clé API",
+ "AccountAddedBy": "Ajouté par %1$s sur %2$s ",
+ "AccountConnectionValidationError": "Une erreur s'est produite lors de la validation de la connexion du compte :",
+ "AccountDoesNotExist": "Le compte configuré %1$s n'existe plus",
+ "AccountNoAccess": "Ce compte n'a actuellement accès à aucun site web.",
+ "AccountRemovalConfirm": "Vous êtes sur le point de supprimer le compte %1$s. Cela pourrait désactiver l'importation de mots-clés pour tous les site(s) connecté(s). Souhaitez-vous poursuivre ?",
+ "ActivityAccountAdded": "a ajouté un nouveau compte pour le fournisseur de mots-clés %1$s : %2$s",
+ "ActivityAccountRemoved": "a supprimé un compte pour le fournisseur de mots-clés %1$s : %2$s",
+ "ActivityGoogleClientConfigChanged": "a modifié le fichier de configuration du client Google.",
+ "ActivityYandexClientConfigChanged": "a modifié le fichier de configuration du client Yandex.",
+ "AddAPIKey": "Ajouter la clé API",
+ "AddConfiguration": "Ajouter la configuration",
+ "AdminMenuTitle": "Performance de la recherche",
+ "AllReferrersImported": "Référents (avec mots-clés importés)",
+ "AllReferrersOriginal": "Référents (avec mots-clés suivis)",
+ "AvailableSites": "Sites disponibles pour l'import :",
+ "BingAPIKeyInstruction": "Connectez-vous à %1$sBing Webmaster Tools%2$s, puis ajoutez votre site Web dans Bing Webmaster Tools. Après l'avoir validé, vous pouvez %3$scopier votre clé API%4$s.",
+ "BingAccountError": "Une erreur s'est produite lors de la validation de la clé API : %1$s. Si vous venez de générer cette clé API dans le service Bing Webmaster Tools, veuillez réessayer dans une ou deux minutes (les clés API dans Bing Webmaster Tools mettent un peu de temps à s'activer).",
+ "BingAccountOk": "Clé API vérifiée avec succès",
+ "BingConfigurationDescription": "Bing Webmaster Tools nécessite une clé API pour être accessible. Vous pouvez ajouter ici des clés API pour accéder aux données de vos sites web.",
+ "BingConfigurationTitle": "Configurer l'import à partir de Bing Webmaster Tools",
+ "BingCrawlBlockedByRobotsTxt": "Exclusion du fichier Robots.txt",
+ "BingCrawlBlockedByRobotsTxtDesc": "Ces URLs sont actuellement bloquées par le fichier robots.txt de votre site.",
+ "BingCrawlConnectionTimeout": "Dépassement du délai de connexion",
+ "BingCrawlConnectionTimeoutDesc": "Ce nombre représente les occurrences récentes où Bing n'a pas pu accéder à votre site en raison d'erreurs de connexion. Il peut s'agir d'un problème temporaire, mais vous devriez vérifier les journaux de votre serveur pour voir si les requêtes ne sont pas accidentellement bloquées.",
+ "BingCrawlCrawledPages": "Pages crawlées",
+ "BingCrawlCrawledPagesDesc": "Nombre de pages parcourus par le crawler de Bing.",
+ "BingCrawlDNSFailures": "Erreurs DNS",
+ "BingCrawlDNSFailuresDesc": "Ce type de problème répertorie les erreurs récentes rencontrées lors de la tentative de communication avec le serveur DNS lorsque le robot a essayé d'accéder à vos pages. Il est possible que votre serveur soit en panne, ou qu'une mauvaise configuration empêche le routage DNS, par exemple, le TTL a été fixé à 0.",
+ "BingCrawlErrors": "Erreurs de crawl sur Bing",
+ "BingCrawlErrorsDesc": "Nombre d'erreurs survenues pour le crawler Bing.",
+ "BingCrawlErrorsFromDateX": "Le rapport montre les erreurs de crawl récemment rencontrées par Bing. Il ne fournit pas de données historiques. Dernière mise à jour %s",
+ "BingCrawlHttpStatus2xx": "Code HTTP 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Ces codes apparaissent lorsque le serveur renvoi une page avec succès",
+ "BingCrawlHttpStatus301": "Code HTTP 301 (redirigé de façon permanente)",
+ "BingCrawlHttpStatus301Desc": "Ces codes apparaissent lorsque vous avez déplacé de façon permanente un contenu d'un emplacement (URL) à un autre.",
+ "BingCrawlHttpStatus302": "Code HTTP 302 (déplacé temporairement)",
+ "BingCrawlHttpStatus302Desc": "Ces codes apparaissent lorsque vous avez temporairement déplacé du contenu d'un emplacement (URL) à un autre.",
+ "BingCrawlHttpStatus4xx": "Code HTTP 400-499 (erreurs de requêtes)",
+ "BingCrawlHttpStatus4xxDesc": "Ces codes apparaissent lorsqu'il y a probablement une erreur dans la requête ce qui a empêché le serveur de la traiter.",
+ "BingCrawlHttpStatus5xx": "Code HTTP 500-599 (erreurs internes du serveur)",
+ "BingCrawlHttpStatus5xxDesc": "Ces codes apparaissent lorsque le serveur n'a pas réussi à satisfaire une demande apparemment valide.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Exclusion d'une page importante dans le fichier Robots.txt",
+ "BingCrawlInboundLink": "Nombre total de liens entrants",
+ "BingCrawlInboundLinkDesc": "Liens entrants dont Bing a connaissance, pointant vers des URLs de votre site Web. Il s'agit de liens, provenant de sites Web extérieurs au vôtre, qui ont été dirigés vers votre contenu.",
+ "BingCrawlMalwareInfected": "Sites web infectés par des logiciels malveillants",
+ "BingCrawlMalwareInfectedDesc": "Toutes les URL de pages trouvées par Bing qui sont infectées ou associées à des logiciels malveillants seront regroupées dans cette section.",
+ "BingCrawlPagesInIndex": "Nombre total de pages dans l'index",
+ "BingCrawlPagesInIndexDesc": "Nombre total de pages disponibles dans l'index Bing",
+ "BingCrawlStatsOtherCodes": "Tous les autres codes d'état HTTP",
+ "BingCrawlStatsOtherCodesDesc": "Regroupe tous les autres codes qui ne correspondent à aucune autre valeur (tels que les codes 1xx ou informatifs).",
+ "BingCrawlingStats": "Aperçu des crawls pour Bing et Yahoo !",
+ "BingCrawlingStatsDocumentation": "La vue d'ensemble du crawl vous permet de visualiser les informations relatives au crawl, telles que les erreurs rencontrées par le robot de recherche lors de la visite d'une page, les éléments bloqués par votre fichier robots.txt et les URL potentiellement affectées par des logiciels malveillants.",
+ "BingKeywordImport": "Import des mots-clés Bing",
+ "BingKeywords": "Mots-clés (sur Bing et Yahoo !)",
+ "BingKeywordsDocumentation": "Mots clés utilisés dans la recherche Bing ou Yahoo ! qui ont généré des liens vers votre site web dans la liste des résultats de recherche.",
+ "BingKeywordsNoRangeReports": "Les mots-clés sur Bing et Yahoo ! ne peuvent être traités que pour des plages de dates personnalisées, y compris des semaines ou des mois entiers, car ils ne sont pas disponibles sous forme de rapports quotidiens.",
+ "BingKeywordsNotDaily": "Les mots-clés sur Bing et Yahoo ! ne sont disponibles que sous forme de rapports hebdomadaires. Il n'y a pas de données sur les mots-clés pour les périodes de plusieurs jours.",
+ "BingWebmasterApiUrl": "Url pour Bing Webmaster Tools",
+ "BingWebmasterApiUrlDescription": "Fournissez l'url de ce site Web dans vos outils pour les webmasters de Bing",
+ "Category": "Catégorie",
+ "ChangeConfiguration": "Modifier la configuration",
+ "Clicks": "Clics",
+ "ClicksDocumentation": "Un clic est comptabilisé à chaque fois que quelqu'un clique sur un lien pointant vers votre site web lorsqu'il est sur une page de résultats d'un moteur de recherche.",
+ "ClientConfigImported": "La configuration du client a été importée avec succès !",
+ "ClientConfigSaveError": "Une erreur s'est produite lors de la sauvegarde de la configuration du client. Veuillez vérifier si la configuration fournie est valide, et réessayer.",
+ "ClientId": "id du client",
+ "ClientSecret": "Client secret",
+ "ConfigAvailableNoWebsiteConfigured": "L'intégration a été effectuée avec succès, mais aucun site web n'est actuellement configuré pour l'import.",
+ "ConfigRemovalConfirm": "Vous êtes sur le point de supprimer la configuration pour %1$s. L'import de mots-clés pour ce site Web sera désactivée. Souhaitez-vous continuer ?",
+ "Configuration": "Configuration",
+ "ConfigurationDescription": "Cette extension vous permet d'importer directement dans Matomo tous les mots-clés recherchés par vos utilisateurs sur les moteurs de recherche.",
+ "ConfigurationFile": "Fichier de configuration",
+ "ConfigurationValid": "Votre configuration OAuth est valide.",
+ "ConfigureMeasurableBelow": "Pour configurer un site web, il suffit de cliquer sur le bouton ci-dessous ou de le configurer directement dans les paramètres du site web.",
+ "ConfigureMeasurables": "Configurer les sites web",
+ "ConfigureTheImporterLabel1": "Importez vos mots-clés de Google Search Console et analysez-les à l'aide des puissants outils d'analyse de Matomo. Une fois l'importateur connecté, sélectionnez les sites web à importer, et Matomo commencera à importer les mots-clés pour eux dans le cadre du processus d'archivage planifié.",
+ "ConfigureTheImporterLabel2": "Afin d'importer vos données depuis Google Search Console, Matomo doit y avoir accès.",
+ "ConfigureTheImporterLabel3": "Pour commencer, %1$ssuivez nos instructions pour récupérer la configuration de votre client OAuth%2$s. Ensuite, téléversez le fichier de configuration du client en utilisant le bouton ci-dessous.",
+ "ConfiguredAccounts": "comptes configurés",
+ "ConfiguredUrlNotAvailable": "L'URL configurée n'est pas disponible pour ce compte",
+ "ConnectAccount": "Connecter le compte",
+ "ConnectAccountDescription": "Veuillez cliquer sur le bouton ci-dessous pour être redirigé vers %1$s où vous devez accorder l'accès.",
+ "ConnectAccountYandex": "L'authentification des comptes Yandex n'est valable que pendant %1$s jours. Chaque compte doit être réauthentifié dans ce délai pour que les imports fonctionnent correctement.",
+ "ConnectFirstAccount": "Commencez par connecter votre premier compte ci-dessous.",
+ "ConnectGoogleAccounts": "Connecter le(s) compte(s) Google",
+ "ConnectYandexAccounts": "Connecter le(s) compte(s) Yandex",
+ "ContainingSitemaps": "Qui contient les sitemaps",
+ "CrawlingErrors": "Erreurs de crawl",
+ "CrawlingOverview1": "Le récapitulatif d'exploration présente toutes les informations les plus importantes sur la façon dont les robots des moteurs de recherche explorent vos sites Web. Ces métriques sont mises à jour environ une fois par jour avec les données fournies par les moteurs de recherche.",
+ "CrawlingStats": "Vue d'ensemble du crawl",
+ "CreatedBy": "Créé par",
+ "Ctr": "CTR",
+ "CtrDocumentation": "Taux de clics (CTR) : Ratio montrant combien de fois les personnes qui voient une page de résultats de moteur de recherche avec un lien vers votre site web finissent par cliquer dessus.",
+ "CurrentlyConnectedAccounts": "Il y a actuellement %1$s comptes connectés.",
+ "DeleteUploadedClientConfig": "Si vous souhaitez supprimer la configuration du client téléversée, cliquez ci-dessous",
+ "Domain": "Domaine",
+ "DomainProperty": "Propriété de domaine",
+ "DomainPropertyInfo": "Comprend tous les sous-domaines (m, www, etc.) et les deux protocoles (http, https).",
+ "EnabledSearchTypes": "Types de mots-clés à récupérer",
+ "FetchImageKeyword": "Récupérer les mots-clés pour les images",
+ "FetchImageKeywordDesc": "Récupérer les mots-clés utilisés dans la recherche d'images sur Google",
+ "FetchNewsKeyword": "Récupérer les mots clés des actualités",
+ "FetchNewsKeywordDesc": "Récupérer les mots-clés utilisés dans Google News",
+ "FetchVideoKeyword": "Récupérer les mots-clés liés aux vidéos",
+ "FetchVideoKeywordDesc": "Récupérer les mots-clés utilisés dans la recherche de vidéos sur Google",
+ "FetchWebKeyword": "Récupérer les mots-clés du web",
+ "FetchWebKeywordDesc": "Récupérer les mots-clés utilisés dans les recherches sur le Web de Google",
+ "FirstDetected": "Premier détecté",
+ "GoogleAccountAccessTypeOfflineAccess": "L'accès hors ligne est nécessaire pour pouvoir importer vos mots-clés de recherche même si vous n'êtes pas actuellement connecté.",
+ "GoogleAccountAccessTypeProfileInfo": "Infos sur le profil est utilisé pour montrer le nom du ou des comptes actuellement connectés.",
+ "GoogleAccountAccessTypeSearchConsoleData": "Les données de la Search Console sont nécessaires pour avoir accès à vos mots-clés de recherche Google.",
+ "GoogleAccountError": "Une erreur s'est produite lors de la validation de l'accès OAuth : %1$s",
+ "GoogleAccountOk": "Accès OAuth vérifié avec succès.",
+ "GoogleAuthorizedJavaScriptOrigin": "Origine JavaScript autorisée",
+ "GoogleAuthorizedRedirectUri": "URI de redirection autorisée",
+ "GoogleConfigurationDescription": "La Google Search Console utilise OAuth pour l'authentification et l'autorisation.",
+ "GoogleConfigurationTitle": "Configurer l'import à partir de la Google Search Console",
+ "GoogleDataNotFinal": "Les données relatives aux mots-clés figurant dans ce rapport peuvent ne pas contenir les données définitives. Google fournit les données définitives des mots-clés avec un retard de 2 jours. Les mots-clés des jours plus récents seront réimportés jusqu'à ce qu'ils soient déclarés comme définitifs.",
+ "GoogleDataProvidedWithDelay": "Google fournit des données sur les mots-clés avec un certain délais. Les mots-clés pour cette date seront importés un peu plus tard.",
+ "GoogleKeywordImport": "Import de mots-clés Google",
+ "GooglePendingConfigurationErrorMessage": "La configuration est en attente. Veuillez demander à un super utilisateur de la compléter.",
+ "GoogleSearchConsoleUrl": "Url pour la Google Search Console",
+ "GoogleSearchConsoleUrlDescription": "Fournissez l'url de ce site web dans votre Google Search Console",
+ "GoogleUploadOrPasteClientConfig": "Veuillez télécharger votre configuration du client Google OAuth ou la coller dans le champ ci-dessous.",
+ "HowToGetOAuthClientConfig": "Comment obtenir la configuration de votre client OAuth",
+ "ImageKeywords": "Mots-clés de l'image sur Google",
+ "ImageKeywordsDocumentation": "Mots clés utilisés dans la recherche Google image qui ont généré des liens vers votre site web dans la liste des résultats de recherche.",
+ "Impressions": "Impressions",
+ "ImpressionsDocumentation": "Une impression est comptée chaque fois que votre site web est affiché dans une page de résultats d'un moteur de recherche.",
+ "IntegrationConfigured": "L'intégration a été configurée avec succès",
+ "IntegrationNotConfigured": "L'intégration n'a pas été encore configurée",
+ "InvalidRedirectUriInClientConfiguration": "URI de redirection invalides, au moins une URI doit correspondre à l'URI \"%1$s\" dans le fichier de configuration téléversé",
+ "KeywordStatistics": "Mots-clés de recherche",
+ "KeywordTypeImage": "image",
+ "KeywordTypeNews": "actualités",
+ "KeywordTypeVideo": "vidéo",
+ "KeywordTypeWeb": "web",
+ "KeywordsCombined": "Mots clés combinés",
+ "KeywordsCombinedDocumentation": "Rapport combinant tous les mots-clés détectés par Matomo et importés des moteurs de recherche. Ce rapport ne comprend que la métrique des visites. Vous pouvez passer à l'un des rapports connexes pour obtenir des mesures détaillées.",
+ "KeywordsCombinedImported": "Tous les mots-clés combinés importés",
+ "KeywordsCombinedImportedDocumentation": "Rapport montrant tous les mots-clés importés de tous les moteurs de recherche configurés.",
+ "KeywordsReferrers": "Mots-clés (y compris non définis)",
+ "KeywordsSubtableImported": "Mots clés importés",
+ "KeywordsSubtableOriginal": "Mots-clés suivis (y compris non définis)",
+ "LastCrawled": "Dernier crawlé",
+ "LastDetected": "Dernier détecté",
+ "LastImport": "Dernier import",
+ "LatestAvailableDate": "Les données de mots-clés les plus récentes sont celles de %1$s",
+ "LinksToUrl": "Liens vers %s",
+ "ManageAPIKeys": "Gérer les clés API",
+ "MeasurableConfig": "sites web configurés",
+ "NewsKeywords": "Mots clés Google Actualités",
+ "NewsKeywordsDocumentation": "Mots-clés utilisés dans la recherche Google Actualités qui ont généré des liens vers votre site web dans la liste des résultats de recherche.",
+ "NoSegmentation": "Le rapport ne prend pas en charge la segmentation. Les données affichées sont celles de votre rapport standard, non segmenté.",
+ "NoWebsiteConfigured": "Il n'y a actuellement aucun site web configuré. Pour activer l'import pour un site web spécifique, veuillez configurer la configuration ici.",
+ "NoWebsiteConfiguredWarning": "L'import pour %s n'est pas entièrement configuré. Vous devez configurer certains sites Web pour permettre l'importation.",
+ "NotAvailable": "Non disponible",
+ "OAuthAccessTimedOut": "L'accès OAuth pour ce compte a peut-être expiré. Vous devrez vous réauthentifier pour que les imports fonctionnent à nouveau pour ce compte.",
+ "OAuthAccessWillTimeOut": "L'accès OAuth pour ce compte expirera après %1$s jours. %2$s jours restants ",
+ "OAuthAccessWillTimeOutSoon": "L'accès OAuth pour ce compte expirera dans environ %1$s jours. Veuillez vous réauthentifier pour éviter que ce compte ne cesse de fonctionner.",
+ "OAuthClientConfig": "Configuration du client OAuth",
+ "OAuthError": "Une erreur s'est produite dans le processus OAuth. Veuillez réessayer et vous assurer que vous acceptez les permissions demandées.",
+ "OAuthExampleText": "La configuration nécessite les champs listés ci-dessous. Veuillez utiliser les valeurs fournies :",
+ "OauthFailedMessage": "Nous avons rencontré un problème lors du processus d'autorisation pour votre Google Search Console. Pour réessayer, veuillez cliquer sur le bouton ci-dessous. Si le problème persiste, veuillez contacter notre équipe d'assistance. Elle vous aidera à résoudre le problème et à importer vos mots-clés de Google Search Console.",
+ "OptionQuickConnectWithGoogle": "Connexion rapide avec Google (recommandé)",
+ "Platform": "Plate-forme",
+ "Position": "Position moyenne",
+ "PositionDocumentation": "Position moyenne de votre site web dans la liste des résultats des moteurs de recherche (pour ce mot clé).",
+ "ProvideYandexClientConfig": "Veuillez insérer la configuration de votre client Yandex OAuth.",
+ "ProviderBingDescription": "Importer tous les mots clés utilisés pour trouver votre site web sur les moteurs de recherche Bing et Yahoo! .",
+ "ProviderBingNote": "Remarque: Microsoft fournit des données sur les mots-clés tous les samedis et uniquement pour des semaines entières. Par conséquent, vos mots-clés pour Bing et Yahoo mettront quelques jours à apparaître dans vos rapports et ne seront disponibles que lors de la consultation des semaines, mois ou années.",
+ "ProviderGoogleDescription": "Importez tous les mots-clés utilisés pour trouver votre site Web sur la recherche Google . Les rapports montreront vos mots-clés pour chaque type de recherche séparément (Web, Images et Vidéos).",
+ "ProviderGoogleNote": "Remarque: Google fournit les données définitives des mots-clés avec un retard de 2 jours. Les données non finales pour les jours plus récents seront déjà affichées, mais seront réimportées jusqu'à ce qu'elles soient définitives. Votre premier import peut permettre d'importer vos données historiques de mots-clés jusqu'aux 486 derniers jours.",
+ "ProviderListDescription": "Lorsque vous avez configuré avec succès un (ou plusieurs) moteur de recherche ci-dessous, vous pouvez configurer dans quel(s) site(s) web Matomo doit importer vos mots-clés de recherche.",
+ "ProviderXAccountWarning": "Problème de configuration de compte détecté Veuillez vérifier les comptes configurés pour %s .",
+ "ProviderXSitesWarning": "Problèmes de configuration de site détectés Veuillez vérifier la configuration des sites pour %s .",
+ "ProviderYandexDescription": "Importez tous les mots-clés utilisés pour trouver votre site web sur la recherche Yandex .",
+ "ProviderYandexNote": "Remarque: Yandex fournit les mots-clés avec un retard allant jusqu'à 5 jours. La première importation tentera d'importer vos mots-clés historiques jusqu'aux 100 derniers jours.",
+ "ReAddAccountIfPermanentError": "S'il s'agit d'une erreur permanente, essayez de supprimer le compte et de le connecter à nouveau.",
+ "ReAuthenticateIfPermanentError": "S'il s'agit d'une erreur permanente, essayez de réauthentifier le compte ou de le supprimer et de le connecter à nouveau.",
+ "Reauthenticate": "Réauthentification",
+ "RecentApiErrorsWarning": "Erreurs d'importation de mots-clés détectées Veuillez vérifier les configurations pour : %s Si votre configuration est correcte et que les erreurs persistent, veuillez contacter notre support.",
+ "ReportShowMaximumValues": "Les valeurs affichées sont les valeurs maximales qui se sont produites pendant cette période.",
+ "RequiredAccessTypes": "Ces types d'accès sont nécessaires :",
+ "ResponseCode": "Code de réponse",
+ "RoundKeywordPosition": "Position arrondi des mots-clés",
+ "SearchEngineKeywordsPerformance": "Performance des mots-clés dans les moteurs de recherche",
+ "SearchEnginesImported": "Moteurs de recherche (avec mots-clés importés)",
+ "SearchEnginesOriginal": "Moteurs de recherche (avec mots-clés suivis)",
+ "SetUpOAuthClientConfig": "Configurer votre client OAuth",
+ "SetupConfiguration": "Configuration de l'installation",
+ "SitemapsContainingUrl": "Sitemaps contenant %s",
+ "StartOAuth": "Démarrer le processus OAuth",
+ "URLPrefix": "Préfixe de l'URL",
+ "URLPrefixProperty": "Propriété de préfixe d'URL",
+ "URLPrefixPropertyInfo": "Comprend uniquement les URL avec le préfixe exact indiqué, y compris le protocole (http/https). Si vous souhaitez que votre propriété corresponde à n'importe quel protocole ou sous-domaine (http/https/www/m, etc.), envisagez plutôt d'ajouter une propriété de domaine.",
+ "UnverifiedSites": "Sites web non vérifiés :",
+ "UploadOAuthClientConfig": "Téléversez la configuration de votre client OAuth",
+ "Uploading": "Téléversement en cours...",
+ "UrlOfAccount": "URL (Compte)",
+ "VideoKeywords": "Mots clés vidéo sur Google",
+ "VideoKeywordsDocumentation": "Mots-clés utilisés dans la recherche Google vidéo qui ont généré des liens vers votre site web dans la liste des résultats de recherche.",
+ "VisitOAuthHowTo": "Veuillez consulter notre guide %1$sen ligne%2$s pour savoir comment mettre en place votre configuration client OAuth %3$s.",
+ "WebKeywords": "Mots clés issus de la recherche Google Web",
+ "WebKeywordsDocumentation": "Mots-clés utilisés dans la recherche Google web qui ont généré des liens vers votre site web dans la liste des résultats de recherche.",
+ "WebsiteSuccessfulConfigured": "Félicitations! Vous avez configuré avec succès l'import de mots-clés pour le site Web %1$s. Il se peut que quelques jours s'écoulent avant que vos premiers mots-clés de recherche soient importés et affichés dans Référents > Mots-clés de recherche. Vous pouvez trouver plus d'informations sur les délais et les limitations de l'import de mots-clés dans notre %2$sFAQ%3$s",
+ "WebsiteTypeUnsupported": "L’élément mesurable sélectionné %1$s ne peut pas être configuré car il a un type non pris en charge. Seuls les éléments mesurables de type 'site web' sont supportés.",
+ "WebsiteTypeUnsupportedRollUp": "Remarque : les sites de type \"Roll-Up\" combinent automatiquement les données importées de tous leurs sites associés",
+ "YandexConfigurationDescription": "Yandex Webmaster API utilise OAuth pour l'authentification et l'autorisation.",
+ "YandexConfigurationTitle": "Configurer l'import à partir de Yandex Webmaster API",
+ "YandexCrawlAppearedPages": "Pages apparaissant dans la recherche",
+ "YandexCrawlAppearedPagesDesc": "Pages nouvellement ajoutées à l'index de recherche Yandex",
+ "YandexCrawlCrawledPages": "Pages explorées",
+ "YandexCrawlCrawledPagesDesc": "Nombre de pages demandées par le crawler Yandex.",
+ "YandexCrawlErrors": "Autres erreurs de requêtes",
+ "YandexCrawlErrorsDesc": "Pages explorées qui ont échoué pour une toute autre raison",
+ "YandexCrawlHttpStatus2xx": "Code HTTP 200-299",
+ "YandexCrawlHttpStatus2xxDesc": "Pages explorées avec un code 2xx",
+ "YandexCrawlHttpStatus3xx": "Code HTTP 300-399 (pages déplacées)",
+ "YandexCrawlHttpStatus3xxDesc": "Pages explorées avec un code 3xx",
+ "YandexCrawlHttpStatus4xx": "Code HTTP 400-499 (erreurs de requête)",
+ "YandexCrawlHttpStatus4xxDesc": "Pages explorées avec un code 4xx",
+ "YandexCrawlHttpStatus5xx": "Code HTTP 500-599 (erreurs internes du serveur)",
+ "YandexCrawlHttpStatus5xxDesc": "Pages explorées avec un code 5xx",
+ "YandexCrawlInIndex": "Nombre total de pages dans l'index",
+ "YandexCrawlInIndexDesc": "Nombre total de pages disponibles dans l'index de recherche Yandex",
+ "YandexCrawlRemovedPages": "Pages retirées de la recherche",
+ "YandexCrawlRemovedPagesDesc": "Pages retirées de l'index de recherche Yandex",
+ "YandexCrawlingStats": "Récapitulatif du crawl pour Yandex !",
+ "YandexCrawlingStatsDocumentation": "Le récapitulatif du crawl vous permet de visualiser les informations relatives au crawl, telles que les erreurs rencontrées par le robot de recherche lors de la visite d'une page, les éléments bloqués par votre fichier robots.txt et le nombre total de pages dans l'index.",
+ "YandexFieldCallbackUri": "URI de rappel",
+ "YandexFieldUrlToAppSite": "URL du site de l'application",
+ "YandexKeywords": "Mots clés sur Yandex",
+ "YandexKeywordsDocumentation": "Mots clés utilisés dans la recherche Yandex qui ont généré des liens vers votre site web dans la liste des résultats de recherche.",
+ "YandexWebmasterApiUrl": "Url pour Yandex Webmaster Tools",
+ "YandexWebmasterApiUrlDescription": "Fournissez l'url de ce site dans vos Yandex Webmaster Tools"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/hi.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/hi.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/hi.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/it.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/it.json
new file mode 100644
index 0000000..5f1cff9
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/it.json
@@ -0,0 +1,225 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "AccountAddedBy": "Aggiunta da %1$s il %2$s ",
+ "AccountConnectionValidationError": "Si è verificato un errore durante la convalida della connessione all'account:",
+ "AccountDoesNotExist": "L'account configurato %1$s non esiste più",
+ "AccountNoAccess": "Al momento questo account non ha accesso ad alcun sito web.",
+ "AccountRemovalConfirm": "Stai per rimuovere l'account %1$s. Ciò potrebbe disabilitare l'importazione delle parole chiave per tutti i siti web collegati. Vuoi procedere comunque?",
+ "ActivityAccountAdded": "aggiunto un nuovo account per il provider di parole chiave: %1$s: %2$s",
+ "ActivityAccountRemoved": "rimosso un account per il provider di parole chiave %1$s: %2$s",
+ "ActivityGoogleClientConfigChanged": "configurazione del client di Google cambiata.",
+ "ActivityYandexClientConfigChanged": "configurazione client Yandex cambiata",
+ "AddAPIKey": "Aggiungi Chiave API",
+ "AddConfiguration": "Aggiungi Configurazione",
+ "AdminMenuTitle": "Rendimento Ricerca",
+ "APIKey": "Chiave API",
+ "AvailableSites": "Siti disponibili per l'importazione:",
+ "Domain": "Dominio",
+ "DomainProperty": "Proprietà dominio",
+ "DomainPropertyInfo": "Include tutti i sottodomini (m, www e così via) e entrambi i protocolli (http, https).",
+ "URLPrefix": "Prefisso URL",
+ "URLPrefixProperty": "Proprietà del prefisso URL",
+ "URLPrefixPropertyInfo": "Include solo gli URL con il prefisso esatto specificato, incluso il protocollo (http o https). Se vuoi che la tua proprietà corrisponda a un protocollo o a un sottodominio (http/https/www./m. e così via), prendi in considerazione l'aggiunta di una proprietà Dominio.",
+ "BingAccountError": "Si è verificato un errore durante la convalida della chiave API: %1$s. Se hai appena generato questa chiave API in Strumenti per Bing Webmaster Tools, riprova tra uno o due minuti (le chiavi API in Bing Webmaster Tools richiedono un po' di tempo per essere attivate).",
+ "BingAccountOk": "Chiave API verificata con successo",
+ "BingAPIKeyInstruction": "Accedi a %1$sBing Webmaster Tools%2$s, quindi aggiungi il tuo sito web a Bing Webmaster. Dopo averlo convalidato, puoi %3$scopiare la tua chiave API%4$s.",
+ "BingConfigurationDescription": "Bing Webmaster Tools richiede una chiave API per l'accesso. Qui puoi aggiungere le chiavi API per accedere ai dati dei tuoi siti web.",
+ "BingConfigurationTitle": "Configura l'importazione da Bing Webmaster Tools",
+ "BingCrawlBlockedByRobotsTxt": "Esclusioni tramite robots.txt",
+ "BingCrawlBlockedByRobotsTxtDesc": "URL attualmente bloccati dal file robots.txt del tuo sito.",
+ "BingCrawlConnectionTimeout": "Timeout della connessione",
+ "BingCrawlConnectionTimeoutDesc": "Questo numero rappresenta le occorrenze recenti quando Bing non ha potuto accedere al tuo sito a causa di errori di connessione. Questo potrebbe essere un problema temporaneo, ma è necessario controllare i log del server per vedere se si stanno accidentalmente perdendo le richieste.",
+ "BingCrawlCrawledPages": "Pagine esplorate",
+ "BingCrawlCrawledPagesDesc": "Numero di pagine richieste dal crawler di Bing.",
+ "BingCrawlDNSFailures": "Insuccessi DNS",
+ "BingCrawlDNSFailuresDesc": "Questo tipo di problema cataloga gli errori recenti riscontrati durante il tentativo di comunicare con il server DNS quando il bot ha tentato di accedere alle tue pagine. Forse il tuo server era inattivo, oppure c'era un errore di configurazione che impediva il routing DNS, ad esempio TTL era impostato a 0.",
+ "BingCrawlErrors": "Errori di scansione su Bing",
+ "BingCrawlErrorsDesc": "Numero di errori verificatisi per il crawler di Bing.",
+ "BingCrawlErrorsFromDateX": "Il report mostra errori di scansione recentemente segnalati da Bing. Non fornisce dati storici. Ultimo aggiornamento %s",
+ "BingCrawlHttpStatus2xx": "Codici HTTP 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Questi codici vengono visualizzati quando il server presenta una pagina correttamente",
+ "BingCrawlHttpStatus301": "Codice HTTP 301 (Spostata definitivamente)",
+ "BingCrawlHttpStatus301Desc": "Questi codici vengono visualizzati quando si sposta permanentemente il contenuto da una posizione (URL) a un'altra.",
+ "BingCrawlHttpStatus302": "Codice HTTP 302 (Spostata temporaneamente)",
+ "BingCrawlHttpStatus302Desc": "Questi codici vengono visualizzati quando si sposta temporaneamente il contenuto da una posizione (URL) a un'altra.",
+ "BingCrawlHttpStatus4xx": "Codici HTTP 400-499 (Errori richiesta)",
+ "BingCrawlHttpStatus4xxDesc": "Questi codici vengono visualizzati quando c'è stato probabilmente un errore nella richiesta che ha impedito al server di elaborarla.",
+ "BingCrawlHttpStatus5xx": "Codici HTTP 500-599 (Errori interni al server)",
+ "BingCrawlHttpStatus5xxDesc": "Questi codici vengono visualizzati quando il server non è riuscito a soddisfare una richiesta apparentemente valida.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Esclusione tramite robots.txt di una pagina importante",
+ "BingCrawlInboundLink": "Totale link in entrata",
+ "BingCrawlInboundLinkDesc": "I collegamenti in entrata di cui Bing è a conoscenza, hanno puntato agli URL sul tuo sito web. Questi sono collegamenti da siti web esterni al tuo che sono stati indirizzati verso i tuoi contenuti.",
+ "BingCrawlingStats": "Panoramica della scansione per Bing e Yahoo!",
+ "BingCrawlingStatsDocumentation": "La panoramica Scansione consente di visualizzare informazioni correlate alla ricerca per l'indicizzazione, quali errori riscontrati dal bot di ricerca quando visita una pagina, elementi bloccati dal proprio file robots.txt e URL potenzialmente interessati da malware.",
+ "BingCrawlMalwareInfected": "Siti web infettati da malware",
+ "BingCrawlMalwareInfectedDesc": "In questa sezione sono raggruppati gli URL delle pagine che Bing ha rilevato come infette o associate a malware.",
+ "BingCrawlPagesInIndex": "Totale delle pagine nell'indice",
+ "BingCrawlPagesInIndexDesc": "Numero totale delle pagine disponibili nell'indice di Bing",
+ "BingCrawlStatsOtherCodes": "Tutti gli altri codici di stato HTTP",
+ "BingCrawlStatsOtherCodesDesc": "Raggruppa tutti gli altri codici che non corrispondono a nessun altro valore (come 1xx o codici informativi).",
+ "BingKeywordImport": "Importazione di parole chiave Bing",
+ "BingKeywords": "Parole chiave (su Bing e Yahoo!)",
+ "BingKeywordsDocumentation": "Parole chiave utilizzate nella ricerca di Bing o Yahoo! che hanno generato collegamenti al tuo sito web nell'elenco dei risultati.",
+ "BingKeywordsNoRangeReports": "Parole chiave su Bing e Yahoo! che possono essere elaborate solo per intervalli di date personalizzati, comprese settimane o mesi interi in quanto non sono disponibili come rapporti giornalieri.",
+ "BingKeywordsNotDaily": "Parole chiave su Bing e Yahoo! che sono disponibili solo come rapporti settimanali. Non ci sono dati sulle parole chiave per i periodi di giorni.",
+ "BingWebmasterApiUrl": "Url di Bing Webmaster Tools",
+ "BingWebmasterApiUrlDescription": "Fornisce l'url di questo sito Web disponibile in Bing Webmaster Tools",
+ "Category": "Categoria",
+ "ChangeConfiguration": "Cambia configurazione",
+ "Clicks": "Clicks",
+ "ClicksDocumentation": "Viene conteggiato un click ogni volta che un utente clicca su un link che punta al tuo sito web sulla pagina dei risultati di un motore di ricerca.",
+ "ClientConfigImported": "La configurazione del client è stata importata con successo!",
+ "ClientConfigSaveError": "Si è verificato un errore durante il salvataggio della configurazione del client. Si prega di verificare se la configurazione fornita è valida e poi riprovare.",
+ "ClientId": "Id client",
+ "ClientSecret": "Segreto del client",
+ "ConfigAvailableNoWebsiteConfigured": "Integrazione correttamente configurata, ma attualmente nessun sito web è configurato per l'importazione.",
+ "ConfigRemovalConfirm": "Stai per rimuovere la configurazione per %1$s. L'importazione di parole chiave per quel sito web sarà disabilitata. Procedere comunque?",
+ "Configuration": "Configurazione",
+ "ConfigurationDescription": "Questo plugin consente di importare direttamente in Matomo tutte le parole chiave cercate dagli utenti sui motori di ricerca.",
+ "ConfigurationFile": "File di configurazione",
+ "ConfigurationValid": "La tua configurazione OAuth è valida.",
+ "ConfiguredAccounts": "account configurati",
+ "ConfiguredUrlNotAvailable": "L'URL configurato non è disponibile per questo account",
+ "ConfigureMeasurableBelow": "Per configurare un sito Web, è sufficiente cliccare sul pulsante in basso o configurarlo direttamente nelle sue impostazioni.",
+ "ConfigureMeasurables": "Configura siti web",
+ "ConnectAccount": "Collega Account",
+ "ConnectAccountDescription": "Clicca sul pulsante in basso per essere reindirizzati a %1$s dove è necessario dare l'accesso.",
+ "ConnectAccountYandex": "L'autenticazione per gli account Yandex è valida solo per %1$s giorni. Ogni account deve essere nuovamente autenticato entro questo tempo per garantire che le importazioni funzionino correttamente.",
+ "ConnectFirstAccount": "Inizia collegando il tuo primo account qui sotto.",
+ "ConnectGoogleAccounts": "Collega Account di Google",
+ "ContainingSitemaps": "Contenenti Sitemap",
+ "CrawlingErrors": "Errori di scansione",
+ "ConnectYandexAccounts": "Collega account Yandex",
+ "CrawlingStats": "Panoramica scansione",
+ "Ctr": "Percentuale di clicks (CTR)",
+ "CtrDocumentation": "Percentuale di click: report che mostra la frequenza con cui le persone che vedono la pagina dei risultati di un motore di ricerca con un link al tuo sito web finiscono per cliccarci sopra.",
+ "CurrentlyConnectedAccounts": "Al momento sei collegato con %1$s accounts.",
+ "EnabledSearchTypes": "Tipi di parole chiave da recuperare",
+ "FetchImageKeyword": "Recupera parole chiave immagine",
+ "FetchImageKeywordDesc": "Recupera le parole chiave utilizzate nella ricerca immagini di Google",
+ "FetchNewsKeyword": "Recupera le parole chiave delle notizie",
+ "FetchNewsKeywordDesc": "Recupera le parole chiave usate in Google News",
+ "FetchVideoKeyword": "Recupera parole chiave video",
+ "FetchVideoKeywordDesc": "Recupera le parole chiave utilizzate nella ricerca video di Google",
+ "FetchWebKeyword": "Recupera parole chiave web",
+ "FetchWebKeywordDesc": "Recupera le parole chiave utilizzate nella ricerca web di Google",
+ "FirstDetected": "Prima individuata",
+ "GoogleAccountAccessTypeOfflineAccess": "È necessario l'Accesso offline per poter importare le parole chiave di ricerca anche quando non si è connessi.",
+ "GoogleAccountAccessTypeProfileInfo": "Le Informazioni del profilo vengono utilizzate per mostrare il nome degli account attualmente connessi.",
+ "GoogleAccountAccessTypeSearchConsoleData": "I Dati della Console di Ricerca sono necessari per ottenere l'accesso alle tue parole chiave di ricerca di Google.",
+ "GoogleAccountError": "Si è verificato un errore durante la convalida dell'accesso OAuth: %1$s",
+ "GoogleAccountOk": "Accesso OAuth verificato con successo.",
+ "GoogleConfigurationDescription": "La Console di Ricerca di Google utilizza OAuth per l'autenticazione e l'autorizzazione.",
+ "GoogleConfigurationTitle": "Configura l'importazione dalla Console di Ricerca di Google",
+ "GoogleDataProvidedWithDelay": "Google fornisce i dati delle parole chiave con un ritardo. Le parole chiave per questa data verranno importate un po' più tardi.",
+ "GoogleDataNotFinal": "I dati delle parole chiave in questo report potrebbero non contenere ancora i dati finali. Google fornisce i dati finali sulle parole chiave con un ritardo di 2 giorni. Le parole chiave per i giorni più recenti verranno reimportate fino a quando non verranno segnalate come definitive.",
+ "GoogleKeywordImport": "Importazione di parole chiave di Google",
+ "GoogleSearchConsoleUrl": "Url per la Console di Ricerca di Google",
+ "GoogleSearchConsoleUrlDescription": "Indica l'url per questo sito web disponibile nella tua Search Console di Google",
+ "GoogleUploadOrPasteClientConfig": "Carica la configurazione del client Google OAuth o incollala nel campo sottostante.",
+ "HowToGetOAuthClientConfig": "Come ottenere la configurazione del tuo client OAuth",
+ "ImageKeywords": "Parole chiave immagini su Google",
+ "ImageKeywordsDocumentation": "Parole chiave utilizzate nella ricerca per immagini di Google che hanno generato collegamenti al tuo sito web nell'elenco dei risultati della ricerca.",
+ "Impressions": "Impressioni",
+ "ImpressionsDocumentation": "Viene conteggiata un'impressione ogni volta che il tuo sito web viene visualizzato in una pagina dei risultati dei motori di ricerca.",
+ "IntegrationConfigured": "Integrazione configurata con successo",
+ "IntegrationNotConfigured": "Integrazione non ancora configurata",
+ "KeywordsCombined": "Keyword combinate",
+ "KeywordsCombinedDocumentation": "Report che combina tutte le parole chiave rilevate da Matomo e importate dai motori di ricerca. Questo report include solo la metrica visita. È possibile passare a uno dei report correlati per ottenere delle metriche dettagliate.",
+ "KeywordsCombinedImported": "Keyword importate combinate",
+ "KeywordsCombinedImportedDocumentation": "Segnala tutte le parole chiave importate da tutti i motori di ricerca configurati.",
+ "KeywordsReferrers": "Parole chiave (incluse quelle non definite)",
+ "KeywordStatistics": "Keywords di Ricerca",
+ "KeywordTypeImage": "immagine",
+ "KeywordTypeVideo": "video",
+ "KeywordTypeWeb": "web",
+ "KeywordTypeNews": "notizie",
+ "LastCrawled": "Ultima scansione",
+ "LastDetected": "Ultima rilevata",
+ "LastImport": "Ultima importazione",
+ "LatestAvailableDate": "Dati delle parole chiave più recenti disponibili per %1$s",
+ "LinksToUrl": "Collegamenti a %s",
+ "ManageAPIKeys": "Gestisci Chiavi API",
+ "MeasurableConfig": "siti configurati",
+ "NoSegmentation": "Il report non supporta la segmentazione. I dati visualizzati sono i dati del report standard non segmentati.",
+ "NotAvailable": "Non disponibile",
+ "NoWebsiteConfigured": "Al momento non ci sono siti web configurati. Per abilitare l'importazione per un sito web specifico, impostare qui la configurazione.",
+ "NoWebsiteConfiguredWarning": "Importazione per %s non completamente configurata. È necessario configurare alcuni siti web per abilitare l'importazione.",
+ "OAuthAccessTimedOut": "L'accesso OAuth per questo account potrebbe essere scaduto. Sarà necessario eseguire nuovamente l'autenticazione per ripristinare il funzionamento delle importazioni per questo account.",
+ "OAuthAccessWillTimeOutSoon": "L'accesso OAuth per questo account scadrà tra circa %1$s giorni. Esegui nuovamente l'autenticazione per evitare che le importazioni per questo account smettano di funzionare.",
+ "OAuthAccessWillTimeOut": "L'accesso OAuth per questo account scadrà dopo %1$s %2$s giorni. giorni rimasti ",
+ "OAuthClientConfig": "Configurazione Client OAuth",
+ "OAuthError": "Si è verificato un errore all'interno del processo OAuth. Si prega di riprovare e di assicurarsi di accettare le autorizzazioni richieste.",
+ "Platform": "Piattaforma",
+ "Position": "Media della Posizione",
+ "PositionDocumentation": "Media della posizione del tuo sito web nell'elenco dei risultati dei motori di ricerca (per questa parola chiave).",
+ "ProviderBingDescription": "Importa tutte le keyword utilizzate per trovare il tuo sito web sulla ricerca di Bing e Yahoo! .",
+ "ProviderBingNote": "Nota: Microsoft fornisce dati sulle parole chiave ogni sabato e solo per settimane complete. Di conseguenza, le parole chiave per Bing e Yahoo impiegheranno alcuni giorni per comparire nei tuoi report e saranno disponibili solo durante la visualizzazione di settimane, mesi o anni.",
+ "ProviderGoogleDescription": "Importa tutte le parole chiave utilizzate per trovare il tuo sito web sulla ricerca di Google . I report mostreranno le tue parole chiave per ogni tipo di ricerca separatamente (Web, Immagini e Video).",
+ "ProviderGoogleNote": "Nota: Google fornisce i dati finali sulle parole chiave con un ritardo di 2 giorni. I dati non definitivi per i giorni più recenti verranno già visualizzati, ma verranno reimportati fino a quando non saranno definitivi. La tua prima importazione potrebbe essere in grado di importare i dati storici delle parole chiave fino agli ultimi 486 giorni.",
+ "ProviderListDescription": "Dopo avere impostato correttamente uno (o più) motori di ricerca qui di seguito, puoi configurare in quale sito (o siti) Matomo deve importare le tue parole chiave di ricerca.",
+ "ProviderXAccountWarning": "Rilevati problemi di configurazione dell'account Si prega di verificare gli account configurati per %s .",
+ "ProviderXSitesWarning": "Rilevati problemi di configurazione di un sito web Si prega di verificare i siti configurati per %s .",
+ "ProviderYandexDescription": "Importa tutte le parole chiave utilizzate per trovare il tuo sito web nella ricerca Yandex .",
+ "ProviderYandexNote": "Nota : Yandex fornisce le parole chiave con un ritardo fino a 5 giorni. La prima importazione proverà a importare le tue parole chiave storiche fino agli ultimi 100 giorni.",
+ "ProvideYandexClientConfig": "Inserisci la configurazione del tuo client OAuth Yandex.",
+ "Reauthenticate": "Autenticati di nuovo",
+ "ReAddAccountIfPermanentError": "Se questo è un errore permanente, prova a rimuovere l'account e a ricollegarlo.",
+ "ReAuthenticateIfPermanentError": "Se si tratta di un errore permanente, prova ad autenticare nuovamente l'account o rimuovilo e ricollegalo.",
+ "ReportShowMaximumValues": "I valori visualizzati sono i valori massimi che si sono verificati durante questo periodo.",
+ "RequiredAccessTypes": "Sono richiesti questi tipi di accesso:",
+ "ResponseCode": "Codice risposta",
+ "RoundKeywordPosition": "Posizione round keyword",
+ "SearchEngineKeywordsPerformance": "Rendimentodelle keyword dei motori di ricerca",
+ "SetupConfiguration": "Imposta configurazione",
+ "SitemapsContainingUrl": "Sitemap che contengono %s",
+ "SetUpOAuthClientConfig": "Imposta la configurazione del tuo client OAuth",
+ "KeywordsSubtableOriginal": "Keyword tracciate (incluse quelle non definite)",
+ "KeywordsSubtableImported": "Keyword importate",
+ "AllReferrersOriginal": "Referrer (con keyword tracciate)",
+ "AllReferrersImported": "Referrer (con keyword importate)",
+ "SearchEnginesOriginal": "Motori di ricerca (con keyword tracciate)",
+ "SearchEnginesImported": "Motori di ricerca (con keyword importate)",
+ "StartOAuth": "Inizia Processo OAuth",
+ "UnverifiedSites": "Siti non verificati:",
+ "UploadOAuthClientConfig": "Carica la configurazione del tuo client OAuth",
+ "UrlOfAccount": "URL (Account)",
+ "VideoKeywords": "Parole chiave per video su Google",
+ "VideoKeywordsDocumentation": "Parole chiave utilizzate nella ricerca video di Google che hanno generato collegamenti al tuo sito web nell'elenco dei risultati della ricerca.",
+ "NewsKeywords": "Parole chiave news su Google",
+ "NewsKeywordsDocumentation": "Parole chiave utilizzate nella ricerca di Google News che hanno generato collegamenti al tuo sito web nell'elenco dei risultati di ricerca.",
+ "VisitOAuthHowTo": "Visita la nostra %1$sguida in linea%2$s per scoprire come impostare la configurazione del tuo %3$s client OAuth.",
+ "WebKeywords": "Parole chiave web su Google",
+ "WebKeywordsDocumentation": "Parole chiave utilizzate nella ricerca web di Google che hanno generato collegamenti al tuo sito web nell'elenco dei risultati della ricerca.",
+ "WebsiteSuccessfulConfigured": "Congratulazioni! Hai configurato correttamente l'importazione delle parole chiave per il sito web %1$s. Potrebbero essere necessari alcuni giorni prima che le prime parole chiave di ricerca vengano importate e visualizzate in Referenti > Keywords di Ricerca. Puoi trovare ulteriori informazioni su ritardi e limitazioni delle importazioni delle parole chiave nelle nostre %2$sFAQ%3$s",
+ "WebsiteTypeUnsupported": "Il misurabile selezionato %1$s non può essere configurato perché è di un tipo non supportato. Sono supportati solamente i misurabili di tipo 'sito web'.",
+ "WebsiteTypeUnsupportedRollUp": "Nota: I siti Roll-Up combineranno automaticamente i dati importati di tutti i siti figlio",
+ "YandexConfigurationDescription": "L'API Webmaster di Yandex utilizza OAuth per l'autenticazione e l'autorizzazione.",
+ "YandexConfigurationTitle": "Configura l'importazione dall'API Webmaster di Yandex",
+ "YandexCrawlingStats": "Guarda una panoramica su Yandex!",
+ "YandexCrawlingStatsDocumentation": "La panoramica della Scansione ti consente di visualizzare le informazioni relative alla scansione come gli errori riscontrati dal bot di ricerca durante la visita di una pagina, gli elementi bloccati dal tuo file robots.txt e il numero totale di pagine nell'indice.",
+ "YandexCrawlHttpStatus2xx": "Codici HTTP 200-299",
+ "YandexCrawlHttpStatus2xxDesc": "Pagine con un codice 2xx esplorate",
+ "YandexCrawlHttpStatus3xx": "Codici HTTP 300-399 (Pagine spostate)",
+ "YandexCrawlHttpStatus3xxDesc": "Pagine con un codice 3xx esplorate",
+ "YandexCrawlHttpStatus4xx": "Codici HTTP 400-499 (Errori richiesta)",
+ "YandexCrawlHttpStatus4xxDesc": "Pagine con un codice 4xx esplorate",
+ "YandexCrawlHttpStatus5xx": "Codici HTTP 500-599 (Errori interni al server)",
+ "YandexCrawlHttpStatus5xxDesc": "Pagine con un codice 5xx esplorate",
+ "YandexCrawlErrors": "Altri errori richiesta",
+ "YandexCrawlErrorsDesc": "Pagine esplorate che hanno fallito per qualsiasi altra ragione",
+ "YandexCrawlCrawledPages": "Pagine Esplorate",
+ "YandexCrawlCrawledPagesDesc": "Numero di pagine richieste dal crawler Yandex",
+ "YandexCrawlInIndex": "Totale delle pagine nell'indice",
+ "YandexCrawlInIndexDesc": "Numero totale di pagine disponibili nell'indice di ricerca Yandex",
+ "YandexCrawlAppearedPages": "Pagine apparse nella ricerca",
+ "YandexCrawlAppearedPagesDesc": "Pagine che sono state appena aggiunte all'indice di ricerca Yandex",
+ "YandexCrawlRemovedPages": "Pagine rimosse dalla ricerca",
+ "YandexCrawlRemovedPagesDesc": "Pagine che sono state rimosse dall'indice di ricerca Yandex",
+ "YandexKeywords": "Parole chiave su Yandex",
+ "YandexKeywordsDocumentation": "Parole chiave utilizzate nella ricerca Yandex che hanno generato collegamenti al tuo sito Web nell'elenco dei risultati di ricerca.",
+ "YandexWebmasterApiUrl": "Url di Yandex Webmaster Tools",
+ "YandexWebmasterApiUrlDescription": "Fornisci l'url di questo sito web disponibile nei tuoi Strumenti per i Webmaster di Yandex",
+ "CrawlingOverview1": "La panoramica delle scansioni riporta tutte le informazioni più importanti su come i robot dei motori di ricerca eseguono la scansione dei tuoi siti web. Queste metriche vengono aggiornate circa una volta al giorno con i dati forniti dai motori di ricerca."
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ja.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ja.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ja.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/nb.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/nb.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/nb.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/nl.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/nl.json
new file mode 100644
index 0000000..fb5a46c
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/nl.json
@@ -0,0 +1,183 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "AccountAddedBy": "Toegevoegd door %1$s<\/em> op %2$s<\/em>",
+ "AccountConnectionValidationError": "Er is een fout opgetreden tijdens het valideren van de connectie met het account:",
+ "AccountDoesNotExist": "Dit account %1$sbestaat niet meer.",
+ "AccountNoAccess": "Dit account heeft momenteel geen toegang tot een website.",
+ "AccountRemovalConfirm": "Je staat op het punt het account %1$s te verwijderen. Dit kan het importeren van zoekwoorden voor alle gekoppelde website(s) uitschakelen. Toch doorgaan?",
+ "ActivityAccountAdded": "een nieuw account voor keyword provider %1$s is toegevoegd: %2$s",
+ "ActivityAccountRemoved": "een account voor keyword provider %1$s is verwijderd: %2$s",
+ "ActivityGoogleClientConfigChanged": "de configuratie van de Google-client gewijzigd.",
+ "ActivityYandexClientConfigChanged": "de configuratie van de Yandex client gewijzigd.",
+ "AddAPIKey": "API Key toegevoegd",
+ "AddConfiguration": "Configuratie is toegevoegd",
+ "AdminMenuTitle": "Zoekprestaties",
+ "APIKey": "API Key",
+ "AvailableSites": "Beschikbare websites voor import:",
+ "Domain": "Domein",
+ "DomainProperty": "Domein property",
+ "DomainPropertyInfo": "Bevat alle subdomeinen (m, www, enzovoort) en beide protocollen (http, https).",
+ "URLPrefix": "URL-voorvoegsel",
+ "URLPrefixProperty": "Property met URL-voorvoegsel",
+ "URLPrefixPropertyInfo": "Bevat alleen URL's met het exact opgegeven voorvoegsel, inclusief het protocol (http\/https). Als je wilt dat je property overeenkomt met elk protocol of subdomein (http\/https\/www.\/m. enzovoort), overweeg dan een domein property toe te voegen.",
+ "BingAccountError": "Er is een fout opgetreden tijdens het valideren van de API Key: %1$s. Wanneer je de API Key zojuist heb gegenereerd in Bing Webmast Tools probeer het dan nog eens over één of twee minuten(API Keys in Bing Webmaster Tools hebben even tijd nodig om te worden geactiveerd)",
+ "BingAccountOk": "API Key is succesvol gecontroleerd",
+ "BingConfigurationDescription": "De Bing Webmaster Tools heeft een API Key nodig om te openen. Hier kan je je API Key toevoegen om toegang te krijgen tot je website data.",
+ "BingConfigurationTitle": "Configureer de import van Bing Webmaster Tools",
+ "BingCrawlBlockedByRobotsTxt": "Robots.txt uitgesloten",
+ "BingCrawlBlockedByRobotsTxtDesc": "URL's momenteel geblokkeerd door de websites robots.txt",
+ "BingCrawlConnectionTimeout": "Connectie time outs",
+ "BingCrawlConnectionTimeoutDesc": "Dit nummer toont het aantal recente keren dat Bing je website niet heeft kunnen benaderen door connectie problemen. Dit kan een tijdelijk probleem zijn maar je zou je server logs kunnen controleren om te zien of er verzoeken verloren gaan.",
+ "BingCrawlCrawledPages": "Doorzochte pagina's",
+ "BingCrawlCrawledPagesDesc": "Aantal pagina the Bing crawler heeft aangevraagd.",
+ "BingCrawlDNSFailures": "DNS storing",
+ "BingCrawlDNSFailuresDesc": "Dit issue type verzameld recente fouten die zijn tegen gekomen bij het communiceren met de DNS server wanneer de bot toegang tot je website probeerde te krijgen. Mogelijk stond de server uit, of de instellingen stonden niet goed waardoor DNS routing werd tegengehouden, bijvoorbeeld TTL stond op 0.",
+ "BingCrawlErrors": "Crawl errors op Bing",
+ "BingCrawlErrorsDesc": "Aantal foutmeldingen die voorkwamen bij de Bing crawler",
+ "BingCrawlErrorsFromDateX": "Dit rapport toont crawling fouten welke recent zijn gerapporteerd door Bing. Het toont geen historische data. Laatste update %s",
+ "BingCrawlHttpStatus2xx": "HTTP Code 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Deze codes verschijnen wanneer de server een pagina succesvol laad",
+ "BingCrawlHttpStatus301": "HTTP Code 301 (Permanent verwijderd)",
+ "BingCrawlHttpStatus301Desc": "Deze codes verschijnen wanneer bepaalde content permanent is verplaatst van de ene locatie (URL) naar een andere.",
+ "BingCrawlHttpStatus302": "HTTP Code 302 (Tijdelijk verwijderd)",
+ "BingCrawlHttpStatus302Desc": "Deze codes verschijnen wanneer bepaalde content tijdelijk is verplaatst van de ene locatie (URL) naar een andere.",
+ "BingCrawlHttpStatus4xx": "HTTP Code 400-499 (Request errors)",
+ "BingCrawlHttpStatus4xxDesc": "Deze codes verschijnen wanneer er waarschijnlijk een fout was het verzoek welke ervoor zorgde dat de server het niet kon verwerken.",
+ "BingCrawlHttpStatus5xx": "HTTP Code 500-599 (Interne server errors)",
+ "BingCrawlHttpStatus5xxDesc": "Deze codes verschijnen wanneer het een server niet lukt om een schijnbaar geldig verzoek uit te voeren.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Robots.txt uitsluiting van belangrijke pagina",
+ "BingCrawlInboundLink": "Totaal inbound links",
+ "BingCrawlInboundLinkDesc": "Inbound links waar Bing weet van heeft, gericht op URL's op jouw website. Deze links zijn, van externe websites of van jezelf, welke gelinkt zijn naar je content.",
+ "BingCrawlingStats": "Crawl overzicht voor Bing en Yahoo!",
+ "BingCrawlingStatsDocumentation": "Het crawl overzicht laat je crawl gerelateerde informatie zien zoals fouten die ondervonden zijn door zoek bot bij het bezoeken van een pagina, items geblokkeerd door je robots.txt bestand en URL's welke potentieel getroffen zijn door malware.",
+ "BingCrawlMalwareInfected": "Met malware geïnfecteerde websites",
+ "BingCrawlMalwareInfectedDesc": "Elke pagina URL die Bing heeft gevonden welke geïnfecteerd of geassocieerd worden met malware worden in dit deel gegroepeerd.",
+ "BingCrawlPagesInIndex": "Totaal pagina's in index",
+ "BingCrawlPagesInIndexDesc": "Totaal aantal pagina's beschikbaar in de Bing index",
+ "BingCrawlStatsOtherCodes": "Alle andere HTTP status codes",
+ "BingCrawlStatsOtherCodesDesc": "Overige groepen codes die niet overeenkomen met elke andere waarde (zoals 1xx of informatieve codes)",
+ "BingKeywordImport": "Bing zoekwoorden import",
+ "BingKeywords": "Zoekwoorden (on Bing en Yahoo!)",
+ "BingKeywordsDocumentation": "Zoekwoorden gebruikt in Bing of Yahoo! zoekmachine die gegenereerde links naar je website in het zoekresultaten overzicht tonen.",
+ "BingKeywordsNoRangeReports": "Zoekwoorden op Bing en Yahoo! kunnen alleen verwerkt worden voor aangepaste datum periodes inclusief volledige weken of maanden aangezien de dagelijkse rapporten niet beschikbaar zijn.",
+ "BingKeywordsNotDaily": "Zoekwoorden van Bing en Yahoo! zijn alleen beschikbaar als wekelijkse rapporten. Er is geen sleutelwoord data beschikbaar voor perioden van een dag.",
+ "BingWebmasterApiUrl": "URL voor Bing Webmaster Tools",
+ "BingWebmasterApiUrlDescription": "Geef de URL voor deze website welke beschikbaar is in je Bing Webmaster Tools",
+ "Category": "Categorie",
+ "ChangeConfiguration": "Wijzig configuratie",
+ "Clicks": "Kliks",
+ "ClicksDocumentation": "Een klik wordt elke keer geteld wanneer iemand klikt op een link welke gericht is op jouw website vanaf een zoekmachine resultaat pagina.",
+ "ClientConfigImported": "Client configuratie is succesvol geïmporteerd!",
+ "ClientConfigSaveError": "Er is een fout opgetreden tijdens het opslaan van de client configuratie. Controleer of de configuratie goed is en probeer het dan nogmaals.",
+ "ClientId": "Client id",
+ "ClientSecret": "Client geheim",
+ "ConfigAvailableNoWebsiteConfigured": "De integratie is succesvol geconfigureerd. Er zijn nog geen websites geconfigureerd voor het importeren.",
+ "ConfigRemovalConfirm": "Je staat op het punt om de instellingen for %1$s te verwijderen. Het importeren van zoekwoorden voor die website zal worden uitgeschakeld. Toch doorgaan?",
+ "Configuration": "Configuratie",
+ "ConfigurationDescription": "Deze plugin geeft je de mogelijkheid om alle zoekwoorden ingevoerd door gebruikers op een zoekmachine direct te importeren in Matomo.",
+ "ConfigurationFile": "Configuratie bestand",
+ "ConfigurationValid": "De OAuth configuratie is geldig.",
+ "ConfiguredAccounts": "geconfigureerde accounts",
+ "ConfiguredUrlNotAvailable": "De geconfigureerde URL is niet beschikbaar voor dit account",
+ "ConfigureMeasurableBelow": "Om een website te configureren, klik daarvoor op de button hieronder of configureer het direct in de website instellingen.",
+ "ConfigureMeasurables": "Configureer websites",
+ "ConnectAccount": "Koppel account",
+ "ConnectFirstAccount": "Begin hieronder met het koppelen van je eerste account.",
+ "ConnectGoogleAccounts": "Koppel Google Account(s)",
+ "ContainingSitemaps": "Bevat Sitemaps",
+ "CrawlingErrors": "Crawling fouten",
+ "ConnectYandexAccounts": "Koppel Yandex Account(s)",
+ "CrawlingStats": "Crawling overzicht",
+ "Ctr": "CTR",
+ "CtrDocumentation": "Clickthrough rate: een ratio van hoe vaak mensen die vanaf een zoekmachine resultaten pagina met een link naar je website op je website terecht komen door op de link te klikken.",
+ "CurrentlyConnectedAccounts": "Er zijn momenteel %1$s accounts gekoppeld.",
+ "EnabledSearchTypes": "Zoekwoorden types om op te halen",
+ "FetchImageKeyword": "Opgehaalde plaatjes zoekwoorden",
+ "FetchImageKeywordDesc": "Opgehaalde zoekwoorden gebruikt in Google image search",
+ "FetchVideoKeyword": "Opgehaalde video zoekwoorden",
+ "FetchVideoKeywordDesc": "Opgehaalde zoekwoorden gebruikt in Google video search",
+ "FetchWebKeyword": "Ophalen web zoekwoorden",
+ "FetchWebKeywordDesc": "Opgehaalde zoekwoorden gebruikt in Google web search",
+ "FirstDetected": "Als eerst gevonden",
+ "GoogleAccountAccessTypeOfflineAccess": "Offline toegang<\/strong>is vereist om in staat te zijn om de zoekwoorden te importeren, zelfs wanneer je niet ingelogd bent.",
+ "GoogleAccountAccessTypeProfileInfo": "Profiel info<\/strong>wordt gebruikt om de namen van de account(s) te tonen die momenteel gekoppeld zijn.",
+ "GoogleAccountAccessTypeSearchConsoleData": "Search Console Data<\/strong>is vereist om toegang te krijgen tot je Google Search Keywords.",
+ "GoogleAccountError": "Er is een fout opgetreden tijdens het valideren van OAuth toegang: %1$s",
+ "GoogleAccountOk": "OAuth toegang is succesvol gecontroleerd.",
+ "GoogleConfigurationDescription": "Google Search Console gebruikt OAuth voor authenticate en autorisatie.",
+ "GoogleConfigurationTitle": "Configureer import van Google Search Console",
+ "GoogleKeywordImport": "Google zoekwoorden import",
+ "GoogleSearchConsoleUrl": "URL voor Google Search Console",
+ "GoogleSearchConsoleUrlDescription": "Verschaf de url voor deze website waarmee deze beschikbaar is in je Google Search Console",
+ "GoogleUploadOrPasteClientConfig": "Upload je Google OAuth configuratie, of plak het in het veld hieronder.",
+ "HowToGetOAuthClientConfig": "Waar vind je je OAuth Client configuratie",
+ "ImageKeywords": "Plaatjes zoekwoorden op Google",
+ "ImageKeywordsDocumentation": "Zoekwoorden gebruikt in Google image<\/b>search die een link hebben gegenereerd naar je website in het zoekresultaten overzicht.",
+ "Impressions": "Impressies",
+ "ImpressionsDocumentation": "Een impressie wordt geteld elke keer wanneer je website wordt getoond in een zoekmachine resultaat pagina.",
+ "IntegrationConfigured": "Integratie is succesvol geconfigureerd",
+ "IntegrationNotConfigured": "Integratie is nog niet geconfigureerd",
+ "KeywordsCombined": "Gecombineerde zoekwoorden",
+ "KeywordsCombinedDocumentation": "Het rapport combineert alle zoekwoorden gevonden door Matomo en geïmporteerd uit zoekmachines. Dit rapport bevat alleen de bezoekers statistieken. Je kan naar een ander rapport gaan om meer gedetailleerde statistieken te zien.",
+ "KeywordsCombinedImported": "Gecombineerde geïmporteerde zoekwoorden",
+ "KeywordsCombinedImportedDocumentation": "Het rapport toont alle zoekwoorden welke geïmporteerd zijn vanuit alle geconfigureerde zoekmachines.",
+ "KeywordsReferrers": "Sleutelwoorden (inclusief zoekwoord niet gedefinieerd)",
+ "KeywordStatistics": "Zoekwoorden",
+ "KeywordTypeImage": "plaatje",
+ "KeywordTypeVideo": "video",
+ "KeywordTypeWeb": "web",
+ "KeywordTypeNews": "nieuws",
+ "LastCrawled": "Laatst crawled",
+ "LastDetected": "Laatst gedetecteerde",
+ "LastImport": "Laatste import",
+ "LatestAvailableDate": "Meest recente zoekwoorden data is beschikbaar voor %1$s",
+ "LinksToUrl": "Links naar %s",
+ "ManageAPIKeys": "Beheer API Keys",
+ "MeasurableConfig": "geconfigureerde websites",
+ "NoSegmentation": "Het rapport ondersteund geen segmentatie. De data die getoond wordt is standaard, niet gesegmenteerde rapportage data.",
+ "NotAvailable": "Niet beschikbaar",
+ "NoWebsiteConfigured": "Er is momenteel geen website geconfigureerd. Om het importeren voor een specifieke website te activeren, stel hier je configuratie in.",
+ "NoWebsiteConfiguredWarning": "Import voor %s is niet volledig ingericht. Voor sommige website moeten de instellingen geconfigureerd worden om het importeren te activeren.",
+ "OAuthClientConfig": "OAuth Client Configuratie",
+ "OAuthError": "Er is een fout opgetreden in het OAuth proces. Probeer het nogmaals en zorg ervoor dat je de verzochte permissies accepteert.",
+ "Platform": "Platform",
+ "Position": "Gemiddelde positie",
+ "PositionDocumentation": "Gemiddelde positie van je website in de zoekresultaten pagina (voor dit zoekwoord).",
+ "ProviderBingDescription": "Importeer alle zoekwoorden die gebruikt zijn om je website te vinden via Bing<\/strong> en Yahoo! <\/strong>.",
+ "ProviderBingNote": "Let op: <\/u> Microsoft levert zoekwoorden data elke zaterdag en alleen voor een hele week. Dat heeft als gevolg dat de zoekwoorden van Bing en Yahoo! een aantal dagen nodig hebben om in het rapport te verschijnen en alleen zichtbaar zijn wanneer er weken, maanden of jaren als bereik zijn geselecteerd.",
+ "ProviderGoogleDescription": "Importeer all zoekwoorden die gebruikt zijn op de Google zoekmachine <\/strong>. De zoekwoorden worden onderverdeeld in het rapport onder de verschillende zoek opties (web, plaatjes en video's)",
+ "ProviderListDescription": "Wanneer je één )of meer) zoek machines succesvol hebt ingericht, kan je aangeven in welke website(s) Matomo de zoekwoorden moet importeren.",
+ "ProviderXAccountWarning": "Account configuratie probleem ontdekt <\/strong> Controleer de geconfigureerde accounts voor %s<\/strong>.",
+ "ProviderXSitesWarning": "Website configuratie problemen ontdekt <\/strong> Controleer de geconfigureerde accounts voor %s<\/strong>",
+ "ReAddAccountIfPermanentError": "Wanneer dit een permanente fout is, probeer dan het account te verwijderen en koppel het account opnieuw.",
+ "RequiredAccessTypes": "Deze toegangstypen zijn verplicht:",
+ "ResponseCode": "Reactie code",
+ "RoundKeywordPosition": "Round zoekwoord positie",
+ "SearchEngineKeywordsPerformance": "Zoekmachine zoekwoorden prestaties",
+ "SetupConfiguration": "Setup configuratie",
+ "SitemapsContainingUrl": "Sitemaps bevat %s",
+ "KeywordsSubtableOriginal": "Bijgehouden zoekwoorden (inclusief niet gedefinieerde)",
+ "KeywordsSubtableImported": "Geïmporteerde zoekwoorden",
+ "AllReferrersOriginal": "Referrers (met bijgehouden zoekwoorden)",
+ "AllReferrersImported": "Referrers (met geïmporteerde zoekwoorden)",
+ "SearchEnginesOriginal": "Zoekmachines (met bijgehouden zoekwoorden)",
+ "SearchEnginesImported": "Zoekmachines (met geïmporteerde zoekwoorden)",
+ "StartOAuth": "Start OAuth Proces",
+ "UnverifiedSites": "Niet gevalideerde websites:",
+ "UploadOAuthClientConfig": "Upload je OAuth client configuratie",
+ "UrlOfAccount": "URL (Account)",
+ "VideoKeywords": "Video zoekwoorden op Google",
+ "VideoKeywordsDocumentation": "Zoekwoorden die zijn gebruikt in de zoekfunctie van Google video<\/b> search die gegenereerde links naar je website in het zoekresultaten overzicht tonen.",
+ "NewsKeywords": "Nieuws zoekwoorden op Google",
+ "WebKeywords": "Web zoekwoorden op Google",
+ "WebKeywordsDocumentation": "Zoekwoorden die zijn gebruikt in de zoekfunctie van Google web<\/b> search die gegenereerde links naar je website in het zoekresultaten overzicht tonen.",
+ "WebsiteSuccessfulConfigured": "Gefeliciteerd! Je hebt succesvol de zoekwoorden import voor website %1$s geconfigureerd. Het kan een paar dagen duren voordat de eerste zoekwoorden worden geïmporteerd en getoond worden in Herkomst > Zoekwoorden. Meer informatie over de vertraging voor het importeren van zoekwoorden kan je vinden in onze %2$sFAQ%3$s",
+ "WebsiteTypeUnsupported": "De geselecteerde meetwaarde %1$s kan niet geconfigureerd worden aangezien het een niet ondersteunde type is. Alleen meetwaarde 'website' worden ondersteund.",
+ "WebsiteTypeUnsupportedRollUp": "Opmerking: Roll-Up sites combineert automatisch de geïmporteerde gegevens van al hun onderliggende sites",
+ "YandexCrawlingStats": "Crawl overzicht voor Yandex!",
+ "YandexCrawlHttpStatus2xx": "HTTP Code 200-299",
+ "YandexCrawlHttpStatus4xx": "HTTP Code 400-499 (Request errors)",
+ "YandexCrawlHttpStatus5xx": "HTTP Code 500-599 (Interne server errors)",
+ "YandexCrawlInIndex": "Totaal pagina's in index"
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/pl.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/pl.json
new file mode 100644
index 0000000..771aa94
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/pl.json
@@ -0,0 +1,178 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "AccountAddedBy": "Dodane przez %1$s<\/em>- %2$s<\/em>",
+ "AccountConnectionValidationError": "Wystąpił błąd podczas weryfikacji połączenia konta:",
+ "AccountDoesNotExist": "Skonfigurowane konto %1$s już nie istnieje",
+ "AccountNoAccess": "To konto nie posiada obecnie dostępu do żadnego serwisu.",
+ "AccountRemovalConfirm": "Zamierzasz usunąć konto %1$s. Może to spowodować niedostępność zaimportowanych słów kluczowych dla dowolnego połączonego z nim serwisu. Czy na pewno kontynuować?",
+ "ActivityAccountAdded": "dodano nowe konto dostawcy słów kluczowych %1$s: %2$s",
+ "ActivityAccountRemoved": "usunięto konto dostawcy słów kluczowych %1$s: %2$s",
+ "ActivityGoogleClientConfigChanged": "zmodyfikowano konfigurację klienta Google.",
+ "AddAPIKey": "Dodaj klucz API",
+ "AddConfiguration": "Dodaj konfigurację",
+ "AdminMenuTitle": "Wydajność wyszukiwania",
+ "APIKey": "Klucz API",
+ "AvailableSites": "Serwisy dostępne do zaimportowania:",
+ "Domain": "Domena",
+ "DomainProperty": "Usługa domeny",
+ "DomainPropertyInfo": "Obejmuje wszystkie subdomeny (m, www itp.) i oba protokoły (http, https).",
+ "URLPrefix": "Prefiks adresu URL",
+ "URLPrefixProperty": "Usługa z prefiksem URL",
+ "URLPrefixPropertyInfo": "Obejmuje tylko adresy URL z dokładnie określonym prefiksem, w tym protokołem (http\/https). Jeśli chcesz, by usługa pasowała do dowolnego protokołu lub subdomeny (http\/https\/www.\/m. itp.), zamiast tej usługi dodaj usługę domeny.",
+ "BingAccountError": "Wystąpił błąd podczas weryfikacji klucza API: %1$s. Jeśli klucz został wygenerowany przed chwilą w Narzędziach Webmastera Bing, proszę spróbuj ponownie za minutę lub dwie (klucze API w tym narzędziu potrzebują chwilę na aktywację).",
+ "BingAccountOk": "Klucz API zweryfikowany pozytywnie",
+ "BingConfigurationDescription": "Narzędzia Webmastera Bing wymagają dostępowego klucza API. Tu możesz dodać klucz API, aby uzyskać dostęp do danych swoich serwisów.",
+ "BingConfigurationTitle": "Skonfiguruj import z Narzędzi dla Webmasterów Bing",
+ "BingCrawlBlockedByRobotsTxt": "Wykluczenia robots.txt",
+ "BingCrawlBlockedByRobotsTxtDesc": "Adresy URL obecnie blokowane przez plik robots.txt.",
+ "BingCrawlConnectionTimeout": "Wygaszone połączenia",
+ "BingCrawlConnectionTimeoutDesc": "Ta liczba prezentuje ostatnie próby połączenia, podczas których Bing nie było w stanie uzyskać dostępu do serwisu ze względu na błędy połączenia. Może to być sytuacja przejściowa, ale zalecamy weryfikację logów serwera pod kątem przypadkowo zerwanych połączeń.",
+ "BingCrawlCrawledPages": "Zindeksowane strony",
+ "BingCrawlCrawledPagesDesc": "Liczba odwiedzonych przez robota Bing stron.",
+ "BingCrawlDNSFailures": "Błędy DNS",
+ "BingCrawlDNSFailuresDesc": "Ten typ błędów zawiera błędy powstałe podczas próby rozwiązania adresu Twojego serwisu przy pomocy serwera DNS. Prawdopodobną przyczyną sytuacji mogło być wyłączenie serwera lub błędna konfiguracja serwera DNS uniemożliwiająca routing, jak na przykład ustawienie TTL na 0.",
+ "BingCrawlErrors": "Błędy indeksowania robota Bing",
+ "BingCrawlErrorsDesc": "Liczba błędów zauważonych przez robota Bing.",
+ "BingCrawlErrorsFromDateX": "Ten raport wskazuje błędy indeksowania zgłoszone przez robota Bing. Nie zawiera danych historycznych. Ostatnia aktualizacja %s",
+ "BingCrawlHttpStatus2xx": "Kody HTTP 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Te kody są zwracane przez serwer, gdy strona zostanie wygenerowana poprawnie",
+ "BingCrawlHttpStatus301": "Kod HTTP 301 (Trwale przeniesiony)",
+ "BingCrawlHttpStatus301Desc": "Ten kod występuje gdy na stałe przeniesiesz zawartość z jednej lokalizacji (adresu URL) do innej.",
+ "BingCrawlHttpStatus302": "Kod HTTP 302 (Przeniesiony czasowo)",
+ "BingCrawlHttpStatus302Desc": "Ten kod jest zwracany przez serwer dla zawartości, którą czasowo przeniesiono z jednej lokalizacji (adres URL) do innej.",
+ "BingCrawlHttpStatus4xx": "Kody HTTP 400 - 499 (Błędy żądania)",
+ "BingCrawlHttpStatus4xxDesc": "Kody te są zwracane, gdy zaistnieje obawa, że żądanie wysłane do serwera było niepoprawne, co uniemożliwiło serwerowi jego przetworzenie.",
+ "BingCrawlHttpStatus5xx": "Kody HTTP 500 - 599 (Wewnętrzne błędy serwera)",
+ "BingCrawlHttpStatus5xxDesc": "Te kody zwracane są w sytuacji , gdy serwerowi nie uda się zwrócić odpowiedzi dla poprawnego żądania.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Ważna strona wykluczona w pliku robots.txt",
+ "BingCrawlInboundLink": "Link wprowadzające",
+ "BingCrawlInboundLinkDesc": "Liczba linków postrzeganych przez Bing jako linki kierujące do wybranego serwisu. Są to linki z zewnętrznych stron, które wskazują na Twoje treści.",
+ "BingCrawlingStats": "Przegląd indeksowania Bing i Yahoo!",
+ "BingCrawlingStatsDocumentation": "Zestawienie indeksowania pozwala Ci zapoznać się z informacjami, które pochodzą z interfejsu robota takimi jak błędy indeksowania, elementy blokowane przez Twój plik robots.txt i adresu URL potencjalnie zainfekowane malware'm.",
+ "BingCrawlMalwareInfected": "Strony zainfekowane malware",
+ "BingCrawlMalwareInfectedDesc": "Dowolne strony, które Bing uzna za zainfekowane lub powiązane z malware zostaną zgrupowane w tej sekcji.",
+ "BingCrawlPagesInIndex": "Liczba zindeksowanych stron",
+ "BingCrawlPagesInIndexDesc": "Całkowita liczba stron dostępnych w indeksie Bing",
+ "BingCrawlStatsOtherCodes": "Pozostałe kody HTTP",
+ "BingCrawlStatsOtherCodesDesc": "Zawiera pozostałe kody, które nie zostały dopasowane (takie jak 1xx czy kody informacyjne).",
+ "BingKeywordImport": "Import słów kluczowych z Bing",
+ "BingKeywords": "Słowa kluczowe (Bing i Yahoo!)",
+ "BingKeywordsDocumentation": "Słowa kluczowe użyte w wyszukiwaniach Bing lub Yahoo!, które spowodowały wygenerowanie linków do Twojego serwisu na liście wyników.",
+ "BingKeywordsNoRangeReports": "Słowa kluczowe dla wyszukiwarek Bing lub Yahoo! mogą być przetwarzane jedynie w zakresie dat obejmujących pełne tygodnie lub miesiące, ponieważ nie są dostępne jako raporty dzienne.",
+ "BingKeywordsNotDaily": "Słowa kluczowe w Bing i Yahoo! dostępne są jedynie jako raporty tygodniowe. Nie występują tam zestawienia dzienne.",
+ "BingWebmasterApiUrl": "Adres Narzędzi Webmastera Bing",
+ "BingWebmasterApiUrlDescription": "Wprowadź adres serwisu dostępny w Twoich Narzędziach Webmastera Bing",
+ "Category": "Kategoria",
+ "ChangeConfiguration": "Zmiana konfiguracji",
+ "Clicks": "Kliknięcia",
+ "ClicksDocumentation": "Kliknięcie zaliczane jest za każdym razem, gdy odwiedzający kliknie w link prowadzący do Twojego serwisu ze strony wyników wyszukiwarki.",
+ "ClientConfigImported": "Konfiguracja klienta została poprawnie zaimportowana!",
+ "ClientConfigSaveError": "Wystąpił błąd podczas zapisu konfiguracji klienta. Proszę upewnij się, że podana konfiguracja jest poprawna i spróbuj ponownie.",
+ "ClientId": "ID klienta",
+ "ClientSecret": "Hasło klienta",
+ "ConfigAvailableNoWebsiteConfigured": "Integracja została poprawnie skonfigurowana, lecz nie skonfigurowano żadnego serwisu do importu.",
+ "ConfigRemovalConfirm": "Zamierzasz usunąć konfigurację dla %1$s. Import słów kluczowych dla tego serwisu zostanie wyłączony. Czy chcesz kontynuować?",
+ "Configuration": "Ustawienia",
+ "ConfigurationDescription": "Ta wtyczka pozwala Ci zaimportować bezpośrednio do Matomo wszystkie słowa kluczowe wyszukiwane przez Twoich odwiedzających w wyszukiwarkach.",
+ "ConfigurationFile": "Plik ustawień",
+ "ConfigurationValid": "Twoja konfiguracja OAuth jest prawidłowa.",
+ "ConfiguredAccounts": "ustawione konta",
+ "ConfiguredUrlNotAvailable": "Ustawiony adres URL nie jest dostępny dla tego konta",
+ "ConfigureMeasurableBelow": "Aby skonfigurować serwis, kliknij przycisk poniżej lub skonfiguruj go bezpośrednio w ustawieniach serwisu.",
+ "ConfigureMeasurables": "Skonfiguruj serwisy",
+ "ConnectAccount": "Podłącz konto",
+ "ConnectFirstAccount": "Zacznij od podłączenia poniżej pierwszego konta.",
+ "ConnectGoogleAccounts": "Połącz konto(a) Google",
+ "ContainingSitemaps": "Zawierające mapy strony",
+ "CrawlingErrors": "Błędy indeksowania",
+ "CrawlingStats": "Przegląd indeksowania",
+ "Ctr": "CTR",
+ "CtrDocumentation": "Współczynnik kliknięć: współczynnik pokazujący jak często użytkownicy wyszukiwarki klikają link do Twojego serwisu wyświetlany na stronie wyników.",
+ "CurrentlyConnectedAccounts": "Obecnie podłączone są %1$s konto\/a.",
+ "EnabledSearchTypes": "Przechwytywane słowa kluczowe",
+ "FetchImageKeyword": "Przechwytuj słowa kluczowe grafik",
+ "FetchImageKeywordDesc": "Przechwytuj słowa kluczowe użyte w wyszukiwarce grafik Google",
+ "FetchVideoKeyword": "Przechwytuj słowa kluczowe wideo",
+ "FetchVideoKeywordDesc": "Przechwytuj słowa kluczowe użyte w wyszukiwarce wideo Google",
+ "FetchWebKeyword": "Przechwytuj słowa kluczowe wyszukiwarki",
+ "FetchWebKeywordDesc": "Przechwytuj słowa kluczowe użyte w wyszukiwarce Google",
+ "FirstDetected": "Pierwsze wykryte",
+ "GoogleAccountAccessTypeOfflineAccess": "Dostęp offline<\/strong> jest wymagany do importu Twoich słów kluczowych podczas, gdy ty nie jesteś zalogowany\/a.",
+ "GoogleAccountAccessTypeProfileInfo": "Dane profilu<\/strong> wykorzystujemy do wyświetlenia nazwy obecnie podłączonego kont(a).",
+ "GoogleAccountAccessTypeSearchConsoleData": "Dane Konsoli Wyszukiwania<\/strong> są niezbędne do uzyskania dostępu do Twoich słów kluczowych w Google.",
+ "GoogleAccountError": "Wystąpił błąd podczas walidacji dostępu OAuth: %1$s",
+ "GoogleAccountOk": "Dostęp OAuth zweryfikowany poprawnie.",
+ "GoogleConfigurationDescription": "Konsola Wyszukiwania Google wykorzystuje OAuth do autentykacji i autoryzacji.",
+ "GoogleConfigurationTitle": "Skonfiguruj import z Konsoli Wyszukiwania Google",
+ "GoogleKeywordImport": "Import słów kluczowych Google",
+ "GoogleSearchConsoleUrl": "Adres Konsoli Wyszukiwania Google",
+ "GoogleSearchConsoleUrlDescription": "Wprowadź adres, pod którym ta strona dostępna jest w Twojej Konsoli Wyszukiwania Google",
+ "GoogleUploadOrPasteClientConfig": "Proszę prześlij swoją konfigurację klienta Google OAuth, lub wklej ją w pola poniżej.",
+ "HowToGetOAuthClientConfig": "Skąd pobrać konfigurację klienta OAuth",
+ "ImageKeywords": "Słowa kluczowe grafik w Google",
+ "ImageKeywordsDocumentation": "Słowa kluczowe użyte w wyszukiwarce grafik<\/b> Google, które na liście wyników wygenerowały linki do Twojego serwisu.",
+ "Impressions": "Dopasowania",
+ "ImpressionsDocumentation": "Dopasowanie zostanie zaliczone za każdym razem, gdy Twój serwis pojawi się na stronie wyników wyszukiwania wyszukiwarki.",
+ "IntegrationConfigured": "Integracja skonfigurowana pomyślnie",
+ "IntegrationNotConfigured": "Integracja jeszcze nie skonfigurowana",
+ "KeywordsCombined": "Skojarzone słowa kluczowe",
+ "KeywordsCombinedDocumentation": "Raport kojarzący wszystkie słowa kluczowe wykryte przez Matomo i zaimportowane z wyszukiwarki. Raport ten prezentuje jedynie wskaźniki odwiedzin. Możesz przełączyć się na powiązany raport, aby uzyskać więcej szczegółowych wskaźników.",
+ "KeywordsCombinedImported": "Skojarzone słowa kluczowe zaimportowane",
+ "KeywordsCombinedImportedDocumentation": "Raport prezentujący wszystkie zaimportowane słowa kluczowe ze wszystkich skonfigurowanych wyszukiwarek.",
+ "KeywordsReferrers": "Słowa kluczowe (włączając niezdefiniowane)",
+ "KeywordStatistics": "Wyszukiwane słowa kluczowe",
+ "KeywordTypeImage": "grafika",
+ "KeywordTypeVideo": "wideo",
+ "KeywordTypeWeb": "sieć",
+ "LastCrawled": "Ostatnia indeksacja",
+ "LastDetected": "Ostatnie sprawdzenie",
+ "LastImport": "Ostatni import",
+ "LatestAvailableDate": "Najświeższe słowa kluczowe dostępne dla %1$s",
+ "LinksToUrl": "Linkuje do %s",
+ "ManageAPIKeys": "Zarządzaj kluczami API",
+ "MeasurableConfig": "skonfigurowane serwisy",
+ "NoSegmentation": "Raport nie wspiera segmentacji. Dane wyświetlane są w standardowym, ciągłym raporcie.",
+ "NotAvailable": "Niedostępne",
+ "NoWebsiteConfigured": "Nie masz żadnego skonfigurowanego serwisu. Import dla wybranego serwisu możliwy jest dopiero po skonfigurowaniu go tu.",
+ "NoWebsiteConfiguredWarning": "Import dla %s nie jest w pełni skonfigurowany. Musisz skonfigurować serwis, aby umożliwić import.",
+ "OAuthClientConfig": "Konfiguracja klienta OAuth",
+ "OAuthError": "Wystąpił błąd podczas autentykacji OAuth. Proszę spróbuj ponownie i upewnij się, że zaakceptujesz wymagane uprawnienia.",
+ "Platform": "Platforma",
+ "Position": "Śr. pozycja",
+ "PositionDocumentation": "Średnia pozycja Twojego serwisu na liście wyników wyszukiwania (dla tego słowa kluczowego).",
+ "ProviderBingDescription": "Importuj wszystkie słowa kluczowe użyte do odnalezienia Twojego serwisu w Bing'u<\/strong> i Yahoo!<\/strong>.",
+ "ProviderBingNote": "Uwaga:<\/u> Microsoft dostarcza zestawienia słów kluczowych w każdą sobotę i tylko dla pełnych tygodni. W rezultacie Twoje słowa kluczowe z Bing i Yahoo! potrzebują paru dni, aby pojawić się w Twoich raportach i dostępne są tylko w widokach tygodniowych, miesięcznych i rocznych.",
+ "ProviderGoogleDescription": "Importuj wszystkie słowa kluczowe użyte do odnalezienia Twojego serwisu w Google<\/strong>. Raporty prezentują słowa kluczowe podzielone na typy wyszukiwań (sieć, grafika i wideo).",
+ "ProviderListDescription": "Po zakończeniu konfiguracji jednej (lub kilku) wyszukiwarek, możesz poniżej skonfigurować dla których serwisów Matomo będzie importowało słowa kluczowe.",
+ "ProviderXAccountWarning": "Wykryto błędy konfiguracji konta<\/strong> Proszę sprawdź skonfigurowane serwisu pod kątem %s<\/strong>.",
+ "ProviderXSitesWarning": "Wykryto błędy konfiguracji serwisu<\/strong> Proszę sprawdź skonfigurowane serwisy pod koątem %s<\/strong>.",
+ "ReAddAccountIfPermanentError": "Jeśli ten błąd będzie się powtarzał, spróbuj usunąć i ponownie dodać konto.",
+ "RequiredAccessTypes": "Wymagane typy dostępów:",
+ "ResponseCode": "Kod odpowiedzi",
+ "RoundKeywordPosition": "Przybliżona pozycja słowa kluczowego",
+ "SearchEngineKeywordsPerformance": "Wydajność wyszukiwania słów kluczowych",
+ "SetupConfiguration": "Rozpocznij konfigurację",
+ "SitemapsContainingUrl": "Mapy stron zawierają %s",
+ "KeywordsSubtableOriginal": "Śledzone słowa kluczowe (włączając niezdefiniowane)",
+ "KeywordsSubtableImported": "Zaimportowane słowa kluczowe",
+ "AllReferrersOriginal": "Odnośniki (ze śledzonymi słowa kluczowe)",
+ "AllReferrersImported": "Odnośniki (z zaimportowanymi słowa kluczowe)",
+ "SearchEnginesOriginal": "Wyszukiwarki (z śledzonymi słowami kluczowymi)",
+ "SearchEnginesImported": "Wyszukiwarki (z zaimportowanymi słowami kluczowymi)",
+ "StartOAuth": "Rozpocznij autoryzację OAuth",
+ "UnverifiedSites": "Niezweryfikowane serwisy:",
+ "UploadOAuthClientConfig": "Prześlij swoją konfigurację klienta OAuth",
+ "UrlOfAccount": "Adres (Konto)",
+ "VideoKeywords": "Słowa kluczowe w wynikach Google Wideo",
+ "VideoKeywordsDocumentation": "Słowa kluczowe użyte w wyszukiwarce Filmy<\/b> Google, które na liście wyników wygenerowały linki do Twojego serwisu.",
+ "WebKeywords": "Wszystkie słowa kluczowe Google",
+ "WebKeywordsDocumentation": "Słowa kluczowe użyte w wyszukiwarce Wszystko<\/b> Google, które na liście wyników wygenerowały linki do Twojego serwisu.",
+ "WebsiteSuccessfulConfigured": "Gratulacje! Udało Ci się poprawnie skonfigurować import słów kluczowych dla serwisu %1$s. Do importu i pojawienia się pierwszych słów kluczowych w Odnośniki > Słowa kluczowe może upłynąć kilka dni. Więcej informacji odnośnie opóźnień podczas importu słów kluczowych i ograniczeń znajdziesz w naszym %2$sFAQ%3$s",
+ "WebsiteTypeUnsupported": "Wybrany wskaźnik %1$s nie może zostać skonfigurowany ponieważ ma niewspierany typ. Tylko wskaźniki typu 'website' są wspierane.",
+ "WebsiteTypeUnsupportedRollUp": "Uwaga: Dla stron zbiorczych zaimportowane dane automatycznie zostaną połączone dla wszystkich podstron",
+ "YandexCrawlHttpStatus2xx": "Kody HTTP 200-299",
+ "YandexCrawlHttpStatus4xx": "Kody HTTP 400 - 499 (Błędy żądania)",
+ "YandexCrawlHttpStatus5xx": "Kody HTTP 500 - 599 (Wewnętrzne błędy serwera)",
+ "YandexCrawlInIndex": "Liczba zindeksowanych stron"
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/pt.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/pt.json
new file mode 100644
index 0000000..5bcb03e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/pt.json
@@ -0,0 +1,178 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "AccountAddedBy": "Adicionado por %1$s a %2$s ",
+ "AccountConnectionValidationError": "Ocorreu um erro a validar a ligação à conta:",
+ "AccountDoesNotExist": "A conta configurada %1$s já não existe",
+ "AccountNoAccess": "Atualmente esta conta não tem acesso a qualquer site.",
+ "AccountRemovalConfirm": "Você vai remover a conta %1$s. Isto pode desativar a importação de palavras-chave para quaisquer sites ligados. Continuar mesmo assim?",
+ "ActivityAccountAdded": "foi adicionada uma nova conta para fornecimento de palavras-chave %1$s: %2$s",
+ "ActivityAccountRemoved": "removeu uma conta para fornecimento de palavras-chave %1$s: %2$s",
+ "ActivityGoogleClientConfigChanged": "a configuração do cliente Google foi alterada.",
+ "AddAPIKey": "Adicionar uma chave da API",
+ "AddConfiguration": "Adicionar configuração",
+ "AdminMenuTitle": "Desempenho da pesquisa",
+ "APIKey": "Chave da API",
+ "AvailableSites": "Sites disponíveis para importação:",
+ "Domain": "Domínio",
+ "DomainProperty": "Propriedade do domínio",
+ "DomainPropertyInfo": "Inclui todos os subdomínios (m, www e assim por diante) e os ambos protocolos (http e https).",
+ "URLPrefix": "Prefixo do endereço",
+ "URLPrefixProperty": "Propriedade do prefixo do endereço",
+ "URLPrefixPropertyInfo": "Inclui somente endereços com o prefixo exato especificado, incluindo o protocolo (http/https). Se quiser que sua propriedade corresponda a qualquer protocolo ou subdomínio (http/https/www./m e assim por diante), adicione uma propriedade do domínio.",
+ "BingAccountError": "Ocorreu um erro ao validar a chave da API: %1$s. Se acabou de ferar esta chave da API nas Bing Webmaster Tools, por favor tente novamente dentro de um ou dois minutos (as chaves da API nas Bing Webmaster Tools demoram algum tempo até serem ativadas).",
+ "BingAccountOk": "Chave da API confirmada com sucesso",
+ "BingConfigurationDescription": "As Bing Webmaster Tools necessitam de uma chave da API para serem acedidas. Pode adicionar aqui chaves da API para aceder aos dados dos seus sites.",
+ "BingConfigurationTitle": "Configurar a importação das Bing Webmaster Tools",
+ "BingCrawlBlockedByRobotsTxt": "Exclusão do Robots.txt",
+ "BingCrawlBlockedByRobotsTxtDesc": "Endereços atualmente bloqueados pelo robots.txt do seu site.",
+ "BingCrawlConnectionTimeout": "Ligações expiradas",
+ "BingCrawlConnectionTimeoutDesc": "Este número representa ocorrências recentes quando o Bing não conseguiu aceder ao seu site devido a erros de ligação. Isto pode ser um problema temporário mas deve confirmar os registos do servidor para confirmar se está, acidentalmente, a perder pedidos.",
+ "BingCrawlCrawledPages": "Páginas indexadas",
+ "BingCrawlCrawledPagesDesc": "Número de páginas que o rastreador do Bing solicitou.",
+ "BingCrawlDNSFailures": "Falhas de DNS",
+ "BingCrawlDNSFailuresDesc": "Este tipo de problema cataloga erros recentes encontrados ao tentar comunicar com o servidor de DNS quando o bot tentou aceder às suas páginas. Possivelmente o servidor esteve em baixo ou ocorreu uma falha na configuração que impediu o encaminhamento DNS, por exemplo, o TTL foi definido como 0.",
+ "BingCrawlErrors": "Erros de indexação no Bing",
+ "BingCrawlErrorsDesc": "O número de erros que ocorreram para o indexador do Bing.",
+ "BingCrawlErrorsFromDateX": "O relatório mostra erros de indexação reportados recentemente pelo Bing. Não fornece qualquer dado histórico. Última atualização: %s",
+ "BingCrawlHttpStatus2xx": "Código HTTP 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Estes códigos aparecem quando um servidor serve uma página com sucesso",
+ "BingCrawlHttpStatus301": "Código HTTP 301 (Movido de forma permanente)",
+ "BingCrawlHttpStatus301Desc": "Estes códigos aparecem quando moveu, de forma permanente, conteúdos de uma localização (endereço) para outra.",
+ "BingCrawlHttpStatus302": "Código HTTP 302 (Movido de forma temporária)",
+ "BingCrawlHttpStatus302Desc": "Estes códigos aparecem quando moveu, de forma temporária, conteúdos de uma localização (endereço) para outra.",
+ "BingCrawlHttpStatus4xx": "Código HTTP 400-499 (Erros de solicitação)",
+ "BingCrawlHttpStatus4xxDesc": "Estes erros aparecem quando provavelmente ocorreu um erro no pedido que impediu o servidor de o processar.",
+ "BingCrawlHttpStatus5xx": "Código HTTP 500-599 (Erros interno do servidor)",
+ "BingCrawlHttpStatus5xxDesc": "Estes códigos aparecem quando o servidor não conseguiu satisfazer um pedido aparentemente válido.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Exclusão de uma página importante por parte do robots.txt",
+ "BingCrawlInboundLink": "Total de ligações de entrada",
+ "BingCrawlInboundLinkDesc": "Ligações de entrada que apontam para o seu site e que o Bing conhece. Estas são ligações que sites externos ao seu, apontam para o seu conteúdo.",
+ "BingCrawlingStats": "Visão geral da indexação para o Bing e Yahoo!",
+ "BingCrawlingStatsDocumentation": "A visão geral da indexação permite-lhe ver informação relacionada com o rastreamento, tais como erros encontrados pelo bot de pesquisa ao visitar uma página, itens bloqueados pelo seu ficheiro robots.txt e endereços potencialmente afetados por software malicioso.",
+ "BingCrawlMalwareInfected": "Sites infetados com software malicioso",
+ "BingCrawlMalwareInfectedDesc": "Quaisquer endereços de páginas que o Bing encontrou e que estão infetadas ou associadas a software malicioso serão agrupadas nesta secção.",
+ "BingCrawlPagesInIndex": "Total de páginas no índice",
+ "BingCrawlPagesInIndexDesc": "Número total de páginas disponíveis no índice do Bing",
+ "BingCrawlStatsOtherCodes": "Todos os outros códigos de estado HTTP",
+ "BingCrawlStatsOtherCodesDesc": "Agrupa todos os outros códigos que não coincidem com qualquer outro valor (tais como 1xx ou códigos de informação).",
+ "BingKeywordImport": "Importação de palavras-chave do Bing",
+ "BingKeywords": "Palavras-chave (no Bing e Yahoo!)",
+ "BingKeywordsDocumentation": "Palavras-chave utilizadas na pesquisa do Bing ou Yahoo! que geraram ligações para o seu site na listagem de resultados de pesquisa.",
+ "BingKeywordsNoRangeReports": "As palavras-chave no Bing e Yahoo! apenas podem ser processadas para intervalos de datas personalizados, incluindo semanas ou meses completos dado que não estão disponíveis como relatórios diários.",
+ "BingKeywordsNotDaily": "As palavras-chave no Bing e Yahoo! apenas estão disponíveis como relatórios semanais. Não existem dados de palavras-chave para períodos diários.",
+ "BingWebmasterApiUrl": "Endereço para as Bing Webmaster Tools",
+ "BingWebmasterApiUrlDescription": "Forneça o endereço no qual este site está disponível nas suas Bing Webmaster Tools",
+ "Category": "Categoria",
+ "ChangeConfiguration": "Alterar configuração",
+ "Clicks": "Cliques",
+ "ClicksDocumentation": "É contabilizado um clique de cada vez que alguém clica numa ligação que aponta para o seu site numa página de resultados do motor de pesquisa.",
+ "ClientConfigImported": "A configuração do cliente foi importada com sucesso!",
+ "ClientConfigSaveError": "Ocorreu um erro ao guardar a configuração do cliente. Por favor, confirme se a configuração fornecida é válida e tente novamente.",
+ "ClientId": "ID do cliente",
+ "ClientSecret": "Segredo do cliente",
+ "ConfigAvailableNoWebsiteConfigured": "A integração foi configurada com sucesso, mas neste momento não existe nenhum site configurado para importação.",
+ "ConfigRemovalConfirm": "Você vai remover a configuração para %1$s. A importação de palavras-chave para esse site vai ser desativada. Continuar mesmo assim?",
+ "Configuration": "Configuração",
+ "ConfigurationDescription": "Esta extensão permite-lhe importar diretamente para o Matomo todas as palavras-chave pesquisadas pelos seus utilizadores em motores de pesquisa.",
+ "ConfigurationFile": "Ficheiro de configuração",
+ "ConfigurationValid": "A sua configuração OAuth é válida.",
+ "ConfiguredAccounts": "contas configuradas",
+ "ConfiguredUrlNotAvailable": "O endereço configurado não está disponível para esta conta",
+ "ConfigureMeasurableBelow": "Para configurar um site, simplesmente clique no botão abaixo ou configure o mesmo diretamente nas definições do site.",
+ "ConfigureMeasurables": "Configurar sites",
+ "ConnectAccount": "Associar conta",
+ "ConnectFirstAccount": "Comece por associar a sua primeira conta abaixo.",
+ "ConnectGoogleAccounts": "Associar Conta(s) Google",
+ "ContainingSitemaps": "Mapas do site incluídos",
+ "CrawlingErrors": "Erros de indexação",
+ "CrawlingStats": "Visão geral da indexação",
+ "Ctr": "CTR",
+ "CtrDocumentation": "Taxa de cliques CTR: Uma taxa que mostra com que frequência as pessoas que vêm uma página de resultados com uma ligação para o seu site, acabam por clicar na mesma.",
+ "CurrentlyConnectedAccounts": "Atualmente existem %1$s contas associadas.",
+ "EnabledSearchTypes": "Tipos de palavras-chave a obter",
+ "FetchImageKeyword": "Obter palavras-chave de imagens",
+ "FetchImageKeywordDesc": "Obter palavras-chave utilizadas na pesquisa de imagens do Google",
+ "FetchVideoKeyword": "Obter palavras-chave de vídeos",
+ "FetchVideoKeywordDesc": "Obter palavras-chave utilizadas na pesquisa de vídeos do Google",
+ "FetchWebKeyword": "Obter palavras-chave da web",
+ "FetchWebKeywordDesc": "Obter palavras-chave utilizadas na pesquisa web do Google",
+ "FirstDetected": "Detetado pela primeira vez",
+ "GoogleAccountAccessTypeOfflineAccess": "Acesso offline é necessário para ser possível importar palavras-chave mesmo quando não tem sessão iniciada.",
+ "GoogleAccountAccessTypeProfileInfo": "A informação do perfil é utilizada para mostrar o nome da(s) conta(s) atualmente associada(s).",
+ "GoogleAccountAccessTypeSearchConsoleData": "Dados da consola de pesquisa é necessário para obter acesso às suas palavras-chave da pesquisa do Google.",
+ "GoogleAccountError": "Ocorreu um erro ao validar o acesso OAuth: %1$s",
+ "GoogleAccountOk": "Acesso OAuth confirmado com sucesso.",
+ "GoogleConfigurationDescription": "A consola de pesquisa do Google utiliza o OAuth para autenticação e autorização.",
+ "GoogleConfigurationTitle": "Configurar a importação da consola de pesquisa do Google",
+ "GoogleKeywordImport": "Importação das palavras-chave do Google",
+ "GoogleSearchConsoleUrl": "Endereço para a consola de pesquisa do Google",
+ "GoogleSearchConsoleUrlDescription": "Forneça o endereço no qual este site está disponível na Consola de pesquisa do Google",
+ "GoogleUploadOrPasteClientConfig": "Por favor, envie a sua configuração do cliente Google OAuth ou insira a mesma no campo abaixo.",
+ "HowToGetOAuthClientConfig": "Como obter a sua configuração do cliente Google OAuth",
+ "ImageKeywords": "Palavras-chave de imagem no Google",
+ "ImageKeywordsDocumentation": "Palavras-chave utilizadas na pesquisa de imagens do Google que geraram ligações para o seu site na lista de resultados de pesquisa.",
+ "Impressions": "Impressões",
+ "ImpressionsDocumentation": "É contabillizada uma impressão sempre que o seu site é apresentado numa página de resultados de pesquisa.",
+ "IntegrationConfigured": "Integração configurada com sucesso",
+ "IntegrationNotConfigured": "Integração não configurada",
+ "KeywordsCombined": "Palavras-chave combinadas",
+ "KeywordsCombinedDocumentation": "Relatório que combina todas as palavras-chave detetadas pelo Matomo e importadas dos motores de pesquisa. Este relatório apenas inclui a métrica das visitas. Pode mudar para um dos relatórios relacionados para obter métricas detalhadas.",
+ "KeywordsCombinedImported": "Palavras-chaves importadas combinadas",
+ "KeywordsCombinedImportedDocumentation": "Relatório que mostra todas as palavras-chave importadas de todos os motores de pesquisa configurados.",
+ "KeywordsReferrers": "Palavras-chave (incluindo não definidas)",
+ "KeywordStatistics": "Palavras-chave de Pesquisa",
+ "KeywordTypeImage": "imagem",
+ "KeywordTypeVideo": "vídeo",
+ "KeywordTypeWeb": "web",
+ "LastCrawled": "Última indexação",
+ "LastDetected": "Última deteção",
+ "LastImport": "Última importação",
+ "LatestAvailableDate": "Os dados mais recentes de palavras-chave são para %1$s",
+ "LinksToUrl": "Ligações para %s",
+ "ManageAPIKeys": "Gerir Chaves da API",
+ "MeasurableConfig": "sites configurados",
+ "NoSegmentation": "O relatório não suporta segmentação. Os dados apresentados são os dados predefinidos e não-segmentados.",
+ "NotAvailable": "Não disponível",
+ "NoWebsiteConfigured": "Não existe atualmente nenhum site configurado. Para ativar a importação para um site específico, por favor, defina a configuração aqui.",
+ "NoWebsiteConfiguredWarning": "A importação para %s não está totalmente configurada. Necessita de configurar alguns sites para ativar a importação.",
+ "OAuthClientConfig": "Configuração do cliente OAuth",
+ "OAuthError": "Ocorreu um erro no processo OAuth. Por favor, tente novamente e garanta que aceita as permissões solicitadas.",
+ "Platform": "Plataforma",
+ "Position": "Posição média",
+ "PositionDocumentation": "Posição média do seu site na lista de resultados do motor de pesquisa (para esta palavra-chave).",
+ "ProviderBingDescription": "Importar todas as palavras-chave utilizadas para encontrar o seu site na pesquisa do Bing e Yahoo! .",
+ "ProviderBingNote": "Nota: A Microsoft fornece dados de palavras-chave todos os sábados e apenas para semanas inteiras. Como consequência, as suas palavras-chave do Bing e Yahoo irão demorar alguns dias a serem apresentadas nos seus relatórios e só estarão disponíveis quando visualizar semanas, meses ou anos.",
+ "ProviderGoogleDescription": "Importar todas as palavras-chave utilizadas para encontrar o seu site na pesquisa do Google . Os relatórios irão mostrar as suas palavras-chave separadamente para cada tipo de pesquisa (Web, Imagens e Vídeos).",
+ "ProviderListDescription": "Quando tiver configurado com sucesso um (ou mais) motor(es) de pesquisa em baixo, pode configurar para que site(s) da Web deverá o Matomo importar as suas palavras-chave de pesquisa.",
+ "ProviderXAccountWarning": "Detetados problemas de configuração de conta Por favor, confirme as contas configuradas para %s .",
+ "ProviderXSitesWarning": "Detetados problemas de configuração do site da Web Por favor, confirme os sites da Web configurados para %s .",
+ "ReAddAccountIfPermanentError": "Este é um erro permanente, tente remover a conta e associe a mesma novamente.",
+ "RequiredAccessTypes": "São necessários estes tipos de acesso:",
+ "ResponseCode": "Código de resposta",
+ "RoundKeywordPosition": "Posição arredondada da palavra-chave",
+ "SearchEngineKeywordsPerformance": "Desempenho das palavras chave no motor de pesquisa",
+ "SetupConfiguration": "Configuração",
+ "SitemapsContainingUrl": "Mapas de site que contêm %s",
+ "KeywordsSubtableOriginal": "Palavras chave rastreadas (incluindo não definidas)",
+ "KeywordsSubtableImported": "Palavras-chave importadas",
+ "AllReferrersOriginal": "Referentes (com palavras-chave rastreadas)",
+ "AllReferrersImported": "Referentes (com palavras-chave importadas)",
+ "SearchEnginesOriginal": "Motores de pesquisa (com palavras-chave rastreadas)",
+ "SearchEnginesImported": "Motores de pesquisa (com palavras-chave importadas)",
+ "StartOAuth": "Iniciar processo OAuth",
+ "UnverifiedSites": "Sites não-confirmados:",
+ "UploadOAuthClientConfig": "Enviar a configuração do cliente OAuth",
+ "UrlOfAccount": "Endereço (conta)",
+ "VideoKeywords": "Palavras-chave de vídeo no Google",
+ "VideoKeywordsDocumentation": "As palavras-chave utilizadas na pesquisa vídeo do Google que geraram ligações para o seu site na lista de resultados de pesquisa.",
+ "WebKeywords": "Palavras-chave da Web no Google",
+ "WebKeywordsDocumentation": "As palavras-chave utilizadas na pesquisa da Web do Google que geraram hiperligações para o seu site da Web na lista de resultados de pesquisa.",
+ "WebsiteSuccessfulConfigured": "Parabéns! Configurou com sucesso a importação de palavras-chave para o site %1$s. Pode demorar alguns dias até que as suas primeiras palavras-chave de pesquisa sejam importadas e apresentadas em Referentes > Palavras-chave de pesquisa. Pode encontrar mais informação sobre os atrasos e limitações na importação de palavras-chave na nossa %2$sFAQ%3$s",
+ "WebsiteTypeUnsupported": "A métrica %1$s selecionada não pode ser configurada dados que não é um tipo suportado. Apenas métricas do tipo 'site' são suportadas.",
+ "WebsiteTypeUnsupportedRollUp": "Nota: Sites agregados irão combinar automaticamente os dados importados de todos os respetivos sites descendentes.",
+ "YandexCrawlHttpStatus2xx": "Código HTTP 200-299",
+ "YandexCrawlHttpStatus4xx": "Código HTTP 400-499 (Erros de solicitação)",
+ "YandexCrawlHttpStatus5xx": "Código HTTP 500-599 (Erros interno do servidor)",
+ "YandexCrawlInIndex": "Total de páginas no índice"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ro.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ro.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ro.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ru.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ru.json
new file mode 100644
index 0000000..d662b53
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ru.json
@@ -0,0 +1,81 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "APIKey": "API-ключ",
+ "AccountAddedBy": "Добавлено %1$s для %2$s ",
+ "AccountConnectionValidationError": "Произошла ошибка при проверке подключения учетной записи:",
+ "AccountDoesNotExist": "Настроенный аккаунт %1$s больше не существует",
+ "AccountNoAccess": "У данной учётной записи сейчас нет доступа к какому либо веб-сайту.",
+ "AccountRemovalConfirm": "Вы собираетесь удалить аккаунт %1$s. Это может отключить импорт ключевых слов для любых подключенных веб-сайтов. Все равно продолжить?",
+ "ActivityAccountAdded": "добавлен новый аккаунт для провайдера ключевых слов %1$s: %2$s",
+ "ActivityAccountRemoved": "удалён аккаунт для провайдера ключевых слов %1$s: %2$s",
+ "ActivityGoogleClientConfigChanged": "изменили конфигурацию клиента Google.",
+ "AddAPIKey": "Добавить API-ключ",
+ "AddConfiguration": "Добавить конфигурацию",
+ "AdminMenuTitle": "Поисковые фразы",
+ "AvailableSites": "Сайты, доступные для импорта:",
+ "BingAPIKeyInstruction": "Войдите в %1$sBing Webmaster Tools%2$s, затем добавьте свой сайт в Bing Webmaster. После его проверки вы можете %3$sскопировать свой API-ключ%4$s.",
+ "BingAccountError": "Произошла ошибка при проверке API-ключа: %1$s. Если вы только что сгенерировали этот API-ключ в Bing Webmaster Tools, повторите попытку через пару минут (для активации API-ключей в Bing Webmaster Tools требуется некоторое время).",
+ "BingAccountOk": "API-ключ успешно проверен",
+ "BingConfigurationDescription": "Для доступа к Bing Webmaster Tools необходим ключ API. Здесь вы можете добавить API-ключи для доступа к данным вашего сайта.",
+ "BingConfigurationTitle": "Настройка импорта из Bing Webmaster Tools",
+ "BingCrawlBlockedByRobotsTxt": "Исключение robots.txt",
+ "BingCrawlBlockedByRobotsTxtDesc": "URLы в настоящее время заблокированы в файле robots.txt.",
+ "BingCrawlConnectionTimeout": "Тайм-ауты соединения",
+ "BingCrawlConnectionTimeoutDesc": "Это число отражает недавние случаи, когда Bing не мог получить доступ к вашему сайту из-за ошибок подключения. Это может быть временная проблема, но вы должны проверить журналы вашего сервера, чтобы убедиться, что вы случайно не закрываете запросы.",
+ "BingCrawlCrawledPages": "Просканировано страниц",
+ "BingCrawlCrawledPagesDesc": "Количество страниц, просканированных Bing.",
+ "BingCrawlDNSFailures": "Сбои DNS",
+ "BingCrawlDNSFailuresDesc": "Этот тип проблемы каталогизирует недавние ошибки, возникающие при попытке связаться с DNS-сервером, когда бот пытался получить доступ к вашим страницам. Возможно, ваш сервер не работает или произошла неправильная конфигурация, которая препятствовала маршрутизации DNS, например, для TTL было установлено значение 0.",
+ "BingCrawlErrors": "Ошибки сканирования у Bing",
+ "BingCrawlErrorsDesc": "Количество ошибок, произошедших у сканера Bing.",
+ "BingCrawlErrorsFromDateX": "Отчет показывает ошибки сканирования, недавно обнаруженные Bing. Он не предоставляет никаких исторических данных. Последнее обновление %s",
+ "BingCrawlHttpStatus2xx": "HTTP код 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Эти коды означают успешную загрузку страниц",
+ "BingCrawlHttpStatus301": "HTTP код 301 (постоянный редирект)",
+ "BingCrawlHttpStatus301Desc": "Этот код означает, что вы навсегда переместили контент из одного местоположения (URL-адрес) в другое.",
+ "BingCrawlHttpStatus302": "HTTP код 302 (временный редирект)",
+ "BingCrawlHttpStatus302Desc": "Этот код означает, что вы временно переместили контент из одного местоположения (URL-адрес) в другое.",
+ "BingCrawlHttpStatus4xx": "HTTP код 400-499 (ошибки запроса)",
+ "BingCrawlHttpStatus4xxDesc": "Эти коды означают, что в запросе, вероятно, произошла ошибка, из-за которой сервер не смог её обработать.",
+ "BingCrawlHttpStatus5xx": "HTTP код 500-599 (ошибки сервера)",
+ "BingCrawlHttpStatus5xxDesc": "Эти коды означают, когда сервер не в состоянии выполнить корректный запрос.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Исключение важной страницы robots.txt",
+ "BingCrawlInboundLink": "Всего входящих ссылок",
+ "BingCrawlPagesInIndexDesc": "Общее количество страниц, доступных в индексе Bing",
+ "BingCrawlingStats": "Обзор сканирования для Bing и Yahoo!",
+ "BingKeywords": "Ключевые слова (на Bing и Yahoo!)",
+ "BingKeywordsNoRangeReports": "Ключевые слова на Bing и Yahoo! могут обрабатываться только для пользовательских диапазонов дат, включая полные недели или месяцы, поскольку они недоступны в качестве ежедневных отчётов.",
+ "BingWebmasterApiUrl": "URL для инструментов веб-мастеров Bing",
+ "ChangeConfiguration": "Изменить конфигурацию",
+ "ConfigureMeasurableBelow": "Чтобы настроить сайт, просто нажмите кнопку ниже или настройте его прямо в настройках сайта.",
+ "ConfigureMeasurables": "Настроить веб-сайты",
+ "ConnectAccount": "Подключить аккаунт",
+ "ConnectGoogleAccounts": "Подключить аккаунт(ы) Google",
+ "CrawlingErrors": "Ошибки сканирования",
+ "CrawlingStats": "Обзор сканирования",
+ "Domain": "Доменный ресурс",
+ "DomainProperty": "Доменный ресурс",
+ "DomainPropertyInfo": "Включает URL с любыми субдоменами (m, www и др.) и обоими префиксами протокола (http, https).",
+ "GoogleConfigurationTitle": "Настройка импорта из Google Search Console",
+ "GoogleSearchConsoleUrl": "URL для консоли поиска Google",
+ "ImageKeywords": "Ключевые слова изображений",
+ "KeywordsCombined": "Словосочетания",
+ "KeywordsCombinedImported": "Импортированные словосочетания",
+ "KeywordsReferrers": "Ключевые слова (исключая импорт)",
+ "ManageAPIKeys": "Управление API-ключами",
+ "ProviderBingDescription": "Импортируйте все ключевые слова, используемые для поиска вашего сайта в поиске Bing и Yahoo! .",
+ "ProviderBingNote": "Примечание: Microsoft предоставляет данные о ключевых словах каждую субботу и только неделей целиком. В результате ваши ключевые слова для Bing и Yahoo появятся в ваших отчётах через несколько дней и будут доступны только при просмотре недель, месяцев или лет.",
+ "ProviderGoogleDescription": "Импортируйте все ключевые слова, используемые для поиска вашего сайта в поиске Google . Отчёты будут показывать ваши ключевые слова для каждого типа поиска отдельно (общий топ, изображения или видео).",
+ "SearchEngineKeywordsPerformance": "Поисковые запросы и ключевые слова",
+ "SetupConfiguration": "Настройка конфигурации",
+ "URLPrefix": "Ресурс с префиксом в URL",
+ "URLPrefixProperty": "Ресурс с префиксом в URL",
+ "URLPrefixPropertyInfo": "Включает только URL с конкретным префиксом, в том числе указывающим на протокол (http или https). Если вам нужно, чтобы к ресурсу относились URL с любым префиксом протокола или субдоменом (http, https, www, m и т. д.), лучше добавить доменный ресурс.",
+ "VideoKeywords": "Ключевые слова видео",
+ "WebKeywords": "Ключевые слова общего топа",
+ "WebsiteSuccessfulConfigured": "Поздравляем! Вы успешно настроили импорт ключевых слов для сайта %1$s. Это может занять несколько дней, прежде чем ваши первые поисковые ключевые слова будут импортированы и отображены в меню Источники > Ключевые слова. Вы можете найти больше информации о задержках и ограничениях импорта ключевых слов в нашем %2$sFAQ%3$s",
+ "YandexCrawlHttpStatus2xx": "HTTP код 200-299",
+ "YandexCrawlHttpStatus4xx": "HTTP код 400-499 (ошибки запроса)",
+ "YandexCrawlHttpStatus5xx": "HTTP код 500-599 (ошибки сервера)"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/sq.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/sq.json
new file mode 100644
index 0000000..99c1766
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/sq.json
@@ -0,0 +1,240 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "APIKey": "Kyç API",
+ "AccountAddedBy": "Shtuar nga %1$s në %2$s ",
+ "AccountConnectionValidationError": "Ndodhi një gabim teksa vleftësohej lidhja e llogarisë:",
+ "AccountDoesNotExist": "Llogaria e formësuar %1$s s’ekziston më",
+ "AccountNoAccess": "Kjo llogari s’ka hyrje në ndonjë sajt.",
+ "AccountRemovalConfirm": "Ju ndan një hap nga heqja e kësaj llogarie %1$s. Kjo mund të sjellë çaktivizimin e importimit të fjalëkyçeve për cilindo sajte(e) të lidhur. Të vazhdohet sido qoftë?",
+ "ActivityAccountAdded": "u shtua llogari e re për furnizues fjalëkyçesh %1$s: %2$s",
+ "ActivityAccountRemoved": "u hoq llogari për furnizues fjalëkyçesh %1$s: %2$s",
+ "ActivityGoogleClientConfigChanged": "ndryshoi formësimi për klientin Google.",
+ "ActivityYandexClientConfigChanged": "u ndryshua formësimi i klientit Yandex.",
+ "AddAPIKey": "Shtoni Kyç API",
+ "AddConfiguration": "Shtoni Formësim",
+ "AdminMenuTitle": "Funksionim Kërkimi",
+ "AllReferrersImported": "Referues (me fjalëkyçe të importuar)",
+ "AllReferrersOriginal": "Referues (me fjalëkyçe të ndjekur)",
+ "AvailableSites": "Sajte të gatshëm për import:",
+ "BingAPIKeyInstruction": "Bëni hyrjen që nga %1$sBing Webmaster Tools%2$s, mandej shtoni sajtin tuaj te Bing Webmaster. Pasi të jetë vlerësuar, mund të %3$skopjoni kyçin tuaj API%4$s.",
+ "BingAccountError": "Ndodhi një gabim teksa vleftësohej Kyçi API: %1$s. Nëse sapo e keni krijuar këtë kyç API te Bing Webmaster Tools, ju lutemi riprovoni pas një a dy minutash (kyçet API në Bing Webmaster Tools duan pakëz kohë të aktivizohen).",
+ "BingAccountOk": "Kyç API i kontrolluar me sukses",
+ "BingConfigurationDescription": "Bing Webmaster Tools lyp një kyç API, që të mund të përdoret. Këtu mund të shtoni kyçe API për hyrje te të dhënat e sajtit tuaj.",
+ "BingConfigurationTitle": "Formësoni importim prej Bing Webmaster Tools",
+ "BingCrawlBlockedByRobotsTxt": "Përjashtim nga robots.txt",
+ "BingCrawlBlockedByRobotsTxtDesc": "URL të bllokuara aktualisht nga robots.txt i sajtit tuaj.",
+ "BingCrawlConnectionTimeout": "Mbarim kohe për lidhjet",
+ "BingCrawlConnectionTimeoutDesc": "Ky numër përfaqëson rastet së fundi kur Bing s’u fut dot te sajti juaj për shkak gabimesh lidhjeje. Ky mund të jetë një problem i përkohshëm, por do të duhej të shihnit regjistrat e shërbyesit për të parë se s’jeni duke mos pranuar aksidentalisht kërkesa.",
+ "BingCrawlCrawledPages": "Faqe të indeksuara",
+ "BingCrawlCrawledPagesDesc": "Numër faqesh të kërkuara nga indeksuesi i Bing-ut.",
+ "BingCrawlDNSFailures": "Dështime DNS",
+ "BingCrawlDNSFailuresDesc": "Ky lloj problemi katalogon gabime së fundi, të hasura teksa bëheshin përpjekje për të komunikuar me shërbyesin DNS, kur roboti u përpoq të hynte në faqet tuaja. Ka gjasa që shërbyesi juaj të ishte jashtë funksionimi, ose pati një keqformësim i cili pengoi rrugëzim DNS, për shembull, TTL qe caktuar 0.",
+ "BingCrawlErrors": "Gabimi indeksimi në Bing",
+ "BingCrawlErrorsDesc": "Numër gabimesh të hasura nga indeksuesi i Bing-ut.",
+ "BingCrawlErrorsFromDateX": "Raporti shfaq gabime indeksimi të raportuara së fundi nga Bing-u. Nuk jep të dhëna historike. Përditësuar së fundi më %s",
+ "BingCrawlHttpStatus2xx": "Kod HTTP 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Këto kode shfaqen kur shërbyesi e shërben me sukses një faqe",
+ "BingCrawlHttpStatus301": "Kod HTTP 301 (Lëvizur përgjithmonë)",
+ "BingCrawlHttpStatus301Desc": "Këto kode shfaqen kur keni lëvizur përgjithmonë lëndë nga një vendndodhje (URL) te një tjetër.",
+ "BingCrawlHttpStatus302": "Kod HTTP 302 (Lëvizur përkohësisht)",
+ "BingCrawlHttpStatus302Desc": "Këto kode shfaqen kur keni lëvizur përkohësisht lëndë nga një vendndodhje (URL) te një tjetër.",
+ "BingCrawlHttpStatus4xx": "Kod HTTP 400-499 (Gabime kërkese)",
+ "BingCrawlHttpStatus4xxDesc": "Këto kode shfaqen kur, sipas gjasash, pati një gabim te kërkesa, çka e pengoi shërbyesin të mundte ta përpunonte.",
+ "BingCrawlHttpStatus5xx": "Kod HTTP 500-599 (Gabime të brendshme shërbyesi)",
+ "BingCrawlHttpStatus5xxDesc": "Këto kode shfaqen kur shërbyesi s’arriti të plotësonte një kërkesë në dukje të vlefshme.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Përjashtim nga robots.txt faqeje të rëndësishme",
+ "BingCrawlInboundLink": "Lidhje ardhëse gjithsej",
+ "BingCrawlInboundLinkDesc": "Lidhje ardhëse të njohura nga Bing, që shpien te URL në sajtin tuaj. Këto janë lidhje, prej sajtesh tjetër nga ai i juaji, që shpien te lëndë e juaja.",
+ "BingCrawlMalwareInfected": "Sajte të infektuar me “malware”",
+ "BingCrawlMalwareInfectedDesc": "Në këtë ndarje do të grupohen çfarëdo URL faqeje gjetur nga Bing-u, e cila është e infektuar ose e lidhur me malware .",
+ "BingCrawlPagesInIndex": "Faqe gjithsej në tregues",
+ "BingCrawlPagesInIndexDesc": "Numër faqesh gjithsej të pranishme në indeksin e Bing-ut",
+ "BingCrawlStatsOtherCodes": "Krejt kodet e tjera për gjendje HTTP",
+ "BingCrawlStatsOtherCodesDesc": "Grupon krejt kodet e tjerë që nuk kanë përputhje me ndonjë vlerë tjetër (bie fjala, 1xx ose kode informative).",
+ "BingCrawlingStats": "Përmbledhje indeksimi për Bing dhe Yahoo!",
+ "BingCrawlingStatsDocumentation": "Përmbledhja e indeksimit ju lejon të shihni informacion të lidhur me indeksimin, bie fjala, gabime të hasur nga roboti i kërkimeve kur vizitohet një faqe, objekte të bllokuar nga kartela juaj robots.txt dhe URL që mund të jenë prekur nga malware.",
+ "BingKeywordImport": "Importim fjalëkyçesh Bing",
+ "BingKeywords": "Fjalëkyçe (në Bing dhe Yahoo!)",
+ "BingKeywordsDocumentation": "Fjalëkyçe të përdorur në kërkime Bing ose Yahoo! që prodhojnë lidhje për te sajti juaj te lista e përfundimeve të kërkimit.",
+ "BingKeywordsNoRangeReports": "Fjalëkyçe në Bing dhe Yahoo! që mund të përpunohen vetëm për intervale vetjakë datash, përfshi javë ose muaj të plotë, meqë s’mund të kihen si raporte ditore.",
+ "BingKeywordsNotDaily": "Fjalëkyçe në Bing dhe Yahoo! mund të kihen vetëm si raporte ditore. S’ka të dhëna fjalëkyçesh për periudha ditësh.",
+ "BingWebmasterApiUrl": "Url për Bing Webmaster Tools",
+ "BingWebmasterApiUrlDescription": "Jepni URL-në te e cila gjendet ky sajt te Bing Webmaster Tools tuaja",
+ "Category": "Kategori",
+ "ChangeConfiguration": "Ndryshoni formësimin",
+ "Clicks": "Klikime",
+ "ClicksDocumentation": "Si klikim llogaritet sa herë që dikush klikon mbi një lidhje që shpie te sajti juaj, në një faqe përfundimesh motori kërkimesh.",
+ "ClientConfigImported": "Formësimi i klientit u importua me sukses!",
+ "ClientConfigSaveError": "Ndodhi një gabim teksa ruhej formësimi i klientit. Ju lutemi, kontrolloni nëse formësimi i dhënë është i vlefshëm dhe riprovoni.",
+ "ClientId": "ID klienti",
+ "ClientSecret": "E fshehtë klienti",
+ "ConfigAvailableNoWebsiteConfigured": "Integrimi i formësua me sukses, por aktualisht nuk ka sajt të formësuar për import.",
+ "ConfigRemovalConfirm": "Ju ndan një hap nga heqja e formësimit për %1$s. Importimi i Fjalëkyçeve për atë sajt do të çaktivizohet. Të veprohet, sido qoftë?",
+ "Configuration": "Formësim",
+ "ConfigurationDescription": "Kjo shtojcë ju lejon të importoni drejt e në Matomo krejt fjalëkyçet e kërkuar nga përdoruesit tuaj në motorë kërkimesh.",
+ "ConfigurationFile": "Kartelë formësimi",
+ "ConfigurationValid": "Formësimi juaj OAuth është i vlefshëm.",
+ "ConfigureMeasurableBelow": "Për të formësuar një sajt, thjesht klikoni mbi butonin më poshtë ose formësojeni drejt e te rregullimet e sajtit.",
+ "ConfigureMeasurables": "Formësoni sajte",
+ "ConfigureTheImporterLabel1": "Importoni fjalëkyçet tuaja Google Search Console dhe analizojini duke përdorur mjetet e fuqishme statistikore të Matomo-s. Pasi të lidhni importuesin, përzgjidhni cilët sajte të importohen dhe Matomo-ja do të nisë të importojë fjalëkyçe për ta, si pjesë e një procesi arkivimi të planifikuar.",
+ "ConfigureTheImporterLabel2": "Që të importoni të dhënat tuaja nga Konsolë Google Search, Matomo ka nevojë të hyjë në këto të dhëna.",
+ "ConfigureTheImporterLabel3": "Për t’ia filluar, %1$sndiqni udhëzimet tona për të marrë formësimin e Klientit tuaj OAuth%2$s. Mandej ngarkojeni kartelën e formësimit të klientit duke përdorur butonin më poshtë.",
+ "ConfiguredAccounts": "llogari të formësuara",
+ "ConfiguredUrlNotAvailable": "Për këtë llogari s’ka URL të formësuar",
+ "ConnectAccount": "Lidhni Llogari",
+ "ConnectAccountDescription": "Ju lutemi, klikoni mbi butonin më poshtë që të ridrejtoheni te %1$s, ku ju duhet të akordoni hyrje.",
+ "ConnectAccountYandex": "Mirëfilltësimi për llogari Yandex është i vlefshëm vetëm për %1$s ditë. Çdo llogari lypset të rimirëfilltësohet brenda asaj kohe, për të siguruar që importimet funksionojnë saktë.",
+ "ConnectFirstAccount": "Fillojani duke lidhur llogarinë tuaj të parë më poshtë.",
+ "ConnectGoogleAccounts": "Lidhni Llogari Google",
+ "ConnectYandexAccounts": "Lidhni Llogari Yandex",
+ "ContainingSitemaps": "Përmban Harta Sajtesh",
+ "CrawlingErrors": "Gabime indeksimi",
+ "CrawlingOverview1": "Përmbledhja e Indeksimit raporton informacionet më kritike rreth se si indeksojnë sajtet tuaj robotë Motorësh Kërkimi. Këto vlera përditësohen afërsisht një herë në ditë me të dhëna të furnizuara nga motorët e kërkimeve.",
+ "CrawlingStats": "Përmbledhje indeksimi",
+ "CreatedBy": "Krijuar Nga",
+ "Ctr": "CTR",
+ "CtrDocumentation": "Koeficient klikimi: Një përpjesëtim që shfaq se sa shpesh persona që shohin një faqe përfundimesh motori kërkimesh me një lidhje për te sajti juaj, e klikojnë atë.",
+ "CurrentlyConnectedAccounts": "Aktualisht ka %1$s llogari të lidhura.",
+ "DeleteUploadedClientConfig": "Nëse do të donit të hiqnit formësimin e ngarkuar të klientit, klikoni më poshtë",
+ "Domain": "Përkatësi",
+ "DomainProperty": "Veti përkatësie",
+ "DomainPropertyInfo": "Përfshin krejt nënpërkatësitë (m, www, e me radhë) dhe që të dy protokollet (http, https).",
+ "EnabledSearchTypes": "Lloje fjalëkyçesh për t’u sjellë",
+ "FetchImageKeyword": "Sill fjalëkyçe figurash",
+ "FetchImageKeywordDesc": "Sill fjalëkyçe të përdorur në kërkime figurash në Google",
+ "FetchNewsKeyword": "Sill fjalëkyçe të rinj",
+ "FetchNewsKeywordDesc": "Sill fjalëkyçe të përdorur në Google News",
+ "FetchVideoKeyword": "Sill fjalëkyçe video",
+ "FetchVideoKeywordDesc": "Sill fjalëkyçe të përdorur në kërkime videosh në Google",
+ "FetchWebKeyword": "Sill fjalëkyçe web",
+ "FetchWebKeywordDesc": "Sill fjalëkyçe të përdorur në kërkime web përmes Google-it",
+ "FirstDetected": "Pikasur së pari",
+ "GoogleAccountAccessTypeOfflineAccess": "Përdorimi jashtë linje është i domosdoshëm për të qenë në gjendje të importohen fjalëkyçet tuaj të kërkimit, edhe kur s’jeni i futur në llogarinë tuaj.",
+ "GoogleAccountAccessTypeProfileInfo": "Të dhëna profili përdoren për të shfaqur emrin e llogarisë(ve) të lidhura në atë çast.",
+ "GoogleAccountAccessTypeSearchConsoleData": "Të dhënat e Konsolës së Kërkimeve janë të domosdoshme për të pasur hyrje te fjalëkyçet tuaj të kërkimeve me Google.",
+ "GoogleAccountError": "Ndodhi një gabim teksa vleftësohej hyrje OAuth: %1$s",
+ "GoogleAccountOk": "Hyrja OAuth u kontrollua me sukses.",
+ "GoogleAuthorizedJavaScriptOrigin": "Origjinë JavaScript-i të autorizuar",
+ "GoogleAuthorizedRedirectUri": "URI ridrejtimi të autorizuar",
+ "GoogleConfigurationDescription": "Konsola e Kërkimeve me Google përdor OAuth për mirëfilltësim dhe autorizim.",
+ "GoogleConfigurationTitle": "Formësoni importim prej Konsolës së Kërkimeve me Google",
+ "GoogleDataNotFinal": "Të dhënat e këtij raporti për fjalëkyçet mund të mos përmbajnë ende të dhënat përfundimtare. Google i furnizon të dhënat përfundimtare të fjalëkyçeve me një vonesë prej 2 ditësh. Fjalëkyçet për ditë më të afërta do të riimportohen, deri sa të raportohen si përfundimtare.",
+ "GoogleDataProvidedWithDelay": "Google i furnizon të dhënat e fjalëkyçeve me një vonesë. Fjalëkyçet për këtë datë do të importohen pakëz më vonë.",
+ "GoogleKeywordImport": "Importim fjalëkyçesh Google",
+ "GooglePendingConfigurationErrorMessage": "Formësimi është pezull. Ju lutemi, kërkojini një superpërdoruesi të plotësojë formësimin.",
+ "GoogleSearchConsoleUrl": "Url për Konsol Kërkimesh me Google",
+ "GoogleSearchConsoleUrlDescription": "Jepni URL-në ku gjendet ky sajt te Konsola juaj e Kërkimeve me Google",
+ "GoogleUploadOrPasteClientConfig": "Ju lutemi, ngarkoni formësimin e klientit tuaj për Google OAuth, ose hidheni atë te fusha më poshtë.",
+ "HowToGetOAuthClientConfig": "Si të merrni formësimin tuaj për OAuth Client",
+ "ImageKeywords": "Fjalëkyçe figurash në Google",
+ "ImageKeywordsDocumentation": "Fjalëkyçe të përdorur në kërkime figurash me Google, të cilët prodhuan lidhje për te sajti juaj, te lista e përfundimeve të kërkimit.",
+ "Impressions": "Përshtypje",
+ "ImpressionsDocumentation": "Si përshtypje llogaritet çdo herë që sajti juaj është shfaqur në një faqe përfundimesh motori kërkimesh.",
+ "IntegrationConfigured": "Integrim i formësuar me sukses",
+ "IntegrationNotConfigured": "Integrim ende i paformësuar",
+ "InvalidRedirectUriInClientConfiguration": "redirect_uris të pavlefshëm, të paktën 1 URI duhet të ketë përputhje me URI-n “%1$s” te kartela e ngarkuar për formësimin",
+ "KeywordStatistics": "Fjalëkyçe Kërkimi",
+ "KeywordTypeImage": "figurë",
+ "KeywordTypeNews": "lajme",
+ "KeywordTypeVideo": "video",
+ "KeywordTypeWeb": "web",
+ "KeywordsCombined": "Fjalëkyçe gjithsej",
+ "KeywordsCombinedDocumentation": "Raport që ndërthur krejt fjalëkyçet e pikasur nga Matomo dhe importuar prej motorësh kërkimesh. Ky raport përfshin vetëm vlera për vizitat. Për të pasur vlera të hollësishme, mund të kaloni te një nga raportet përkatës.",
+ "KeywordsCombinedImported": "Fjalëkyçe të importuar gjithsej",
+ "KeywordsCombinedImportedDocumentation": "Raport që shfaq krejt fjalëkyçet e importuar prej krejt motorëve të kërkimit të formësuar.",
+ "KeywordsReferrers": "Fjalëkyçe (përfshi të papërkufizuarit)",
+ "KeywordsSubtableImported": "Fjalëkyçe të importuar",
+ "KeywordsSubtableOriginal": "Fjalëkyçe të ndjekur (përfshi të papërkufizuarit)",
+ "LastCrawled": "Indeksuar së fundi",
+ "LastDetected": "Pikasur së fundi",
+ "LastImport": "Importimi i Fundit",
+ "LatestAvailableDate": "Të dhënat më të freskëta që mund të kihen për fjalëkyçet janë për %1$s",
+ "LinksToUrl": "Lidhje për te %s",
+ "ManageAPIKeys": "Administroni Kyçe API",
+ "MeasurableConfig": "sajte të formësuar",
+ "NewsKeywords": "Fjalëkyçe lajmesh në Google",
+ "NewsKeywordsDocumentation": "Fjalëkyçe të përdorur në kërkime Lajme Google, të cilët kanë prodhuar lidhje për te sajti juaj, te lista e përfundimeve të kërkimit.",
+ "NoSegmentation": "Raporti nuk mbulon segmentim. Të dhënat e shfaqura janë të dhënat tuaja standarde, të pasegmentuara, të raportit.",
+ "NoWebsiteConfigured": "S’ka sajt të formësuar. Për të aktivizuar importimin për sajtin e dhënë, ju lutemi, ujdisni këtu formësimin.",
+ "NoWebsiteConfiguredWarning": "Importim për %s i paformësuar plotësisht. Lypset të formësoni disa sajte që të aktivizohet importimi.",
+ "NotAvailable": "S’ka",
+ "OAuthAccessTimedOut": "Hyrjes me mirëfilltësim OAuth për këtë llogari mund t’i ketë mbaruar koha. Do t’ju duhet të ribëni mirëfilltësimin, që të funksionojnë sërish importimet për këtë llogari.",
+ "OAuthAccessWillTimeOut": "Hyrjes me OAuth për këtë llogari do t’i mbarojë koha pas %1$s ditësh. Edhe %2$s ditë ",
+ "OAuthAccessWillTimeOutSoon": "Hyrjes me OAuth për këtë llogari do t’i mbarojë koha pas rreth %1$s ditësh. Ju lutemi, ribëni mirëfilltësimin, për të shmangur ndaljen e çfarëdo importimesh për këtë llogari.",
+ "OAuthClientConfig": "Formësim Klienti OAuth",
+ "OAuthError": "Ndodhi një gabim brenda procesit OAuth. Ju lutemi, riprovoni dhe sigurohuni të pranoni lejet e domosdoshme.",
+ "OAuthExampleText": "Formësimi lyp fushat e treguara më poshtë. Ju lutemi, përdorni vlerat e dhëna:",
+ "OauthFailedMessage": "Hasëm një problem gjatë procesit të autorizimit për Konsolën tuaj Google Search. Që të riprovohet, ju lutemi, klikoni mbi butonin më poshtë. Nëse problemi vazhdon, ju lutemi, lidhuni me ekipin tonë të asistencës për ndihmë. Do t’ju ndihmojnë në zgjidhjen e problemit dhe importimin e fjalëkyçeve tuaj Google Search Console.",
+ "OptionQuickConnectWithGoogle": "Lidhuni shpejt, me Google (e rekomanduar)",
+ "Platform": "Platformë",
+ "Position": "Pozicion mesatar",
+ "PositionDocumentation": "Pozicioni mesatar i sajtit tuaj te lista e përfundimeve prej motorësh kërkimi (për këtë fjalëkyç).",
+ "ProvideYandexClientConfig": "Ju lutemi, jepni formësimin tuaj për klientit tuaj Yandex OAuth.",
+ "ProviderBingDescription": "Importoni krejt fjalëkyçet e përdorur për të gjetur sajtin tuaj në kërkime me Bing dhe Yahoo! .",
+ "ProviderBingNote": "Shënim: Microsoft-i furnizon fjalëkyçe çdo të shtunë dhe vetëm për javë të plota. Si rrjedhojë, fjalëkyçet tuaj për Bing-un dhe Yahoo-n do të duan disa ditë, para se të shfaqen në raportet tuaj dhe do të jenë vetëm për javë, muaj ose vite.",
+ "ProviderGoogleDescription": "Importoni krejt fjalëkyçet e përdorur për të gjetur sajtin tuaj përmes kërkimesh Google . Raportet do të shfaqin fjalëkyçet tuaj veçmas për çdo lloj kërkimi (Web, Figura dhe Video).",
+ "ProviderGoogleNote": "Shënim: Google furnizon të dhëna përfundimtare fjalëkyçesh me një vonesë prej 2 ditësh. Të dhëna përfundimtare për ditë më të afërta do të tregohen gjithashtu, por do të riimportohen, deri sa të jenë përfundimtare. Importimi juaj i parë mund të jetë në gjendje të importojë të dhënat tuaja historike mbi fjalëkyçet për deri 486 ditë më herët.",
+ "ProviderListDescription": "Kur keni ujdisur me sukses një (ose më tepër) motor kërkimi më poshtë, mund të formësoni në cilin sajt(e) duhet të importojë Matomo fjalëkyçet tuaj të kërkimit.",
+ "ProviderXAccountWarning": "U pikasën probleme formësimi llogarish Ju lutemi, kontrolloni llogaritë e formësuara për %s .",
+ "ProviderXSitesWarning": "U pikasën probleme formësimi sajtesh Ju lutemi, kontrolloni sajtet e formësuar për %s .",
+ "ProviderYandexDescription": "Importoni krejt fjalëkyçet e përdorur për të gjetur sajtin tuaj në kërkime me Yandex .",
+ "ProviderYandexNote": "Shënim: Yandex-i furnizon të dhëna fjalëkyçesh me një vonesë deri 5 ditësh. Importimi i parë do të provojë të importojë të dhënat tuaja historike mbi fjalëkyçet për deri 100 ditë e fundit.",
+ "ReAddAccountIfPermanentError": "Nëse ky është një gabim i vazhdueshëm, provoni ta hiqni llogarinë dhe ta rilidhni.",
+ "ReAuthenticateIfPermanentError": "Nëse ky është një gabim që nuk ikën, provoni të bëni rimirëfilltësimin e llogarisë ose ta hiqni dhe ta lidhini sërish.",
+ "Reauthenticate": "Ribëni mirëfilltësimin",
+ "RecentApiErrorsWarning": "U pikasën gabime importimi fjalëkyçesh Ju lutemi, kontrolloni formësimet për: %s Nëse formësimi juaj është i saktë dhe gabimet vazhdojnë, ju lutemi, lidhuni me asistencën.",
+ "ReportShowMaximumValues": "Vlerat e shfaqura janë vlerat maksimum që u hasën gjatë kësaj periudhe.",
+ "RequiredAccessTypes": "Këto lloje hyrjeje janë të domosdoshëm:",
+ "ResponseCode": "Kod përgjigjeje",
+ "RoundKeywordPosition": "Pozicion i rrumbullakosur fjalëkyçi",
+ "SearchEngineKeywordsPerformance": "Funksionim Fjalëkyçesh Motori Kërkimesh",
+ "SearchEnginesImported": "Motorë Kërkimesh (me fjalëkyçe të importuar)",
+ "SearchEnginesOriginal": "Motorë Kërkimesh (me fjalëkyçe të ndjekur)",
+ "SetUpOAuthClientConfig": "Ujdisni formësimin e klientit tuaj OAuth",
+ "SetupConfiguration": "Formësim rregullimi",
+ "SitemapsContainingUrl": "Harta sajtesh që përmbajnë %s",
+ "StartOAuth": "Fillo Procesin OAuth",
+ "URLPrefix": "Prefiks URL-sh",
+ "URLPrefixProperty": "Veti prefiksi URLsh",
+ "URLPrefixPropertyInfo": "Përfshin vetëm URL-të me saktësisht prefiksin e treguar, përfshi protokollin (http/https). Nëse doni që vetia juaj të ketë përputhje me çfarëdo protokolli ose nënpërkatësie (http/https/www./m. e me radhë), shihni mundësinë e shtimit të një vetie Përkatësi.",
+ "UnverifiedSites": "Sajte të paverifikuar:",
+ "UploadOAuthClientConfig": "Ngarkoni formësimin e klientit tuaj OAuth",
+ "Uploading": "Po ngarkohet…",
+ "UrlOfAccount": "URL (Llogari)",
+ "VideoKeywords": "Fjalëkyçe video në Google",
+ "VideoKeywordsDocumentation": "Fjalëkyçe të përdorur në kërkime videosh me Google, të cilët kanë prodhuar lidhje për te sajti juaj, te lista e përfundimeve të kërkimit.",
+ "VisitOAuthHowTo": "Ju lutemi, vizitoni %1$sudhërrëfyesin tonë në internet%2$s që të mësoni se si të ujdisni formësimin e klientit tuaj OAuth%3$s.",
+ "WebKeywords": "Fjalëkyçe Web në Google",
+ "WebKeywordsDocumentation": "Fjalëkyçe të përdorur në kërkime web me Google, të cilët kanë prodhuar lidhje për te sajti juaj, te lista e përfundimeve të kërkimit.",
+ "WebsiteSuccessfulConfigured": "Përgëzime! E formësuat me sukses importimin e fjalëkyçeve për sajtin %1$s. Mund të duhen disa ditë derisa të importohen fjalëkyçet tuaj të parë të kërkimit dhe të shfaqen te Referues > Fjalëkyçe Kërkimesh. Te %2$sFAQ%3$s jonë mund të gjeni më tepër informacion mbi vonesa dhe kufizime importimi fjalëkyçesh",
+ "WebsiteTypeUnsupported": "Matja e përzgjedhur %1$s s’mund të formësohet, ngaqë është e një lloji të pambuluar. Mbulohen vetëm matje të llojit “sajt”.",
+ "WebsiteTypeUnsupportedRollUp": "Shënim: Sajtet Agregat do të ndërthurin automatikisht të dhënat e importuara të krejt sajteve pjella",
+ "YandexConfigurationDescription": "Yandex Webmaster API përdor OAuth për mirëfilltësim dhe autorizim.",
+ "YandexConfigurationTitle": "Formësoni importim prej Yandex Webmaster API",
+ "YandexCrawlAppearedPages": "Faqe të pranishme në kërkim",
+ "YandexCrawlAppearedPagesDesc": "Faqe që qenë shtuar rishtas te indeks kërkimesh Yandex",
+ "YandexCrawlCrawledPages": "Faqe të Indeksuara",
+ "YandexCrawlCrawledPagesDesc": "Numër faqesh të kërkuara nga indeksuesi i Yandex-it.",
+ "YandexCrawlErrors": "Gabime të tjera kërkesash",
+ "YandexCrawlErrorsDesc": "Faqe të indeksuara që dështuan për çfarëdo arsye tjetër",
+ "YandexCrawlHttpStatus2xx": "Kod HTTP 200-299",
+ "YandexCrawlHttpStatus2xxDesc": "Faqe të Indeksuara me një kod 2xx",
+ "YandexCrawlHttpStatus3xx": "Kod HTTP 300-399 (Faqe të lëvizura)",
+ "YandexCrawlHttpStatus3xxDesc": "Faqe të Indeksuara me një kod 3xx",
+ "YandexCrawlHttpStatus4xx": "Kod HTTP 400-499 (Gabime kërkese)",
+ "YandexCrawlHttpStatus4xxDesc": "Faqe të Indeksuara me një kod 4xx",
+ "YandexCrawlHttpStatus5xx": "Kod HTTP 500-599 (Gabime të brendshme shërbyesi)",
+ "YandexCrawlHttpStatus5xxDesc": "Faqe të Indeksuara me një kod 5xx",
+ "YandexCrawlInIndex": "Faqe gjithsej në tregues",
+ "YandexCrawlInIndexDesc": "Numër faqesh gjithsej të pranishme në indeks kërkimesh Yandex",
+ "YandexCrawlRemovedPages": "Faqe të hequra prej kërkimi",
+ "YandexCrawlRemovedPagesDesc": "Faqe që qenë hequr prej treguesi kërkimesh Yandex",
+ "YandexCrawlingStats": "Përmbledhje indeksimi për Yandex!",
+ "YandexCrawlingStatsDocumentation": "Përmbledhja e indeksimit ju lejon të shihni informacion të lidhur me indeksimin, bie fjala, gabime të hasur nga roboti i kërkimeve kur vizitohet një faqe, objekte të bllokuar nga kartela juaj robots.txt dhe numri gjithsej i faqeve në tregues.",
+ "YandexFieldUrlToAppSite": "URL për te sajt aplikacioni",
+ "YandexKeywords": "Fjalëkyçe në Yandex",
+ "YandexKeywordsDocumentation": "Fjalëkyçe të përdorur në kërkime Yandex që prodhojnë lidhje për te sajti juaj te lista e përfundimeve të kërkimit.",
+ "YandexWebmasterApiUrl": "Url për Yandex Webmaster Tools",
+ "YandexWebmasterApiUrlDescription": "Jepni URL-në te e cila gjendet ky sajt te Yandex Webmaster Tools tuajat"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/sv.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/sv.json
new file mode 100644
index 0000000..d40ae7b
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/sv.json
@@ -0,0 +1,37 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "APIKey": "API-nyckel",
+ "AccountAddedBy": "Lades till av %1$s vid %2$s ",
+ "AccountConnectionValidationError": "Ett fel inträffade när kontoanslutningen skulle valideras:",
+ "AccountDoesNotExist": "Konfigurerat konto %1$s existerar inte längre",
+ "AccountNoAccess": "Detta konto har för närvarande inte åtkomst till någon webbplats.",
+ "AccountRemovalConfirm": "Du är på väg att ta bort kontot %1$s. Detta kan inaktivera import av nyckelord för anslutna webbplats(er). Vill du fortsätta ändå?",
+ "ActivityAccountAdded": "la till ett nytt konto för leverans av nyckelord %1$s: %2$s",
+ "ActivityAccountRemoved": "tog bort ett konto för leverans av nyckelord %1$s: %2$s",
+ "ActivityGoogleClientConfigChanged": "ändrade klientkonfigurationen för Google.",
+ "ActivityYandexClientConfigChanged": "ändrade klientkonfiguration för Yandex.",
+ "AddAPIKey": "Lägg till API-nyckel",
+ "AddConfiguration": "Lägg till konfiguration",
+ "AdminMenuTitle": "Sökprestation",
+ "AvailableSites": "Webbplatser som kan importeras:",
+ "BingAccountOk": "API-nyckel kontrollerad framgångsrikt",
+ "BingConfigurationDescription": "Bing Webmaster Tools behöver en API-nyckel för att gå att nå. Här kan du lägga till API-nycklar för åtkomst till din webbplats data.",
+ "BingConfigurationTitle": "Konfigurera import från Bing Webmaster Tools",
+ "BingCrawlBlockedByRobotsTxt": "Robots.txt exkludering",
+ "BingCrawlBlockedByRobotsTxtDesc": "URLar som för närvarande blockeras av din webbplats robots.txt.",
+ "BingCrawlConnectionTimeout": "Timeout för anslutning",
+ "BingCrawlCrawledPages": "Kravlade sidor",
+ "BingCrawlCrawledPagesDesc": "Antal sidor som Bings crawler anslöt till.",
+ "BingCrawlDNSFailures": "DNS-misslyckanden",
+ "BingCrawlErrorsDesc": "Antal fel som inträffade för Bings crawler.",
+ "BingCrawlHttpStatus2xx": "HTTP-kod 200-299",
+ "BingCrawlHttpStatus2xxDesc": "De här koderna visas när servern lyckades skicka webbsidan",
+ "BingCrawlHttpStatus301": "HTTP-kod 301 (Permanently moved)",
+ "BingCrawlHttpStatus301Desc": "Dessa koder visas när du permanent flyttat innehåll från en plats (URL) till en annan.",
+ "Domain": "Domän",
+ "DomainProperty": "Domänegendom",
+ "DomainPropertyInfo": "Inkluderar alla subdomäner (m, www, och så vidare) och båda protokoll (http, https).",
+ "URLPrefix": "URL-prefix",
+ "URLPrefixProperty": "URL-prefix för egendom"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/tr.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/tr.json
new file mode 100644
index 0000000..94012ba
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/tr.json
@@ -0,0 +1,241 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "APIKey": "API anahtarı",
+ "AccountAddedBy": "%1$s tarafından %2$s zamanında eklendi",
+ "AccountConnectionValidationError": "Hesap bağlantısı doğrulanırken bir sorun çıktı:",
+ "AccountDoesNotExist": "Yapılandırılmış %1$s hesabı artık yok",
+ "AccountNoAccess": "Bu hesabın şu anda herhangi bir siteye erişimi yok.",
+ "AccountRemovalConfirm": "%1$s hesabını silmek üzeresiniz. Bu işlem bağlı sitelerden anahtar sözcüklerin içe aktarılmasını kullanımdan kaldırabilir. İlerlemek istediğinize emin misiniz?",
+ "ActivityAccountAdded": "yeni bir anahtar sözcük hizmeti sağlayıcısı hesabı ekledi %1$s: %2$s",
+ "ActivityAccountRemoved": "bir anahtar sözcük hizmeti sağlayıcısı hesabını sildi %1$s: %2$s",
+ "ActivityGoogleClientConfigChanged": "Google istemci yapılandırmasını değiştirdi.",
+ "ActivityYandexClientConfigChanged": "Yandex istemci yapılandırmasını değiştirdi.",
+ "AddAPIKey": "API anahtarı ekle",
+ "AddConfiguration": "Yapılandırma ekle",
+ "AdminMenuTitle": "Arama başarımı",
+ "AllReferrersImported": "Yönlendirenler (içe aktarılmış anahtar sözcükler ile)",
+ "AllReferrersOriginal": "Yönlendirenler (izlenen anahtar sözcükler ile)",
+ "AvailableSites": "İçe aktarma için kullanılabilecek siteler:",
+ "BingAPIKeyInstruction": "%1$sBing Webmaster Tools%2$s sitesinde bir hesap açın ve siteyi Bing Webmaster üzerine ekleyin. Siteyi doğruladıktan sonra %3$sAPI anahtarınızı kopyalayın%4$s.",
+ "BingAccountError": "API anahtarı doğrulanırken bir sorun çıktı: %1$s. Bing Webmaster Tools ile bu API anahtarını yeni oluşturduysanız bir kaç dakika sonra yeniden deneyin (Bing Webmaster Tools API anahtarlarının etkinleştirilmesi biraz zaman alabilir).",
+ "BingAccountOk": "API anahtarı doğrulandı",
+ "BingConfigurationDescription": "Bing Webmaster Tools kullanmak için bir API anahtarına gerek vardır. Sitelerinizin verilerine erişmek için gereken API anahtarlarını buradan ekleyebilirsiniz.",
+ "BingConfigurationTitle": "Bing Webmaster Tools üzerinden içe aktarmayı yapılandırın",
+ "BingCrawlBlockedByRobotsTxt": "Robots.txt katılmaması",
+ "BingCrawlBlockedByRobotsTxtDesc": "Sitenin robots.txt dosyasını engelleyen adresler.",
+ "BingCrawlConnectionTimeout": "Bağlantı zaman aşımları",
+ "BingCrawlConnectionTimeoutDesc": "Bu değer yakın zamanda bağlantı sorunları nedeniyle Bing tarafından sitenize erişilememe sayısını gösterir. Bu durum geçici bir sorundan kaynaklanıyor olabilir ancak istekleri kazara reddedip etmediğinizi öğrenmek için sunucu günlüğü kayıtlarını gözden geçirmeniz iyi olur.",
+ "BingCrawlCrawledPages": "Derlenen sayfa sayısı",
+ "BingCrawlCrawledPagesDesc": "Bing crawler tarafından istekte bulunulan sayfa sayısı.",
+ "BingCrawlDNSFailures": "DNS sorunları",
+ "BingCrawlDNSFailuresDesc": "Bu sorun türünde bot tarafından yakın zamanda sayfalarınıza erişmek için DNS sunucu ile iletişim kurulmaya çalışılırken yaşanan sorunlar görüntülenir. Sunucunuz çalışmıyor olabilir ya da TTL değerinin 0 olarak ayarlanması gibi DNS yöneltmesinin yapılmasını engelleyen bir yapılandırma hatası olabilir.",
+ "BingCrawlErrors": "Bing derleme sorunları",
+ "BingCrawlErrorsDesc": "Bing crawler tarafından karşılaşılan sorun sayısı.",
+ "BingCrawlErrorsFromDateX": "Bu rapor, yakın zamanda Bing tarafından karşılaşılan derleme sorunlarını içerir. Geçmişe dönük herhangi bir veri bulunmaz. Son güncelleme: %s",
+ "BingCrawlHttpStatus2xx": "HTTP Kodları 200-299",
+ "BingCrawlHttpStatus2xxDesc": "Sunucu sayfayı doğru olarak sunduğunda bu kodlar görüntülenir",
+ "BingCrawlHttpStatus301": "HTTP Kodu 301 (Kalıcı olarak taşındı)",
+ "BingCrawlHttpStatus301Desc": "İçerik bir konumdan (adres) başka bir konuma kalıcı olarak taşındığında bu kod görüntülenir.",
+ "BingCrawlHttpStatus302": "HTTP Kodu 302 (Geçici olarak taşındı)",
+ "BingCrawlHttpStatus302Desc": "İçerik bir konumdan (adres) başka bir konuma geçici olarak taşındığında bu kod görüntülenir.",
+ "BingCrawlHttpStatus4xx": "HTTP Kodları 400-499 (İstek sorunları)",
+ "BingCrawlHttpStatus4xxDesc": "Bu kodlar sunucunun yapılan isteklerde bulunan hatalar nedeniyle istekleri işleyememesi durumunda görüntülenir.",
+ "BingCrawlHttpStatus5xx": "HTTP Kodları 500-599 (İç sunucu sorunları)",
+ "BingCrawlHttpStatus5xxDesc": "Bu kodlar sunucunun açıkca geçerli olan istekleri karşılayamaması durumunda görüntülenir.",
+ "BingCrawlImportantBlockedByRobotsTxt": "Önemli sayfada robots.txt katılmaması",
+ "BingCrawlInboundLink": "Gelen bağlantı toplamı",
+ "BingCrawlInboundLinkDesc": "Gelen bağlantılar Bing tarafından algılanan sitenize yönelmiş adreslerdir. Bunlar, kendi sitenizden gelen ve içeriğinize işaret eden bağlantılardır.",
+ "BingCrawlMalwareInfected": "Zararlı yazılımdan etkilenmiş siteler",
+ "BingCrawlMalwareInfectedDesc": "Bing tarafından zararlı bir yazılımın bulaştığı ya da ilgisi olduğu anlaşılan sayfa adresleri bu bölümde görüntülenir.",
+ "BingCrawlPagesInIndex": "Dizindeki sayfa sayısı",
+ "BingCrawlPagesInIndexDesc": "Bing arama dizinindeki toplam sayfa sayısı",
+ "BingCrawlStatsOtherCodes": "Tüm diğer HTTP durum kodları",
+ "BingCrawlStatsOtherCodesDesc": "Başka değerler ile eşleşmeyen tüm diğer kodları bir araya getirir (1xx ya da bilgilendirme kodları gibi).",
+ "BingCrawlingStats": "Bing ve Yahoo! için derleme özeti",
+ "BingCrawlingStatsDocumentation": "Derleme özetinde bir sayfa ziyaret edildiğinde arama botu tarafından karşılaşılan sorunlar, robots.txt dosyanız tarafından engellenen ögeler ve zararlı yazılımlar tarafından etkilenmiş adresler gibi derleme ile ilgili bilgiler bulunur.",
+ "BingKeywordImport": "Bing anahtar sözcük içe aktarma",
+ "BingKeywords": "Anahtar sözcükler (Bing ve Yahoo! üzerinde)",
+ "BingKeywordsDocumentation": "Bing ya da Yahoo! arama sonuçlarında sitenize bağlantılar oluşturmak için kullanılan anahtar sözcükler.",
+ "BingKeywordsNoRangeReports": "Bing ve Yahoo! üzerindeki anahtar sözcükler günlük raporlar olarak kullanılamadığından yalnızca tam haftalar ya da aylar gibi özel tarih aralıkları için işlenebilir.",
+ "BingKeywordsNotDaily": "Bing ve Yahoo! üzerindeki anahtar sözcükler yalnızca haftalık raporlar olarak kullanılabilir. Anahtar sözcük verileri günlük olarak kullanılamaz.",
+ "BingWebmasterApiUrl": "Bing Webmaster Tools adresi",
+ "BingWebmasterApiUrlDescription": "Bu sitenin Bing Webmaster Tools üzerindeki adresini yazın",
+ "Category": "Kategori",
+ "ChangeConfiguration": "Yapılandırmayı değiştir",
+ "Clicks": "Tıklanma",
+ "ClicksDocumentation": "Birisi bir arama motorunun sonuç sayfasında siteyi gösteren bir bağlantıya tıkladığında bir tıklama sayılır.",
+ "ClientConfigImported": "İstemci yapılandırması içe aktarıldı!",
+ "ClientConfigSaveError": "İstemci yapılandırması içe aktarılırken bir sorun çıktı. Lütfen belirtilen yapılandırmanın geçerli olduğundan emin olduktan sonra yeniden deneyin.",
+ "ClientId": "İstemci kodu",
+ "ClientSecret": "İstemci parolası",
+ "ConfigAvailableNoWebsiteConfigured": "Bütünleştirme yapılandırması tamamlandı, ancak şu anda içe aktarılmak için bir site yapılandırılmamış.",
+ "ConfigRemovalConfirm": "%1$s yapılandırmasını silmek üzereseniz. Bu site için anahtar sözcükler içe aktarılmayacak. İlerlemek istiyor musunuz?",
+ "Configuration": "Yapılandırma",
+ "ConfigurationDescription": "Bu uygulama eki, sitenin kullanıcıları tarafından arama motorlarına sorulan anahtar sözcükleri doğrudan Matomo içine aktarır.",
+ "ConfigurationFile": "Yapılandırma dosyası",
+ "ConfigurationValid": "OAuth yapılandırmanız geçersiz.",
+ "ConfigureMeasurableBelow": "Bir siteyi yapılandırmak için aşağıdaki düğmeye tıklayın ya da doğrudan site ayarları bölümüne gidin.",
+ "ConfigureMeasurables": "Siteleri yapılandır",
+ "ConfigureTheImporterLabel1": "Google Search Console anahtar sözcüklerinizi içe aktarın ve güçlü Matomo analiz araçlarını kullanarak inceleyin. İçe aktarıcıyı bağlantısını kurduktan sonra, içe aktarılacak siteleri seçin. Matomo, programlanmış arşivleme sürecinin bir parçası olarak sitelerin anahtar sözcüklerini içe aktarmaya başlar.",
+ "ConfigureTheImporterLabel2": "Verilerinizi Google Search Console üzerinden içe aktarmak için Matomo bu verilere erişmelidir.",
+ "ConfigureTheImporterLabel3": "Başlamak için, %1$sOAuth İstemci yapılandırmanızı alma yönergelerini izleyin%2$s. Ardından, aşağıdaki düğmeyi kullanarak istemci yapılandırma dosyasını yükleyin.",
+ "ConfiguredAccounts": "yapılandırılmış hesaplar",
+ "ConfiguredUrlNotAvailable": "Bu hesabın yapılandırılmış adresine erişilemiyor",
+ "ConnectAccount": "Hesap bağla",
+ "ConnectAccountDescription": "Gerekli izinleri vermek için aşağıdaki düğme üzerine tıklayarak %1$s sitesine gidin.",
+ "ConnectAccountYandex": "Yandex hesapları için kimlik doğrulaması yalnızca %1$s gün süreyle geçerlidir. İçe aktarma işlemlerinin sorunsuz yapılabilmesi için her hesap kimliğinin bu süre içinde yeniden doğrulanması gerekir.",
+ "ConnectFirstAccount": "Aşağıdan ilk hesabınızı bağlayarak başlayın.",
+ "ConnectGoogleAccounts": "Google hesaplarını bağla",
+ "ConnectYandexAccounts": "Yandex hesaplarını bağla",
+ "ContainingSitemaps": "Site haritaları bulunan",
+ "CrawlingErrors": "Derleme sorunları",
+ "CrawlingOverview1": "Derleme özeti raporu, arama motoru robotlarının siteleri nasıl derlediği ile ilgili kritik bilgileri içerir. Bu ölçümler, aşağı yukarı her gün arama motorları tarafından sağlanan verilerle güncellenir.",
+ "CrawlingStats": "Derleme özeti",
+ "CreatedBy": "Oluşturan",
+ "Ctr": "Tıklayıp geçme oranı",
+ "CtrDocumentation": "Tıklayıp geçme oranı: Kişilerin ne sıklıkta arama sonuçları sayfasında sitenin bağlantısını görüp tıkladığının oranı.",
+ "CurrentlyConnectedAccounts": "Şu anda bağlı %1$s hesap var.",
+ "DeleteUploadedClientConfig": "Yüklenen istemci yapılandırmasını kaldırmak istiyorsanız aşağıya tıklayın",
+ "Domain": "Etki alanı",
+ "DomainProperty": "Etki alanı mülkü",
+ "DomainPropertyInfo": "Tüm alt etki alanlarını (m, www gibi) ve iki iletişim kuralını da (http, https) kapsar.",
+ "EnabledSearchTypes": "Alınacak anahtar sözcük türleri",
+ "FetchImageKeyword": "Görsel anahtar sözcükleri alınsın",
+ "FetchImageKeywordDesc": "Bu seçenek etkinleştirildiğinde, Google görsel aramasında kullanılan anahtar sözcükler alınır",
+ "FetchNewsKeyword": "Yeni anahtar sözcükler alınsın",
+ "FetchNewsKeywordDesc": "Bu seçenek etkinleştirildiğinde, Google Haberler için kullanılan anahtar sözcükler alınır",
+ "FetchVideoKeyword": "Görüntü anahtar sözcükleri alınsın",
+ "FetchVideoKeywordDesc": "Bu seçenek etkinleştirildiğinde, Google görüntü aramasında kullanılan anahtar sözcükler alınır",
+ "FetchWebKeyword": "Site anahtar sözcükleri alınsın",
+ "FetchWebKeywordDesc": "Bu seçenek etkinleştirildiğinde, Google site aramasında kullanılan anahtar sözcükler alınır",
+ "FirstDetected": "İlk algılanan",
+ "GoogleAccountAccessTypeOfflineAccess": "Şu anda oturum açmamış olsanız da arama anahtar sözcüklerini içe aktarmak için çevrinmdışı erişim gereklidir.",
+ "GoogleAccountAccessTypeProfileInfo": "Profil bilgileri şu anda bağlı olan hesapların adlarını görüntülemek için kullanılır.",
+ "GoogleAccountAccessTypeSearchConsoleData": "Konsol verilerinde arama Google arama anahtar sözcüklerini alabilmek için gereklidir.",
+ "GoogleAccountError": "OAuth erişimi doğrulanırken bir sorun çıktı: %1$s",
+ "GoogleAccountOk": "OAuth erişimi doğrulandı.",
+ "GoogleAuthorizedJavaScriptOrigin": "İzin verilen JavaScript kaynağı",
+ "GoogleAuthorizedRedirectUri": "İzin verilen yönlendirme adresi",
+ "GoogleConfigurationDescription": "Google arama konsolunda kimlik doğrulaması için OAuth kullanılır.",
+ "GoogleConfigurationTitle": "Google arama konsolundan içe aktarmayı yapılandır",
+ "GoogleDataNotFinal": "Bu rapordaki anahtar sözcükler henüz son verileri içermiyor olabilir. Google, son anahtar sözcükleri 2 gün gecikme ile sağlar. Daha yakın zamandaki anahtar sözcükler, son olarak bildirildiğinde yeniden içe aktarılır.",
+ "GoogleDataProvidedWithDelay": "Google, anahtar sözcük verilerini bir gecikme ile sağlar. Bu tarihteki anahtar sözcükler biraz daha sonra içe aktarılacak.",
+ "GoogleKeywordImport": "Google anahtar sözcükleri içe aktarma",
+ "GooglePendingConfigurationErrorMessage": "Yapılandırma bekliyor. Lütfen bir süper kullanıcıdan yapılandırmayı tamamlamasını isteyin.",
+ "GoogleSearchConsoleUrl": "Google arama konsolu adresi",
+ "GoogleSearchConsoleUrlDescription": "Bu sitenin Google arama konsolu üzerindeki adresini yazın",
+ "GoogleUploadOrPasteClientConfig": "Lütfen Google OAuth istemci yapılandırmanızı yükleyin ya da aşağıdaki alana yapıştırın.",
+ "HowToGetOAuthClientConfig": "OAuth istemci yapılandırmasını almak",
+ "ImageKeywords": "Google üzerindeki görsel anahtar sözcükleri",
+ "ImageKeywordsDocumentation": "Google görsel arama sonuçlarında sitenize bağlantılar oluşturmak için kullanılan anahtar sözcükler.",
+ "Impressions": "Gösterimler",
+ "ImpressionsDocumentation": "Bir arama motoru sonuç sayfasında siteyi görüntülediğinde bir gösterim sayılır.",
+ "IntegrationConfigured": "Bütünleştirme yapılandırılmış",
+ "IntegrationNotConfigured": "Bütünleştirme yapılandırılmamış",
+ "InvalidRedirectUriInClientConfiguration": "redirect_uris değeri geçersiz. En az 1 uri yüklenen yapılandırma dosyasındaki \"%1$s\" uri değeri ile eşleşmelidir",
+ "KeywordStatistics": "Arama anahtar sözcükleri",
+ "KeywordTypeImage": "görsel",
+ "KeywordTypeNews": "haberler",
+ "KeywordTypeVideo": "görüntü",
+ "KeywordTypeWeb": "site",
+ "KeywordsCombined": "Birleştirilmiş anahtar sözcükler",
+ "KeywordsCombinedDocumentation": "Bu rapor, Matomo tarafından algılanan ve arama motorlarından içe aktarılan tüm anahtar sözcüklerin birleştirilmiş görünümünü içerir. İçinde yalnızca ziyaret ölçümü bulunur. Daha ayrıntılı ölçümleri görebilmek için ilgili raporlardan birine geçebilirsiniz.",
+ "KeywordsCombinedImported": "Birleştirilmiş içe aktarılan sözcükler",
+ "KeywordsCombinedImportedDocumentation": "Yapılandırılmış tüm arama motorlarından içe aktarılan tüm anahtar sözcükleri görüntüleyen rapor.",
+ "KeywordsReferrers": "Anahtar sözcükler (tanımlanmamışlar dahil)",
+ "KeywordsSubtableImported": "İçe aktarılmış anahtar sözcükler",
+ "KeywordsSubtableOriginal": "İzlenen anahtar sözcükler (tanımlanmamışlar dahil)",
+ "LastCrawled": "Son derlenen",
+ "LastDetected": "Son algılanan",
+ "LastImport": "Son içe aktarma",
+ "LatestAvailableDate": "%1$s için kullanılabilecek son anahtar sözcük verileri",
+ "LinksToUrl": "%s adresine bağlanan",
+ "ManageAPIKeys": "API anahtarları yönetimi",
+ "MeasurableConfig": "yapılandırılmış siteler",
+ "NewsKeywords": "Google üzerindeki haber anahtar sözcükleri",
+ "NewsKeywordsDocumentation": "Google Haberler arama sonuçlarında sitenize bağlantılar oluşturmak için kullanılan anahtar sözcükler.",
+ "NoSegmentation": "Bu rapor dilimleri desteklemiyor. Standart, dilimlenmemiş veriler görüntüleniyor.",
+ "NoWebsiteConfigured": "Henüz yapılandırılmış bir site yok. Belirli bir sitenin içe aktarılmasını kullanıma almak için buradan yapılandırmayı tamamlayabilirsiniz.",
+ "NoWebsiteConfiguredWarning": "%s için içe aktarma tam olarak yapılandırılmamış. İçe aktarmayı kullanıma almak için bazı siteler yapılandırmalısınız.",
+ "NotAvailable": "Kullanılamaz",
+ "OAuthAccessTimedOut": "Bu hesap için OAuth erişiminin süresi dolmuş olabilir. Bu hesap ile içe aktarma işlemlerinin yapılabilmesi için kimliği yeniden doğrulamanız gerekecek.",
+ "OAuthAccessWillTimeOut": "Bu hesap için OAuth erişiminin süresi %1$s gün içinde dolacak. %2$s gün kaldı ",
+ "OAuthAccessWillTimeOutSoon": "Bu hesap için OAuth erişiminin süresi %1$s gün içinde dolacak. Lütfen bu hesap ile içe aktarma işlemlerinin yapılabilmesi için kimliği yeniden doğrulayın.",
+ "OAuthClientConfig": "OAuth istemci yapılandırması",
+ "OAuthError": "OAuth işlemi sırasında bir sorun çıktı. Lütfen yeniden deneyin ve gerekli izinleri verdiğinizden emin olun.",
+ "OAuthExampleText": "Yapılandırma için aşağıdaki alanlar gereklidir. Lütfen belirtilen değerleri kullanın:",
+ "OauthFailedMessage": "Google Search Console için yetkilendirme işlemi sırasında bir sorunla karşılaştık. Lütfen yeniden denemek için aşağıdaki düğmeye tıklayın. Sorun sürerse, yardım almak için destek ekibimizle görüşün. Ekibimiz sorunu çözmenize ve Google Search Console anahjtar sözcüklerinizi içe aktarmanıza yardımcı olur.",
+ "OptionQuickConnectWithGoogle": "Google ile hızlı bağlan (önerilir)",
+ "Platform": "Platform",
+ "Position": "Ortalama konum",
+ "PositionDocumentation": "Arama sonuçlarında sitenin ortalama konumu (bu anahtar sözcük için).",
+ "ProvideYandexClientConfig": "Lütfen Yandex Oauth istemci yapılandırmanızı ekleyin.",
+ "ProviderBingDescription": "Bing ve Yahoo! , aramalarında siteyi bulmak için kullanılan tüm anahtar sözcükleri içe aktarır.",
+ "ProviderBingNote": "Not: Microsoft, anahtar sözcük verilerini her Cumartesi günü bir hafta süre ile yayınlar. Bunun sonucu olarak Bing ve Yahoo için anahtar sözcüklerinizin raporlara eklenmesi bir kaç gün sürer ve yalnızca haftalık, aylık ve yıllık aralıklarda görüntülenir.",
+ "ProviderGoogleDescription": "Google , aramalarında siteyi bulmak için kullanılan tüm anahtar sözcükleri içe aktarır. Raporlarda anahtar sözcükler her bir arama türüne göre ayrılarak görüntülenir (Site, Görsel ve Görüntü).",
+ "ProviderGoogleNote": "Not: Google son anahtar sözcük verilerini 2 gün gecikmeli olarak yayınlar. Daha yakın günlerdeki son olmayan veriler her zaman görüntülenir ancak son olarak bildirildiğinde yeniden içe aktarılır. İlk içe aktarımda anahtar sözcük verilerinin 486 güne kadar olan geçmişi de içe aktarılabilir.",
+ "ProviderListDescription": "Aşağıdaki bir (ya da bir kaç) arama motorunu ayarladığınızda, Matomo tarafından arama anahtar sözcüklerinin içe aktarılacağı siteleri yapılandırabilirsiniz.",
+ "ProviderXAccountWarning": "Hesap yapılandırma sorunları bulundu Lütfen %s için yapılandırılmış hesapları denetleyin.",
+ "ProviderXSitesWarning": "Site yapılandırma sorunları bulundu Lütfen %s için yapılandırılmış siteleri denetleyin.",
+ "ProviderYandexDescription": "Yandex aramalarında siteyi bulmak için kullanılan tüm anahtar sözcükleri içe aktarır.",
+ "ProviderYandexNote": "Not: Yandex anahtar sözcük verilerini 5 güne kadar gecikmeli olarak yayınlar. İlk içe aktarımda anahtar sözcük verilerinin 100 güne kadar olan geçmişi de içe aktarılabilir.",
+ "ReAddAccountIfPermanentError": "Sorun düzelmiyorsa hesabı kaldırıp bağlantıyı yeniden kurmayı deneyin.",
+ "ReAuthenticateIfPermanentError": "Sorun düzelmiyorsa kimliği yeniden doğrulamayı ya da hesabı kaldırıp bağlantıyı yeniden kurmayı deneyin.",
+ "Reauthenticate": "Kimliği yeniden doğrula",
+ "RecentApiErrorsWarning": "Anahtar sözcük içe aktarma sorunları bulundu Lütfen şu yapılandırmaları denetleyin: %s Yapılandırmanız doğruysa ve sorun sürüyorsa, lütfen destek ekibiyle görüşün.",
+ "ReportShowMaximumValues": "Görüntülenen değerler bu aralıktaki en yüksek değerlerdir.",
+ "RequiredAccessTypes": "Şu erişim türleri gereklidir:",
+ "ResponseCode": "Yanıt kodu",
+ "RoundKeywordPosition": "Anahtar konumu yuvarlansın",
+ "SearchEngineKeywordsPerformance": "Arama motoru anahtar sözcük başarımı",
+ "SearchEnginesImported": "Arama motorları (içe aktarılmış anahtar sözcükler ile)",
+ "SearchEnginesOriginal": "Arama motorları (izlenen anahtar sözcükler ile)",
+ "SetUpOAuthClientConfig": "OAuth istemci yapılandırmanızı ayarlayın",
+ "SetupConfiguration": "Yapılandırma ayarları",
+ "SitemapsContainingUrl": "%s içeren site haritaları",
+ "StartOAuth": "OAuth işlemini başlat",
+ "URLPrefix": "Adres ön eki",
+ "URLPrefixProperty": "Adres ön eki mülkü",
+ "URLPrefixPropertyInfo": "Yalnızca iletişim kuralının da (http/https) bulunduğu belirtilen tam ön eki içeren adresleri kapsar. Mülkünüzün herhangi bir iletişim kuralı ya da alt etki alanı ile (http/https/www./m. gibi) eşleşmesini istiyorsanız bunun yerine bir etki alanı mülkü eklemeyi düşünebilirsiniz.",
+ "UnverifiedSites": "Doğrulanmamış siteler:",
+ "UploadOAuthClientConfig": "OAuth istemci yapılandırmanızı yükleyin",
+ "Uploading": "Yükleniyor...",
+ "UrlOfAccount": "Adres (hesap)",
+ "VideoKeywords": "Google üzerindeki görüntü anahtar sözcükleri",
+ "VideoKeywordsDocumentation": "Google görüntü arama sonuçlarında sitenize bağlantılar oluşturmak için kullanılan anahtar sözcükler.",
+ "VisitOAuthHowTo": "%3$s OAuth istemcinizin nasıl yapılandırılacağını öğrenmek için %1$sçevrimiçi rehberimize%2$s bakabilirsiniz.",
+ "WebKeywords": "Google üzerindeki site anahtar sözcükleri",
+ "WebKeywordsDocumentation": "Google site arama sonuçlarında sitenize bağlantılar oluşturmak için kullanılan anahtar sözcükler.",
+ "WebsiteSuccessfulConfigured": "Tebrikler! %1$s sitesi için anahtar sözcükleri içe aktarma yapılandırmasını tamamladınız. İlk arama sözcüklerinizin içe aktarılması ve Yönlendirenler > Arama anahtar sözcükleri bölümünde görüntülenmesi bir kaç gün alabilir. Anahtar sözcüklerin içe aktarılmasındaki gecikmeler ve sınırlamalar hakkında bilgi almak için %2$sSSS%3$s bölümüne bakabilirsiniz",
+ "WebsiteTypeUnsupported": "Seçilmiş ölçülebilir %1$s türü desteklenmediğinden yapılandırılamaz. Yalnızca 'site' türündeki ölçülebilirler desteklenir.",
+ "WebsiteTypeUnsupportedRollUp": "Not: Toplu rapor siteleri otomatik olarak alt sitelerindeki içe aktarılmış verileri derler",
+ "YandexConfigurationDescription": "Yandex Webmaster API kimlik doğrulaması için OAuth kullanır.",
+ "YandexConfigurationTitle": "Yandex Webmaster API üzerinden içe aktarmayı yapılandırın",
+ "YandexCrawlAppearedPages": "Aramada görüntülenen sayfalar",
+ "YandexCrawlAppearedPagesDesc": "Yandex arama dizinine yeni eklenmiş sayfalar",
+ "YandexCrawlCrawledPages": "Derlenmiş sayfalar",
+ "YandexCrawlCrawledPagesDesc": "Yandex crawler tarafından istekte bulunulan sayfa sayısı.",
+ "YandexCrawlErrors": "Diğer istek sorunları",
+ "YandexCrawlErrorsDesc": "Diğer sorunlar ile derlenmiş sayfalar",
+ "YandexCrawlHttpStatus2xx": "HTTP kodları 200-299",
+ "YandexCrawlHttpStatus2xxDesc": "2xx koduyla derlenmiş sayfalar",
+ "YandexCrawlHttpStatus3xx": "HTTP kodları 300-399 (Taşınmış sayfalar)",
+ "YandexCrawlHttpStatus3xxDesc": "3xx koduyla derlenmiş sayfalar",
+ "YandexCrawlHttpStatus4xx": "HTTP kodları 400-499 (İstek sorunları)",
+ "YandexCrawlHttpStatus4xxDesc": "4xx koduyla derlenmiş sayfalar",
+ "YandexCrawlHttpStatus5xx": "HTTP kodları 500-599 (İç sunucu sorunları)",
+ "YandexCrawlHttpStatus5xxDesc": "5xx koduyla derlenmiş sayfalar",
+ "YandexCrawlInIndex": "Dizindeki sayfa sayısı",
+ "YandexCrawlInIndexDesc": "Yandex arama dizinindeki toplam sayfa",
+ "YandexCrawlRemovedPages": "Aramadan kaldırılmış sayfalar",
+ "YandexCrawlRemovedPagesDesc": "Yandex arama dizininden kaldırılmış sayfalar",
+ "YandexCrawlingStats": "Yandex! için derleme özeti",
+ "YandexCrawlingStatsDocumentation": "Derleme özetinde bir sayfa ziyaret edildiğinde arama botu tarafından karşılaşılan sorunlar, robots.txt dosyanız tarafından engellenen ögeler ve dizine eklenmiş toplam sayfa sayısı gibi derleme ile ilgili bilgiler bulunur.",
+ "YandexFieldCallbackUri": "Geri dönüş adresi",
+ "YandexFieldUrlToAppSite": "Uygulama sitesinin adresi",
+ "YandexKeywords": "Yandex üzerindeki anahtar sözcükler",
+ "YandexKeywordsDocumentation": "Yandex arama sonuçlarında sitenize bağlantılar oluşturmak için kullanılan anahtar sözcükler.",
+ "YandexWebmasterApiUrl": "Yandex Webmaster Tools adresi",
+ "YandexWebmasterApiUrlDescription": "Bu sitenin Yandex Webmaster Tools üzerindeki adresini yazın"
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/uk.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/uk.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/uk.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/zh-cn.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/zh-cn.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/zh-cn.json
@@ -0,0 +1 @@
+{}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/zh-tw.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/zh-tw.json
new file mode 100644
index 0000000..4b6f47b
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/zh-tw.json
@@ -0,0 +1,178 @@
+{
+ "SearchEngineKeywordsPerformance": {
+ "AccountAddedBy": "由 %1$s<\/em> 新增於 %2$s<\/em>",
+ "AccountConnectionValidationError": "驗證帳戶連線時出現錯誤:",
+ "AccountDoesNotExist": "設置的帳戶 %1$s 已不存在",
+ "AccountNoAccess": "這個帳戶目前似乎沒有能存取的網站。",
+ "AccountRemovalConfirm": "你將要移除帳戶 %1$s。這將可能停止任何已連結網站的關鍵字匯入。確定要繼續?",
+ "ActivityAccountAdded": "為關鍵字提供商 %1$s 新增帳戶:%2$s",
+ "ActivityAccountRemoved": "為關鍵字提供商 %1$s 移除帳戶:%2$s",
+ "ActivityGoogleClientConfigChanged": "變更 Google 用戶端設定",
+ "AddAPIKey": "新增 API 金鑰",
+ "AddConfiguration": "新增設定",
+ "AdminMenuTitle": "搜尋關鍵字優化",
+ "APIKey": "API 金鑰",
+ "AvailableSites": "可匯入的網站:",
+ "Domain": "網域",
+ "DomainProperty": "網域資源",
+ "DomainPropertyInfo": "包含所有子網域 (m、www 等) 以及兩種通訊協定 (http、https)。",
+ "URLPrefix": "網址前置字元",
+ "URLPrefixProperty": "網址前置字元資源",
+ "URLPrefixPropertyInfo": "只包含具有指定前置字元 (包括 http\/https 通訊協定) 的網址。如果要讓您的資源涵蓋任何通訊協定或子網域 (http\/https\/www.\/m. 等),請考慮改為新增網域資源。",
+ "BingAccountError": "驗證 API 金鑰時發生錯誤:%1$s。如果你剛剛才在 Bing 網站管理員中產生這組 API 金鑰,請在 1~2 分鐘後再重試(Bing 網站管理員工具需要一點時間來啟用 API 金鑰)。",
+ "BingAccountOk": "API 金鑰成功完成檢查",
+ "BingConfigurationDescription": "Bing 網站管理員工具需要 API 金鑰來存取。你可以在這裡新增 API 金鑰來存取你的網站資料。",
+ "BingConfigurationTitle": "從 Bing 網站管理員工具匯入",
+ "BingCrawlBlockedByRobotsTxt": "Robots.txt 排除項目",
+ "BingCrawlBlockedByRobotsTxtDesc": "目前被你網站中 robots.txt 封鎖的網址。",
+ "BingCrawlConnectionTimeout": "連線逾時",
+ "BingCrawlConnectionTimeoutDesc": "這個數字為目前 Bing 因連線錯誤而無法存取你的網站的計數。這可能是暫時性的錯誤但你應該檢查你的伺服器紀錄檔來查看是否意外遺漏了請求。",
+ "BingCrawlCrawledPages": "已檢索網頁",
+ "BingCrawlCrawledPagesDesc": "Bing 爬蟲請求的網頁數量",
+ "BingCrawlDNSFailures": "DNS 失敗",
+ "BingCrawlDNSFailuresDesc": "這個項目列出最近機器人嘗試存取你的網頁並向 DNS 伺服器連線時所發生的錯誤。可能是你的伺服器出現問題,或有其他錯誤的設定防止了 DNS 路由,例如 TTL 設定成 0。",
+ "BingCrawlErrors": "Bing 檢索錯誤",
+ "BingCrawlErrorsDesc": "Bing 爬蟲遇到的錯誤數量",
+ "BingCrawlErrorsFromDateX": "這份報表顯示 Bing 最近的檢索錯誤。它不會提供任何歷史資料。最後更新於 %s",
+ "BingCrawlHttpStatus2xx": "HTTP 狀態碼 200-299",
+ "BingCrawlHttpStatus2xxDesc": "於伺服器提供網頁完成時所回傳的狀態碼",
+ "BingCrawlHttpStatus301": "HTTP 狀態碼 301(網頁已永久移動)",
+ "BingCrawlHttpStatus301Desc": "當內容已永久移動至其他位置(網址)時所回傳的狀態碼",
+ "BingCrawlHttpStatus302": "HTTP 狀態碼 302(網頁已暫時移動)",
+ "BingCrawlHttpStatus302Desc": "當內容暫時移動至其他位置(網址)時所回傳的狀態碼",
+ "BingCrawlHttpStatus4xx": "HTTP 狀態碼 400-499(要求錯誤)",
+ "BingCrawlHttpStatus4xxDesc": "當請求疑似發生錯誤,伺服器防止繼續處理時所回傳的狀態碼",
+ "BingCrawlHttpStatus5xx": "HTTP 狀態碼 500-599(內部伺服器錯誤)",
+ "BingCrawlHttpStatus5xxDesc": "當伺服器處理一個有效請求失敗時所回傳的狀態碼",
+ "BingCrawlImportantBlockedByRobotsTxt": "被 robots.txt 排除的重要網頁",
+ "BingCrawlInboundLink": "傳入連結總數",
+ "BingCrawlInboundLinkDesc": "傳入連結是 Bing 所察覺且指向你網站的網址。這些連結的來源是已指向你的內容且為你所擁有的外部網站。",
+ "BingCrawlingStats": "Bing 和 Yahoo! 的檢索總覽",
+ "BingCrawlingStatsDocumentation": "檢索總覽提供你查看檢索相關資訊,如搜尋引擎機器人訪問網頁或項目時遇到的錯誤,可能是被你的 robot.txt 檔案封鎖和網站可能被惡意程式碼感染。",
+ "BingCrawlMalwareInfected": "已感染惡意程式碼",
+ "BingCrawlMalwareInfectedDesc": "Bing 所發現受到惡意程式碼感染或有關聯的任何網頁會顯示於此群組。",
+ "BingCrawlPagesInIndex": "已索引總數",
+ "BingCrawlPagesInIndexDesc": "出現於 Bing 索引中的網頁計數",
+ "BingCrawlStatsOtherCodes": "其他 HTTP 狀態碼",
+ "BingCrawlStatsOtherCodesDesc": "集合其它未列出的狀態碼(如 1xx 或資訊狀態碼)。",
+ "BingKeywordImport": "Bing 關鍵字匯入",
+ "BingKeywords": "關鍵字(來自 Bing 和 Yahoo!)",
+ "BingKeywordsDocumentation": "使用於 Bing 或 Yahoo! 搜尋並於搜尋結果連結至你的網站的關鍵字。",
+ "BingKeywordsNoRangeReports": "Bing 和 Yahoo! 上的關鍵字只能在包含完整一周或一個月的自訂日期範圍內處理,因為它們不支援日報表。",
+ "BingKeywordsNotDaily": "Bing 和 Yahoo! 的關鍵字只適用於周報表。單日範圍內沒有關鍵字資料。",
+ "BingWebmasterApiUrl": "Bing 網站管理員工具網址",
+ "BingWebmasterApiUrlDescription": "提供 Bing 網站管理員工具中此網站的網址",
+ "Category": "分類",
+ "ChangeConfiguration": "變更設定",
+ "Clicks": "點擊",
+ "ClicksDocumentation": "每當有人透過搜尋結果頁面點擊連結進入你的網站時計算一次。",
+ "ClientConfigImported": "用戶端設定已成功匯入!",
+ "ClientConfigSaveError": "保存用戶端設定時發生錯誤。請檢查提供的設定是否有效並重試。",
+ "ClientId": "用戶端 ID",
+ "ClientSecret": "用戶端密碼",
+ "ConfigAvailableNoWebsiteConfigured": "整合已成功設定,但目前還沒有設定要匯入的網站。",
+ "ConfigRemovalConfirm": "你將要移除 %1$s 的設定。這將會停用該站匯入關鍵字。要繼續嗎?",
+ "Configuration": "設定",
+ "ConfigurationDescription": "這個外掛讓你可以直接將你的使用者在搜尋引擎中使用的關鍵字匯入 Matomo。",
+ "ConfigurationFile": "設定檔案",
+ "ConfigurationValid": "你的 OAuth 設定有效。",
+ "ConfiguredAccounts": "已設定帳戶",
+ "ConfiguredUrlNotAvailable": "設定的 URL 無法於這個帳戶使用",
+ "ConfigureMeasurableBelow": "點擊下方的按鈕來設置網站,也可以直接於網站設定中設置。",
+ "ConfigureMeasurables": "網站設置",
+ "ConnectAccount": "連結帳戶",
+ "ConnectFirstAccount": "連接你的第一個帳戶來開始。",
+ "ConnectGoogleAccounts": "連結 Google 帳戶",
+ "ContainingSitemaps": "包含 Sitemap",
+ "CrawlingErrors": "檢索錯誤",
+ "CrawlingStats": "檢索總覽",
+ "Ctr": "CTR",
+ "CtrDocumentation": "點擊率:顯示使用者於搜尋結果中看到你的網站連結後,最終點擊進入的比例。",
+ "CurrentlyConnectedAccounts": "目前已經連結了 %1$s 個帳戶。",
+ "EnabledSearchTypes": "收集的關鍵字類型",
+ "FetchImageKeyword": "收集圖片關鍵字",
+ "FetchImageKeywordDesc": "收集於 Google 圖片搜尋的關鍵字",
+ "FetchVideoKeyword": "收集影片關鍵字",
+ "FetchVideoKeywordDesc": "收集於 Google 影片搜尋的關鍵字",
+ "FetchWebKeyword": "收集網頁關鍵字",
+ "FetchWebKeywordDesc": "收集於 Google 網頁搜尋的關鍵字",
+ "FirstDetected": "首次偵測",
+ "GoogleAccountAccessTypeOfflineAccess": "離線存取<\/strong>為必要選項來在你為登入時也能匯入你的搜尋關鍵字。",
+ "GoogleAccountAccessTypeProfileInfo": "帳戶資料<\/strong>用來顯示目前已連結的帳戶名稱。",
+ "GoogleAccountAccessTypeSearchConsoleData": "Search Console 資料<\/strong>為必要選項來取得你的 Google 搜尋關鍵字。",
+ "GoogleAccountError": "驗證 OAuth 存取時發生錯誤:%1$s",
+ "GoogleAccountOk": "OAuth 存取成功完成檢查。",
+ "GoogleConfigurationDescription": "Google Search Console 使用 OAuth 來驗證和授權。",
+ "GoogleConfigurationTitle": "從 Google Search Console 匯入",
+ "GoogleKeywordImport": "Google 關鍵字匯入",
+ "GoogleSearchConsoleUrl": "Google Search Console 網址",
+ "GoogleSearchConsoleUrlDescription": "提供 Google Search Console 中此網站的網址",
+ "GoogleUploadOrPasteClientConfig": "請上傳你的 Google OAuth 用戶端設定,或將它貼至下方的文字框內。",
+ "HowToGetOAuthClientConfig": "如何取得你的 OAuth 用戶端設定",
+ "ImageKeywords": "來自 Google 的圖片關鍵字",
+ "ImageKeywordsDocumentation": "使用於 Google 圖片<\/b>搜尋並於搜尋結果連結至你的網站的關鍵字。",
+ "Impressions": "印象",
+ "ImpressionsDocumentation": "印象是每當你的網站出現於搜尋結果頁面中時計算一次。",
+ "IntegrationConfigured": "已成功完成整合設定",
+ "IntegrationNotConfigured": "尚未完成整合設定",
+ "KeywordsCombined": "綜合關鍵字",
+ "KeywordsCombinedDocumentation": "顯示所有於 Matomo 偵測到和從搜尋引擎匯入的全部關鍵字組合。這份報表只包含訪問數據。你可以切換至相關報表來取得詳細數據。",
+ "KeywordsCombinedImported": "綜合已匯入的關鍵字",
+ "KeywordsCombinedImportedDocumentation": "報表顯示所有設置的搜尋引擎所匯入的關鍵字。",
+ "KeywordsReferrers": "關鍵字(包含未定義)",
+ "KeywordStatistics": "搜尋關鍵字",
+ "KeywordTypeImage": "圖片",
+ "KeywordTypeVideo": "影片",
+ "KeywordTypeWeb": "網頁",
+ "LastCrawled": "最後檢索",
+ "LastDetected": "最後偵測",
+ "LastImport": "最後匯入",
+ "LatestAvailableDate": "最近可用的關鍵字資料日期為:%1$s",
+ "LinksToUrl": "連結至 %s",
+ "ManageAPIKeys": "管理 API 金鑰",
+ "MeasurableConfig": "已設置的網站",
+ "NoSegmentation": "報表不支援區隔。資料將以你的標準、未區隔資料顯示。",
+ "NotAvailable": "不啟用",
+ "NoWebsiteConfigured": "目前還沒有完成設定的網站。要為特定網站啟用匯入,請在此完成設定。",
+ "NoWebsiteConfiguredWarning": "%s 的匯入未設定完整。你必須設定一些網站來啟用匯入。",
+ "OAuthClientConfig": "OAuth 用戶端設定",
+ "OAuthError": "OAuth 過程發生錯誤。請重試並確定你允許請求的授權。",
+ "Platform": "平台",
+ "Position": "平均位置",
+ "PositionDocumentation": "你的網站於搜尋結果中出現的平均位置(針對這個關鍵字)。",
+ "ProviderBingDescription": "匯入所有常在 Bing<\/strong> 和 Yahoo!<\/strong> 搜尋中用來搜尋你的網站的關鍵字。",
+ "ProviderBingNote": "備註:<\/u>微軟於每周六提供當周的關鍵字資料。通常在 Bing 和 Yahoo 上搜尋的關鍵字會花幾天的時間才顯示於報表中,而且只會在查看周、月或年報表時才顯示。",
+ "ProviderGoogleDescription": "匯入所有常在 Google<\/strong> 搜尋中用來搜尋你的網站的關鍵字。報表中將會分別顯示不同搜尋類型(網頁、圖片和影片)的關鍵字。",
+ "ProviderListDescription": "當你於下方成功設定一個或多個搜尋引擎後,你可以選擇 Matomo 要將搜尋關鍵字匯入哪個網站。",
+ "ProviderXAccountWarning": "偵測到帳戶設定問題<\/strong> 請檢查 %s<\/strong> 的帳戶設定。",
+ "ProviderXSitesWarning": "偵測到網站設定問題<\/strong> 請檢查 %s<\/strong> 的網站設定。",
+ "ReAddAccountIfPermanentError": "如果這是常駐性的錯誤,請試著先將帳戶移除後重新連結。",
+ "RequiredAccessTypes": "這些授權類型為必要:",
+ "ResponseCode": "回應碼",
+ "RoundKeywordPosition": "將關鍵字位置四捨五入",
+ "SearchEngineKeywordsPerformance": "搜尋引擎關鍵字優化",
+ "SetupConfiguration": "開始設定",
+ "SitemapsContainingUrl": "Sitemaps 包含%s",
+ "KeywordsSubtableOriginal": "已追蹤關鍵字(包含未定義)",
+ "KeywordsSubtableImported": "已匯入關鍵字",
+ "AllReferrersOriginal": "參照連結(已追蹤的關鍵字)",
+ "AllReferrersImported": "參照連結(已匯入的關鍵字)",
+ "SearchEnginesOriginal": "搜尋引擎(已追蹤的關鍵字)",
+ "SearchEnginesImported": "搜尋引擎(已匯入的關鍵字)",
+ "StartOAuth": "開始 OAuth 驗證",
+ "UnverifiedSites": "未驗證網站:",
+ "UploadOAuthClientConfig": "上傳你的 OAuth 用戶端設定",
+ "UrlOfAccount": "網址(帳戶)",
+ "VideoKeywords": "來自 Google 的影片關鍵字",
+ "VideoKeywordsDocumentation": "使用於 Google 影片<\/b>搜尋並於搜尋結果連結至你的網站的關鍵字。",
+ "WebKeywords": "來自 Google 的網頁關鍵字",
+ "WebKeywordsDocumentation": "使用於 Google 網頁<\/b>搜尋並於搜尋結果連結至你的網站的關鍵字。",
+ "WebsiteSuccessfulConfigured": "恭喜! 你已經為網站 %1$s 成功設定關鍵字匯入。 你可能需要等待幾天的時間才能在收穫 > 搜尋引擎和關鍵字中看到關鍵字。你可以在我們的 %2$sFAQ%3$s 中查看更多關於關鍵字匯入延遲和限制的資訊。",
+ "WebsiteTypeUnsupported": "無法設定選擇的監測對象 %1$s,因為它包含了不支援的類型。只支援類型為「網站」的監測對象。",
+ "WebsiteTypeUnsupportedRollUp": "注意:彙整網站將會自動結合它們所有子網站所匯入的資料",
+ "YandexCrawlHttpStatus2xx": "HTTP 狀態碼 200-299",
+ "YandexCrawlHttpStatus4xx": "HTTP 狀態碼 400-499(要求錯誤)",
+ "YandexCrawlHttpStatus5xx": "HTTP 狀態碼 500-599(內部伺服器錯誤)",
+ "YandexCrawlInIndex": "已索引總數"
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/phpcs.xml b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/phpcs.xml
new file mode 100644
index 0000000..d769944
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/phpcs.xml
@@ -0,0 +1,41 @@
+
+
+
+ Matomo Coding Standard for SearchEngineKeywordsPerformance plugin
+
+
+
+ .
+
+ tests/javascript/*
+ */vendor/*
+
+
+
+
+
+
+
+ tests/*
+
+
+
+
+ Updates/*
+
+
+
+
+ tests/*
+
+
+
+
+ tests/*
+
+
+
+
+ Monolog/Handler/SEKPSystemLogHandler.php
+
+
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/plugin.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/plugin.json
new file mode 100644
index 0000000..4956f80
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/plugin.json
@@ -0,0 +1,31 @@
+{
+ "name": "SearchEngineKeywordsPerformance",
+ "version": "5.0.22",
+ "description": "All keywords searched by your users on search engines are now visible into your Referrers reports! The ultimate solution to 'Keyword not defined'.",
+ "theme": false,
+ "require": {
+ "matomo": ">=5.0.0-rc5,<6.0.0-b1"
+ },
+ "authors": [
+ {
+ "name": "InnoCraft",
+ "email": "contact@innocraft.com",
+ "homepage": "https://www.innocraft.com"
+ }
+ ],
+ "price": {
+ "base": 154
+ },
+ "homepage": "https://plugins.matomo.org/SearchEngineKeywordsPerformance",
+ "license": "InnoCraft EULA",
+ "keywords": [
+ "Keyword",
+ "Crawling",
+ "Search",
+ "Google",
+ "Bing",
+ "Yahoo",
+ "Yandex",
+ "SEO"
+ ]
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/pull_request_template.md b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/pull_request_template.md
new file mode 100644
index 0000000..e7d9cf5
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/pull_request_template.md
@@ -0,0 +1,26 @@
+## Description
+
+
+## Issue No
+
+
+## Steps to Replicate the Issue
+1.
+2.
+3.
+
+
+
+## Checklist
+- [✔/✖] Tested locally or on demo2/demo3?
+- [✔/✖/NA] New test case added/updated?
+- [✔/✖/NA] Are all newly added texts included via translation?
+- [✔/✖/NA] Are text sanitized properly? (Eg use of v-text v/s v-html for vue)
+- [✔/✖/NA] Version bumped?
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/scoper.inc.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/scoper.inc.php
new file mode 100644
index 0000000..f79ced1
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/scoper.inc.php
@@ -0,0 +1,141 @@
+files()
+ ->in(__DIR__)
+ ->exclude('vendor')
+ ->exclude('node_modules')
+ ->exclude('lang')
+ ->exclude('javascripts')
+ ->exclude('vue')
+ ->notName(['scoper.inc.php', 'Controller.php'])
+ ->filter(function (\SplFileInfo $file) {
+ return !($file->isLink() && $file->isDir());
+ })
+ ->filter(function (\SplFileInfo $file) {
+ return !($file->isLink() && !$file->getRealPath());
+ }),
+ ];
+} else {
+ $finders = array_map(function ($dependency) {
+ return Finder::create()
+ ->files()
+ ->in($dependency);
+ }, $dependenciesToPrefix);
+}
+
+$namespacesToIncludeRegexes = array_map(function ($n) {
+ $n = rtrim($n, '\\');
+ return '/^' . preg_quote($n) . '(?:\\\\|$)/';
+}, $namespacesToPrefix);
+
+return [
+ 'expose-global-constants' => false,
+ 'expose-global-classes' => false,
+ 'expose-global-functions' => false,
+ 'force-no-global-alias' => $forceNoGlobalAlias,
+ 'prefix' => 'Matomo\\Dependencies\\' . $pluginName,
+ 'finders' => $finders,
+ 'patchers' => [
+ // patcher for files that class_alias new namespaced classes with old un-namespaced classes
+ static function (string $filePath, string $prefix, string $content) use ($isRenamingReferences): string {
+ if ($isRenamingReferences) {
+ return $content;
+ }
+
+ if ($filePath === __DIR__ . '/vendor/google/apiclient/src/Client.php') {
+ $content = str_replace(
+ [
+ 'Monolog\Handler\StreamHandler',
+ 'Monolog\Handler\SyslogHandler', 'Monolog\Logger'
+ ],
+ [
+ '\Piwik\Plugins\Monolog\Handler\FileHandler',
+ '\Piwik\Plugins\SearchEngineKeywordsPerformance\Monolog\Handler\SEKPSystemLogHandler',
+ '\Piwik\Log\Logger'
+ ],
+ $content
+ );
+ }
+
+ if (
+ $filePath === __DIR__ . '/vendor/google/apiclient/src/aliases.php'
+ || $filePath === __DIR__ . '/vendor/google/apiclient-services/autoload.php'
+ ) {
+ $content = preg_replace_callback('/([\'"])Google_/', function ($matches) {
+ return $matches[1] . 'Matomo\\\\Dependencies\\\\SearchEngineKeywordsPerformance\\\\Google_';
+ }, $content);
+ }
+
+ if ($filePath === __DIR__ . '/vendor/google/apiclient/src/aliases.php') {
+ $content = preg_replace('/class Google_Task_Composer.*?}/', "if (!class_exists('Google_Task_Composer')) {\n$1\n}", $content);
+ }
+
+ if ($filePath === __DIR__ . '/vendor/google/apiclient-services/autoload.php') {
+ // there is a core autoloader that will replace 'Matomo' in Matomo\Dependencies\... to Piwik\ if the
+ // Matomo\... class cannot be found.
+ //
+ // normally this wouldn't be an issue, but in the importer we will be unserializing classes that
+ // haven't been autoloaded, and some of those classes are handled by a special autoloader in one
+ // of google's libraries. this autoloader is called after the renaming autoloader changes the name to
+ // Piwik\Dependencies\..., so we need to be able to recognize both Matomo\ and Piwik\ there, or the
+ // target php file won't be loaded properly.
+ $replace = << $namespacesToIncludeRegexes,
+ 'exclude-namespaces' => $namespacesToExclude,
+ 'exclude-constants' => [
+ 'PIWIK_TEST_MODE',
+ '/^self::/', // work around php-scoper bug
+ ],
+ 'exclude-functions' => ['Piwik_ShouldPrintBackTraceWithMessage'],
+];
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/stylesheets/styles.less b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/stylesheets/styles.less
new file mode 100644
index 0000000..b94e333
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/stylesheets/styles.less
@@ -0,0 +1,262 @@
+.keywordproviders, .accounts {
+ margin-top: 15px;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.keywordprovider {
+ width: 300px;
+ float: left;
+ padding: 20px;
+ border: 1px solid #D73F36;
+ margin-right: 20px;
+ position: relative;
+ text-align: justify;
+ padding-bottom: 70px;
+
+ .logo {
+ height: 200px;
+ line-height: 200px;
+ text-align: center;
+
+ img {
+ text-align: center;
+ max-height: 200px;
+ max-width: 100%;
+ }
+ }
+
+ .logo.double {
+ height: 100px;
+ line-height: 100px;
+
+ img {
+ max-height: 100px;
+ }
+ }
+
+ h3 {
+ margin-top: 15px;
+ }
+
+ .cta {
+ text-align: center;
+ bottom: 20px;
+ position: absolute;
+ width: inherit;
+ margin-left: -20px;
+ }
+
+ &.configured {
+
+ border-color: #43a047;
+
+ .cta button {
+ background-color: #43a047;
+ }
+ }
+
+ &.warning {
+
+ border-color: #DF9D27;
+
+ .cta button {
+ background-color: #DF9D27;
+ }
+ }
+}
+
+#content .keywordprovider .experimental {
+ right: 15px;
+ position: absolute;
+ top: 130px;
+ transform: rotate(-30deg);
+ color: red;
+ font-weight: bolder;
+ font-size: 2em;
+ text-shadow: -2px 0 #fff, 0 2px #fff, 2px 0 #fff, 0 -2px #fff;
+}
+
+.websiteconfiguration, .clientconfiguration, .oauthconfiguration, .accountconfiguration {
+
+ > .card {
+ border: 2px solid #D73F36;
+ }
+
+ &.configured {
+ > .card {
+ border: inherit;
+ }
+ }
+
+ .websites-list {
+ max-height: 175px;
+ overflow-y: auto;
+ }
+}
+
+.websiteconfiguration {
+ .property-type {
+ font-style: italic;
+ line-height: 25px;
+ color: grey;
+ }
+ .icon-info {
+ position: relative;
+ top: 2px;
+ }
+}
+
+.account {
+ width: 300px;
+ float: left;
+ padding: 20px;
+ border: 1px solid #43a047;
+ margin-right: 20px;
+ position: relative;
+ text-align: justify;
+ padding-bottom: 70px;
+
+ .yandex & {
+ padding-bottom: 110px;
+ }
+
+ h3 {
+ text-align: center;
+ }
+
+ ul {
+ margin-bottom: 15px;
+ }
+
+ &.add {
+ border: 1px solid #838383;
+ }
+
+ &.invalid {
+ border: 1px solid #D73F36;
+
+ .logo.icon-add {
+ color: #D73F36
+ }
+ }
+
+ .logo {
+ height: 100px;
+ line-height: 100px;
+ text-align: center;
+
+ &.icon-warning {
+ font-size: 50px;
+ color: #D73F36;
+ }
+
+ &.icon-add {
+ font-size: 50px;
+ color: #838383;
+ }
+
+ &.icon-success {
+ font-size: 50px;
+ color: #43a047;
+ }
+
+ img {
+ text-align: center;
+ max-height: 100px;
+ max-width: 100%;
+ }
+ }
+
+ .cta {
+ text-align: center;
+ bottom: 20px;
+ position: absolute;
+ width: 100%;
+ margin-left: -20px;
+
+ form+form {
+ padding-top: 10px;
+ }
+ }
+
+ .accounterror {
+ color: #D73F36!important;
+ font-weight: bolder;
+
+ .icon-warning {
+ display: block;
+ float: left;
+ font-size: 1.5em;
+ line-height: 2em;
+ padding-right: 5px;
+ }
+ }
+}
+
+.configureMeasurableForm {
+ .form-group {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ input[type=submit] {
+ margin-top: 20px;
+ }
+
+ .col {
+ padding: 0;
+ }
+
+ label {
+ left: 0;
+ }
+}
+
+.account-select .dropdown-content {
+ width: 250px!important;
+ word-break: break-all;
+}
+
+table.measurableList {
+
+ tr.error {
+ border: 2px solid #D73F36;
+ }
+
+ .icon-error {
+ color: #D73F36;
+ }
+
+ button.icon-delete {
+ line-height: inherit;
+ height: inherit;
+ }
+}
+
+#googleurlinfo {
+ h2 {
+ word-break: break-all;
+ }
+
+ ul, li {
+ list-style: disc outside;
+ padding-top: 6px;
+ margin-left: 10px;
+ word-break: break-all;
+ }
+}
+
+// hides column values for imported rows on some reports
+tr[data-row-metadata*='"imported":true,'] {
+ td.column {
+ .value, .ratio {
+ visibility: hidden;
+ }
+ }
+ td.label, td.label + td.column {
+ .value, &.highlight .ratio {
+ visibility: visible;
+ }
+ }
+}
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/bing/configuration.twig b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/bing/configuration.twig
new file mode 100644
index 0000000..8caaf50
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/bing/configuration.twig
@@ -0,0 +1,24 @@
+{% extends 'admin.twig' %}
+
+{% set title %}{{ 'SearchEngineKeywordsPerformance_SearchEngineKeywordsPerformance'|translate }}{% endset %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/google/configuration.twig b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/google/configuration.twig
new file mode 100644
index 0000000..507f7bc
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/google/configuration.twig
@@ -0,0 +1,32 @@
+{% extends 'admin.twig' %}
+
+{% set title %}{{ 'SearchEngineKeywordsPerformance_SearchEngineKeywordsPerformance'|translate }}{% endset %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/index.twig b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/index.twig
new file mode 100644
index 0000000..eb185cb
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/index.twig
@@ -0,0 +1,12 @@
+{% extends 'admin.twig' %}
+
+{% set title %}{{ 'SearchEngineKeywordsPerformance_SearchEngineKeywordsPerformance'|translate }}{% endset %}
+
+{% block content %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/messageReferrerKeywordsReport.twig b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/messageReferrerKeywordsReport.twig
new file mode 100644
index 0000000..66df0eb
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/messageReferrerKeywordsReport.twig
@@ -0,0 +1,10 @@
+
+ {% if not hasAdminPriviliges %}
+ Did you know?
+ An Admin or Super User can Configure Search Performance to import all your search keywords into Matomo.
+ {% else %}
+ Did you know?
+ You can Configure
+ Search Performance to import all your search keywords into Matomo.
+ {% endif %}
+
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/yandex/configuration.twig b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/yandex/configuration.twig
new file mode 100644
index 0000000..52d6f35
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/yandex/configuration.twig
@@ -0,0 +1,31 @@
+{% extends 'admin.twig' %}
+
+{% set title %}{{ 'SearchEngineKeywordsPerformance_SearchEngineKeywordsPerformance'|translate }}{% endset %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/autoload.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/autoload.php
new file mode 100644
index 0000000..717d21e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/autoload.php
@@ -0,0 +1,10 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier
+ * @author Jordi Boggiano
+ * @see https://www.php-fig.org/psr/psr-0/
+ * @see https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ /** @var ?string */
+ private $vendorDir;
+
+ // PSR-4
+ /**
+ * @var array[]
+ * @psalm-var array>
+ */
+ private $prefixLengthsPsr4 = array();
+ /**
+ * @var array[]
+ * @psalm-var array>
+ */
+ private $prefixDirsPsr4 = array();
+ /**
+ * @var array[]
+ * @psalm-var array
+ */
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ /**
+ * @var array[]
+ * @psalm-var array>
+ */
+ private $prefixesPsr0 = array();
+ /**
+ * @var array[]
+ * @psalm-var array
+ */
+ private $fallbackDirsPsr0 = array();
+
+ /** @var bool */
+ private $useIncludePath = false;
+
+ /**
+ * @var string[]
+ * @psalm-var array
+ */
+ private $classMap = array();
+
+ /** @var bool */
+ private $classMapAuthoritative = false;
+
+ /**
+ * @var bool[]
+ * @psalm-var array
+ */
+ private $missingClasses = array();
+
+ /** @var ?string */
+ private $apcuPrefix;
+
+ /**
+ * @var self[]
+ */
+ private static $registeredLoaders = array();
+
+ /**
+ * @param ?string $vendorDir
+ */
+ public function __construct($vendorDir = null)
+ {
+ $this->vendorDir = $vendorDir;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+ }
+
+ return array();
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return array>
+ */
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return array
+ */
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return array
+ */
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ /**
+ * @return string[] Array of classname => path
+ * @psalm-return array
+ */
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param string[] $classMap Class to filename map
+ * @psalm-param array $classMap
+ *
+ * @return void
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param string[]|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @return void
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ (array) $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param string[]|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ (array) $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param string[]|string $paths The PSR-0 base directories
+ *
+ * @return void
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param string[]|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ *
+ * @return void
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ *
+ * @return void
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ *
+ * @return void
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ *
+ * @return void
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+ if (null === $this->vendorDir) {
+ return;
+ }
+
+ if ($prepend) {
+ self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+ } else {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ self::$registeredLoaders[$this->vendorDir] = $this;
+ }
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ *
+ * @return void
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+
+ if (null !== $this->vendorDir) {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ }
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return true|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ includeFile($file);
+
+ return true;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Returns the currently registered loaders indexed by their corresponding vendor directories.
+ *
+ * @return self[]
+ */
+ public static function getRegisteredLoaders()
+ {
+ return self::$registeredLoaders;
+ }
+
+ /**
+ * @param string $class
+ * @param string $ext
+ * @return string|false
+ */
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath . '\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ * @private
+ */
+function includeFile($file)
+{
+ include $file;
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/InstalledVersions.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/InstalledVersions.php
new file mode 100644
index 0000000..c6b54af
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/InstalledVersions.php
@@ -0,0 +1,352 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+ /**
+ * @var mixed[]|null
+ * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null
+ */
+ private static $installed;
+
+ /**
+ * @var bool|null
+ */
+ private static $canGetVendors;
+
+ /**
+ * @var array[]
+ * @psalm-var array}>
+ */
+ private static $installedByVendor = array();
+
+ /**
+ * Returns a list of all package names which are present, either by being installed, replaced or provided
+ *
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackages()
+ {
+ $packages = array();
+ foreach (self::getInstalled() as $installed) {
+ $packages[] = array_keys($installed['versions']);
+ }
+
+ if (1 === \count($packages)) {
+ return $packages[0];
+ }
+
+ return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+ }
+
+ /**
+ * Returns a list of all package names with a specific type e.g. 'library'
+ *
+ * @param string $type
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackagesByType($type)
+ {
+ $packagesByType = array();
+
+ foreach (self::getInstalled() as $installed) {
+ foreach ($installed['versions'] as $name => $package) {
+ if (isset($package['type']) && $package['type'] === $type) {
+ $packagesByType[] = $name;
+ }
+ }
+ }
+
+ return $packagesByType;
+ }
+
+ /**
+ * Checks whether the given package is installed
+ *
+ * This also returns true if the package name is provided or replaced by another package
+ *
+ * @param string $packageName
+ * @param bool $includeDevRequirements
+ * @return bool
+ */
+ public static function isInstalled($packageName, $includeDevRequirements = true)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (isset($installed['versions'][$packageName])) {
+ return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the given package satisfies a version constraint
+ *
+ * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+ *
+ * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+ *
+ * @param VersionParser $parser Install composer/semver to have access to this class and functionality
+ * @param string $packageName
+ * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+ * @return bool
+ */
+ public static function satisfies(VersionParser $parser, $packageName, $constraint)
+ {
+ $constraint = $parser->parseConstraints($constraint);
+ $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+ return $provided->matches($constraint);
+ }
+
+ /**
+ * Returns a version constraint representing all the range(s) which are installed for a given package
+ *
+ * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+ * whether a given version of a package is installed, and not just whether it exists
+ *
+ * @param string $packageName
+ * @return string Version constraint usable with composer/semver
+ */
+ public static function getVersionRanges($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ $ranges = array();
+ if (isset($installed['versions'][$packageName]['pretty_version'])) {
+ $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+ }
+ if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+ }
+ if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+ }
+ if (array_key_exists('provided', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+ }
+
+ return implode(' || ', $ranges);
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getPrettyVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['pretty_version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+ */
+ public static function getReference($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['reference'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['reference'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+ */
+ public static function getInstallPath($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @return array
+ * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+ */
+ public static function getRootPackage()
+ {
+ $installed = self::getInstalled();
+
+ return $installed[0]['root'];
+ }
+
+ /**
+ * Returns the raw installed.php data for custom implementations
+ *
+ * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+ * @return array[]
+ * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}
+ */
+ public static function getRawData()
+ {
+ @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ self::$installed = include __DIR__ . '/installed.php';
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ return self::$installed;
+ }
+
+ /**
+ * Returns the raw data of all installed.php which are currently loaded for custom implementations
+ *
+ * @return array[]
+ * @psalm-return list}>
+ */
+ public static function getAllRawData()
+ {
+ return self::getInstalled();
+ }
+
+ /**
+ * Lets you reload the static array from another file
+ *
+ * This is only useful for complex integrations in which a project needs to use
+ * this class but then also needs to execute another project's autoloader in process,
+ * and wants to ensure both projects have access to their version of installed.php.
+ *
+ * A typical case would be PHPUnit, where it would need to make sure it reads all
+ * the data it needs from this class, then call reload() with
+ * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+ * the project in which it runs can then also use this class safely, without
+ * interference between PHPUnit's dependencies and the project's dependencies.
+ *
+ * @param array[] $data A vendor/composer/installed.php data set
+ * @return void
+ *
+ * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data
+ */
+ public static function reload($data)
+ {
+ self::$installed = $data;
+ self::$installedByVendor = array();
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return list}>
+ */
+ private static function getInstalled()
+ {
+ if (null === self::$canGetVendors) {
+ self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+ }
+
+ $installed = array();
+
+ if (self::$canGetVendors) {
+ foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ if (isset(self::$installedByVendor[$vendorDir])) {
+ $installed[] = self::$installedByVendor[$vendorDir];
+ } elseif (is_file($vendorDir.'/composer/installed.php')) {
+ $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
+ if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
+ self::$installed = $installed[count($installed) - 1];
+ }
+ }
+ }
+ }
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ self::$installed = require __DIR__ . '/installed.php';
+ } else {
+ self::$installed = array();
+ }
+ }
+ $installed[] = self::$installed;
+
+ return $installed;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/LICENSE b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/LICENSE
new file mode 100644
index 0000000..f27399a
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_classmap.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000..0fb0a2c
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_classmap.php
@@ -0,0 +1,10 @@
+ $vendorDir . '/composer/InstalledVersions.php',
+);
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_files.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_files.php
new file mode 100644
index 0000000..5333040
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_files.php
@@ -0,0 +1,16 @@
+ $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
+ 'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
+ '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
+ '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
+ '1f87db08236948d07391152dccb70f04' => $vendorDir . '/google/apiclient-services/autoload.php',
+ 'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
+ 'a8d3953fd9959404dd22d3dfcd0a79f0' => $vendorDir . '/google/apiclient/src/aliases.php',
+);
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_namespaces.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000..15a2ff3
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+ array($vendorDir . '/phpseclib/phpseclib/phpseclib'),
+ 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
+ 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
+ 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
+ 'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
+ 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
+ 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
+ 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
+ 'Google\\Service\\' => array($vendorDir . '/google/apiclient-services/src'),
+ 'Google\\Auth\\' => array($vendorDir . '/google/auth/src'),
+ 'Google\\' => array($vendorDir . '/google/apiclient/src'),
+ 'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
+);
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_real.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_real.php
new file mode 100644
index 0000000..2691ae8
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_real.php
@@ -0,0 +1,55 @@
+register(true);
+
+ $includeFiles = \Composer\Autoload\ComposerStaticInita45dbc566c3f138ca6114173227009ed::$files;
+ foreach ($includeFiles as $fileIdentifier => $file) {
+ composerRequirea45dbc566c3f138ca6114173227009ed($fileIdentifier, $file);
+ }
+
+ return $loader;
+ }
+}
+
+/**
+ * @param string $fileIdentifier
+ * @param string $file
+ * @return void
+ */
+function composerRequirea45dbc566c3f138ca6114173227009ed($fileIdentifier, $file)
+{
+ if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+ $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+ require $file;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_static.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_static.php
new file mode 100644
index 0000000..2bb076a
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_static.php
@@ -0,0 +1,105 @@
+
+ array (
+ 'phpseclib3\\' => 11,
+ ),
+ 'P' =>
+ array (
+ 'Psr\\Http\\Message\\' => 17,
+ 'Psr\\Http\\Client\\' => 16,
+ 'Psr\\Cache\\' => 10,
+ 'ParagonIE\\ConstantTime\\' => 23,
+ ),
+ 'G' =>
+ array (
+ 'GuzzleHttp\\Psr7\\' => 16,
+ 'GuzzleHttp\\Promise\\' => 19,
+ 'GuzzleHttp\\' => 11,
+ 'Google\\Service\\' => 15,
+ 'Google\\Auth\\' => 12,
+ 'Google\\' => 7,
+ ),
+ 'F' =>
+ array (
+ 'Firebase\\JWT\\' => 13,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'phpseclib3\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib',
+ ),
+ 'Psr\\Http\\Message\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/psr/http-factory/src',
+ 1 => __DIR__ . '/..' . '/psr/http-message/src',
+ ),
+ 'Psr\\Http\\Client\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/psr/http-client/src',
+ ),
+ 'Psr\\Cache\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/psr/cache/src',
+ ),
+ 'ParagonIE\\ConstantTime\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src',
+ ),
+ 'GuzzleHttp\\Psr7\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
+ ),
+ 'GuzzleHttp\\Promise\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
+ ),
+ 'GuzzleHttp\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
+ ),
+ 'Google\\Service\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/google/apiclient-services/src',
+ ),
+ 'Google\\Auth\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/google/auth/src',
+ ),
+ 'Google\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/google/apiclient/src',
+ ),
+ 'Firebase\\JWT\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/firebase/php-jwt/src',
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInita45dbc566c3f138ca6114173227009ed::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInita45dbc566c3f138ca6114173227009ed::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInita45dbc566c3f138ca6114173227009ed::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/installed.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/installed.json
new file mode 100644
index 0000000..ee778d0
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/installed.json
@@ -0,0 +1,1156 @@
+{
+ "packages": [
+ {
+ "name": "firebase/php-jwt",
+ "version": "v6.10.0",
+ "version_normalized": "6.10.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "a49db6f0a5033aef5143295342f1c95521b075ff"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff",
+ "reference": "a49db6f0a5033aef5143295342f1c95521b075ff",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4||^8.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^6.5||^7.4",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psr/cache": "^1.0||^2.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "suggest": {
+ "ext-sodium": "Support EdDSA (Ed25519) signatures",
+ "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
+ },
+ "time": "2023-12-01T16:26:39+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "keywords": [
+ "jwt",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/firebase/php-jwt/issues",
+ "source": "https://github.com/firebase/php-jwt/tree/v6.10.0"
+ },
+ "install-path": "../firebase/php-jwt"
+ },
+ {
+ "name": "google/apiclient",
+ "version": "v2.15.3",
+ "version_normalized": "2.15.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/googleapis/google-api-php-client.git",
+ "reference": "e70273c06d18824de77e114247ae3102f8aec64d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/e70273c06d18824de77e114247ae3102f8aec64d",
+ "reference": "e70273c06d18824de77e114247ae3102f8aec64d",
+ "shasum": ""
+ },
+ "require": {
+ "firebase/php-jwt": "~6.0",
+ "google/apiclient-services": "~0.200",
+ "google/auth": "^1.33",
+ "guzzlehttp/guzzle": "^6.5.8||^7.4.5",
+ "guzzlehttp/psr7": "^1.8.4||^2.2.1",
+ "monolog/monolog": "^2.9||^3.0",
+ "php": "^7.4|^8.0",
+ "phpseclib/phpseclib": "^3.0.34"
+ },
+ "require-dev": {
+ "cache/filesystem-adapter": "^1.1",
+ "composer/composer": "^1.10.22",
+ "phpcompatibility/php-compatibility": "^9.2",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "squizlabs/php_codesniffer": "^3.8",
+ "symfony/css-selector": "~2.1",
+ "symfony/dom-crawler": "~2.1"
+ },
+ "suggest": {
+ "cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)"
+ },
+ "time": "2024-01-04T19:15:22+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "src/aliases.php"
+ ],
+ "psr-4": {
+ "Google\\": "src/"
+ },
+ "classmap": [
+ "src/aliases.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "description": "Client library for Google APIs",
+ "homepage": "http://developers.google.com/api-client-library/php",
+ "keywords": [
+ "google"
+ ],
+ "support": {
+ "issues": "https://github.com/googleapis/google-api-php-client/issues",
+ "source": "https://github.com/googleapis/google-api-php-client/tree/v2.15.3"
+ },
+ "install-path": "../google/apiclient"
+ },
+ {
+ "name": "google/apiclient-services",
+ "version": "v0.224.1",
+ "version_normalized": "0.224.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/googleapis/google-api-php-client-services.git",
+ "reference": "06e515176ebf32c3dcf7c01b3f377af6bfca6ae3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/06e515176ebf32c3dcf7c01b3f377af6bfca6ae3",
+ "reference": "06e515176ebf32c3dcf7c01b3f377af6bfca6ae3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7||^8.5.13"
+ },
+ "time": "2021-12-05T12:26:30+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "autoload.php"
+ ],
+ "psr-4": {
+ "Google\\Service\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "description": "Client library for Google APIs",
+ "homepage": "http://developers.google.com/api-client-library/php",
+ "keywords": [
+ "google"
+ ],
+ "support": {
+ "issues": "https://github.com/googleapis/google-api-php-client-services/issues",
+ "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.224.1"
+ },
+ "install-path": "../google/apiclient-services"
+ },
+ {
+ "name": "google/auth",
+ "version": "v1.34.0",
+ "version_normalized": "1.34.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/googleapis/google-auth-library-php.git",
+ "reference": "155daeadfd2f09743f611ea493b828d382519575"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/155daeadfd2f09743f611ea493b828d382519575",
+ "reference": "155daeadfd2f09743f611ea493b828d382519575",
+ "shasum": ""
+ },
+ "require": {
+ "firebase/php-jwt": "^6.0",
+ "guzzlehttp/guzzle": "^6.2.1|^7.0",
+ "guzzlehttp/psr7": "^2.4.5",
+ "php": "^7.4||^8.0",
+ "psr/cache": "^1.0||^2.0||^3.0",
+ "psr/http-message": "^1.1||^2.0"
+ },
+ "require-dev": {
+ "guzzlehttp/promises": "^2.0",
+ "kelvinmo/simplejwt": "0.7.1",
+ "phpseclib/phpseclib": "^3.0",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.0.0",
+ "sebastian/comparator": ">=1.2.3",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "suggest": {
+ "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
+ },
+ "time": "2024-01-03T20:45:15+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Google\\Auth\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "description": "Google Auth Library for PHP",
+ "homepage": "http://github.com/google/google-auth-library-php",
+ "keywords": [
+ "Authentication",
+ "google",
+ "oauth2"
+ ],
+ "support": {
+ "docs": "https://googleapis.github.io/google-auth-library-php/main/",
+ "issues": "https://github.com/googleapis/google-auth-library-php/issues",
+ "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.34.0"
+ },
+ "install-path": "../google/auth"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.8.1",
+ "version_normalized": "7.8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "41042bc7ab002487b876a0683fc8dce04ddce104"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104",
+ "reference": "41042bc7ab002487b876a0683fc8dce04ddce104",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
+ "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.36 || ^9.6.15",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "time": "2023-12-03T20:35:24+00:00",
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "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": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "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"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../guzzlehttp/guzzle"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "1.5.3",
+ "version_normalized": "1.5.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
+ "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^4.4 || ^5.1"
+ },
+ "time": "2023-05-21T12:31:43+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "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": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/1.5.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../guzzlehttp/promises"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.5.1",
+ "version_normalized": "2.5.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "a0b3a03e8e8005257fbc408ce5f0fd0a8274dc7f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/a0b3a03e8e8005257fbc408ce5f0fd0a8274dc7f",
+ "reference": "a0b3a03e8e8005257fbc408ce5f0fd0a8274dc7f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "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"
+ },
+ "time": "2023-08-03T15:02:42+00:00",
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "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",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.5.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../guzzlehttp/psr7"
+ },
+ {
+ "name": "paragonie/constant_time_encoding",
+ "version": "v2.6.3",
+ "version_normalized": "2.6.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/constant_time_encoding.git",
+ "reference": "58c3f47f650c94ec05a151692652a868995d2938"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938",
+ "reference": "58c3f47f650c94ec05a151692652a868995d2938",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7|^8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6|^7|^8|^9",
+ "vimeo/psalm": "^1|^2|^3|^4"
+ },
+ "time": "2022-06-14T06:56:20+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\ConstantTime\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com",
+ "role": "Maintainer"
+ },
+ {
+ "name": "Steve 'Sc00bz' Thomas",
+ "email": "steve@tobtu.com",
+ "homepage": "https://www.tobtu.com",
+ "role": "Original Developer"
+ }
+ ],
+ "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+ "keywords": [
+ "base16",
+ "base32",
+ "base32_decode",
+ "base32_encode",
+ "base64",
+ "base64_decode",
+ "base64_encode",
+ "bin2hex",
+ "encoding",
+ "hex",
+ "hex2bin",
+ "rfc4648"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/constant_time_encoding/issues",
+ "source": "https://github.com/paragonie/constant_time_encoding"
+ },
+ "install-path": "../paragonie/constant_time_encoding"
+ },
+ {
+ "name": "paragonie/random_compat",
+ "version": "v9.99.100",
+ "version_normalized": "9.99.100.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/random_compat.git",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">= 7"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*|5.*",
+ "vimeo/psalm": "^1"
+ },
+ "suggest": {
+ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+ },
+ "time": "2020-10-15T08:29:30+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+ "keywords": [
+ "csprng",
+ "polyfill",
+ "pseudorandom",
+ "random"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/random_compat/issues",
+ "source": "https://github.com/paragonie/random_compat"
+ },
+ "install-path": "../paragonie/random_compat"
+ },
+ {
+ "name": "phpseclib/phpseclib",
+ "version": "3.0.36",
+ "version_normalized": "3.0.36.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpseclib/phpseclib.git",
+ "reference": "c2fb5136162d4be18fdd4da9980696f3aee96d7b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/c2fb5136162d4be18fdd4da9980696f3aee96d7b",
+ "reference": "c2fb5136162d4be18fdd4da9980696f3aee96d7b",
+ "shasum": ""
+ },
+ "require": {
+ "paragonie/constant_time_encoding": "^1|^2",
+ "paragonie/random_compat": "^1.4|^2.0|^9.99.99",
+ "php": ">=5.6.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*"
+ },
+ "suggest": {
+ "ext-dom": "Install the DOM extension to load XML formatted public keys.",
+ "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
+ "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
+ "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
+ "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
+ },
+ "time": "2024-02-26T05:13:14+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "phpseclib/bootstrap.php"
+ ],
+ "psr-4": {
+ "phpseclib3\\": "phpseclib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jim Wigginton",
+ "email": "terrafrost@php.net",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Patrick Monnerat",
+ "email": "pm@datasphere.ch",
+ "role": "Developer"
+ },
+ {
+ "name": "Andreas Fischer",
+ "email": "bantu@phpbb.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Hans-Jürgen Petrich",
+ "email": "petrich@tronic-media.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Graham Campbell",
+ "email": "graham@alt-three.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
+ "homepage": "http://phpseclib.sourceforge.net",
+ "keywords": [
+ "BigInteger",
+ "aes",
+ "asn.1",
+ "asn1",
+ "blowfish",
+ "crypto",
+ "cryptography",
+ "encryption",
+ "rsa",
+ "security",
+ "sftp",
+ "signature",
+ "signing",
+ "ssh",
+ "twofish",
+ "x.509",
+ "x509"
+ ],
+ "support": {
+ "issues": "https://github.com/phpseclib/phpseclib/issues",
+ "source": "https://github.com/phpseclib/phpseclib/tree/3.0.36"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/terrafrost",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/phpseclib",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../phpseclib/phpseclib"
+ },
+ {
+ "name": "psr/cache",
+ "version": "1.0.1",
+ "version_normalized": "1.0.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+ "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "time": "2016-08-06T20:24:11+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/master"
+ },
+ "install-path": "../psr/cache"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "version_normalized": "1.0.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "time": "2023-09-23T14:17:50+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "install-path": "../psr/http-client"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.0.2",
+ "version_normalized": "1.0.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "e616d01114759c4c489f93b099585439f795fe35"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
+ "reference": "e616d01114759c4c489f93b099585439f795fe35",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "time": "2023-04-10T20:10:41+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+ },
+ "install-path": "../psr/http-factory"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "version_normalized": "2.0.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "install-path": "../psr/http-message"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "version_normalized": "3.0.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "time": "2019-03-08T08:55:37+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "install-path": "../ralouphie/getallheaders"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.4.0",
+ "version_normalized": "3.4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
+ "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "time": "2023-05-23T14:45:45+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.4-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "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": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.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"
+ }
+ ],
+ "install-path": "../symfony/deprecation-contracts"
+ }
+ ],
+ "dev": true,
+ "dev-package-names": []
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/installed.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/installed.php
new file mode 100644
index 0000000..749765b
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/installed.php
@@ -0,0 +1,197 @@
+ array(
+ 'name' => '__root__',
+ 'pretty_version' => 'dev-5.x-dev',
+ 'version' => 'dev-5.x-dev',
+ 'reference' => 'c8681aedeabf58cf88284235a697df3ca885ff99',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev' => true,
+ ),
+ 'versions' => array(
+ '__root__' => array(
+ 'pretty_version' => 'dev-5.x-dev',
+ 'version' => 'dev-5.x-dev',
+ 'reference' => 'c8681aedeabf58cf88284235a697df3ca885ff99',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'firebase/php-jwt' => array(
+ 'pretty_version' => 'v6.10.0',
+ 'version' => '6.10.0.0',
+ 'reference' => 'a49db6f0a5033aef5143295342f1c95521b075ff',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../firebase/php-jwt',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'google/apiclient' => array(
+ 'pretty_version' => 'v2.15.3',
+ 'version' => '2.15.3.0',
+ 'reference' => 'e70273c06d18824de77e114247ae3102f8aec64d',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../google/apiclient',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'google/apiclient-services' => array(
+ 'pretty_version' => 'v0.224.1',
+ 'version' => '0.224.1.0',
+ 'reference' => '06e515176ebf32c3dcf7c01b3f377af6bfca6ae3',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../google/apiclient-services',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'google/auth' => array(
+ 'pretty_version' => 'v1.34.0',
+ 'version' => '1.34.0.0',
+ 'reference' => '155daeadfd2f09743f611ea493b828d382519575',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../google/auth',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'guzzlehttp/guzzle' => array(
+ 'pretty_version' => '7.8.1',
+ 'version' => '7.8.1.0',
+ 'reference' => '41042bc7ab002487b876a0683fc8dce04ddce104',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'guzzlehttp/promises' => array(
+ 'pretty_version' => '1.5.3',
+ 'version' => '1.5.3.0',
+ 'reference' => '67ab6e18aaa14d753cc148911d273f6e6cb6721e',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../guzzlehttp/promises',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'guzzlehttp/psr7' => array(
+ 'pretty_version' => '2.5.1',
+ 'version' => '2.5.1.0',
+ 'reference' => 'a0b3a03e8e8005257fbc408ce5f0fd0a8274dc7f',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'monolog/monolog' => array(
+ 'dev_requirement' => false,
+ 'replaced' => array(
+ 0 => '*',
+ ),
+ ),
+ 'paragonie/constant_time_encoding' => array(
+ 'pretty_version' => 'v2.6.3',
+ 'version' => '2.6.3.0',
+ 'reference' => '58c3f47f650c94ec05a151692652a868995d2938',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../paragonie/constant_time_encoding',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'paragonie/random_compat' => array(
+ 'pretty_version' => 'v9.99.100',
+ 'version' => '9.99.100.0',
+ 'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../paragonie/random_compat',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'phpseclib/phpseclib' => array(
+ 'pretty_version' => '3.0.36',
+ 'version' => '3.0.36.0',
+ 'reference' => 'c2fb5136162d4be18fdd4da9980696f3aee96d7b',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../phpseclib/phpseclib',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'psr/cache' => array(
+ 'pretty_version' => '1.0.1',
+ 'version' => '1.0.1.0',
+ 'reference' => 'd11b50ad223250cf17b86e38383413f5a6764bf8',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../psr/cache',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'psr/http-client' => array(
+ 'pretty_version' => '1.0.3',
+ 'version' => '1.0.3.0',
+ 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../psr/http-client',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'psr/http-client-implementation' => array(
+ 'dev_requirement' => false,
+ 'provided' => array(
+ 0 => '1.0',
+ ),
+ ),
+ 'psr/http-factory' => array(
+ 'pretty_version' => '1.0.2',
+ 'version' => '1.0.2.0',
+ 'reference' => 'e616d01114759c4c489f93b099585439f795fe35',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../psr/http-factory',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'psr/http-factory-implementation' => array(
+ 'dev_requirement' => false,
+ 'provided' => array(
+ 0 => '1.0',
+ ),
+ ),
+ 'psr/http-message' => array(
+ 'pretty_version' => '2.0',
+ 'version' => '2.0.0.0',
+ 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../psr/http-message',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'psr/http-message-implementation' => array(
+ 'dev_requirement' => false,
+ 'provided' => array(
+ 0 => '1.0',
+ ),
+ ),
+ 'psr/log' => array(
+ 'dev_requirement' => false,
+ 'replaced' => array(
+ 0 => '*',
+ ),
+ ),
+ 'ralouphie/getallheaders' => array(
+ 'pretty_version' => '3.0.3',
+ 'version' => '3.0.3.0',
+ 'reference' => '120b605dfeb996808c31b6477290a714d356e822',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../ralouphie/getallheaders',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'symfony/deprecation-contracts' => array(
+ 'pretty_version' => 'v3.4.0',
+ 'version' => '3.4.0.0',
+ 'reference' => '7c3aff79d10325257a001fcf92d991f24fc967cf',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ ),
+);
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/LICENSE b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/LICENSE
new file mode 100644
index 0000000..11c0146
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2011, Neuman Vong
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * 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.
+
+ * Neither the name of the copyright holder nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+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 THE COPYRIGHT
+OWNER OR CONTRIBUTORS 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.
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/BeforeValidException.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/BeforeValidException.php
new file mode 100644
index 0000000..abe5695
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/BeforeValidException.php
@@ -0,0 +1,19 @@
+payload = $payload;
+ }
+ public function getPayload() : object
+ {
+ return $this->payload;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/CachedKeySet.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/CachedKeySet.php
new file mode 100644
index 0000000..0c088e6
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/CachedKeySet.php
@@ -0,0 +1,226 @@
+
+ */
+class CachedKeySet implements ArrayAccess
+{
+ /**
+ * @var string
+ */
+ private $jwksUri;
+ /**
+ * @var ClientInterface
+ */
+ private $httpClient;
+ /**
+ * @var RequestFactoryInterface
+ */
+ private $httpFactory;
+ /**
+ * @var CacheItemPoolInterface
+ */
+ private $cache;
+ /**
+ * @var ?int
+ */
+ private $expiresAfter;
+ /**
+ * @var ?CacheItemInterface
+ */
+ private $cacheItem;
+ /**
+ * @var array>
+ */
+ private $keySet;
+ /**
+ * @var string
+ */
+ private $cacheKey;
+ /**
+ * @var string
+ */
+ private $cacheKeyPrefix = 'jwks';
+ /**
+ * @var int
+ */
+ private $maxKeyLength = 64;
+ /**
+ * @var bool
+ */
+ private $rateLimit;
+ /**
+ * @var string
+ */
+ private $rateLimitCacheKey;
+ /**
+ * @var int
+ */
+ private $maxCallsPerMinute = 10;
+ /**
+ * @var string|null
+ */
+ private $defaultAlg;
+ public function __construct(string $jwksUri, ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, int $expiresAfter = null, bool $rateLimit = \false, string $defaultAlg = null)
+ {
+ $this->jwksUri = $jwksUri;
+ $this->httpClient = $httpClient;
+ $this->httpFactory = $httpFactory;
+ $this->cache = $cache;
+ $this->expiresAfter = $expiresAfter;
+ $this->rateLimit = $rateLimit;
+ $this->defaultAlg = $defaultAlg;
+ $this->setCacheKeys();
+ }
+ /**
+ * @param string $keyId
+ * @return Key
+ */
+ public function offsetGet($keyId) : Key
+ {
+ if (!$this->keyIdExists($keyId)) {
+ throw new OutOfBoundsException('Key ID not found');
+ }
+ return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
+ }
+ /**
+ * @param string $keyId
+ * @return bool
+ */
+ public function offsetExists($keyId) : bool
+ {
+ return $this->keyIdExists($keyId);
+ }
+ /**
+ * @param string $offset
+ * @param Key $value
+ */
+ public function offsetSet($offset, $value) : void
+ {
+ throw new LogicException('Method not implemented');
+ }
+ /**
+ * @param string $offset
+ */
+ public function offsetUnset($offset) : void
+ {
+ throw new LogicException('Method not implemented');
+ }
+ /**
+ * @return array
+ */
+ private function formatJwksForCache(string $jwks) : array
+ {
+ $jwks = json_decode($jwks, \true);
+ if (!isset($jwks['keys'])) {
+ throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
+ }
+ if (empty($jwks['keys'])) {
+ throw new InvalidArgumentException('JWK Set did not contain any keys');
+ }
+ $keys = [];
+ foreach ($jwks['keys'] as $k => $v) {
+ $kid = isset($v['kid']) ? $v['kid'] : $k;
+ $keys[(string) $kid] = $v;
+ }
+ return $keys;
+ }
+ private function keyIdExists(string $keyId) : bool
+ {
+ if (null === $this->keySet) {
+ $item = $this->getCacheItem();
+ // Try to load keys from cache
+ if ($item->isHit()) {
+ // item found! retrieve it
+ $this->keySet = $item->get();
+ // If the cached item is a string, the JWKS response was cached (previous behavior).
+ // Parse this into expected format array instead.
+ if (\is_string($this->keySet)) {
+ $this->keySet = $this->formatJwksForCache($this->keySet);
+ }
+ }
+ }
+ if (!isset($this->keySet[$keyId])) {
+ if ($this->rateLimitExceeded()) {
+ return \false;
+ }
+ $request = $this->httpFactory->createRequest('GET', $this->jwksUri);
+ $jwksResponse = $this->httpClient->sendRequest($request);
+ if ($jwksResponse->getStatusCode() !== 200) {
+ throw new UnexpectedValueException(sprintf('HTTP Error: %d %s for URI "%s"', $jwksResponse->getStatusCode(), $jwksResponse->getReasonPhrase(), $this->jwksUri), $jwksResponse->getStatusCode());
+ }
+ $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
+ if (!isset($this->keySet[$keyId])) {
+ return \false;
+ }
+ $item = $this->getCacheItem();
+ $item->set($this->keySet);
+ if ($this->expiresAfter) {
+ $item->expiresAfter($this->expiresAfter);
+ }
+ $this->cache->save($item);
+ }
+ return \true;
+ }
+ private function rateLimitExceeded() : bool
+ {
+ if (!$this->rateLimit) {
+ return \false;
+ }
+ $cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
+ if (!$cacheItem->isHit()) {
+ $cacheItem->expiresAfter(1);
+ // # of calls are cached each minute
+ }
+ $callsPerMinute = (int) $cacheItem->get();
+ if (++$callsPerMinute > $this->maxCallsPerMinute) {
+ return \true;
+ }
+ $cacheItem->set($callsPerMinute);
+ $this->cache->save($cacheItem);
+ return \false;
+ }
+ private function getCacheItem() : CacheItemInterface
+ {
+ if (\is_null($this->cacheItem)) {
+ $this->cacheItem = $this->cache->getItem($this->cacheKey);
+ }
+ return $this->cacheItem;
+ }
+ private function setCacheKeys() : void
+ {
+ if (empty($this->jwksUri)) {
+ throw new RuntimeException('JWKS URI is empty');
+ }
+ // ensure we do not have illegal characters
+ $key = preg_replace('|[^a-zA-Z0-9_\\.!]|', '', $this->jwksUri);
+ // add prefix
+ $key = $this->cacheKeyPrefix . $key;
+ // Hash keys if they exceed $maxKeyLength of 64
+ if (\strlen($key) > $this->maxKeyLength) {
+ $key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
+ }
+ $this->cacheKey = $key;
+ if ($this->rateLimit) {
+ // add prefix
+ $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
+ // Hash keys if they exceed $maxKeyLength of 64
+ if (\strlen($rateLimitKey) > $this->maxKeyLength) {
+ $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
+ }
+ $this->rateLimitCacheKey = $rateLimitKey;
+ }
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/ExpiredException.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/ExpiredException.php
new file mode 100644
index 0000000..3525011
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/ExpiredException.php
@@ -0,0 +1,19 @@
+payload = $payload;
+ }
+ public function getPayload() : object
+ {
+ return $this->payload;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWK.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWK.php
new file mode 100644
index 0000000..680053c
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWK.php
@@ -0,0 +1,267 @@
+
+ * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link https://github.com/firebase/php-jwt
+ */
+class JWK
+{
+ private const OID = '1.2.840.10045.2.1';
+ private const ASN1_OBJECT_IDENTIFIER = 0x6;
+ private const ASN1_SEQUENCE = 0x10;
+ // also defined in JWT
+ private const ASN1_BIT_STRING = 0x3;
+ private const EC_CURVES = [
+ 'P-256' => '1.2.840.10045.3.1.7',
+ // Len: 64
+ 'secp256k1' => '1.3.132.0.10',
+ // Len: 64
+ 'P-384' => '1.3.132.0.34',
+ ];
+ // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype.
+ // This library supports the following subtypes:
+ private const OKP_SUBTYPES = ['Ed25519' => \true];
+ /**
+ * Parse a set of JWK keys
+ *
+ * @param array $jwks The JSON Web Key Set as an associative array
+ * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
+ * JSON Web Key Set
+ *
+ * @return array An associative array of key IDs (kid) to Key objects
+ *
+ * @throws InvalidArgumentException Provided JWK Set is empty
+ * @throws UnexpectedValueException Provided JWK Set was invalid
+ * @throws DomainException OpenSSL failure
+ *
+ * @uses parseKey
+ */
+ public static function parseKeySet(array $jwks, string $defaultAlg = null) : array
+ {
+ $keys = [];
+ if (!isset($jwks['keys'])) {
+ throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
+ }
+ if (empty($jwks['keys'])) {
+ throw new InvalidArgumentException('JWK Set did not contain any keys');
+ }
+ foreach ($jwks['keys'] as $k => $v) {
+ $kid = isset($v['kid']) ? $v['kid'] : $k;
+ if ($key = self::parseKey($v, $defaultAlg)) {
+ $keys[(string) $kid] = $key;
+ }
+ }
+ if (0 === \count($keys)) {
+ throw new UnexpectedValueException('No supported algorithms found in JWK Set');
+ }
+ return $keys;
+ }
+ /**
+ * Parse a JWK key
+ *
+ * @param array $jwk An individual JWK
+ * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
+ * JSON Web Key Set
+ *
+ * @return Key The key object for the JWK
+ *
+ * @throws InvalidArgumentException Provided JWK is empty
+ * @throws UnexpectedValueException Provided JWK was invalid
+ * @throws DomainException OpenSSL failure
+ *
+ * @uses createPemFromModulusAndExponent
+ */
+ public static function parseKey(array $jwk, string $defaultAlg = null) : ?Key
+ {
+ if (empty($jwk)) {
+ throw new InvalidArgumentException('JWK must not be empty');
+ }
+ if (!isset($jwk['kty'])) {
+ throw new UnexpectedValueException('JWK must contain a "kty" parameter');
+ }
+ if (!isset($jwk['alg'])) {
+ if (\is_null($defaultAlg)) {
+ // The "alg" parameter is optional in a KTY, but an algorithm is required
+ // for parsing in this library. Use the $defaultAlg parameter when parsing the
+ // key set in order to prevent this error.
+ // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
+ throw new UnexpectedValueException('JWK must contain an "alg" parameter');
+ }
+ $jwk['alg'] = $defaultAlg;
+ }
+ switch ($jwk['kty']) {
+ case 'RSA':
+ if (!empty($jwk['d'])) {
+ throw new UnexpectedValueException('RSA private keys are not supported');
+ }
+ if (!isset($jwk['n']) || !isset($jwk['e'])) {
+ throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
+ }
+ $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
+ $publicKey = \openssl_pkey_get_public($pem);
+ if (\false === $publicKey) {
+ throw new DomainException('OpenSSL error: ' . \openssl_error_string());
+ }
+ return new Key($publicKey, $jwk['alg']);
+ case 'EC':
+ if (isset($jwk['d'])) {
+ // The key is actually a private key
+ throw new UnexpectedValueException('Key data must be for a public key');
+ }
+ if (empty($jwk['crv'])) {
+ throw new UnexpectedValueException('crv not set');
+ }
+ if (!isset(self::EC_CURVES[$jwk['crv']])) {
+ throw new DomainException('Unrecognised or unsupported EC curve');
+ }
+ if (empty($jwk['x']) || empty($jwk['y'])) {
+ throw new UnexpectedValueException('x and y not set');
+ }
+ $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
+ return new Key($publicKey, $jwk['alg']);
+ case 'OKP':
+ if (isset($jwk['d'])) {
+ // The key is actually a private key
+ throw new UnexpectedValueException('Key data must be for a public key');
+ }
+ if (!isset($jwk['crv'])) {
+ throw new UnexpectedValueException('crv not set');
+ }
+ if (empty(self::OKP_SUBTYPES[$jwk['crv']])) {
+ throw new DomainException('Unrecognised or unsupported OKP key subtype');
+ }
+ if (empty($jwk['x'])) {
+ throw new UnexpectedValueException('x not set');
+ }
+ // This library works internally with EdDSA keys (Ed25519) encoded in standard base64.
+ $publicKey = JWT::convertBase64urlToBase64($jwk['x']);
+ return new Key($publicKey, $jwk['alg']);
+ default:
+ break;
+ }
+ return null;
+ }
+ /**
+ * Converts the EC JWK values to pem format.
+ *
+ * @param string $crv The EC curve (only P-256 & P-384 is supported)
+ * @param string $x The EC x-coordinate
+ * @param string $y The EC y-coordinate
+ *
+ * @return string
+ */
+ private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y) : string
+ {
+ $pem = self::encodeDER(self::ASN1_SEQUENCE, self::encodeDER(self::ASN1_SEQUENCE, self::encodeDER(self::ASN1_OBJECT_IDENTIFIER, self::encodeOID(self::OID)) . self::encodeDER(self::ASN1_OBJECT_IDENTIFIER, self::encodeOID(self::EC_CURVES[$crv]))) . self::encodeDER(self::ASN1_BIT_STRING, \chr(0x0) . \chr(0x4) . JWT::urlsafeB64Decode($x) . JWT::urlsafeB64Decode($y)));
+ return sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", wordwrap(base64_encode($pem), 64, "\n", \true));
+ }
+ /**
+ * Create a public key represented in PEM format from RSA modulus and exponent information
+ *
+ * @param string $n The RSA modulus encoded in Base64
+ * @param string $e The RSA exponent encoded in Base64
+ *
+ * @return string The RSA public key represented in PEM format
+ *
+ * @uses encodeLength
+ */
+ private static function createPemFromModulusAndExponent(string $n, string $e) : string
+ {
+ $mod = JWT::urlsafeB64Decode($n);
+ $exp = JWT::urlsafeB64Decode($e);
+ $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
+ $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
+ $rsaPublicKey = \pack('Ca*a*a*', 48, self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), $modulus, $publicExponent);
+ // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
+ $rsaOID = \pack('H*', '300d06092a864886f70d0101010500');
+ // hex version of MA0GCSqGSIb3DQEBAQUA
+ $rsaPublicKey = \chr(0) . $rsaPublicKey;
+ $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
+ $rsaPublicKey = \pack('Ca*a*', 48, self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), $rsaOID . $rsaPublicKey);
+ return "-----BEGIN PUBLIC KEY-----\r\n" . \chunk_split(\base64_encode($rsaPublicKey), 64) . '-----END PUBLIC KEY-----';
+ }
+ /**
+ * DER-encode the length
+ *
+ * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
+ * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
+ *
+ * @param int $length
+ * @return string
+ */
+ private static function encodeLength(int $length) : string
+ {
+ if ($length <= 0x7f) {
+ return \chr($length);
+ }
+ $temp = \ltrim(\pack('N', $length), \chr(0));
+ return \pack('Ca*', 0x80 | \strlen($temp), $temp);
+ }
+ /**
+ * Encodes a value into a DER object.
+ * Also defined in Firebase\JWT\JWT
+ *
+ * @param int $type DER tag
+ * @param string $value the value to encode
+ * @return string the encoded object
+ */
+ private static function encodeDER(int $type, string $value) : string
+ {
+ $tag_header = 0;
+ if ($type === self::ASN1_SEQUENCE) {
+ $tag_header |= 0x20;
+ }
+ // Type
+ $der = \chr($tag_header | $type);
+ // Length
+ $der .= \chr(\strlen($value));
+ return $der . $value;
+ }
+ /**
+ * Encodes a string into a DER-encoded OID.
+ *
+ * @param string $oid the OID string
+ * @return string the binary DER-encoded OID
+ */
+ private static function encodeOID(string $oid) : string
+ {
+ $octets = explode('.', $oid);
+ // Get the first octet
+ $first = (int) array_shift($octets);
+ $second = (int) array_shift($octets);
+ $oid = \chr($first * 40 + $second);
+ // Iterate over subsequent octets
+ foreach ($octets as $octet) {
+ if ($octet == 0) {
+ $oid .= \chr(0x0);
+ continue;
+ }
+ $bin = '';
+ while ($octet) {
+ $bin .= \chr(0x80 | $octet & 0x7f);
+ $octet >>= 7;
+ }
+ $bin[0] = $bin[0] & \chr(0x7f);
+ // Convert to big endian if necessary
+ if (pack('V', 65534) == pack('L', 65534)) {
+ $oid .= strrev($bin);
+ } else {
+ $oid .= $bin;
+ }
+ }
+ return $oid;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWT.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWT.php
new file mode 100644
index 0000000..32add50
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWT.php
@@ -0,0 +1,572 @@
+
+ * @author Anant Narayanan
+ * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link https://github.com/firebase/php-jwt
+ */
+class JWT
+{
+ private const ASN1_INTEGER = 0x2;
+ private const ASN1_SEQUENCE = 0x10;
+ private const ASN1_BIT_STRING = 0x3;
+ /**
+ * When checking nbf, iat or expiration times,
+ * we want to provide some extra leeway time to
+ * account for clock skew.
+ *
+ * @var int
+ */
+ public static $leeway = 0;
+ /**
+ * Allow the current timestamp to be specified.
+ * Useful for fixing a value within unit testing.
+ * Will default to PHP time() value if null.
+ *
+ * @var ?int
+ */
+ public static $timestamp = null;
+ /**
+ * @var array
+ */
+ public static $supported_algs = ['ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], 'ES256K' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], 'HS384' => ['hash_hmac', 'SHA384'], 'HS512' => ['hash_hmac', 'SHA512'], 'RS256' => ['openssl', 'SHA256'], 'RS384' => ['openssl', 'SHA384'], 'RS512' => ['openssl', 'SHA512'], 'EdDSA' => ['sodium_crypto', 'EdDSA']];
+ /**
+ * Decodes a JWT string into a PHP object.
+ *
+ * @param string $jwt The JWT
+ * @param Key|ArrayAccess|array $keyOrKeyArray The Key or associative array of key IDs
+ * (kid) to Key objects.
+ * If the algorithm used is asymmetric, this is
+ * the public key.
+ * Each Key object contains an algorithm and
+ * matching key.
+ * Supported algorithms are 'ES384','ES256',
+ * 'HS256', 'HS384', 'HS512', 'RS256', 'RS384'
+ * and 'RS512'.
+ * @param stdClass $headers Optional. Populates stdClass with headers.
+ *
+ * @return stdClass The JWT's payload as a PHP object
+ *
+ * @throws InvalidArgumentException Provided key/key-array was empty or malformed
+ * @throws DomainException Provided JWT is malformed
+ * @throws UnexpectedValueException Provided JWT was invalid
+ * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
+ * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
+ * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
+ * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
+ *
+ * @uses jsonDecode
+ * @uses urlsafeB64Decode
+ */
+ public static function decode(string $jwt, $keyOrKeyArray, stdClass &$headers = null) : stdClass
+ {
+ // Validate JWT
+ $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
+ if (empty($keyOrKeyArray)) {
+ throw new InvalidArgumentException('Key may not be empty');
+ }
+ $tks = \explode('.', $jwt);
+ if (\count($tks) !== 3) {
+ throw new UnexpectedValueException('Wrong number of segments');
+ }
+ list($headb64, $bodyb64, $cryptob64) = $tks;
+ $headerRaw = static::urlsafeB64Decode($headb64);
+ if (null === ($header = static::jsonDecode($headerRaw))) {
+ throw new UnexpectedValueException('Invalid header encoding');
+ }
+ if ($headers !== null) {
+ $headers = $header;
+ }
+ $payloadRaw = static::urlsafeB64Decode($bodyb64);
+ if (null === ($payload = static::jsonDecode($payloadRaw))) {
+ throw new UnexpectedValueException('Invalid claims encoding');
+ }
+ if (\is_array($payload)) {
+ // prevent PHP Fatal Error in edge-cases when payload is empty array
+ $payload = (object) $payload;
+ }
+ if (!$payload instanceof stdClass) {
+ throw new UnexpectedValueException('Payload must be a JSON object');
+ }
+ $sig = static::urlsafeB64Decode($cryptob64);
+ if (empty($header->alg)) {
+ throw new UnexpectedValueException('Empty algorithm');
+ }
+ if (empty(static::$supported_algs[$header->alg])) {
+ throw new UnexpectedValueException('Algorithm not supported');
+ }
+ $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
+ // Check the algorithm
+ if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
+ // See issue #351
+ throw new UnexpectedValueException('Incorrect key for this algorithm');
+ }
+ if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], \true)) {
+ // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures
+ $sig = self::signatureToDER($sig);
+ }
+ if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) {
+ throw new SignatureInvalidException('Signature verification failed');
+ }
+ // Check the nbf if it is defined. This is the time that the
+ // token can actually be used. If it's not yet that time, abort.
+ if (isset($payload->nbf) && floor($payload->nbf) > $timestamp + static::$leeway) {
+ $ex = new BeforeValidException('Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf));
+ $ex->setPayload($payload);
+ throw $ex;
+ }
+ // Check that this token has been created before 'now'. This prevents
+ // using tokens that have been created for later use (and haven't
+ // correctly used the nbf claim).
+ if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > $timestamp + static::$leeway) {
+ $ex = new BeforeValidException('Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat));
+ $ex->setPayload($payload);
+ throw $ex;
+ }
+ // Check if this token has expired.
+ if (isset($payload->exp) && $timestamp - static::$leeway >= $payload->exp) {
+ $ex = new ExpiredException('Expired token');
+ $ex->setPayload($payload);
+ throw $ex;
+ }
+ return $payload;
+ }
+ /**
+ * Converts and signs a PHP array into a JWT string.
+ *
+ * @param array $payload PHP array
+ * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
+ * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
+ * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
+ * @param string $keyId
+ * @param array $head An array with header elements to attach
+ *
+ * @return string A signed JWT
+ *
+ * @uses jsonEncode
+ * @uses urlsafeB64Encode
+ */
+ public static function encode(array $payload, $key, string $alg, string $keyId = null, array $head = null) : string
+ {
+ $header = ['typ' => 'JWT'];
+ if (isset($head) && \is_array($head)) {
+ $header = \array_merge($header, $head);
+ }
+ $header['alg'] = $alg;
+ if ($keyId !== null) {
+ $header['kid'] = $keyId;
+ }
+ $segments = [];
+ $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
+ $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
+ $signing_input = \implode('.', $segments);
+ $signature = static::sign($signing_input, $key, $alg);
+ $segments[] = static::urlsafeB64Encode($signature);
+ return \implode('.', $segments);
+ }
+ /**
+ * Sign a string with a given key and algorithm.
+ *
+ * @param string $msg The message to sign
+ * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
+ * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256',
+ * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
+ *
+ * @return string An encrypted message
+ *
+ * @throws DomainException Unsupported algorithm or bad key was specified
+ */
+ public static function sign(string $msg, $key, string $alg) : string
+ {
+ if (empty(static::$supported_algs[$alg])) {
+ throw new DomainException('Algorithm not supported');
+ }
+ list($function, $algorithm) = static::$supported_algs[$alg];
+ switch ($function) {
+ case 'hash_hmac':
+ if (!\is_string($key)) {
+ throw new InvalidArgumentException('key must be a string when using hmac');
+ }
+ return \hash_hmac($algorithm, $msg, $key, \true);
+ case 'openssl':
+ $signature = '';
+ $success = \openssl_sign($msg, $signature, $key, $algorithm);
+ // @phpstan-ignore-line
+ if (!$success) {
+ throw new DomainException('OpenSSL unable to sign data');
+ }
+ if ($alg === 'ES256' || $alg === 'ES256K') {
+ $signature = self::signatureFromDER($signature, 256);
+ } elseif ($alg === 'ES384') {
+ $signature = self::signatureFromDER($signature, 384);
+ }
+ return $signature;
+ case 'sodium_crypto':
+ if (!\function_exists('sodium_crypto_sign_detached')) {
+ throw new DomainException('libsodium is not available');
+ }
+ if (!\is_string($key)) {
+ throw new InvalidArgumentException('key must be a string when using EdDSA');
+ }
+ try {
+ // The last non-empty line is used as the key.
+ $lines = array_filter(explode("\n", $key));
+ $key = base64_decode((string) end($lines));
+ if (\strlen($key) === 0) {
+ throw new DomainException('Key cannot be empty string');
+ }
+ return sodium_crypto_sign_detached($msg, $key);
+ } catch (Exception $e) {
+ throw new DomainException($e->getMessage(), 0, $e);
+ }
+ }
+ throw new DomainException('Algorithm not supported');
+ }
+ /**
+ * Verify a signature with the message, key and method. Not all methods
+ * are symmetric, so we must have a separate verify and sign method.
+ *
+ * @param string $msg The original message (header and body)
+ * @param string $signature The original signature
+ * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
+ * @param string $alg The algorithm
+ *
+ * @return bool
+ *
+ * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
+ */
+ private static function verify(string $msg, string $signature, $keyMaterial, string $alg) : bool
+ {
+ if (empty(static::$supported_algs[$alg])) {
+ throw new DomainException('Algorithm not supported');
+ }
+ list($function, $algorithm) = static::$supported_algs[$alg];
+ switch ($function) {
+ case 'openssl':
+ $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm);
+ // @phpstan-ignore-line
+ if ($success === 1) {
+ return \true;
+ }
+ if ($success === 0) {
+ return \false;
+ }
+ // returns 1 on success, 0 on failure, -1 on error.
+ throw new DomainException('OpenSSL error: ' . \openssl_error_string());
+ case 'sodium_crypto':
+ if (!\function_exists('sodium_crypto_sign_verify_detached')) {
+ throw new DomainException('libsodium is not available');
+ }
+ if (!\is_string($keyMaterial)) {
+ throw new InvalidArgumentException('key must be a string when using EdDSA');
+ }
+ try {
+ // The last non-empty line is used as the key.
+ $lines = array_filter(explode("\n", $keyMaterial));
+ $key = base64_decode((string) end($lines));
+ if (\strlen($key) === 0) {
+ throw new DomainException('Key cannot be empty string');
+ }
+ if (\strlen($signature) === 0) {
+ throw new DomainException('Signature cannot be empty string');
+ }
+ return sodium_crypto_sign_verify_detached($signature, $msg, $key);
+ } catch (Exception $e) {
+ throw new DomainException($e->getMessage(), 0, $e);
+ }
+ case 'hash_hmac':
+ default:
+ if (!\is_string($keyMaterial)) {
+ throw new InvalidArgumentException('key must be a string when using hmac');
+ }
+ $hash = \hash_hmac($algorithm, $msg, $keyMaterial, \true);
+ return self::constantTimeEquals($hash, $signature);
+ }
+ }
+ /**
+ * Decode a JSON string into a PHP object.
+ *
+ * @param string $input JSON string
+ *
+ * @return mixed The decoded JSON string
+ *
+ * @throws DomainException Provided string was invalid JSON
+ */
+ public static function jsonDecode(string $input)
+ {
+ $obj = \json_decode($input, \false, 512, \JSON_BIGINT_AS_STRING);
+ if ($errno = \json_last_error()) {
+ self::handleJsonError($errno);
+ } elseif ($obj === null && $input !== 'null') {
+ throw new DomainException('Null result with non-null input');
+ }
+ return $obj;
+ }
+ /**
+ * Encode a PHP array into a JSON string.
+ *
+ * @param array $input A PHP array
+ *
+ * @return string JSON representation of the PHP array
+ *
+ * @throws DomainException Provided object could not be encoded to valid JSON
+ */
+ public static function jsonEncode(array $input) : string
+ {
+ if (\PHP_VERSION_ID >= 50400) {
+ $json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
+ } else {
+ // PHP 5.3 only
+ $json = \json_encode($input);
+ }
+ if ($errno = \json_last_error()) {
+ self::handleJsonError($errno);
+ } elseif ($json === 'null') {
+ throw new DomainException('Null result with non-null input');
+ }
+ if ($json === \false) {
+ throw new DomainException('Provided object could not be encoded to valid JSON');
+ }
+ return $json;
+ }
+ /**
+ * Decode a string with URL-safe Base64.
+ *
+ * @param string $input A Base64 encoded string
+ *
+ * @return string A decoded string
+ *
+ * @throws InvalidArgumentException invalid base64 characters
+ */
+ public static function urlsafeB64Decode(string $input) : string
+ {
+ return \base64_decode(self::convertBase64UrlToBase64($input));
+ }
+ /**
+ * Convert a string in the base64url (URL-safe Base64) encoding to standard base64.
+ *
+ * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding)
+ *
+ * @return string A Base64 encoded string with standard characters (+/) and padding (=), when
+ * needed.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc4648
+ */
+ public static function convertBase64UrlToBase64(string $input) : string
+ {
+ $remainder = \strlen($input) % 4;
+ if ($remainder) {
+ $padlen = 4 - $remainder;
+ $input .= \str_repeat('=', $padlen);
+ }
+ return \strtr($input, '-_', '+/');
+ }
+ /**
+ * Encode a string with URL-safe Base64.
+ *
+ * @param string $input The string you want encoded
+ *
+ * @return string The base64 encode of what you passed in
+ */
+ public static function urlsafeB64Encode(string $input) : string
+ {
+ return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
+ }
+ /**
+ * Determine if an algorithm has been provided for each Key
+ *
+ * @param Key|ArrayAccess|array $keyOrKeyArray
+ * @param string|null $kid
+ *
+ * @throws UnexpectedValueException
+ *
+ * @return Key
+ */
+ private static function getKey($keyOrKeyArray, ?string $kid) : Key
+ {
+ if ($keyOrKeyArray instanceof Key) {
+ return $keyOrKeyArray;
+ }
+ if (empty($kid) && $kid !== '0') {
+ throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
+ }
+ if ($keyOrKeyArray instanceof CachedKeySet) {
+ // Skip "isset" check, as this will automatically refresh if not set
+ return $keyOrKeyArray[$kid];
+ }
+ if (!isset($keyOrKeyArray[$kid])) {
+ throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
+ }
+ return $keyOrKeyArray[$kid];
+ }
+ /**
+ * @param string $left The string of known length to compare against
+ * @param string $right The user-supplied string
+ * @return bool
+ */
+ public static function constantTimeEquals(string $left, string $right) : bool
+ {
+ if (\function_exists('hash_equals')) {
+ return \hash_equals($left, $right);
+ }
+ $len = \min(self::safeStrlen($left), self::safeStrlen($right));
+ $status = 0;
+ for ($i = 0; $i < $len; $i++) {
+ $status |= \ord($left[$i]) ^ \ord($right[$i]);
+ }
+ $status |= self::safeStrlen($left) ^ self::safeStrlen($right);
+ return $status === 0;
+ }
+ /**
+ * Helper method to create a JSON error.
+ *
+ * @param int $errno An error number from json_last_error()
+ *
+ * @throws DomainException
+ *
+ * @return void
+ */
+ private static function handleJsonError(int $errno) : void
+ {
+ $messages = [\JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', \JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', \JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', \JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', \JSON_ERROR_UTF8 => 'Malformed UTF-8 characters'];
+ throw new DomainException(isset($messages[$errno]) ? $messages[$errno] : 'Unknown JSON error: ' . $errno);
+ }
+ /**
+ * Get the number of bytes in cryptographic strings.
+ *
+ * @param string $str
+ *
+ * @return int
+ */
+ private static function safeStrlen(string $str) : int
+ {
+ if (\function_exists('mb_strlen')) {
+ return \mb_strlen($str, '8bit');
+ }
+ return \strlen($str);
+ }
+ /**
+ * Convert an ECDSA signature to an ASN.1 DER sequence
+ *
+ * @param string $sig The ECDSA signature to convert
+ * @return string The encoded DER object
+ */
+ private static function signatureToDER(string $sig) : string
+ {
+ // Separate the signature into r-value and s-value
+ $length = max(1, (int) (\strlen($sig) / 2));
+ list($r, $s) = \str_split($sig, $length);
+ // Trim leading zeros
+ $r = \ltrim($r, "\x00");
+ $s = \ltrim($s, "\x00");
+ // Convert r-value and s-value from unsigned big-endian integers to
+ // signed two's complement
+ if (\ord($r[0]) > 0x7f) {
+ $r = "\x00" . $r;
+ }
+ if (\ord($s[0]) > 0x7f) {
+ $s = "\x00" . $s;
+ }
+ return self::encodeDER(self::ASN1_SEQUENCE, self::encodeDER(self::ASN1_INTEGER, $r) . self::encodeDER(self::ASN1_INTEGER, $s));
+ }
+ /**
+ * Encodes a value into a DER object.
+ *
+ * @param int $type DER tag
+ * @param string $value the value to encode
+ *
+ * @return string the encoded object
+ */
+ private static function encodeDER(int $type, string $value) : string
+ {
+ $tag_header = 0;
+ if ($type === self::ASN1_SEQUENCE) {
+ $tag_header |= 0x20;
+ }
+ // Type
+ $der = \chr($tag_header | $type);
+ // Length
+ $der .= \chr(\strlen($value));
+ return $der . $value;
+ }
+ /**
+ * Encodes signature from a DER object.
+ *
+ * @param string $der binary signature in DER format
+ * @param int $keySize the number of bits in the key
+ *
+ * @return string the signature
+ */
+ private static function signatureFromDER(string $der, int $keySize) : string
+ {
+ // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
+ list($offset, $_) = self::readDER($der);
+ list($offset, $r) = self::readDER($der, $offset);
+ list($offset, $s) = self::readDER($der, $offset);
+ // Convert r-value and s-value from signed two's compliment to unsigned
+ // big-endian integers
+ $r = \ltrim($r, "\x00");
+ $s = \ltrim($s, "\x00");
+ // Pad out r and s so that they are $keySize bits long
+ $r = \str_pad($r, $keySize / 8, "\x00", \STR_PAD_LEFT);
+ $s = \str_pad($s, $keySize / 8, "\x00", \STR_PAD_LEFT);
+ return $r . $s;
+ }
+ /**
+ * Reads binary DER-encoded data and decodes into a single object
+ *
+ * @param string $der the binary data in DER format
+ * @param int $offset the offset of the data stream containing the object
+ * to decode
+ *
+ * @return array{int, string|null} the new offset and the decoded object
+ */
+ private static function readDER(string $der, int $offset = 0) : array
+ {
+ $pos = $offset;
+ $size = \strlen($der);
+ $constructed = \ord($der[$pos]) >> 5 & 0x1;
+ $type = \ord($der[$pos++]) & 0x1f;
+ // Length
+ $len = \ord($der[$pos++]);
+ if ($len & 0x80) {
+ $n = $len & 0x1f;
+ $len = 0;
+ while ($n-- && $pos < $size) {
+ $len = $len << 8 | \ord($der[$pos++]);
+ }
+ }
+ // Value
+ if ($type === self::ASN1_BIT_STRING) {
+ $pos++;
+ // Skip the first contents octet (padding indicator)
+ $data = \substr($der, $pos, $len - 1);
+ $pos += $len - 1;
+ } elseif (!$constructed) {
+ $data = \substr($der, $pos, $len);
+ $pos += $len;
+ } else {
+ $data = null;
+ }
+ return [$pos, $data];
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php
new file mode 100644
index 0000000..dcfc18f
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php
@@ -0,0 +1,20 @@
+keyMaterial = $keyMaterial;
+ $this->algorithm = $algorithm;
+ }
+ /**
+ * Return the algorithm valid for this key
+ *
+ * @return string
+ */
+ public function getAlgorithm() : string
+ {
+ return $this->algorithm;
+ }
+ /**
+ * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate
+ */
+ public function getKeyMaterial()
+ {
+ return $this->keyMaterial;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/SignatureInvalidException.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/SignatureInvalidException.php
new file mode 100644
index 0000000..5c57363
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/SignatureInvalidException.php
@@ -0,0 +1,7 @@
+ 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Client', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Service' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Service\\Resource' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Resource', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Model' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Model', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Collection' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Collection'];
+ foreach ($servicesClassMap as $alias => $class) {
+ \class_alias($class, $alias);
+ }
+ }
+}
+\spl_autoload_register(function ($class) {
+ $class = preg_replace('/^Piwik\\\\Dependencies\\\\/', 'Matomo\\Dependencies\\', $class);
+
+ if (0 === \strpos($class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_')) {
+ // Autoload the new class, which will also create an alias for the
+ // old class by changing underscores to namespaces:
+ // Google_Service_Speech_Resource_Operations
+ // => Google\Service\Speech\Resource\Operations
+ $classExists = \class_exists($newClass = \str_replace('_', '\\', $class));
+ if ($classExists) {
+ return \true;
+ }
+ }
+}, \true, \true);
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/renovate.json b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/renovate.json
new file mode 100644
index 0000000..b31203b
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/renovate.json
@@ -0,0 +1,7 @@
+{
+ "extends": [
+ "config:base"
+ ],
+ "pinVersions": false,
+ "rebaseStalePrs": true
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2.php
new file mode 100644
index 0000000..9d707d0
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2.php
@@ -0,0 +1,82 @@
+
+ * Obtains end-user authorization grants for use with other Google APIs.
+ *
+ *
+ * For more information about this service, see the API
+ * Documentation
+ *
+ *
+ * @author Google, Inc.
+ */
+class Oauth2 extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service
+{
+ /** View your email address. */
+ const USERINFO_EMAIL = "https://www.googleapis.com/auth/userinfo.email";
+ /** See your personal info, including any personal info you've made publicly available. */
+ const USERINFO_PROFILE = "https://www.googleapis.com/auth/userinfo.profile";
+ /** Associate you with your personal info on Google. */
+ const OPENID = "openid";
+ public $userinfo;
+ public $userinfo_v2_me;
+ private $base_methods;
+ /**
+ * Constructs the internal representation of the Oauth2 service.
+ *
+ * @param Client|array $clientOrConfig The client used to deliver requests, or a
+ * config array to pass to a new Client instance.
+ * @param string $rootUrl The root URL used for requests to the service.
+ */
+ public function __construct($clientOrConfig = [], $rootUrl = null)
+ {
+ parent::__construct($clientOrConfig);
+ $this->rootUrl = $rootUrl ?: 'https://www.googleapis.com/';
+ $this->servicePath = '';
+ $this->batchPath = 'batch/oauth2/v2';
+ $this->version = 'v2';
+ $this->serviceName = 'oauth2';
+ $this->userinfo = new Oauth2\Resource\Userinfo($this, $this->serviceName, 'userinfo', ['methods' => ['get' => ['path' => 'oauth2/v2/userinfo', 'httpMethod' => 'GET', 'parameters' => []]]]);
+ $this->userinfo_v2_me = new Oauth2\Resource\UserinfoV2Me($this, $this->serviceName, 'me', ['methods' => ['get' => ['path' => 'userinfo/v2/me', 'httpMethod' => 'GET', 'parameters' => []]]]);
+ $this->base_methods = new Resource($this, $this->serviceName, '', ['methods' => ['tokeninfo' => ['path' => 'oauth2/v2/tokeninfo', 'httpMethod' => 'POST', 'parameters' => ['access_token' => ['location' => 'query', 'type' => 'string'], 'id_token' => ['location' => 'query', 'type' => 'string']]]]]);
+ }
+ /**
+ * (tokeninfo)
+ *
+ * @param array $optParams Optional parameters.
+ *
+ * @opt_param string access_token
+ * @opt_param string id_token
+ * @return Tokeninfo
+ */
+ public function tokeninfo($optParams = [])
+ {
+ $params = [];
+ $params = array_merge($params, $optParams);
+ return $this->base_methods->call('tokeninfo', [$params], Tokeninfo::class);
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Oauth2::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Oauth2');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/Userinfo.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/Userinfo.php
new file mode 100644
index 0000000..1c22318
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/Userinfo.php
@@ -0,0 +1,45 @@
+
+ * $oauth2Service = new Google\Service\Oauth2(...);
+ * $userinfo = $oauth2Service->userinfo;
+ *
+ */
+class Userinfo extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+{
+ /**
+ * (userinfo.get)
+ *
+ * @param array $optParams Optional parameters.
+ * @return UserinfoModel
+ */
+ public function get($optParams = [])
+ {
+ $params = [];
+ $params = array_merge($params, $optParams);
+ return $this->call('get', [$params], UserinfoModel::class);
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Userinfo::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Oauth2_Resource_Userinfo');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/UserinfoV2.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/UserinfoV2.php
new file mode 100644
index 0000000..7d62bf2
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/UserinfoV2.php
@@ -0,0 +1,32 @@
+
+ * $oauth2Service = new Google\Service\Oauth2(...);
+ * $v2 = $oauth2Service->v2;
+ *
+ */
+class UserinfoV2 extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+{
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(UserinfoV2::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Oauth2_Resource_UserinfoV2');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/UserinfoV2Me.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/UserinfoV2Me.php
new file mode 100644
index 0000000..40cf84c
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/UserinfoV2Me.php
@@ -0,0 +1,45 @@
+
+ * $oauth2Service = new Google\Service\Oauth2(...);
+ * $me = $oauth2Service->me;
+ *
+ */
+class UserinfoV2Me extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+{
+ /**
+ * (me.get)
+ *
+ * @param array $optParams Optional parameters.
+ * @return Userinfo
+ */
+ public function get($optParams = [])
+ {
+ $params = [];
+ $params = array_merge($params, $optParams);
+ return $this->call('get', [$params], UserinfoModel::class);
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(UserinfoV2Me::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Oauth2_Resource_UserinfoV2Me');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Tokeninfo.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Tokeninfo.php
new file mode 100644
index 0000000..0e1383c
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Tokeninfo.php
@@ -0,0 +1,88 @@
+ "expires_in", "issuedTo" => "issued_to", "userId" => "user_id", "verifiedEmail" => "verified_email"];
+ public $audience;
+ public $email;
+ public $expiresIn;
+ public $issuedTo;
+ public $scope;
+ public $userId;
+ public $verifiedEmail;
+ public function setAudience($audience)
+ {
+ $this->audience = $audience;
+ }
+ public function getAudience()
+ {
+ return $this->audience;
+ }
+ public function setEmail($email)
+ {
+ $this->email = $email;
+ }
+ public function getEmail()
+ {
+ return $this->email;
+ }
+ public function setExpiresIn($expiresIn)
+ {
+ $this->expiresIn = $expiresIn;
+ }
+ public function getExpiresIn()
+ {
+ return $this->expiresIn;
+ }
+ public function setIssuedTo($issuedTo)
+ {
+ $this->issuedTo = $issuedTo;
+ }
+ public function getIssuedTo()
+ {
+ return $this->issuedTo;
+ }
+ public function setScope($scope)
+ {
+ $this->scope = $scope;
+ }
+ public function getScope()
+ {
+ return $this->scope;
+ }
+ public function setUserId($userId)
+ {
+ $this->userId = $userId;
+ }
+ public function getUserId()
+ {
+ return $this->userId;
+ }
+ public function setVerifiedEmail($verifiedEmail)
+ {
+ $this->verifiedEmail = $verifiedEmail;
+ }
+ public function getVerifiedEmail()
+ {
+ return $this->verifiedEmail;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Tokeninfo::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Oauth2_Tokeninfo');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Userinfo.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Userinfo.php
new file mode 100644
index 0000000..49f82aa
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Userinfo.php
@@ -0,0 +1,124 @@
+ "family_name", "givenName" => "given_name", "verifiedEmail" => "verified_email"];
+ public $email;
+ public $familyName;
+ public $gender;
+ public $givenName;
+ public $hd;
+ public $id;
+ public $link;
+ public $locale;
+ public $name;
+ public $picture;
+ public $verifiedEmail;
+ public function setEmail($email)
+ {
+ $this->email = $email;
+ }
+ public function getEmail()
+ {
+ return $this->email;
+ }
+ public function setFamilyName($familyName)
+ {
+ $this->familyName = $familyName;
+ }
+ public function getFamilyName()
+ {
+ return $this->familyName;
+ }
+ public function setGender($gender)
+ {
+ $this->gender = $gender;
+ }
+ public function getGender()
+ {
+ return $this->gender;
+ }
+ public function setGivenName($givenName)
+ {
+ $this->givenName = $givenName;
+ }
+ public function getGivenName()
+ {
+ return $this->givenName;
+ }
+ public function setHd($hd)
+ {
+ $this->hd = $hd;
+ }
+ public function getHd()
+ {
+ return $this->hd;
+ }
+ public function setId($id)
+ {
+ $this->id = $id;
+ }
+ public function getId()
+ {
+ return $this->id;
+ }
+ public function setLink($link)
+ {
+ $this->link = $link;
+ }
+ public function getLink()
+ {
+ return $this->link;
+ }
+ public function setLocale($locale)
+ {
+ $this->locale = $locale;
+ }
+ public function getLocale()
+ {
+ return $this->locale;
+ }
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+ public function getName()
+ {
+ return $this->name;
+ }
+ public function setPicture($picture)
+ {
+ $this->picture = $picture;
+ }
+ public function getPicture()
+ {
+ return $this->picture;
+ }
+ public function setVerifiedEmail($verifiedEmail)
+ {
+ $this->verifiedEmail = $verifiedEmail;
+ }
+ public function getVerifiedEmail()
+ {
+ return $this->verifiedEmail;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Userinfo::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Oauth2_Userinfo');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole.php
new file mode 100644
index 0000000..076b9af
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole.php
@@ -0,0 +1,67 @@
+
+ * The Search Console API provides access to both Search Console data (verified
+ * users only) and to public information on an URL basis (anyone)
+ *
+ *
+ * For more information about this service, see the API
+ * Documentation
+ *
+ *
+ * @author Google, Inc.
+ */
+class SearchConsole extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service
+{
+ /** View and manage Search Console data for your verified sites. */
+ const WEBMASTERS = "https://www.googleapis.com/auth/webmasters";
+ /** View Search Console data for your verified sites. */
+ const WEBMASTERS_READONLY = "https://www.googleapis.com/auth/webmasters.readonly";
+ public $searchanalytics;
+ public $sitemaps;
+ public $sites;
+ public $urlTestingTools_mobileFriendlyTest;
+ /**
+ * Constructs the internal representation of the SearchConsole service.
+ *
+ * @param Client|array $clientOrConfig The client used to deliver requests, or a
+ * config array to pass to a new Client instance.
+ * @param string $rootUrl The root URL used for requests to the service.
+ */
+ public function __construct($clientOrConfig = [], $rootUrl = null)
+ {
+ parent::__construct($clientOrConfig);
+ $this->rootUrl = $rootUrl ?: 'https://searchconsole.googleapis.com/';
+ $this->servicePath = '';
+ $this->batchPath = 'batch';
+ $this->version = 'v1';
+ $this->serviceName = 'searchconsole';
+ $this->searchanalytics = new SearchConsole\Resource\Searchanalytics($this, $this->serviceName, 'searchanalytics', ['methods' => ['query' => ['path' => 'webmasters/v3/sites/{siteUrl}/searchAnalytics/query', 'httpMethod' => 'POST', 'parameters' => ['siteUrl' => ['location' => 'path', 'type' => 'string', 'required' => \true]]]]]);
+ $this->sitemaps = new SearchConsole\Resource\Sitemaps($this, $this->serviceName, 'sitemaps', ['methods' => ['delete' => ['path' => 'webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}', 'httpMethod' => 'DELETE', 'parameters' => ['siteUrl' => ['location' => 'path', 'type' => 'string', 'required' => \true], 'feedpath' => ['location' => 'path', 'type' => 'string', 'required' => \true]]], 'get' => ['path' => 'webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}', 'httpMethod' => 'GET', 'parameters' => ['siteUrl' => ['location' => 'path', 'type' => 'string', 'required' => \true], 'feedpath' => ['location' => 'path', 'type' => 'string', 'required' => \true]]], 'list' => ['path' => 'webmasters/v3/sites/{siteUrl}/sitemaps', 'httpMethod' => 'GET', 'parameters' => ['siteUrl' => ['location' => 'path', 'type' => 'string', 'required' => \true], 'sitemapIndex' => ['location' => 'query', 'type' => 'string']]], 'submit' => ['path' => 'webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath}', 'httpMethod' => 'PUT', 'parameters' => ['siteUrl' => ['location' => 'path', 'type' => 'string', 'required' => \true], 'feedpath' => ['location' => 'path', 'type' => 'string', 'required' => \true]]]]]);
+ $this->sites = new SearchConsole\Resource\Sites($this, $this->serviceName, 'sites', ['methods' => ['add' => ['path' => 'webmasters/v3/sites/{siteUrl}', 'httpMethod' => 'PUT', 'parameters' => ['siteUrl' => ['location' => 'path', 'type' => 'string', 'required' => \true]]], 'delete' => ['path' => 'webmasters/v3/sites/{siteUrl}', 'httpMethod' => 'DELETE', 'parameters' => ['siteUrl' => ['location' => 'path', 'type' => 'string', 'required' => \true]]], 'get' => ['path' => 'webmasters/v3/sites/{siteUrl}', 'httpMethod' => 'GET', 'parameters' => ['siteUrl' => ['location' => 'path', 'type' => 'string', 'required' => \true]]], 'list' => ['path' => 'webmasters/v3/sites', 'httpMethod' => 'GET', 'parameters' => []]]]);
+ $this->urlTestingTools_mobileFriendlyTest = new SearchConsole\Resource\UrlTestingToolsMobileFriendlyTest($this, $this->serviceName, 'mobileFriendlyTest', ['methods' => ['run' => ['path' => 'v1/urlTestingTools/mobileFriendlyTest:run', 'httpMethod' => 'POST', 'parameters' => []]]]);
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(SearchConsole::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDataRow.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDataRow.php
new file mode 100644
index 0000000..fc941c9
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDataRow.php
@@ -0,0 +1,70 @@
+clicks = $clicks;
+ }
+ public function getClicks()
+ {
+ return $this->clicks;
+ }
+ public function setCtr($ctr)
+ {
+ $this->ctr = $ctr;
+ }
+ public function getCtr()
+ {
+ return $this->ctr;
+ }
+ public function setImpressions($impressions)
+ {
+ $this->impressions = $impressions;
+ }
+ public function getImpressions()
+ {
+ return $this->impressions;
+ }
+ public function setKeys($keys)
+ {
+ $this->keys = $keys;
+ }
+ public function getKeys()
+ {
+ return $this->keys;
+ }
+ public function setPosition($position)
+ {
+ $this->position = $position;
+ }
+ public function getPosition()
+ {
+ return $this->position;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(ApiDataRow::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_ApiDataRow');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDimensionFilter.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDimensionFilter.php
new file mode 100644
index 0000000..f5c1305
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDimensionFilter.php
@@ -0,0 +1,51 @@
+dimension = $dimension;
+ }
+ public function getDimension()
+ {
+ return $this->dimension;
+ }
+ public function setExpression($expression)
+ {
+ $this->expression = $expression;
+ }
+ public function getExpression()
+ {
+ return $this->expression;
+ }
+ public function setOperator($operator)
+ {
+ $this->operator = $operator;
+ }
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(ApiDimensionFilter::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_ApiDimensionFilter');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDimensionFilterGroup.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDimensionFilterGroup.php
new file mode 100644
index 0000000..42b8c14
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDimensionFilterGroup.php
@@ -0,0 +1,50 @@
+filters = $filters;
+ }
+ /**
+ * @return ApiDimensionFilter[]
+ */
+ public function getFilters()
+ {
+ return $this->filters;
+ }
+ public function setGroupType($groupType)
+ {
+ $this->groupType = $groupType;
+ }
+ public function getGroupType()
+ {
+ return $this->groupType;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(ApiDimensionFilterGroup::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_ApiDimensionFilterGroup');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/BlockedResource.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/BlockedResource.php
new file mode 100644
index 0000000..b974ae0
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/BlockedResource.php
@@ -0,0 +1,33 @@
+url = $url;
+ }
+ public function getUrl()
+ {
+ return $this->url;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(BlockedResource::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_BlockedResource');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Image.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Image.php
new file mode 100644
index 0000000..b8ec4e4
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Image.php
@@ -0,0 +1,42 @@
+data = $data;
+ }
+ public function getData()
+ {
+ return $this->data;
+ }
+ public function setMimeType($mimeType)
+ {
+ $this->mimeType = $mimeType;
+ }
+ public function getMimeType()
+ {
+ return $this->mimeType;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Image::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_Image');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/MobileFriendlyIssue.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/MobileFriendlyIssue.php
new file mode 100644
index 0000000..e187f3c
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/MobileFriendlyIssue.php
@@ -0,0 +1,33 @@
+rule = $rule;
+ }
+ public function getRule()
+ {
+ return $this->rule;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(MobileFriendlyIssue::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_MobileFriendlyIssue');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Searchanalytics.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Searchanalytics.php
new file mode 100644
index 0000000..8be08f1
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Searchanalytics.php
@@ -0,0 +1,54 @@
+
+ * $searchconsoleService = new Google\Service\SearchConsole(...);
+ * $searchanalytics = $searchconsoleService->searchanalytics;
+ *
+ */
+class Searchanalytics extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+{
+ /**
+ * Query your data with filters and parameters that you define. Returns zero or
+ * more rows grouped by the row keys that you define. You must define a date
+ * range of one or more days. When date is one of the group by values, any days
+ * without data are omitted from the result list. If you need to know which days
+ * have data, issue a broad date range query grouped by date for any metric, and
+ * see which day rows are returned. (searchanalytics.query)
+ *
+ * @param string $siteUrl The site's URL, including protocol. For example:
+ * `http://www.example.com/`.
+ * @param SearchAnalyticsQueryRequest $postBody
+ * @param array $optParams Optional parameters.
+ * @return SearchAnalyticsQueryResponse
+ */
+ public function query($siteUrl, SearchAnalyticsQueryRequest $postBody, $optParams = [])
+ {
+ $params = ['siteUrl' => $siteUrl, 'postBody' => $postBody];
+ $params = array_merge($params, $optParams);
+ return $this->call('query', [$params], SearchAnalyticsQueryResponse::class);
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Searchanalytics::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_Resource_Searchanalytics');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Sitemaps.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Sitemaps.php
new file mode 100644
index 0000000..5708dc8
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Sitemaps.php
@@ -0,0 +1,99 @@
+
+ * $searchconsoleService = new Google\Service\SearchConsole(...);
+ * $sitemaps = $searchconsoleService->sitemaps;
+ *
+ */
+class Sitemaps extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+{
+ /**
+ * Deletes a sitemap from this site. (sitemaps.delete)
+ *
+ * @param string $siteUrl The site's URL, including protocol. For example:
+ * `http://www.example.com/`.
+ * @param string $feedpath The URL of the actual sitemap. For example:
+ * `http://www.example.com/sitemap.xml`.
+ * @param array $optParams Optional parameters.
+ */
+ public function delete($siteUrl, $feedpath, $optParams = [])
+ {
+ $params = ['siteUrl' => $siteUrl, 'feedpath' => $feedpath];
+ $params = array_merge($params, $optParams);
+ return $this->call('delete', [$params]);
+ }
+ /**
+ * Retrieves information about a specific sitemap. (sitemaps.get)
+ *
+ * @param string $siteUrl The site's URL, including protocol. For example:
+ * `http://www.example.com/`.
+ * @param string $feedpath The URL of the actual sitemap. For example:
+ * `http://www.example.com/sitemap.xml`.
+ * @param array $optParams Optional parameters.
+ * @return WmxSitemap
+ */
+ public function get($siteUrl, $feedpath, $optParams = [])
+ {
+ $params = ['siteUrl' => $siteUrl, 'feedpath' => $feedpath];
+ $params = array_merge($params, $optParams);
+ return $this->call('get', [$params], WmxSitemap::class);
+ }
+ /**
+ * Lists the [sitemaps-entries](/webmaster-tools/v3/sitemaps) submitted for this
+ * site, or included in the sitemap index file (if `sitemapIndex` is specified
+ * in the request). (sitemaps.listSitemaps)
+ *
+ * @param string $siteUrl The site's URL, including protocol. For example:
+ * `http://www.example.com/`.
+ * @param array $optParams Optional parameters.
+ *
+ * @opt_param string sitemapIndex A URL of a site's sitemap index. For example:
+ * `http://www.example.com/sitemapindex.xml`.
+ * @return SitemapsListResponse
+ */
+ public function listSitemaps($siteUrl, $optParams = [])
+ {
+ $params = ['siteUrl' => $siteUrl];
+ $params = array_merge($params, $optParams);
+ return $this->call('list', [$params], SitemapsListResponse::class);
+ }
+ /**
+ * Submits a sitemap for a site. (sitemaps.submit)
+ *
+ * @param string $siteUrl The site's URL, including protocol. For example:
+ * `http://www.example.com/`.
+ * @param string $feedpath The URL of the actual sitemap. For example:
+ * `http://www.example.com/sitemap.xml`.
+ * @param array $optParams Optional parameters.
+ */
+ public function submit($siteUrl, $feedpath, $optParams = [])
+ {
+ $params = ['siteUrl' => $siteUrl, 'feedpath' => $feedpath];
+ $params = array_merge($params, $optParams);
+ return $this->call('submit', [$params]);
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Sitemaps::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_Resource_Sitemaps');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Sites.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Sites.php
new file mode 100644
index 0000000..24b5934
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Sites.php
@@ -0,0 +1,86 @@
+
+ * $searchconsoleService = new Google\Service\SearchConsole(...);
+ * $sites = $searchconsoleService->sites;
+ *
+ */
+class Sites extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+{
+ /**
+ * Adds a site to the set of the user's sites in Search Console. (sites.add)
+ *
+ * @param string $siteUrl The URL of the site to add.
+ * @param array $optParams Optional parameters.
+ */
+ public function add($siteUrl, $optParams = [])
+ {
+ $params = ['siteUrl' => $siteUrl];
+ $params = array_merge($params, $optParams);
+ return $this->call('add', [$params]);
+ }
+ /**
+ * Removes a site from the set of the user's Search Console sites.
+ * (sites.delete)
+ *
+ * @param string $siteUrl The URI of the property as defined in Search Console.
+ * **Examples:** `http://www.example.com/` or `sc-domain:example.com`.
+ * @param array $optParams Optional parameters.
+ */
+ public function delete($siteUrl, $optParams = [])
+ {
+ $params = ['siteUrl' => $siteUrl];
+ $params = array_merge($params, $optParams);
+ return $this->call('delete', [$params]);
+ }
+ /**
+ * Retrieves information about specific site. (sites.get)
+ *
+ * @param string $siteUrl The URI of the property as defined in Search Console.
+ * **Examples:** `http://www.example.com/` or `sc-domain:example.com`.
+ * @param array $optParams Optional parameters.
+ * @return WmxSite
+ */
+ public function get($siteUrl, $optParams = [])
+ {
+ $params = ['siteUrl' => $siteUrl];
+ $params = array_merge($params, $optParams);
+ return $this->call('get', [$params], WmxSite::class);
+ }
+ /**
+ * Lists the user's Search Console sites. (sites.listSites)
+ *
+ * @param array $optParams Optional parameters.
+ * @return SitesListResponse
+ */
+ public function listSites($optParams = [])
+ {
+ $params = [];
+ $params = array_merge($params, $optParams);
+ return $this->call('list', [$params], SitesListResponse::class);
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(Sites::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_Resource_Sites');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/UrlTestingTools.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/UrlTestingTools.php
new file mode 100644
index 0000000..9e022ce
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/UrlTestingTools.php
@@ -0,0 +1,32 @@
+
+ * $searchconsoleService = new Google\Service\SearchConsole(...);
+ * $urlTestingTools = $searchconsoleService->urlTestingTools;
+ *
+ */
+class UrlTestingTools extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+{
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(UrlTestingTools::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_Resource_UrlTestingTools');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/UrlTestingToolsMobileFriendlyTest.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/UrlTestingToolsMobileFriendlyTest.php
new file mode 100644
index 0000000..0d43a14
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/UrlTestingToolsMobileFriendlyTest.php
@@ -0,0 +1,47 @@
+
+ * $searchconsoleService = new Google\Service\SearchConsole(...);
+ * $mobileFriendlyTest = $searchconsoleService->mobileFriendlyTest;
+ *
+ */
+class UrlTestingToolsMobileFriendlyTest extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+{
+ /**
+ * Runs Mobile-Friendly Test for a given URL. (mobileFriendlyTest.run)
+ *
+ * @param RunMobileFriendlyTestRequest $postBody
+ * @param array $optParams Optional parameters.
+ * @return RunMobileFriendlyTestResponse
+ */
+ public function run(RunMobileFriendlyTestRequest $postBody, $optParams = [])
+ {
+ $params = ['postBody' => $postBody];
+ $params = array_merge($params, $optParams);
+ return $this->call('run', [$params], RunMobileFriendlyTestResponse::class);
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(UrlTestingToolsMobileFriendlyTest::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_Resource_UrlTestingToolsMobileFriendlyTest');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ResourceIssue.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ResourceIssue.php
new file mode 100644
index 0000000..1484a00
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ResourceIssue.php
@@ -0,0 +1,40 @@
+blockedResource = $blockedResource;
+ }
+ /**
+ * @return BlockedResource
+ */
+ public function getBlockedResource()
+ {
+ return $this->blockedResource;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(ResourceIssue::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_ResourceIssue');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/RunMobileFriendlyTestRequest.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/RunMobileFriendlyTestRequest.php
new file mode 100644
index 0000000..bec92cc
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/RunMobileFriendlyTestRequest.php
@@ -0,0 +1,42 @@
+requestScreenshot = $requestScreenshot;
+ }
+ public function getRequestScreenshot()
+ {
+ return $this->requestScreenshot;
+ }
+ public function setUrl($url)
+ {
+ $this->url = $url;
+ }
+ public function getUrl()
+ {
+ return $this->url;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(RunMobileFriendlyTestRequest::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_RunMobileFriendlyTestRequest');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/RunMobileFriendlyTestResponse.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/RunMobileFriendlyTestResponse.php
new file mode 100644
index 0000000..6f16f10
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/RunMobileFriendlyTestResponse.php
@@ -0,0 +1,98 @@
+mobileFriendliness = $mobileFriendliness;
+ }
+ public function getMobileFriendliness()
+ {
+ return $this->mobileFriendliness;
+ }
+ /**
+ * @param MobileFriendlyIssue[]
+ */
+ public function setMobileFriendlyIssues($mobileFriendlyIssues)
+ {
+ $this->mobileFriendlyIssues = $mobileFriendlyIssues;
+ }
+ /**
+ * @return MobileFriendlyIssue[]
+ */
+ public function getMobileFriendlyIssues()
+ {
+ return $this->mobileFriendlyIssues;
+ }
+ /**
+ * @param ResourceIssue[]
+ */
+ public function setResourceIssues($resourceIssues)
+ {
+ $this->resourceIssues = $resourceIssues;
+ }
+ /**
+ * @return ResourceIssue[]
+ */
+ public function getResourceIssues()
+ {
+ return $this->resourceIssues;
+ }
+ /**
+ * @param Image
+ */
+ public function setScreenshot(Image $screenshot)
+ {
+ $this->screenshot = $screenshot;
+ }
+ /**
+ * @return Image
+ */
+ public function getScreenshot()
+ {
+ return $this->screenshot;
+ }
+ /**
+ * @param TestStatus
+ */
+ public function setTestStatus(TestStatus $testStatus)
+ {
+ $this->testStatus = $testStatus;
+ }
+ /**
+ * @return TestStatus
+ */
+ public function getTestStatus()
+ {
+ return $this->testStatus;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(RunMobileFriendlyTestResponse::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_RunMobileFriendlyTestResponse');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SearchAnalyticsQueryRequest.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SearchAnalyticsQueryRequest.php
new file mode 100644
index 0000000..ad7a15e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SearchAnalyticsQueryRequest.php
@@ -0,0 +1,122 @@
+aggregationType = $aggregationType;
+ }
+ public function getAggregationType()
+ {
+ return $this->aggregationType;
+ }
+ public function setDataState($dataState)
+ {
+ $this->dataState = $dataState;
+ }
+ public function getDataState()
+ {
+ return $this->dataState;
+ }
+ /**
+ * @param ApiDimensionFilterGroup[]
+ */
+ public function setDimensionFilterGroups($dimensionFilterGroups)
+ {
+ $this->dimensionFilterGroups = $dimensionFilterGroups;
+ }
+ /**
+ * @return ApiDimensionFilterGroup[]
+ */
+ public function getDimensionFilterGroups()
+ {
+ return $this->dimensionFilterGroups;
+ }
+ public function setDimensions($dimensions)
+ {
+ $this->dimensions = $dimensions;
+ }
+ public function getDimensions()
+ {
+ return $this->dimensions;
+ }
+ public function setEndDate($endDate)
+ {
+ $this->endDate = $endDate;
+ }
+ public function getEndDate()
+ {
+ return $this->endDate;
+ }
+ public function setRowLimit($rowLimit)
+ {
+ $this->rowLimit = $rowLimit;
+ }
+ public function getRowLimit()
+ {
+ return $this->rowLimit;
+ }
+ public function setSearchType($searchType)
+ {
+ $this->searchType = $searchType;
+ }
+ public function getSearchType()
+ {
+ return $this->searchType;
+ }
+ public function setStartDate($startDate)
+ {
+ $this->startDate = $startDate;
+ }
+ public function getStartDate()
+ {
+ return $this->startDate;
+ }
+ public function setStartRow($startRow)
+ {
+ $this->startRow = $startRow;
+ }
+ public function getStartRow()
+ {
+ return $this->startRow;
+ }
+ public function setType($type)
+ {
+ $this->type = $type;
+ }
+ public function getType()
+ {
+ return $this->type;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(SearchAnalyticsQueryRequest::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_SearchAnalyticsQueryRequest');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SearchAnalyticsQueryResponse.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SearchAnalyticsQueryResponse.php
new file mode 100644
index 0000000..40f4dbd
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SearchAnalyticsQueryResponse.php
@@ -0,0 +1,50 @@
+responseAggregationType = $responseAggregationType;
+ }
+ public function getResponseAggregationType()
+ {
+ return $this->responseAggregationType;
+ }
+ /**
+ * @param ApiDataRow[]
+ */
+ public function setRows($rows)
+ {
+ $this->rows = $rows;
+ }
+ /**
+ * @return ApiDataRow[]
+ */
+ public function getRows()
+ {
+ return $this->rows;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(SearchAnalyticsQueryResponse::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_SearchAnalyticsQueryResponse');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SitemapsListResponse.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SitemapsListResponse.php
new file mode 100644
index 0000000..3a8e4f8
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SitemapsListResponse.php
@@ -0,0 +1,41 @@
+sitemap = $sitemap;
+ }
+ /**
+ * @return WmxSitemap[]
+ */
+ public function getSitemap()
+ {
+ return $this->sitemap;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(SitemapsListResponse::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_SitemapsListResponse');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SitesListResponse.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SitesListResponse.php
new file mode 100644
index 0000000..794bac0
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SitesListResponse.php
@@ -0,0 +1,41 @@
+siteEntry = $siteEntry;
+ }
+ /**
+ * @return WmxSite[]
+ */
+ public function getSiteEntry()
+ {
+ return $this->siteEntry;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(SitesListResponse::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_SitesListResponse');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/TestStatus.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/TestStatus.php
new file mode 100644
index 0000000..f542027
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/TestStatus.php
@@ -0,0 +1,42 @@
+details = $details;
+ }
+ public function getDetails()
+ {
+ return $this->details;
+ }
+ public function setStatus($status)
+ {
+ $this->status = $status;
+ }
+ public function getStatus()
+ {
+ return $this->status;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(TestStatus::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_TestStatus');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSite.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSite.php
new file mode 100644
index 0000000..40b9f4d
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSite.php
@@ -0,0 +1,42 @@
+permissionLevel = $permissionLevel;
+ }
+ public function getPermissionLevel()
+ {
+ return $this->permissionLevel;
+ }
+ public function setSiteUrl($siteUrl)
+ {
+ $this->siteUrl = $siteUrl;
+ }
+ public function getSiteUrl()
+ {
+ return $this->siteUrl;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(WmxSite::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_WmxSite');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSitemap.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSitemap.php
new file mode 100644
index 0000000..babe038
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSitemap.php
@@ -0,0 +1,113 @@
+contents = $contents;
+ }
+ /**
+ * @return WmxSitemapContent[]
+ */
+ public function getContents()
+ {
+ return $this->contents;
+ }
+ public function setErrors($errors)
+ {
+ $this->errors = $errors;
+ }
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+ public function setIsPending($isPending)
+ {
+ $this->isPending = $isPending;
+ }
+ public function getIsPending()
+ {
+ return $this->isPending;
+ }
+ public function setIsSitemapsIndex($isSitemapsIndex)
+ {
+ $this->isSitemapsIndex = $isSitemapsIndex;
+ }
+ public function getIsSitemapsIndex()
+ {
+ return $this->isSitemapsIndex;
+ }
+ public function setLastDownloaded($lastDownloaded)
+ {
+ $this->lastDownloaded = $lastDownloaded;
+ }
+ public function getLastDownloaded()
+ {
+ return $this->lastDownloaded;
+ }
+ public function setLastSubmitted($lastSubmitted)
+ {
+ $this->lastSubmitted = $lastSubmitted;
+ }
+ public function getLastSubmitted()
+ {
+ return $this->lastSubmitted;
+ }
+ public function setPath($path)
+ {
+ $this->path = $path;
+ }
+ public function getPath()
+ {
+ return $this->path;
+ }
+ public function setType($type)
+ {
+ $this->type = $type;
+ }
+ public function getType()
+ {
+ return $this->type;
+ }
+ public function setWarnings($warnings)
+ {
+ $this->warnings = $warnings;
+ }
+ public function getWarnings()
+ {
+ return $this->warnings;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(WmxSitemap::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_WmxSitemap');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSitemapContent.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSitemapContent.php
new file mode 100644
index 0000000..cb500e8
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSitemapContent.php
@@ -0,0 +1,51 @@
+indexed = $indexed;
+ }
+ public function getIndexed()
+ {
+ return $this->indexed;
+ }
+ public function setSubmitted($submitted)
+ {
+ $this->submitted = $submitted;
+ }
+ public function getSubmitted()
+ {
+ return $this->submitted;
+ }
+ public function setType($type)
+ {
+ $this->type = $type;
+ }
+ public function getType()
+ {
+ return $this->type;
+ }
+}
+// Adding a class alias for backwards compatibility with the previous class name.
+class_alias(WmxSitemapContent::class, 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_SearchConsole_WmxSitemapContent');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/synth.metadata b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/synth.metadata
new file mode 100644
index 0000000..ccae829
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/synth.metadata
@@ -0,0 +1,18 @@
+{
+ "sources": [
+ {
+ "git": {
+ "name": ".",
+ "remote": "https://github.com/googleapis/google-api-php-client-services.git",
+ "sha": "6056867755775c54a179bc49f29028c76b7ae0c2"
+ }
+ },
+ {
+ "git": {
+ "name": "discovery-artifact-manager",
+ "remote": "https://github.com/googleapis/discovery-artifact-manager.git",
+ "sha": "4052db7ca555cbaf48bfaf93f32e575d7a3ff1d5"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/synth.py b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/synth.py
new file mode 100644
index 0000000..5dd63e2
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/synth.py
@@ -0,0 +1,119 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This script is used to synthesize generated parts of this library."""
+
+import synthtool as s
+from synthtool.__main__ import extra_args
+from synthtool import log, shell
+from synthtool.sources import git
+import logging
+from os import path, remove
+from pathlib import Path
+import glob
+import json
+import re
+import sys
+from packaging import version
+
+logging.basicConfig(level=logging.DEBUG)
+
+VERSION_REGEX = r"([^\.]*)\.(.+)\.json$"
+
+TEMPLATE_VERSIONS = [
+ "default",
+]
+discovery_url = "https://github.com/googleapis/discovery-artifact-manager.git"
+
+repository = Path('.')
+
+log.debug(f"Cloning {discovery_url}.")
+discovery = git.clone(discovery_url)
+
+log.debug("Cleaning output directory.")
+shell.run("rm -rf .cache".split(), cwd=repository)
+
+log.debug("Installing dependencies.")
+shell.run(
+ "python2 -m pip install -e generator/ --user".split(),
+ cwd=repository
+)
+
+def generate_service(disco: str):
+ m = re.search(VERSION_REGEX, disco)
+ name = m.group(1)
+ version = m.group(2)
+ template = TEMPLATE_VERSIONS[-1] # Generate for latest version
+
+ log.info(f"Generating {name} {version} ({template}).")
+
+ output_dir = repository / ".cache" / name / version
+ input_file = discovery / "discoveries" / disco
+
+ command = (
+ f"python2 -m googleapis.codegen --output_dir={output_dir}" +
+ f" --input={input_file} --language=php --language_variant={template}" +
+ f" --package_path=api/services"
+ )
+
+ shell.run(f"mkdir -p {output_dir}".split(), cwd=repository / "generator")
+ shell.run(command.split(), cwd=repository, hide_output=False)
+
+ s.copy(output_dir, f"src")
+
+def all_discoveries(skip=None, prefer=None):
+ """Returns a map of API IDs to Discovery document filenames.
+
+ Args:
+ skip (list, optional): a list of API IDs to skip.
+ prefer (list, optional): a list of API IDs to include.
+
+ Returns:
+ list(string): A list of Discovery document filenames.
+ """
+ discos = {}
+ for file in sorted(glob.glob(str(discovery / 'discoveries/*.*.json'))):
+ api_id = None
+ with open(file) as api_file:
+ api_id = json.load(api_file)['id']
+ # If an API has already been visited, skip it.
+ if api_id in discos:
+ continue
+ # Skip APIs explicitly listed in "skip" arg
+ if skip and api_id in skip:
+ continue
+ discos[api_id] = path.basename(file)
+
+ # Skip APIs not preferred in index.json and not listed in "prefer" arg
+ index = {}
+ with open(str(discovery / 'discoveries/index.json')) as file:
+ index = json.load(file)
+ for api in index['items']:
+ api_id = api['id']
+ if prefer and api_id in prefer:
+ continue
+ if api['preferred']:
+ continue
+ discos.pop(api_id, None)
+
+ return discos.values()
+
+def generate_services(services):
+ for service in services:
+ generate_service(service)
+
+skip = ['discovery:v1', 'websecurityscanner:v1']
+prefer = ['admin:directory_v1', 'admin:datatransfer_v1']
+discoveries = all_discoveries(skip, prefer)
+generate_services(discoveries)
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/LICENSE b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/LICENSE
new file mode 100644
index 0000000..a148ba5
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/LICENSE
@@ -0,0 +1,203 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by
+the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity
+exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but
+not limited to compiled object code, generated documentation,
+and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or
+Object form, made available under the License, as indicated by a
+copyright notice that is included in or attached to the work
+(an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes
+of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of,
+the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including
+the original version of the Work and any modifications or additions
+to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner
+or by an individual or Legal Entity authorized to submit on behalf of
+the copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems,
+and issue tracking systems that are managed by, or on behalf of, the
+Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the
+Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+(except as stated in this section) patent license to make, have made,
+use, offer to sell, sell, import, and otherwise transfer the Work,
+where such license applies only to those patent claims licensable
+by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s)
+with the Work to which such Contribution(s) was submitted. If You
+institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work
+or a Contribution incorporated within the Work constitutes direct
+or contributory patent infringement, then any patent licenses
+granted to You under this License for that Work shall terminate
+as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+Work or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You
+meet the following conditions:
+
+(a) You must give any other recipients of the Work or
+Derivative Works a copy of this License; and
+
+(b) You must cause any modified files to carry prominent notices
+stating that You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works
+that You distribute, all copyright, patent, trademark, and
+attribution notices from the Source form of the Work,
+excluding those notices that do not pertain to any part of
+the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its
+distribution, then any Derivative Works that You distribute must
+include a readable copy of the attribution notices contained
+within such NOTICE file, excluding those notices that do not
+pertain to any part of the Derivative Works, in at least one
+of the following places: within a NOTICE text file distributed
+as part of the Derivative Works; within the Source form or
+documentation, if provided along with the Derivative Works; or,
+within a display generated by the Derivative Works, if and
+wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and
+do not modify the License. You may add Your own attribution
+notices within Derivative Works that You distribute, alongside
+or as an addendum to the NOTICE text from the Work, provided
+that such additional attribution notices cannot be construed
+as modifying the License.
+
+You may add Your own copyright statement to Your modifications and
+may provide additional or different license terms and conditions
+for use, reproduction, or distribution of Your modifications, or
+for any such Derivative Works as a whole, provided Your use,
+reproduction, and distribution of the Work otherwise complies with
+the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+any Contribution intentionally submitted for inclusion in the Work
+by You to the Licensor shall be under the terms and conditions of
+this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify
+the terms of any separate license agreement you may have executed
+with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+names, trademarks, service marks, or product names of the Licensor,
+except as required for reasonable and customary use in describing the
+origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+agreed to in writing, Licensor provides the Work (and each
+Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied, including, without limitation, any warranties or conditions
+of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any
+risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise,
+unless required by applicable law (such as deliberate and grossly
+negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special,
+incidental, or consequential damages of any character arising as a
+result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses), even if such Contributor
+has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer,
+and charge a fee for, acceptance of support, warranty, indemnity,
+or other liability obligations and/or rights consistent with this
+License. However, in accepting such obligations, You may act only
+on Your own behalf and on Your sole responsibility, not on behalf
+of any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason
+of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following
+boilerplate notice, with the fields enclosed by brackets "[]"
+replaced with your own identifying information. (Don't include
+the brackets!) The text should be enclosed in the appropriate
+comment syntax for the file format. We also recommend that a
+file or class name and description of purpose be included on the
+same "printed page" as the copyright notice for easier
+identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AccessToken/Revoke.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AccessToken/Revoke.php
new file mode 100644
index 0000000..9ac4da2
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AccessToken/Revoke.php
@@ -0,0 +1,65 @@
+http = $http;
+ }
+ /**
+ * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
+ * token, if a token isn't provided.
+ *
+ * @param string|array $token The token (access token or a refresh token) that should be revoked.
+ * @return boolean Returns True if the revocation was successful, otherwise False.
+ */
+ public function revokeToken($token)
+ {
+ if (is_array($token)) {
+ if (isset($token['refresh_token'])) {
+ $token = $token['refresh_token'];
+ } else {
+ $token = $token['access_token'];
+ }
+ }
+ $body = Psr7\Utils::streamFor(http_build_query(['token' => $token]));
+ $request = new Request('POST', Client::OAUTH2_REVOKE_URI, ['Cache-Control' => 'no-store', 'Content-Type' => 'application/x-www-form-urlencoded'], $body);
+ $httpHandler = HttpHandlerFactory::build($this->http);
+ $response = $httpHandler($request);
+ return $response->getStatusCode() == 200;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AccessToken/Verify.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AccessToken/Verify.php
new file mode 100644
index 0000000..7793e4d
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AccessToken/Verify.php
@@ -0,0 +1,217 @@
+http = $http;
+ $this->cache = $cache;
+ $this->jwt = $jwt ?: $this->getJwtService();
+ }
+ /**
+ * Verifies an id token and returns the authenticated apiLoginTicket.
+ * Throws an exception if the id token is not valid.
+ * The audience parameter can be used to control which id tokens are
+ * accepted. By default, the id token must have been issued to this OAuth2 client.
+ *
+ * @param string $idToken the ID token in JWT format
+ * @param string $audience Optional. The audience to verify against JWt "aud"
+ * @return array|false the token payload, if successful
+ */
+ public function verifyIdToken($idToken, $audience = null)
+ {
+ if (empty($idToken)) {
+ throw new LogicException('id_token cannot be null');
+ }
+ // set phpseclib constants if applicable
+ $this->setPhpsecConstants();
+ // Check signature
+ $certs = $this->getFederatedSignOnCerts();
+ foreach ($certs as $cert) {
+ try {
+ $args = [$idToken];
+ $publicKey = $this->getPublicKey($cert);
+ if (class_exists(Key::class)) {
+ $args[] = new Key($publicKey, 'RS256');
+ } else {
+ $args[] = $publicKey;
+ $args[] = ['RS256'];
+ }
+ $payload = \call_user_func_array([$this->jwt, 'decode'], $args);
+ if (property_exists($payload, 'aud')) {
+ if ($audience && $payload->aud != $audience) {
+ return \false;
+ }
+ }
+ // support HTTP and HTTPS issuers
+ // @see https://developers.google.com/identity/sign-in/web/backend-auth
+ $issuers = [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS];
+ if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
+ return \false;
+ }
+ return (array) $payload;
+ } catch (ExpiredException $e) {
+ // @phpstan-ignore-line
+ return \false;
+ } catch (ExpiredExceptionV3 $e) {
+ return \false;
+ } catch (SignatureInvalidException $e) {
+ // continue
+ } catch (DomainException $e) {
+ // continue
+ }
+ }
+ return \false;
+ }
+ private function getCache()
+ {
+ return $this->cache;
+ }
+ /**
+ * Retrieve and cache a certificates file.
+ *
+ * @param string $url location
+ * @throws \Google\Exception
+ * @return array certificates
+ */
+ private function retrieveCertsFromLocation($url)
+ {
+ // If we're retrieving a local file, just grab it.
+ if (0 !== strpos($url, 'http')) {
+ if (!($file = file_get_contents($url))) {
+ throw new GoogleException("Failed to retrieve verification certificates: '" . $url . "'.");
+ }
+ return json_decode($file, \true);
+ }
+ // @phpstan-ignore-next-line
+ $response = $this->http->get($url);
+ if ($response->getStatusCode() == 200) {
+ return json_decode((string) $response->getBody(), \true);
+ }
+ throw new GoogleException(sprintf('Failed to retrieve verification certificates: "%s".', $response->getBody()->getContents()), $response->getStatusCode());
+ }
+ // Gets federated sign-on certificates to use for verifying identity tokens.
+ // Returns certs as array structure, where keys are key ids, and values
+ // are PEM encoded certificates.
+ private function getFederatedSignOnCerts()
+ {
+ $certs = null;
+ if ($cache = $this->getCache()) {
+ $cacheItem = $cache->getItem('federated_signon_certs_v3');
+ $certs = $cacheItem->get();
+ }
+ if (!$certs) {
+ $certs = $this->retrieveCertsFromLocation(self::FEDERATED_SIGNON_CERT_URL);
+ if ($cache) {
+ $cacheItem->expiresAt(new DateTime('+1 hour'));
+ $cacheItem->set($certs);
+ $cache->save($cacheItem);
+ }
+ }
+ if (!isset($certs['keys'])) {
+ throw new InvalidArgumentException('federated sign-on certs expects "keys" to be set');
+ }
+ return $certs['keys'];
+ }
+ private function getJwtService()
+ {
+ $jwt = new JWT();
+ if ($jwt::$leeway < 1) {
+ // Ensures JWT leeway is at least 1
+ // @see https://github.com/google/google-api-php-client/issues/827
+ $jwt::$leeway = 1;
+ }
+ return $jwt;
+ }
+ private function getPublicKey($cert)
+ {
+ $modulus = new BigInteger($this->jwt->urlsafeB64Decode($cert['n']), 256);
+ $exponent = new BigInteger($this->jwt->urlsafeB64Decode($cert['e']), 256);
+ $component = ['n' => $modulus, 'e' => $exponent];
+ $loader = PublicKeyLoader::load($component);
+ return $loader->toString('PKCS8');
+ }
+ /**
+ * phpseclib calls "phpinfo" by default, which requires special
+ * whitelisting in the AppEngine VM environment. This function
+ * sets constants to bypass the need for phpseclib to check phpinfo
+ *
+ * @see phpseclib/Math/BigInteger
+ * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85
+ */
+ private function setPhpsecConstants()
+ {
+ if (filter_var(getenv('GAE_VM'), \FILTER_VALIDATE_BOOLEAN)) {
+ if (!defined('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\MATH_BIGINTEGER_OPENSSL_ENABLED')) {
+ define('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\MATH_BIGINTEGER_OPENSSL_ENABLED', \true);
+ }
+ if (!defined('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\CRYPT_RSA_MODE')) {
+ define('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\CRYPT_RSA_MODE', AES::ENGINE_OPENSSL);
+ }
+ }
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/AuthHandlerFactory.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/AuthHandlerFactory.php
new file mode 100644
index 0000000..efd004f
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/AuthHandlerFactory.php
@@ -0,0 +1,47 @@
+cache = $cache;
+ $this->cacheConfig = $cacheConfig;
+ }
+ public function attachCredentials(ClientInterface $http, CredentialsLoader $credentials, callable $tokenCallback = null)
+ {
+ // use the provided cache
+ if ($this->cache) {
+ $credentials = new FetchAuthTokenCache($credentials, $this->cacheConfig, $this->cache);
+ }
+ return $this->attachCredentialsCache($http, $credentials, $tokenCallback);
+ }
+ public function attachCredentialsCache(ClientInterface $http, FetchAuthTokenCache $credentials, callable $tokenCallback = null)
+ {
+ // if we end up needing to make an HTTP request to retrieve credentials, we
+ // can use our existing one, but we need to throw exceptions so the error
+ // bubbles up.
+ $authHttp = $this->createAuthHttp($http);
+ $authHttpHandler = HttpHandlerFactory::build($authHttp);
+ $middleware = new AuthTokenMiddleware($credentials, $authHttpHandler, $tokenCallback);
+ $config = $http->getConfig();
+ $config['handler']->remove('google_auth');
+ $config['handler']->push($middleware, 'google_auth');
+ $config['auth'] = 'google_auth';
+ $http = new Client($config);
+ return $http;
+ }
+ public function attachToken(ClientInterface $http, array $token, array $scopes)
+ {
+ $tokenFunc = function ($scopes) use($token) {
+ return $token['access_token'];
+ };
+ $middleware = new ScopedAccessTokenMiddleware($tokenFunc, $scopes, $this->cacheConfig, $this->cache);
+ $config = $http->getConfig();
+ $config['handler']->remove('google_auth');
+ $config['handler']->push($middleware, 'google_auth');
+ $config['auth'] = 'scoped';
+ $http = new Client($config);
+ return $http;
+ }
+ public function attachKey(ClientInterface $http, $key)
+ {
+ $middleware = new SimpleMiddleware(['key' => $key]);
+ $config = $http->getConfig();
+ $config['handler']->remove('google_auth');
+ $config['handler']->push($middleware, 'google_auth');
+ $config['auth'] = 'simple';
+ $http = new Client($config);
+ return $http;
+ }
+ private function createAuthHttp(ClientInterface $http)
+ {
+ return new Client(['http_errors' => \true] + $http->getConfig());
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/Guzzle7AuthHandler.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/Guzzle7AuthHandler.php
new file mode 100644
index 0000000..967861a
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/Guzzle7AuthHandler.php
@@ -0,0 +1,25 @@
+config = array_merge([
+ 'application_name' => '',
+ // Don't change these unless you're working against a special development
+ // or testing environment.
+ 'base_path' => self::API_BASE_PATH,
+ // https://developers.google.com/console
+ 'client_id' => '',
+ 'client_secret' => '',
+ // Can be a path to JSON credentials or an array representing those
+ // credentials (@see Google\Client::setAuthConfig), or an instance of
+ // Google\Auth\CredentialsLoader.
+ 'credentials' => null,
+ // @see Google\Client::setScopes
+ 'scopes' => null,
+ // Sets X-Goog-User-Project, which specifies a user project to bill
+ // for access charges associated with the request
+ 'quota_project' => null,
+ 'redirect_uri' => null,
+ 'state' => null,
+ // Simple API access key, also from the API console. Ensure you get
+ // a Server key, and not a Browser key.
+ 'developer_key' => '',
+ // For use with Google Cloud Platform
+ // fetch the ApplicationDefaultCredentials, if applicable
+ // @see https://developers.google.com/identity/protocols/application-default-credentials
+ 'use_application_default_credentials' => \false,
+ 'signing_key' => null,
+ 'signing_algorithm' => null,
+ 'subject' => null,
+ // Other OAuth2 parameters.
+ 'hd' => '',
+ 'prompt' => '',
+ 'openid.realm' => '',
+ 'include_granted_scopes' => null,
+ 'login_hint' => '',
+ 'request_visible_actions' => '',
+ 'access_type' => 'online',
+ 'approval_prompt' => 'auto',
+ // Task Runner retry configuration
+ // @see Google\Task\Runner
+ 'retry' => [],
+ 'retry_map' => null,
+ // Cache class implementing Psr\Cache\CacheItemPoolInterface.
+ // Defaults to Google\Auth\Cache\MemoryCacheItemPool.
+ 'cache' => null,
+ // cache config for downstream auth caching
+ 'cache_config' => [],
+ // function to be called when an access token is fetched
+ // follows the signature function ($cacheKey, $accessToken)
+ 'token_callback' => null,
+ // Service class used in Google\Client::verifyIdToken.
+ // Explicitly pass this in to avoid setting JWT::$leeway
+ 'jwt' => null,
+ // Setting api_format_v2 will return more detailed error messages
+ // from certain APIs.
+ 'api_format_v2' => \false,
+ ], $config);
+ if (!is_null($this->config['credentials'])) {
+ if ($this->config['credentials'] instanceof CredentialsLoader) {
+ $this->credentials = $this->config['credentials'];
+ } else {
+ $this->setAuthConfig($this->config['credentials']);
+ }
+ unset($this->config['credentials']);
+ }
+ if (!is_null($this->config['scopes'])) {
+ $this->setScopes($this->config['scopes']);
+ unset($this->config['scopes']);
+ }
+ // Set a default token callback to update the in-memory access token
+ if (is_null($this->config['token_callback'])) {
+ $this->config['token_callback'] = function ($cacheKey, $newAccessToken) {
+ $this->setAccessToken([
+ 'access_token' => $newAccessToken,
+ 'expires_in' => 3600,
+ // Google default
+ 'created' => time(),
+ ]);
+ };
+ }
+ if (!is_null($this->config['cache'])) {
+ $this->setCache($this->config['cache']);
+ unset($this->config['cache']);
+ }
+ }
+ /**
+ * Get a string containing the version of the library.
+ *
+ * @return string
+ */
+ public function getLibraryVersion()
+ {
+ return self::LIBVER;
+ }
+ /**
+ * For backwards compatibility
+ * alias for fetchAccessTokenWithAuthCode
+ *
+ * @param string $code string code from accounts.google.com
+ * @return array access token
+ * @deprecated
+ */
+ public function authenticate($code)
+ {
+ return $this->fetchAccessTokenWithAuthCode($code);
+ }
+ /**
+ * Attempt to exchange a code for an valid authentication token.
+ * Helper wrapped around the OAuth 2.0 implementation.
+ *
+ * @param string $code code from accounts.google.com
+ * @param string $codeVerifier the code verifier used for PKCE (if applicable)
+ * @return array access token
+ */
+ public function fetchAccessTokenWithAuthCode($code, $codeVerifier = null)
+ {
+ if (strlen($code) == 0) {
+ throw new InvalidArgumentException("Invalid code");
+ }
+ $auth = $this->getOAuth2Service();
+ $auth->setCode($code);
+ $auth->setRedirectUri($this->getRedirectUri());
+ if ($codeVerifier) {
+ $auth->setCodeVerifier($codeVerifier);
+ }
+ $httpHandler = HttpHandlerFactory::build($this->getHttpClient());
+ $creds = $auth->fetchAuthToken($httpHandler);
+ if ($creds && isset($creds['access_token'])) {
+ $creds['created'] = time();
+ $this->setAccessToken($creds);
+ }
+ return $creds;
+ }
+ /**
+ * For backwards compatibility
+ * alias for fetchAccessTokenWithAssertion
+ *
+ * @return array access token
+ * @deprecated
+ */
+ public function refreshTokenWithAssertion()
+ {
+ return $this->fetchAccessTokenWithAssertion();
+ }
+ /**
+ * Fetches a fresh access token with a given assertion token.
+ * @param ClientInterface $authHttp optional.
+ * @return array access token
+ */
+ public function fetchAccessTokenWithAssertion(ClientInterface $authHttp = null)
+ {
+ if (!$this->isUsingApplicationDefaultCredentials()) {
+ throw new DomainException('set the JSON service account credentials using' . ' Google\\Client::setAuthConfig or set the path to your JSON file' . ' with the "GOOGLE_APPLICATION_CREDENTIALS" environment variable' . ' and call Google\\Client::useApplicationDefaultCredentials to' . ' refresh a token with assertion.');
+ }
+ $this->getLogger()->log('info', 'OAuth2 access token refresh with Signed JWT assertion grants.');
+ $credentials = $this->createApplicationDefaultCredentials();
+ $httpHandler = HttpHandlerFactory::build($authHttp);
+ $creds = $credentials->fetchAuthToken($httpHandler);
+ if ($creds && isset($creds['access_token'])) {
+ $creds['created'] = time();
+ $this->setAccessToken($creds);
+ }
+ return $creds;
+ }
+ /**
+ * For backwards compatibility
+ * alias for fetchAccessTokenWithRefreshToken
+ *
+ * @param string $refreshToken
+ * @return array access token
+ */
+ public function refreshToken($refreshToken)
+ {
+ return $this->fetchAccessTokenWithRefreshToken($refreshToken);
+ }
+ /**
+ * Fetches a fresh OAuth 2.0 access token with the given refresh token.
+ * @param string $refreshToken
+ * @return array access token
+ */
+ public function fetchAccessTokenWithRefreshToken($refreshToken = null)
+ {
+ if (null === $refreshToken) {
+ if (!isset($this->token['refresh_token'])) {
+ throw new LogicException('refresh token must be passed in or set as part of setAccessToken');
+ }
+ $refreshToken = $this->token['refresh_token'];
+ }
+ $this->getLogger()->info('OAuth2 access token refresh');
+ $auth = $this->getOAuth2Service();
+ $auth->setRefreshToken($refreshToken);
+ $httpHandler = HttpHandlerFactory::build($this->getHttpClient());
+ $creds = $auth->fetchAuthToken($httpHandler);
+ if ($creds && isset($creds['access_token'])) {
+ $creds['created'] = time();
+ if (!isset($creds['refresh_token'])) {
+ $creds['refresh_token'] = $refreshToken;
+ }
+ $this->setAccessToken($creds);
+ }
+ return $creds;
+ }
+ /**
+ * Create a URL to obtain user authorization.
+ * The authorization endpoint allows the user to first
+ * authenticate, and then grant/deny the access request.
+ * @param string|array $scope The scope is expressed as an array or list of space-delimited strings.
+ * @param array $queryParams Querystring params to add to the authorization URL.
+ * @return string
+ */
+ public function createAuthUrl($scope = null, array $queryParams = [])
+ {
+ if (empty($scope)) {
+ $scope = $this->prepareScopes();
+ }
+ if (is_array($scope)) {
+ $scope = implode(' ', $scope);
+ }
+ // only accept one of prompt or approval_prompt
+ $approvalPrompt = $this->config['prompt'] ? null : $this->config['approval_prompt'];
+ // include_granted_scopes should be string "true", string "false", or null
+ $includeGrantedScopes = $this->config['include_granted_scopes'] === null ? null : var_export($this->config['include_granted_scopes'], \true);
+ $params = array_filter(['access_type' => $this->config['access_type'], 'approval_prompt' => $approvalPrompt, 'hd' => $this->config['hd'], 'include_granted_scopes' => $includeGrantedScopes, 'login_hint' => $this->config['login_hint'], 'openid.realm' => $this->config['openid.realm'], 'prompt' => $this->config['prompt'], 'redirect_uri' => $this->config['redirect_uri'], 'response_type' => 'code', 'scope' => $scope, 'state' => $this->config['state']]) + $queryParams;
+ // If the list of scopes contains plus.login, add request_visible_actions
+ // to auth URL.
+ $rva = $this->config['request_visible_actions'];
+ if (strlen($rva) > 0 && \false !== strpos($scope, 'plus.login')) {
+ $params['request_visible_actions'] = $rva;
+ }
+ $auth = $this->getOAuth2Service();
+ return (string) $auth->buildFullAuthorizationUri($params);
+ }
+ /**
+ * Adds auth listeners to the HTTP client based on the credentials
+ * set in the Google API Client object
+ *
+ * @param ClientInterface $http the http client object.
+ * @return ClientInterface the http client object
+ */
+ public function authorize(ClientInterface $http = null)
+ {
+ $http = $http ?: $this->getHttpClient();
+ $authHandler = $this->getAuthHandler();
+ // These conditionals represent the decision tree for authentication
+ // 1. Check if a Google\Auth\CredentialsLoader instance has been supplied via the "credentials" option
+ // 2. Check for Application Default Credentials
+ // 3a. Check for an Access Token
+ // 3b. If access token exists but is expired, try to refresh it
+ // 4. Check for API Key
+ if ($this->credentials) {
+ return $authHandler->attachCredentials($http, $this->credentials, $this->config['token_callback']);
+ }
+ if ($this->isUsingApplicationDefaultCredentials()) {
+ $credentials = $this->createApplicationDefaultCredentials();
+ return $authHandler->attachCredentialsCache($http, $credentials, $this->config['token_callback']);
+ }
+ if ($token = $this->getAccessToken()) {
+ $scopes = $this->prepareScopes();
+ // add refresh subscriber to request a new token
+ if (isset($token['refresh_token']) && $this->isAccessTokenExpired()) {
+ $credentials = $this->createUserRefreshCredentials($scopes, $token['refresh_token']);
+ return $authHandler->attachCredentials($http, $credentials, $this->config['token_callback']);
+ }
+ return $authHandler->attachToken($http, $token, (array) $scopes);
+ }
+ if ($key = $this->config['developer_key']) {
+ return $authHandler->attachKey($http, $key);
+ }
+ return $http;
+ }
+ /**
+ * Set the configuration to use application default credentials for
+ * authentication
+ *
+ * @see https://developers.google.com/identity/protocols/application-default-credentials
+ * @param boolean $useAppCreds
+ */
+ public function useApplicationDefaultCredentials($useAppCreds = \true)
+ {
+ $this->config['use_application_default_credentials'] = $useAppCreds;
+ }
+ /**
+ * To prevent useApplicationDefaultCredentials from inappropriately being
+ * called in a conditional
+ *
+ * @see https://developers.google.com/identity/protocols/application-default-credentials
+ */
+ public function isUsingApplicationDefaultCredentials()
+ {
+ return $this->config['use_application_default_credentials'];
+ }
+ /**
+ * Set the access token used for requests.
+ *
+ * Note that at the time requests are sent, tokens are cached. A token will be
+ * cached for each combination of service and authentication scopes. If a
+ * cache pool is not provided, creating a new instance of the client will
+ * allow modification of access tokens. If a persistent cache pool is
+ * provided, in order to change the access token, you must clear the cached
+ * token by calling `$client->getCache()->clear()`. (Use caution in this case,
+ * as calling `clear()` will remove all cache items, including any items not
+ * related to Google API PHP Client.)
+ *
+ * @param string|array $token
+ * @throws InvalidArgumentException
+ */
+ public function setAccessToken($token)
+ {
+ if (is_string($token)) {
+ if ($json = json_decode($token, \true)) {
+ $token = $json;
+ } else {
+ // assume $token is just the token string
+ $token = ['access_token' => $token];
+ }
+ }
+ if ($token == null) {
+ throw new InvalidArgumentException('invalid json token');
+ }
+ if (!isset($token['access_token'])) {
+ throw new InvalidArgumentException("Invalid token format");
+ }
+ $this->token = $token;
+ }
+ public function getAccessToken()
+ {
+ return $this->token;
+ }
+ /**
+ * @return string|null
+ */
+ public function getRefreshToken()
+ {
+ if (isset($this->token['refresh_token'])) {
+ return $this->token['refresh_token'];
+ }
+ return null;
+ }
+ /**
+ * Returns if the access_token is expired.
+ * @return bool Returns True if the access_token is expired.
+ */
+ public function isAccessTokenExpired()
+ {
+ if (!$this->token) {
+ return \true;
+ }
+ $created = 0;
+ if (isset($this->token['created'])) {
+ $created = $this->token['created'];
+ } elseif (isset($this->token['id_token'])) {
+ // check the ID token for "iat"
+ // signature verification is not required here, as we are just
+ // using this for convenience to save a round trip request
+ // to the Google API server
+ $idToken = $this->token['id_token'];
+ if (substr_count($idToken, '.') == 2) {
+ $parts = explode('.', $idToken);
+ $payload = json_decode(base64_decode($parts[1]), \true);
+ if ($payload && isset($payload['iat'])) {
+ $created = $payload['iat'];
+ }
+ }
+ }
+ if (!isset($this->token['expires_in'])) {
+ // if the token does not have an "expires_in", then it's considered expired
+ return \true;
+ }
+ // If the token is set to expire in the next 30 seconds.
+ return $created + ($this->token['expires_in'] - 30) < time();
+ }
+ /**
+ * @deprecated See UPGRADING.md for more information
+ */
+ public function getAuth()
+ {
+ throw new BadMethodCallException('This function no longer exists. See UPGRADING.md for more information');
+ }
+ /**
+ * @deprecated See UPGRADING.md for more information
+ */
+ public function setAuth($auth)
+ {
+ throw new BadMethodCallException('This function no longer exists. See UPGRADING.md for more information');
+ }
+ /**
+ * Set the OAuth 2.0 Client ID.
+ * @param string $clientId
+ */
+ public function setClientId($clientId)
+ {
+ $this->config['client_id'] = $clientId;
+ }
+ public function getClientId()
+ {
+ return $this->config['client_id'];
+ }
+ /**
+ * Set the OAuth 2.0 Client Secret.
+ * @param string $clientSecret
+ */
+ public function setClientSecret($clientSecret)
+ {
+ $this->config['client_secret'] = $clientSecret;
+ }
+ public function getClientSecret()
+ {
+ return $this->config['client_secret'];
+ }
+ /**
+ * Set the OAuth 2.0 Redirect URI.
+ * @param string $redirectUri
+ */
+ public function setRedirectUri($redirectUri)
+ {
+ $this->config['redirect_uri'] = $redirectUri;
+ }
+ public function getRedirectUri()
+ {
+ return $this->config['redirect_uri'];
+ }
+ /**
+ * Set OAuth 2.0 "state" parameter to achieve per-request customization.
+ * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2.2
+ * @param string $state
+ */
+ public function setState($state)
+ {
+ $this->config['state'] = $state;
+ }
+ /**
+ * @param string $accessType Possible values for access_type include:
+ * {@code "offline"} to request offline access from the user.
+ * {@code "online"} to request online access from the user.
+ */
+ public function setAccessType($accessType)
+ {
+ $this->config['access_type'] = $accessType;
+ }
+ /**
+ * @param string $approvalPrompt Possible values for approval_prompt include:
+ * {@code "force"} to force the approval UI to appear.
+ * {@code "auto"} to request auto-approval when possible. (This is the default value)
+ */
+ public function setApprovalPrompt($approvalPrompt)
+ {
+ $this->config['approval_prompt'] = $approvalPrompt;
+ }
+ /**
+ * Set the login hint, email address or sub id.
+ * @param string $loginHint
+ */
+ public function setLoginHint($loginHint)
+ {
+ $this->config['login_hint'] = $loginHint;
+ }
+ /**
+ * Set the application name, this is included in the User-Agent HTTP header.
+ * @param string $applicationName
+ */
+ public function setApplicationName($applicationName)
+ {
+ $this->config['application_name'] = $applicationName;
+ }
+ /**
+ * If 'plus.login' is included in the list of requested scopes, you can use
+ * this method to define types of app activities that your app will write.
+ * You can find a list of available types here:
+ * @link https://developers.google.com/+/api/moment-types
+ *
+ * @param array $requestVisibleActions Array of app activity types
+ */
+ public function setRequestVisibleActions($requestVisibleActions)
+ {
+ if (is_array($requestVisibleActions)) {
+ $requestVisibleActions = implode(" ", $requestVisibleActions);
+ }
+ $this->config['request_visible_actions'] = $requestVisibleActions;
+ }
+ /**
+ * Set the developer key to use, these are obtained through the API Console.
+ * @see http://code.google.com/apis/console-help/#generatingdevkeys
+ * @param string $developerKey
+ */
+ public function setDeveloperKey($developerKey)
+ {
+ $this->config['developer_key'] = $developerKey;
+ }
+ /**
+ * Set the hd (hosted domain) parameter streamlines the login process for
+ * Google Apps hosted accounts. By including the domain of the user, you
+ * restrict sign-in to accounts at that domain.
+ * @param string $hd the domain to use.
+ */
+ public function setHostedDomain($hd)
+ {
+ $this->config['hd'] = $hd;
+ }
+ /**
+ * Set the prompt hint. Valid values are none, consent and select_account.
+ * If no value is specified and the user has not previously authorized
+ * access, then the user is shown a consent screen.
+ * @param string $prompt
+ * {@code "none"} Do not display any authentication or consent screens. Must not be specified with other values.
+ * {@code "consent"} Prompt the user for consent.
+ * {@code "select_account"} Prompt the user to select an account.
+ */
+ public function setPrompt($prompt)
+ {
+ $this->config['prompt'] = $prompt;
+ }
+ /**
+ * openid.realm is a parameter from the OpenID 2.0 protocol, not from OAuth
+ * 2.0. It is used in OpenID 2.0 requests to signify the URL-space for which
+ * an authentication request is valid.
+ * @param string $realm the URL-space to use.
+ */
+ public function setOpenidRealm($realm)
+ {
+ $this->config['openid.realm'] = $realm;
+ }
+ /**
+ * If this is provided with the value true, and the authorization request is
+ * granted, the authorization will include any previous authorizations
+ * granted to this user/application combination for other scopes.
+ * @param bool $include the URL-space to use.
+ */
+ public function setIncludeGrantedScopes($include)
+ {
+ $this->config['include_granted_scopes'] = $include;
+ }
+ /**
+ * sets function to be called when an access token is fetched
+ * @param callable $tokenCallback - function ($cacheKey, $accessToken)
+ */
+ public function setTokenCallback(callable $tokenCallback)
+ {
+ $this->config['token_callback'] = $tokenCallback;
+ }
+ /**
+ * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
+ * token, if a token isn't provided.
+ *
+ * @param string|array|null $token The token (access token or a refresh token) that should be revoked.
+ * @return boolean Returns True if the revocation was successful, otherwise False.
+ */
+ public function revokeToken($token = null)
+ {
+ $tokenRevoker = new Revoke($this->getHttpClient());
+ return $tokenRevoker->revokeToken($token ?: $this->getAccessToken());
+ }
+ /**
+ * Verify an id_token. This method will verify the current id_token, if one
+ * isn't provided.
+ *
+ * @throws LogicException If no token was provided and no token was set using `setAccessToken`.
+ * @throws UnexpectedValueException If the token is not a valid JWT.
+ * @param string|null $idToken The token (id_token) that should be verified.
+ * @return array|false Returns the token payload as an array if the verification was
+ * successful, false otherwise.
+ */
+ public function verifyIdToken($idToken = null)
+ {
+ $tokenVerifier = new Verify($this->getHttpClient(), $this->getCache(), $this->config['jwt']);
+ if (null === $idToken) {
+ $token = $this->getAccessToken();
+ if (!isset($token['id_token'])) {
+ throw new LogicException('id_token must be passed in or set as part of setAccessToken');
+ }
+ $idToken = $token['id_token'];
+ }
+ return $tokenVerifier->verifyIdToken($idToken, $this->getClientId());
+ }
+ /**
+ * Set the scopes to be requested. Must be called before createAuthUrl().
+ * Will remove any previously configured scopes.
+ * @param string|array $scope_or_scopes, ie:
+ * array(
+ * 'https://www.googleapis.com/auth/plus.login',
+ * 'https://www.googleapis.com/auth/moderator'
+ * );
+ */
+ public function setScopes($scope_or_scopes)
+ {
+ $this->requestedScopes = [];
+ $this->addScope($scope_or_scopes);
+ }
+ /**
+ * This functions adds a scope to be requested as part of the OAuth2.0 flow.
+ * Will append any scopes not previously requested to the scope parameter.
+ * A single string will be treated as a scope to request. An array of strings
+ * will each be appended.
+ * @param string|string[] $scope_or_scopes e.g. "profile"
+ */
+ public function addScope($scope_or_scopes)
+ {
+ if (is_string($scope_or_scopes) && !in_array($scope_or_scopes, $this->requestedScopes)) {
+ $this->requestedScopes[] = $scope_or_scopes;
+ } elseif (is_array($scope_or_scopes)) {
+ foreach ($scope_or_scopes as $scope) {
+ $this->addScope($scope);
+ }
+ }
+ }
+ /**
+ * Returns the list of scopes requested by the client
+ * @return array the list of scopes
+ *
+ */
+ public function getScopes()
+ {
+ return $this->requestedScopes;
+ }
+ /**
+ * @return string|null
+ * @visible For Testing
+ */
+ public function prepareScopes()
+ {
+ if (empty($this->requestedScopes)) {
+ return null;
+ }
+ return implode(' ', $this->requestedScopes);
+ }
+ /**
+ * Helper method to execute deferred HTTP requests.
+ *
+ * @template T
+ * @param RequestInterface $request
+ * @param class-string|false|null $expectedClass
+ * @throws \Google\Exception
+ * @return mixed|T|ResponseInterface
+ */
+ public function execute(RequestInterface $request, $expectedClass = null)
+ {
+ $request = $request->withHeader('User-Agent', sprintf('%s %s%s', $this->config['application_name'], self::USER_AGENT_SUFFIX, $this->getLibraryVersion()))->withHeader('x-goog-api-client', sprintf('gl-php/%s gdcl/%s', phpversion(), $this->getLibraryVersion()));
+ if ($this->config['api_format_v2']) {
+ $request = $request->withHeader('X-GOOG-API-FORMAT-VERSION', '2');
+ }
+ // call the authorize method
+ // this is where most of the grunt work is done
+ $http = $this->authorize();
+ return REST::execute($http, $request, $expectedClass, $this->config['retry'], $this->config['retry_map']);
+ }
+ /**
+ * Declare whether batch calls should be used. This may increase throughput
+ * by making multiple requests in one connection.
+ *
+ * @param boolean $useBatch True if the batch support should
+ * be enabled. Defaults to False.
+ */
+ public function setUseBatch($useBatch)
+ {
+ // This is actually an alias for setDefer.
+ $this->setDefer($useBatch);
+ }
+ /**
+ * Are we running in Google AppEngine?
+ * return bool
+ */
+ public function isAppEngine()
+ {
+ return isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine') !== \false;
+ }
+ public function setConfig($name, $value)
+ {
+ $this->config[$name] = $value;
+ }
+ public function getConfig($name, $default = null)
+ {
+ return isset($this->config[$name]) ? $this->config[$name] : $default;
+ }
+ /**
+ * For backwards compatibility
+ * alias for setAuthConfig
+ *
+ * @param string $file the configuration file
+ * @throws \Google\Exception
+ * @deprecated
+ */
+ public function setAuthConfigFile($file)
+ {
+ $this->setAuthConfig($file);
+ }
+ /**
+ * Set the auth config from new or deprecated JSON config.
+ * This structure should match the file downloaded from
+ * the "Download JSON" button on in the Google Developer
+ * Console.
+ * @param string|array $config the configuration json
+ * @throws \Google\Exception
+ */
+ public function setAuthConfig($config)
+ {
+ if (is_string($config)) {
+ if (!file_exists($config)) {
+ throw new InvalidArgumentException(sprintf('file "%s" does not exist', $config));
+ }
+ $json = file_get_contents($config);
+ if (!($config = json_decode($json, \true))) {
+ throw new LogicException('invalid json for auth config');
+ }
+ }
+ $key = isset($config['installed']) ? 'installed' : 'web';
+ if (isset($config['type']) && $config['type'] == 'service_account') {
+ // application default credentials
+ $this->useApplicationDefaultCredentials();
+ // set the information from the config
+ $this->setClientId($config['client_id']);
+ $this->config['client_email'] = $config['client_email'];
+ $this->config['signing_key'] = $config['private_key'];
+ $this->config['signing_algorithm'] = 'HS256';
+ } elseif (isset($config[$key])) {
+ // old-style
+ $this->setClientId($config[$key]['client_id']);
+ $this->setClientSecret($config[$key]['client_secret']);
+ if (isset($config[$key]['redirect_uris'])) {
+ $this->setRedirectUri($config[$key]['redirect_uris'][0]);
+ }
+ } else {
+ // new-style
+ $this->setClientId($config['client_id']);
+ $this->setClientSecret($config['client_secret']);
+ if (isset($config['redirect_uris'])) {
+ $this->setRedirectUri($config['redirect_uris'][0]);
+ }
+ }
+ }
+ /**
+ * Use when the service account has been delegated domain wide access.
+ *
+ * @param string $subject an email address account to impersonate
+ */
+ public function setSubject($subject)
+ {
+ $this->config['subject'] = $subject;
+ }
+ /**
+ * Declare whether making API calls should make the call immediately, or
+ * return a request which can be called with ->execute();
+ *
+ * @param boolean $defer True if calls should not be executed right away.
+ */
+ public function setDefer($defer)
+ {
+ $this->deferExecution = $defer;
+ }
+ /**
+ * Whether or not to return raw requests
+ * @return boolean
+ */
+ public function shouldDefer()
+ {
+ return $this->deferExecution;
+ }
+ /**
+ * @return OAuth2 implementation
+ */
+ public function getOAuth2Service()
+ {
+ if (!isset($this->auth)) {
+ $this->auth = $this->createOAuth2Service();
+ }
+ return $this->auth;
+ }
+ /**
+ * create a default google auth object
+ */
+ protected function createOAuth2Service()
+ {
+ $auth = new OAuth2(['clientId' => $this->getClientId(), 'clientSecret' => $this->getClientSecret(), 'authorizationUri' => self::OAUTH2_AUTH_URL, 'tokenCredentialUri' => self::OAUTH2_TOKEN_URI, 'redirectUri' => $this->getRedirectUri(), 'issuer' => $this->config['client_id'], 'signingKey' => $this->config['signing_key'], 'signingAlgorithm' => $this->config['signing_algorithm']]);
+ return $auth;
+ }
+ /**
+ * Set the Cache object
+ * @param CacheItemPoolInterface $cache
+ */
+ public function setCache(CacheItemPoolInterface $cache)
+ {
+ $this->cache = $cache;
+ }
+ /**
+ * @return CacheItemPoolInterface
+ */
+ public function getCache()
+ {
+ if (!$this->cache) {
+ $this->cache = $this->createDefaultCache();
+ }
+ return $this->cache;
+ }
+ /**
+ * @param array $cacheConfig
+ */
+ public function setCacheConfig(array $cacheConfig)
+ {
+ $this->config['cache_config'] = $cacheConfig;
+ }
+ /**
+ * Set the Logger object
+ * @param LoggerInterface $logger
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ }
+ /**
+ * @return LoggerInterface
+ */
+ public function getLogger()
+ {
+ if (!isset($this->logger)) {
+ $this->logger = $this->createDefaultLogger();
+ }
+ return $this->logger;
+ }
+ protected function createDefaultLogger()
+ {
+ $logger = new Logger('google-api-php-client');
+ if ($this->isAppEngine()) {
+ $handler = new MonologSyslogHandler('app', \LOG_USER, Logger::NOTICE);
+ } else {
+ $handler = new MonologStreamHandler('php://stderr', Logger::NOTICE);
+ }
+ $logger->pushHandler($handler);
+ return $logger;
+ }
+ protected function createDefaultCache()
+ {
+ return new MemoryCacheItemPool();
+ }
+ /**
+ * Set the Http Client object
+ * @param ClientInterface $http
+ */
+ public function setHttpClient(ClientInterface $http)
+ {
+ $this->http = $http;
+ }
+ /**
+ * @return ClientInterface
+ */
+ public function getHttpClient()
+ {
+ if (null === $this->http) {
+ $this->http = $this->createDefaultHttpClient();
+ }
+ return $this->http;
+ }
+ /**
+ * Set the API format version.
+ *
+ * `true` will use V2, which may return more useful error messages.
+ *
+ * @param bool $value
+ */
+ public function setApiFormatV2($value)
+ {
+ $this->config['api_format_v2'] = (bool) $value;
+ }
+ protected function createDefaultHttpClient()
+ {
+ $guzzleVersion = null;
+ if (defined('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\GuzzleHttp\\ClientInterface::MAJOR_VERSION')) {
+ $guzzleVersion = ClientInterface::MAJOR_VERSION;
+ } elseif (defined('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\GuzzleHttp\\ClientInterface::VERSION')) {
+ $guzzleVersion = (int) substr(ClientInterface::VERSION, 0, 1);
+ }
+ if (5 === $guzzleVersion) {
+ $options = ['base_url' => $this->config['base_path'], 'defaults' => ['exceptions' => \false]];
+ if ($this->isAppEngine()) {
+ if (class_exists(StreamHandler::class)) {
+ // set StreamHandler on AppEngine by default
+ $options['handler'] = new StreamHandler();
+ $options['defaults']['verify'] = '/etc/ca-certificates.crt';
+ }
+ }
+ } elseif (6 === $guzzleVersion || 7 === $guzzleVersion) {
+ // guzzle 6 or 7
+ $options = ['base_uri' => $this->config['base_path'], 'http_errors' => \false];
+ } else {
+ throw new LogicException('Could not find supported version of Guzzle.');
+ }
+ return new GuzzleClient($options);
+ }
+ /**
+ * @return FetchAuthTokenCache
+ */
+ private function createApplicationDefaultCredentials()
+ {
+ $scopes = $this->prepareScopes();
+ $sub = $this->config['subject'];
+ $signingKey = $this->config['signing_key'];
+ // create credentials using values supplied in setAuthConfig
+ if ($signingKey) {
+ $serviceAccountCredentials = ['client_id' => $this->config['client_id'], 'client_email' => $this->config['client_email'], 'private_key' => $signingKey, 'type' => 'service_account', 'quota_project_id' => $this->config['quota_project']];
+ $credentials = CredentialsLoader::makeCredentials($scopes, $serviceAccountCredentials);
+ } else {
+ // When $sub is provided, we cannot pass cache classes to ::getCredentials
+ // because FetchAuthTokenCache::setSub does not exist.
+ // The result is when $sub is provided, calls to ::onGce are not cached.
+ $credentials = ApplicationDefaultCredentials::getCredentials($scopes, null, $sub ? null : $this->config['cache_config'], $sub ? null : $this->getCache(), $this->config['quota_project']);
+ }
+ // for service account domain-wide authority (impersonating a user)
+ // @see https://developers.google.com/identity/protocols/OAuth2ServiceAccount
+ if ($sub) {
+ if (!$credentials instanceof ServiceAccountCredentials) {
+ throw new DomainException('domain-wide authority requires service account credentials');
+ }
+ $credentials->setSub($sub);
+ }
+ // If we are not using FetchAuthTokenCache yet, create it now
+ if (!$credentials instanceof FetchAuthTokenCache) {
+ $credentials = new FetchAuthTokenCache($credentials, $this->config['cache_config'], $this->getCache());
+ }
+ return $credentials;
+ }
+ protected function getAuthHandler()
+ {
+ // Be very careful using the cache, as the underlying auth library's cache
+ // implementation is naive, and the cache keys do not account for user
+ // sessions.
+ //
+ // @see https://github.com/google/google-api-php-client/issues/821
+ return AuthHandlerFactory::build($this->getCache(), $this->config['cache_config']);
+ }
+ private function createUserRefreshCredentials($scope, $refreshToken)
+ {
+ $creds = array_filter(['client_id' => $this->getClientId(), 'client_secret' => $this->getClientSecret(), 'refresh_token' => $refreshToken]);
+ return new UserRefreshCredentials($scope, $creds);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Collection.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Collection.php
new file mode 100644
index 0000000..83fd2c7
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Collection.php
@@ -0,0 +1,104 @@
+{$this->collection_key}) && is_array($this->{$this->collection_key})) {
+ reset($this->{$this->collection_key});
+ }
+ }
+ /** @return mixed */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ $this->coerceType($this->key());
+ if (is_array($this->{$this->collection_key})) {
+ return current($this->{$this->collection_key});
+ }
+ }
+ /** @return mixed */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ if (isset($this->{$this->collection_key}) && is_array($this->{$this->collection_key})) {
+ return key($this->{$this->collection_key});
+ }
+ }
+ /** @return mixed */
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ return next($this->{$this->collection_key});
+ }
+ /** @return bool */
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ $key = $this->key();
+ return $key !== null && $key !== \false;
+ }
+ /** @return int */
+ #[\ReturnTypeWillChange]
+ public function count()
+ {
+ if (!isset($this->{$this->collection_key})) {
+ return 0;
+ }
+ return count($this->{$this->collection_key});
+ }
+ /** @return bool */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ if (!is_numeric($offset)) {
+ return parent::offsetExists($offset);
+ }
+ return isset($this->{$this->collection_key}[$offset]);
+ }
+ /** @return mixed */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset)
+ {
+ if (!is_numeric($offset)) {
+ return parent::offsetGet($offset);
+ }
+ $this->coerceType($offset);
+ return $this->{$this->collection_key}[$offset];
+ }
+ /** @return void */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ if (!is_numeric($offset)) {
+ parent::offsetSet($offset, $value);
+ }
+ $this->{$this->collection_key}[$offset] = $value;
+ }
+ /** @return void */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ if (!is_numeric($offset)) {
+ parent::offsetUnset($offset);
+ }
+ unset($this->{$this->collection_key}[$offset]);
+ }
+ private function coerceType($offset)
+ {
+ $keyType = $this->keyType($this->collection_key);
+ if ($keyType && !is_object($this->{$this->collection_key}[$offset])) {
+ $this->{$this->collection_key}[$offset] = new $keyType($this->{$this->collection_key}[$offset]);
+ }
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Exception.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Exception.php
new file mode 100644
index 0000000..462066e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Exception.php
@@ -0,0 +1,23 @@
+client = $client;
+ $this->boundary = $boundary ?: mt_rand();
+ $this->rootUrl = rtrim($rootUrl ?: $this->client->getConfig('base_path'), '/');
+ $this->batchPath = $batchPath ?: self::BATCH_PATH;
+ }
+ public function add(RequestInterface $request, $key = \false)
+ {
+ if (\false == $key) {
+ $key = mt_rand();
+ }
+ $this->requests[$key] = $request;
+ }
+ public function execute()
+ {
+ $body = '';
+ $classes = [];
+ $batchHttpTemplate = <<requests as $key => $request) {
+ $firstLine = sprintf('%s %s HTTP/%s', $request->getMethod(), $request->getRequestTarget(), $request->getProtocolVersion());
+ $content = (string) $request->getBody();
+ $headers = '';
+ foreach ($request->getHeaders() as $name => $values) {
+ $headers .= sprintf("%s:%s\r\n", $name, implode(', ', $values));
+ }
+ $body .= sprintf($batchHttpTemplate, $this->boundary, $key, $firstLine, $headers, $content ? "\n" . $content : '');
+ $classes['response-' . $key] = $request->getHeaderLine('X-Php-Expected-Class');
+ }
+ $body .= "--{$this->boundary}--";
+ $body = trim($body);
+ $url = $this->rootUrl . '/' . $this->batchPath;
+ $headers = ['Content-Type' => sprintf('multipart/mixed; boundary=%s', $this->boundary), 'Content-Length' => (string) strlen($body)];
+ $request = new Request('POST', $url, $headers, $body);
+ $response = $this->client->execute($request);
+ return $this->parseResponse($response, $classes);
+ }
+ public function parseResponse(ResponseInterface $response, $classes = [])
+ {
+ $contentType = $response->getHeaderLine('content-type');
+ $contentType = explode(';', $contentType);
+ $boundary = \false;
+ foreach ($contentType as $part) {
+ $part = explode('=', $part, 2);
+ if (isset($part[0]) && 'boundary' == trim($part[0])) {
+ $boundary = $part[1];
+ }
+ }
+ $body = (string) $response->getBody();
+ if (!empty($body)) {
+ $body = str_replace("--{$boundary}--", "--{$boundary}", $body);
+ $parts = explode("--{$boundary}", $body);
+ $responses = [];
+ $requests = array_values($this->requests);
+ foreach ($parts as $i => $part) {
+ $part = trim($part);
+ if (!empty($part)) {
+ list($rawHeaders, $part) = explode("\r\n\r\n", $part, 2);
+ $headers = $this->parseRawHeaders($rawHeaders);
+ $status = substr($part, 0, strpos($part, "\n"));
+ $status = explode(" ", $status);
+ $status = $status[1];
+ list($partHeaders, $partBody) = $this->parseHttpResponse($part, 0);
+ $response = new Response((int) $status, $partHeaders, Psr7\Utils::streamFor($partBody));
+ // Need content id.
+ $key = $headers['content-id'];
+ try {
+ $response = REST::decodeHttpResponse($response, $requests[$i - 1]);
+ } catch (GoogleServiceException $e) {
+ // Store the exception as the response, so successful responses
+ // can be processed.
+ $response = $e;
+ }
+ $responses[$key] = $response;
+ }
+ }
+ return $responses;
+ }
+ return null;
+ }
+ private function parseRawHeaders($rawHeaders)
+ {
+ $headers = [];
+ $responseHeaderLines = explode("\r\n", $rawHeaders);
+ foreach ($responseHeaderLines as $headerLine) {
+ if ($headerLine && strpos($headerLine, ':') !== \false) {
+ list($header, $value) = explode(': ', $headerLine, 2);
+ $header = strtolower($header);
+ if (isset($headers[$header])) {
+ $headers[$header] = array_merge((array) $headers[$header], (array) $value);
+ } else {
+ $headers[$header] = $value;
+ }
+ }
+ }
+ return $headers;
+ }
+ /**
+ * Used by the IO lib and also the batch processing.
+ *
+ * @param string $respData
+ * @param int $headerSize
+ * @return array
+ */
+ private function parseHttpResponse($respData, $headerSize)
+ {
+ // check proxy header
+ foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header) {
+ if (stripos($respData, $established_header) !== \false) {
+ // existed, remove it
+ $respData = str_ireplace($established_header, '', $respData);
+ // Subtract the proxy header size unless the cURL bug prior to 7.30.0
+ // is present which prevented the proxy header size from being taken into
+ // account.
+ // @TODO look into this
+ // if (!$this->needsQuirk()) {
+ // $headerSize -= strlen($established_header);
+ // }
+ break;
+ }
+ }
+ if ($headerSize) {
+ $responseBody = substr($respData, $headerSize);
+ $responseHeaders = substr($respData, 0, $headerSize);
+ } else {
+ $responseSegments = explode("\r\n\r\n", $respData, 2);
+ $responseHeaders = $responseSegments[0];
+ $responseBody = isset($responseSegments[1]) ? $responseSegments[1] : null;
+ }
+ $responseHeaders = $this->parseRawHeaders($responseHeaders);
+ return [$responseHeaders, $responseBody];
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/MediaFileUpload.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/MediaFileUpload.php
new file mode 100644
index 0000000..eaae907
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/MediaFileUpload.php
@@ -0,0 +1,273 @@
+client = $client;
+ $this->request = $request;
+ $this->mimeType = $mimeType;
+ $this->data = $data;
+ $this->resumable = $resumable;
+ $this->chunkSize = $chunkSize;
+ $this->progress = 0;
+ $this->process();
+ }
+ /**
+ * Set the size of the file that is being uploaded.
+ * @param int $size - int file size in bytes
+ */
+ public function setFileSize($size)
+ {
+ $this->size = $size;
+ }
+ /**
+ * Return the progress on the upload
+ * @return int progress in bytes uploaded.
+ */
+ public function getProgress()
+ {
+ return $this->progress;
+ }
+ /**
+ * Send the next part of the file to upload.
+ * @param string|bool $chunk Optional. The next set of bytes to send. If false will
+ * use $data passed at construct time.
+ */
+ public function nextChunk($chunk = \false)
+ {
+ $resumeUri = $this->getResumeUri();
+ if (\false == $chunk) {
+ $chunk = substr($this->data, $this->progress, $this->chunkSize);
+ }
+ $lastBytePos = $this->progress + strlen($chunk) - 1;
+ $headers = ['content-range' => "bytes {$this->progress}-{$lastBytePos}/{$this->size}", 'content-length' => (string) strlen($chunk), 'expect' => ''];
+ $request = new Request('PUT', $resumeUri, $headers, Psr7\Utils::streamFor($chunk));
+ return $this->makePutRequest($request);
+ }
+ /**
+ * Return the HTTP result code from the last call made.
+ * @return int code
+ */
+ public function getHttpResultCode()
+ {
+ return $this->httpResultCode;
+ }
+ /**
+ * Sends a PUT-Request to google drive and parses the response,
+ * setting the appropiate variables from the response()
+ *
+ * @param RequestInterface $request the Request which will be send
+ *
+ * @return false|mixed false when the upload is unfinished or the decoded http response
+ *
+ */
+ private function makePutRequest(RequestInterface $request)
+ {
+ $response = $this->client->execute($request);
+ $this->httpResultCode = $response->getStatusCode();
+ if (308 == $this->httpResultCode) {
+ // Track the amount uploaded.
+ $range = $response->getHeaderLine('range');
+ if ($range) {
+ $range_array = explode('-', $range);
+ $this->progress = (int) $range_array[1] + 1;
+ }
+ // Allow for changing upload URLs.
+ $location = $response->getHeaderLine('location');
+ if ($location) {
+ $this->resumeUri = $location;
+ }
+ // No problems, but upload not complete.
+ return \false;
+ }
+ return REST::decodeHttpResponse($response, $this->request);
+ }
+ /**
+ * Resume a previously unfinished upload
+ * @param string $resumeUri the resume-URI of the unfinished, resumable upload.
+ */
+ public function resume($resumeUri)
+ {
+ $this->resumeUri = $resumeUri;
+ $headers = ['content-range' => "bytes */{$this->size}", 'content-length' => '0'];
+ $httpRequest = new Request('PUT', $this->resumeUri, $headers);
+ return $this->makePutRequest($httpRequest);
+ }
+ /**
+ * @return RequestInterface
+ * @visible for testing
+ */
+ private function process()
+ {
+ $this->transformToUploadUrl();
+ $request = $this->request;
+ $postBody = '';
+ $contentType = \false;
+ $meta = json_decode((string) $request->getBody(), \true);
+ $uploadType = $this->getUploadType($meta);
+ $request = $request->withUri(Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType));
+ $mimeType = $this->mimeType ?: $request->getHeaderLine('content-type');
+ if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) {
+ $contentType = $mimeType;
+ $postBody = is_string($meta) ? $meta : json_encode($meta);
+ } elseif (self::UPLOAD_MEDIA_TYPE == $uploadType) {
+ $contentType = $mimeType;
+ $postBody = $this->data;
+ } elseif (self::UPLOAD_MULTIPART_TYPE == $uploadType) {
+ // This is a multipart/related upload.
+ $boundary = $this->boundary ?: mt_rand();
+ $boundary = str_replace('"', '', $boundary);
+ $contentType = 'multipart/related; boundary=' . $boundary;
+ $related = "--{$boundary}\r\n";
+ $related .= "Content-Type: application/json; charset=UTF-8\r\n";
+ $related .= "\r\n" . json_encode($meta) . "\r\n";
+ $related .= "--{$boundary}\r\n";
+ $related .= "Content-Type: {$mimeType}\r\n";
+ $related .= "Content-Transfer-Encoding: base64\r\n";
+ $related .= "\r\n" . base64_encode($this->data) . "\r\n";
+ $related .= "--{$boundary}--";
+ $postBody = $related;
+ }
+ $request = $request->withBody(Psr7\Utils::streamFor($postBody));
+ if ($contentType) {
+ $request = $request->withHeader('content-type', $contentType);
+ }
+ return $this->request = $request;
+ }
+ /**
+ * Valid upload types:
+ * - resumable (UPLOAD_RESUMABLE_TYPE)
+ * - media (UPLOAD_MEDIA_TYPE)
+ * - multipart (UPLOAD_MULTIPART_TYPE)
+ * @param string|false $meta
+ * @return string
+ * @visible for testing
+ */
+ public function getUploadType($meta)
+ {
+ if ($this->resumable) {
+ return self::UPLOAD_RESUMABLE_TYPE;
+ }
+ if (\false == $meta && $this->data) {
+ return self::UPLOAD_MEDIA_TYPE;
+ }
+ return self::UPLOAD_MULTIPART_TYPE;
+ }
+ public function getResumeUri()
+ {
+ if (null === $this->resumeUri) {
+ $this->resumeUri = $this->fetchResumeUri();
+ }
+ return $this->resumeUri;
+ }
+ private function fetchResumeUri()
+ {
+ $body = $this->request->getBody();
+ $headers = ['content-type' => 'application/json; charset=UTF-8', 'content-length' => $body->getSize(), 'x-upload-content-type' => $this->mimeType, 'x-upload-content-length' => $this->size, 'expect' => ''];
+ foreach ($headers as $key => $value) {
+ $this->request = $this->request->withHeader($key, $value);
+ }
+ $response = $this->client->execute($this->request, \false);
+ $location = $response->getHeaderLine('location');
+ $code = $response->getStatusCode();
+ if (200 == $code && \true == $location) {
+ return $location;
+ }
+ $message = $code;
+ $body = json_decode((string) $this->request->getBody(), \true);
+ if (isset($body['error']['errors'])) {
+ $message .= ': ';
+ foreach ($body['error']['errors'] as $error) {
+ $message .= "{$error['domain']}, {$error['message']};";
+ }
+ $message = rtrim($message, ';');
+ }
+ $error = "Failed to start the resumable upload (HTTP {$message})";
+ $this->client->getLogger()->error($error);
+ throw new GoogleException($error);
+ }
+ private function transformToUploadUrl()
+ {
+ $parts = parse_url((string) $this->request->getUri());
+ if (!isset($parts['path'])) {
+ $parts['path'] = '';
+ }
+ $parts['path'] = '/upload' . $parts['path'];
+ $uri = Uri::fromParts($parts);
+ $this->request = $this->request->withUri($uri);
+ }
+ public function setChunkSize($chunkSize)
+ {
+ $this->chunkSize = $chunkSize;
+ }
+ public function getRequest()
+ {
+ return $this->request;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/REST.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/REST.php
new file mode 100644
index 0000000..0160b0f
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/REST.php
@@ -0,0 +1,153 @@
+|false|null $expectedClass
+ * @param array $config
+ * @param array $retryMap
+ * @return mixed|T|null
+ * @throws \Google\Service\Exception on server side error (ie: not authenticated,
+ * invalid or malformed post body, invalid url)
+ */
+ public static function execute(ClientInterface $client, RequestInterface $request, $expectedClass = null, $config = [], $retryMap = null)
+ {
+ $runner = new Runner($config, sprintf('%s %s', $request->getMethod(), (string) $request->getUri()), [self::class, 'doExecute'], [$client, $request, $expectedClass]);
+ if (null !== $retryMap) {
+ $runner->setRetryMap($retryMap);
+ }
+ return $runner->run();
+ }
+ /**
+ * Executes a Psr\Http\Message\RequestInterface
+ *
+ * @template T
+ * @param ClientInterface $client
+ * @param RequestInterface $request
+ * @param class-string|false|null $expectedClass
+ * @return mixed|T|null
+ * @throws \Google\Service\Exception on server side error (ie: not authenticated,
+ * invalid or malformed post body, invalid url)
+ */
+ public static function doExecute(ClientInterface $client, RequestInterface $request, $expectedClass = null)
+ {
+ try {
+ $httpHandler = HttpHandlerFactory::build($client);
+ $response = $httpHandler($request);
+ } catch (RequestException $e) {
+ // if Guzzle throws an exception, catch it and handle the response
+ if (!$e->hasResponse()) {
+ throw $e;
+ }
+ $response = $e->getResponse();
+ // specific checking for Guzzle 5: convert to PSR7 response
+ if (interface_exists('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\GuzzleHttp\\Message\\ResponseInterface') && $response instanceof \Matomo\Dependencies\SearchEngineKeywordsPerformance\GuzzleHttp\Message\ResponseInterface) {
+ $response = new Response($response->getStatusCode(), $response->getHeaders() ?: [], $response->getBody(), $response->getProtocolVersion(), $response->getReasonPhrase());
+ }
+ }
+ return self::decodeHttpResponse($response, $request, $expectedClass);
+ }
+ /**
+ * Decode an HTTP Response.
+ * @static
+ *
+ * @template T
+ * @param RequestInterface $response The http response to be decoded.
+ * @param ResponseInterface $response
+ * @param class-string|false|null $expectedClass
+ * @return mixed|T|null
+ * @throws \Google\Service\Exception
+ */
+ public static function decodeHttpResponse(ResponseInterface $response, RequestInterface $request = null, $expectedClass = null)
+ {
+ $code = $response->getStatusCode();
+ // retry strategy
+ if (intVal($code) >= 400) {
+ // if we errored out, it should be safe to grab the response body
+ $body = (string) $response->getBody();
+ // Check if we received errors, and add those to the Exception for convenience
+ throw new GoogleServiceException($body, $code, null, self::getResponseErrors($body));
+ }
+ // Ensure we only pull the entire body into memory if the request is not
+ // of media type
+ $body = self::decodeBody($response, $request);
+ if ($expectedClass = self::determineExpectedClass($expectedClass, $request)) {
+ $json = json_decode($body, \true);
+ return new $expectedClass($json);
+ }
+ return $response;
+ }
+ private static function decodeBody(ResponseInterface $response, RequestInterface $request = null)
+ {
+ if (self::isAltMedia($request)) {
+ // don't decode the body, it's probably a really long string
+ return '';
+ }
+ return (string) $response->getBody();
+ }
+ private static function determineExpectedClass($expectedClass, RequestInterface $request = null)
+ {
+ // "false" is used to explicitly prevent an expected class from being returned
+ if (\false === $expectedClass) {
+ return null;
+ }
+ // if we don't have a request, we just use what's passed in
+ if (null === $request) {
+ return $expectedClass;
+ }
+ // return what we have in the request header if one was not supplied
+ return $expectedClass ?: $request->getHeaderLine('X-Php-Expected-Class');
+ }
+ private static function getResponseErrors($body)
+ {
+ $json = json_decode($body, \true);
+ if (isset($json['error']['errors'])) {
+ return $json['error']['errors'];
+ }
+ return null;
+ }
+ private static function isAltMedia(RequestInterface $request = null)
+ {
+ if ($request && ($qs = $request->getUri()->getQuery())) {
+ parse_str($qs, $query);
+ if (isset($query['alt']) && $query['alt'] == 'media') {
+ return \true;
+ }
+ }
+ return \false;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Model.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Model.php
new file mode 100644
index 0000000..3c8ac56
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Model.php
@@ -0,0 +1,301 @@
+mapTypes($array);
+ }
+ $this->gapiInit();
+ }
+ /**
+ * Getter that handles passthrough access to the data array, and lazy object creation.
+ * @param string $key Property name.
+ * @return mixed The value if any, or null.
+ */
+ public function __get($key)
+ {
+ $keyType = $this->keyType($key);
+ $keyDataType = $this->dataType($key);
+ if ($keyType && !isset($this->processed[$key])) {
+ if (isset($this->modelData[$key])) {
+ $val = $this->modelData[$key];
+ } elseif ($keyDataType == 'array' || $keyDataType == 'map') {
+ $val = [];
+ } else {
+ $val = null;
+ }
+ if ($this->isAssociativeArray($val)) {
+ if ($keyDataType && 'map' == $keyDataType) {
+ foreach ($val as $arrayKey => $arrayItem) {
+ $this->modelData[$key][$arrayKey] = new $keyType($arrayItem);
+ }
+ } else {
+ $this->modelData[$key] = new $keyType($val);
+ }
+ } elseif (is_array($val)) {
+ $arrayObject = [];
+ foreach ($val as $arrayIndex => $arrayItem) {
+ $arrayObject[$arrayIndex] = new $keyType($arrayItem);
+ }
+ $this->modelData[$key] = $arrayObject;
+ }
+ $this->processed[$key] = \true;
+ }
+ return isset($this->modelData[$key]) ? $this->modelData[$key] : null;
+ }
+ /**
+ * Initialize this object's properties from an array.
+ *
+ * @param array $array Used to seed this object's properties.
+ * @return void
+ */
+ protected function mapTypes($array)
+ {
+ // Hard initialise simple types, lazy load more complex ones.
+ foreach ($array as $key => $val) {
+ if ($keyType = $this->keyType($key)) {
+ $dataType = $this->dataType($key);
+ if ($dataType == 'array' || $dataType == 'map') {
+ $this->{$key} = [];
+ foreach ($val as $itemKey => $itemVal) {
+ if ($itemVal instanceof $keyType) {
+ $this->{$key}[$itemKey] = $itemVal;
+ } else {
+ $this->{$key}[$itemKey] = new $keyType($itemVal);
+ }
+ }
+ } elseif ($val instanceof $keyType) {
+ $this->{$key} = $val;
+ } else {
+ $this->{$key} = new $keyType($val);
+ }
+ unset($array[$key]);
+ } elseif (property_exists($this, $key)) {
+ $this->{$key} = $val;
+ unset($array[$key]);
+ } elseif (property_exists($this, $camelKey = $this->camelCase($key))) {
+ // This checks if property exists as camelCase, leaving it in array as snake_case
+ // in case of backwards compatibility issues.
+ $this->{$camelKey} = $val;
+ }
+ }
+ $this->modelData = $array;
+ }
+ /**
+ * Blank initialiser to be used in subclasses to do post-construction initialisation - this
+ * avoids the need for subclasses to have to implement the variadics handling in their
+ * constructors.
+ */
+ protected function gapiInit()
+ {
+ return;
+ }
+ /**
+ * Create a simplified object suitable for straightforward
+ * conversion to JSON. This is relatively expensive
+ * due to the usage of reflection, but shouldn't be called
+ * a whole lot, and is the most straightforward way to filter.
+ */
+ public function toSimpleObject()
+ {
+ $object = new stdClass();
+ // Process all other data.
+ foreach ($this->modelData as $key => $val) {
+ $result = $this->getSimpleValue($val);
+ if ($result !== null) {
+ $object->{$key} = $this->nullPlaceholderCheck($result);
+ }
+ }
+ // Process all public properties.
+ $reflect = new ReflectionObject($this);
+ $props = $reflect->getProperties(ReflectionProperty::IS_PUBLIC);
+ foreach ($props as $member) {
+ $name = $member->getName();
+ $result = $this->getSimpleValue($this->{$name});
+ if ($result !== null) {
+ $name = $this->getMappedName($name);
+ $object->{$name} = $this->nullPlaceholderCheck($result);
+ }
+ }
+ return $object;
+ }
+ /**
+ * Handle different types of values, primarily
+ * other objects and map and array data types.
+ */
+ private function getSimpleValue($value)
+ {
+ if ($value instanceof Model) {
+ return $value->toSimpleObject();
+ } elseif (is_array($value)) {
+ $return = [];
+ foreach ($value as $key => $a_value) {
+ $a_value = $this->getSimpleValue($a_value);
+ if ($a_value !== null) {
+ $key = $this->getMappedName($key);
+ $return[$key] = $this->nullPlaceholderCheck($a_value);
+ }
+ }
+ return $return;
+ }
+ return $value;
+ }
+ /**
+ * Check whether the value is the null placeholder and return true null.
+ */
+ private function nullPlaceholderCheck($value)
+ {
+ if ($value === self::NULL_VALUE) {
+ return null;
+ }
+ return $value;
+ }
+ /**
+ * If there is an internal name mapping, use that.
+ */
+ private function getMappedName($key)
+ {
+ if (isset($this->internal_gapi_mappings, $this->internal_gapi_mappings[$key])) {
+ $key = $this->internal_gapi_mappings[$key];
+ }
+ return $key;
+ }
+ /**
+ * Returns true only if the array is associative.
+ * @param array $array
+ * @return bool True if the array is associative.
+ */
+ protected function isAssociativeArray($array)
+ {
+ if (!is_array($array)) {
+ return \false;
+ }
+ $keys = array_keys($array);
+ foreach ($keys as $key) {
+ if (is_string($key)) {
+ return \true;
+ }
+ }
+ return \false;
+ }
+ /**
+ * Verify if $obj is an array.
+ * @throws \Google\Exception Thrown if $obj isn't an array.
+ * @param array $obj Items that should be validated.
+ * @param string $method Method expecting an array as an argument.
+ */
+ public function assertIsArray($obj, $method)
+ {
+ if ($obj && !is_array($obj)) {
+ throw new GoogleException("Incorrect parameter type passed to {$method}(). Expected an array.");
+ }
+ }
+ /** @return bool */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ return isset($this->{$offset}) || isset($this->modelData[$offset]);
+ }
+ /** @return mixed */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset)
+ {
+ return isset($this->{$offset}) ? $this->{$offset} : $this->__get($offset);
+ }
+ /** @return void */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ if (property_exists($this, $offset)) {
+ $this->{$offset} = $value;
+ } else {
+ $this->modelData[$offset] = $value;
+ $this->processed[$offset] = \true;
+ }
+ }
+ /** @return void */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ unset($this->modelData[$offset]);
+ }
+ protected function keyType($key)
+ {
+ $keyType = $key . "Type";
+ // ensure keyType is a valid class
+ if (property_exists($this, $keyType) && $this->{$keyType} !== null && class_exists($this->{$keyType})) {
+ return $this->{$keyType};
+ }
+ }
+ protected function dataType($key)
+ {
+ $dataType = $key . "DataType";
+ if (property_exists($this, $dataType)) {
+ return $this->{$dataType};
+ }
+ }
+ public function __isset($key)
+ {
+ return isset($this->modelData[$key]);
+ }
+ public function __unset($key)
+ {
+ unset($this->modelData[$key]);
+ }
+ /**
+ * Convert a string to camelCase
+ * @param string $value
+ * @return string
+ */
+ private function camelCase($value)
+ {
+ $value = ucwords(str_replace(['-', '_'], ' ', $value));
+ $value = str_replace(' ', '', $value);
+ $value[0] = strtolower($value[0]);
+ return $value;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service.php
new file mode 100644
index 0000000..531cbc9
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service.php
@@ -0,0 +1,63 @@
+client = $clientOrConfig;
+ } elseif (is_array($clientOrConfig)) {
+ $this->client = new Client($clientOrConfig ?: []);
+ } else {
+ $errorMessage = 'constructor must be array or instance of Google\\Client';
+ if (class_exists('TypeError')) {
+ throw new TypeError($errorMessage);
+ }
+ trigger_error($errorMessage, \E_USER_ERROR);
+ }
+ }
+ /**
+ * Return the associated Google\Client class.
+ * @return \Google\Client
+ */
+ public function getClient()
+ {
+ return $this->client;
+ }
+ /**
+ * Create a new HTTP Batch handler for this service
+ *
+ * @return Batch
+ */
+ public function createBatch()
+ {
+ return new Batch($this->client, \false, $this->rootUrl, $this->batchPath);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service/Exception.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service/Exception.php
new file mode 100644
index 0000000..2b7b708
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service/Exception.php
@@ -0,0 +1,65 @@
+>|null $errors List of errors returned in an HTTP
+ * response or null. Defaults to [].
+ */
+ public function __construct($message, $code = 0, Exception $previous = null, $errors = [])
+ {
+ if (version_compare(\PHP_VERSION, '5.3.0') >= 0) {
+ parent::__construct($message, $code, $previous);
+ } else {
+ parent::__construct($message, $code);
+ }
+ $this->errors = $errors;
+ }
+ /**
+ * An example of the possible errors returned.
+ *
+ * [
+ * {
+ * "domain": "global",
+ * "reason": "authError",
+ * "message": "Invalid Credentials",
+ * "locationType": "header",
+ * "location": "Authorization",
+ * }
+ * ]
+ *
+ * @return array>|null List of errors returned in an HTTP response or null.
+ */
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service/Resource.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service/Resource.php
new file mode 100644
index 0000000..a7578a9
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service/Resource.php
@@ -0,0 +1,214 @@
+ ['type' => 'string', 'location' => 'query'], 'fields' => ['type' => 'string', 'location' => 'query'], 'trace' => ['type' => 'string', 'location' => 'query'], 'userIp' => ['type' => 'string', 'location' => 'query'], 'quotaUser' => ['type' => 'string', 'location' => 'query'], 'data' => ['type' => 'string', 'location' => 'body'], 'mimeType' => ['type' => 'string', 'location' => 'header'], 'uploadType' => ['type' => 'string', 'location' => 'query'], 'mediaUpload' => ['type' => 'complex', 'location' => 'query'], 'prettyPrint' => ['type' => 'string', 'location' => 'query']];
+ /** @var string $rootUrl */
+ private $rootUrl;
+ /** @var \Google\Client $client */
+ private $client;
+ /** @var string $serviceName */
+ private $serviceName;
+ /** @var string $servicePath */
+ private $servicePath;
+ /** @var string $resourceName */
+ private $resourceName;
+ /** @var array $methods */
+ private $methods;
+ public function __construct($service, $serviceName, $resourceName, $resource)
+ {
+ $this->rootUrl = $service->rootUrl;
+ $this->client = $service->getClient();
+ $this->servicePath = $service->servicePath;
+ $this->serviceName = $serviceName;
+ $this->resourceName = $resourceName;
+ $this->methods = is_array($resource) && isset($resource['methods']) ? $resource['methods'] : [$resourceName => $resource];
+ }
+ /**
+ * TODO: This function needs simplifying.
+ *
+ * @template T
+ * @param string $name
+ * @param array $arguments
+ * @param class-string $expectedClass - optional, the expected class name
+ * @return mixed|T|ResponseInterface|RequestInterface
+ * @throws \Google\Exception
+ */
+ public function call($name, $arguments, $expectedClass = null)
+ {
+ if (!isset($this->methods[$name])) {
+ $this->client->getLogger()->error('Service method unknown', ['service' => $this->serviceName, 'resource' => $this->resourceName, 'method' => $name]);
+ throw new GoogleException("Unknown function: " . "{$this->serviceName}->{$this->resourceName}->{$name}()");
+ }
+ $method = $this->methods[$name];
+ $parameters = $arguments[0];
+ // postBody is a special case since it's not defined in the discovery
+ // document as parameter, but we abuse the param entry for storing it.
+ $postBody = null;
+ if (isset($parameters['postBody'])) {
+ if ($parameters['postBody'] instanceof Model) {
+ // In the cases the post body is an existing object, we want
+ // to use the smart method to create a simple object for
+ // for JSONification.
+ $parameters['postBody'] = $parameters['postBody']->toSimpleObject();
+ } elseif (is_object($parameters['postBody'])) {
+ // If the post body is another kind of object, we will try and
+ // wrangle it into a sensible format.
+ $parameters['postBody'] = $this->convertToArrayAndStripNulls($parameters['postBody']);
+ }
+ $postBody = (array) $parameters['postBody'];
+ unset($parameters['postBody']);
+ }
+ // TODO: optParams here probably should have been
+ // handled already - this may well be redundant code.
+ if (isset($parameters['optParams'])) {
+ $optParams = $parameters['optParams'];
+ unset($parameters['optParams']);
+ $parameters = array_merge($parameters, $optParams);
+ }
+ if (!isset($method['parameters'])) {
+ $method['parameters'] = [];
+ }
+ $method['parameters'] = array_merge($this->stackParameters, $method['parameters']);
+ foreach ($parameters as $key => $val) {
+ if ($key != 'postBody' && !isset($method['parameters'][$key])) {
+ $this->client->getLogger()->error('Service parameter unknown', ['service' => $this->serviceName, 'resource' => $this->resourceName, 'method' => $name, 'parameter' => $key]);
+ throw new GoogleException("({$name}) unknown parameter: '{$key}'");
+ }
+ }
+ foreach ($method['parameters'] as $paramName => $paramSpec) {
+ if (isset($paramSpec['required']) && $paramSpec['required'] && !isset($parameters[$paramName])) {
+ $this->client->getLogger()->error('Service parameter missing', ['service' => $this->serviceName, 'resource' => $this->resourceName, 'method' => $name, 'parameter' => $paramName]);
+ throw new GoogleException("({$name}) missing required param: '{$paramName}'");
+ }
+ if (isset($parameters[$paramName])) {
+ $value = $parameters[$paramName];
+ $parameters[$paramName] = $paramSpec;
+ $parameters[$paramName]['value'] = $value;
+ unset($parameters[$paramName]['required']);
+ } else {
+ // Ensure we don't pass nulls.
+ unset($parameters[$paramName]);
+ }
+ }
+ $this->client->getLogger()->info('Service Call', ['service' => $this->serviceName, 'resource' => $this->resourceName, 'method' => $name, 'arguments' => $parameters]);
+ // build the service uri
+ $url = $this->createRequestUri($method['path'], $parameters);
+ // NOTE: because we're creating the request by hand,
+ // and because the service has a rootUrl property
+ // the "base_uri" of the Http Client is not accounted for
+ $request = new Request($method['httpMethod'], $url, $postBody ? ['content-type' => 'application/json'] : [], $postBody ? json_encode($postBody) : '');
+ // support uploads
+ if (isset($parameters['data'])) {
+ $mimeType = isset($parameters['mimeType']) ? $parameters['mimeType']['value'] : 'application/octet-stream';
+ $data = $parameters['data']['value'];
+ $upload = new MediaFileUpload($this->client, $request, $mimeType, $data);
+ // pull down the modified request
+ $request = $upload->getRequest();
+ }
+ // if this is a media type, we will return the raw response
+ // rather than using an expected class
+ if (isset($parameters['alt']) && $parameters['alt']['value'] == 'media') {
+ $expectedClass = null;
+ }
+ // if the client is marked for deferring, rather than
+ // execute the request, return the response
+ if ($this->client->shouldDefer()) {
+ // @TODO find a better way to do this
+ $request = $request->withHeader('X-Php-Expected-Class', $expectedClass);
+ return $request;
+ }
+ return $this->client->execute($request, $expectedClass);
+ }
+ protected function convertToArrayAndStripNulls($o)
+ {
+ $o = (array) $o;
+ foreach ($o as $k => $v) {
+ if ($v === null) {
+ unset($o[$k]);
+ } elseif (is_object($v) || is_array($v)) {
+ $o[$k] = $this->convertToArrayAndStripNulls($o[$k]);
+ }
+ }
+ return $o;
+ }
+ /**
+ * Parse/expand request parameters and create a fully qualified
+ * request uri.
+ * @static
+ * @param string $restPath
+ * @param array $params
+ * @return string $requestUrl
+ */
+ public function createRequestUri($restPath, $params)
+ {
+ // Override the default servicePath address if the $restPath use a /
+ if ('/' == substr($restPath, 0, 1)) {
+ $requestUrl = substr($restPath, 1);
+ } else {
+ $requestUrl = $this->servicePath . $restPath;
+ }
+ // code for leading slash
+ if ($this->rootUrl) {
+ if ('/' !== substr($this->rootUrl, -1) && '/' !== substr($requestUrl, 0, 1)) {
+ $requestUrl = '/' . $requestUrl;
+ }
+ $requestUrl = $this->rootUrl . $requestUrl;
+ }
+ $uriTemplateVars = [];
+ $queryVars = [];
+ foreach ($params as $paramName => $paramSpec) {
+ if ($paramSpec['type'] == 'boolean') {
+ $paramSpec['value'] = $paramSpec['value'] ? 'true' : 'false';
+ }
+ if ($paramSpec['location'] == 'path') {
+ $uriTemplateVars[$paramName] = $paramSpec['value'];
+ } elseif ($paramSpec['location'] == 'query') {
+ if (is_array($paramSpec['value'])) {
+ foreach ($paramSpec['value'] as $value) {
+ $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($value));
+ }
+ } else {
+ $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($paramSpec['value']));
+ }
+ }
+ }
+ if (count($uriTemplateVars)) {
+ $uriTemplateParser = new UriTemplate();
+ $requestUrl = $uriTemplateParser->parse($requestUrl, $uriTemplateVars);
+ }
+ if (count($queryVars)) {
+ $requestUrl .= '?' . implode('&', $queryVars);
+ }
+ return $requestUrl;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Composer.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Composer.php
new file mode 100644
index 0000000..5cb3190
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Composer.php
@@ -0,0 +1,77 @@
+getComposer();
+ $extra = $composer->getPackage()->getExtra();
+ $servicesToKeep = isset($extra['google/apiclient-services']) ? $extra['google/apiclient-services'] : [];
+ if ($servicesToKeep) {
+ $vendorDir = $composer->getConfig()->get('vendor-dir');
+ $serviceDir = sprintf('%s/google/apiclient-services/src/Google/Service', $vendorDir);
+ if (!is_dir($serviceDir)) {
+ // path for google/apiclient-services >= 0.200.0
+ $serviceDir = sprintf('%s/google/apiclient-services/src', $vendorDir);
+ }
+ self::verifyServicesToKeep($serviceDir, $servicesToKeep);
+ $finder = self::getServicesToRemove($serviceDir, $servicesToKeep);
+ $filesystem = $filesystem ?: new Filesystem();
+ if (0 !== ($count = count($finder))) {
+ $event->getIO()->write(sprintf('Removing %s google services', $count));
+ foreach ($finder as $file) {
+ $realpath = $file->getRealPath();
+ $filesystem->remove($realpath);
+ $filesystem->remove($realpath . '.php');
+ }
+ }
+ }
+ }
+ /**
+ * @throws InvalidArgumentException when the service doesn't exist
+ */
+ private static function verifyServicesToKeep($serviceDir, array $servicesToKeep)
+ {
+ $finder = (new Finder())->directories()->depth('== 0');
+ foreach ($servicesToKeep as $service) {
+ if (!preg_match('/^[a-zA-Z0-9]*$/', $service)) {
+ throw new InvalidArgumentException(sprintf('Invalid Google service name "%s"', $service));
+ }
+ try {
+ $finder->in($serviceDir . '/' . $service);
+ } catch (InvalidArgumentException $e) {
+ throw new InvalidArgumentException(sprintf('Google service "%s" does not exist or was removed previously', $service));
+ }
+ }
+ }
+ private static function getServicesToRemove($serviceDir, array $servicesToKeep)
+ {
+ // find all files in the current directory
+ return (new Finder())->directories()->depth('== 0')->in($serviceDir)->exclude($servicesToKeep);
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Exception.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Exception.php
new file mode 100644
index 0000000..5cc4c37
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Exception.php
@@ -0,0 +1,23 @@
+ self::TASK_RETRY_ALWAYS,
+ '503' => self::TASK_RETRY_ALWAYS,
+ 'rateLimitExceeded' => self::TASK_RETRY_ALWAYS,
+ 'userRateLimitExceeded' => self::TASK_RETRY_ALWAYS,
+ 6 => self::TASK_RETRY_ALWAYS,
+ // CURLE_COULDNT_RESOLVE_HOST
+ 7 => self::TASK_RETRY_ALWAYS,
+ // CURLE_COULDNT_CONNECT
+ 28 => self::TASK_RETRY_ALWAYS,
+ // CURLE_OPERATION_TIMEOUTED
+ 35 => self::TASK_RETRY_ALWAYS,
+ // CURLE_SSL_CONNECT_ERROR
+ 52 => self::TASK_RETRY_ALWAYS,
+ // CURLE_GOT_NOTHING
+ 'lighthouseError' => self::TASK_RETRY_NEVER,
+ ];
+ /**
+ * Creates a new task runner with exponential backoff support.
+ *
+ * @param array $config The task runner config
+ * @param string $name The name of the current task (used for logging)
+ * @param callable $action The task to run and possibly retry
+ * @param array $arguments The task arguments
+ * @throws \Google\Task\Exception when misconfigured
+ */
+ // @phpstan-ignore-next-line
+ public function __construct($config, $name, $action, array $arguments = [])
+ {
+ if (isset($config['initial_delay'])) {
+ if ($config['initial_delay'] < 0) {
+ throw new GoogleTaskException('Task configuration `initial_delay` must not be negative.');
+ }
+ $this->delay = $config['initial_delay'];
+ }
+ if (isset($config['max_delay'])) {
+ if ($config['max_delay'] <= 0) {
+ throw new GoogleTaskException('Task configuration `max_delay` must be greater than 0.');
+ }
+ $this->maxDelay = $config['max_delay'];
+ }
+ if (isset($config['factor'])) {
+ if ($config['factor'] <= 0) {
+ throw new GoogleTaskException('Task configuration `factor` must be greater than 0.');
+ }
+ $this->factor = $config['factor'];
+ }
+ if (isset($config['jitter'])) {
+ if ($config['jitter'] <= 0) {
+ throw new GoogleTaskException('Task configuration `jitter` must be greater than 0.');
+ }
+ $this->jitter = $config['jitter'];
+ }
+ if (isset($config['retries'])) {
+ if ($config['retries'] < 0) {
+ throw new GoogleTaskException('Task configuration `retries` must not be negative.');
+ }
+ $this->maxAttempts += $config['retries'];
+ }
+ if (!is_callable($action)) {
+ throw new GoogleTaskException('Task argument `$action` must be a valid callable.');
+ }
+ $this->action = $action;
+ $this->arguments = $arguments;
+ }
+ /**
+ * Checks if a retry can be attempted.
+ *
+ * @return boolean
+ */
+ public function canAttempt()
+ {
+ return $this->attempts < $this->maxAttempts;
+ }
+ /**
+ * Runs the task and (if applicable) automatically retries when errors occur.
+ *
+ * @return mixed
+ * @throws \Google\Service\Exception on failure when no retries are available.
+ */
+ public function run()
+ {
+ while ($this->attempt()) {
+ try {
+ return call_user_func_array($this->action, $this->arguments);
+ } catch (GoogleServiceException $exception) {
+ $allowedRetries = $this->allowedRetries($exception->getCode(), $exception->getErrors());
+ if (!$this->canAttempt() || !$allowedRetries) {
+ throw $exception;
+ }
+ if ($allowedRetries > 0) {
+ $this->maxAttempts = min($this->maxAttempts, $this->attempts + $allowedRetries);
+ }
+ }
+ }
+ }
+ /**
+ * Runs a task once, if possible. This is useful for bypassing the `run()`
+ * loop.
+ *
+ * NOTE: If this is not the first attempt, this function will sleep in
+ * accordance to the backoff configurations before running the task.
+ *
+ * @return boolean
+ */
+ public function attempt()
+ {
+ if (!$this->canAttempt()) {
+ return \false;
+ }
+ if ($this->attempts > 0) {
+ $this->backOff();
+ }
+ $this->attempts++;
+ return \true;
+ }
+ /**
+ * Sleeps in accordance to the backoff configurations.
+ */
+ private function backOff()
+ {
+ $delay = $this->getDelay();
+ usleep((int) ($delay * 1000000));
+ }
+ /**
+ * Gets the delay (in seconds) for the current backoff period.
+ *
+ * @return int
+ */
+ private function getDelay()
+ {
+ $jitter = $this->getJitter();
+ $factor = $this->attempts > 1 ? $this->factor + $jitter : 1 + abs($jitter);
+ return $this->delay = min($this->maxDelay, $this->delay * $factor);
+ }
+ /**
+ * Gets the current jitter (random number between -$this->jitter and
+ * $this->jitter).
+ *
+ * @return float
+ */
+ private function getJitter()
+ {
+ return $this->jitter * 2 * mt_rand() / mt_getrandmax() - $this->jitter;
+ }
+ /**
+ * Gets the number of times the associated task can be retried.
+ *
+ * NOTE: -1 is returned if the task can be retried indefinitely
+ *
+ * @return integer
+ */
+ public function allowedRetries($code, $errors = [])
+ {
+ if (isset($this->retryMap[$code])) {
+ return $this->retryMap[$code];
+ }
+ if (!empty($errors) && isset($errors[0]['reason'], $this->retryMap[$errors[0]['reason']])) {
+ return $this->retryMap[$errors[0]['reason']];
+ }
+ return 0;
+ }
+ public function setRetryMap($retryMap)
+ {
+ $this->retryMap = $retryMap;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Utils/UriTemplate.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Utils/UriTemplate.php
new file mode 100644
index 0000000..611b907
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Utils/UriTemplate.php
@@ -0,0 +1,264 @@
+ "reserved", "/" => "segments", "." => "dotprefix", "#" => "fragment", ";" => "semicolon", "?" => "form", "&" => "continuation"];
+ /**
+ * @var array
+ * These are the characters which should not be URL encoded in reserved
+ * strings.
+ */
+ private $reserved = ["=", ",", "!", "@", "|", ":", "/", "?", "#", "[", "]", '$', "&", "'", "(", ")", "*", "+", ";"];
+ private $reservedEncoded = ["%3D", "%2C", "%21", "%40", "%7C", "%3A", "%2F", "%3F", "%23", "%5B", "%5D", "%24", "%26", "%27", "%28", "%29", "%2A", "%2B", "%3B"];
+ public function parse($string, array $parameters)
+ {
+ return $this->resolveNextSection($string, $parameters);
+ }
+ /**
+ * This function finds the first matching {...} block and
+ * executes the replacement. It then calls itself to find
+ * subsequent blocks, if any.
+ */
+ private function resolveNextSection($string, $parameters)
+ {
+ $start = strpos($string, "{");
+ if ($start === \false) {
+ return $string;
+ }
+ $end = strpos($string, "}");
+ if ($end === \false) {
+ return $string;
+ }
+ $string = $this->replace($string, $start, $end, $parameters);
+ return $this->resolveNextSection($string, $parameters);
+ }
+ private function replace($string, $start, $end, $parameters)
+ {
+ // We know a data block will have {} round it, so we can strip that.
+ $data = substr($string, $start + 1, $end - $start - 1);
+ // If the first character is one of the reserved operators, it effects
+ // the processing of the stream.
+ if (isset($this->operators[$data[0]])) {
+ $op = $this->operators[$data[0]];
+ $data = substr($data, 1);
+ $prefix = "";
+ $prefix_on_missing = \false;
+ switch ($op) {
+ case "reserved":
+ // Reserved means certain characters should not be URL encoded
+ $data = $this->replaceVars($data, $parameters, ",", null, \true);
+ break;
+ case "fragment":
+ // Comma separated with fragment prefix. Bare values only.
+ $prefix = "#";
+ $prefix_on_missing = \true;
+ $data = $this->replaceVars($data, $parameters, ",", null, \true);
+ break;
+ case "segments":
+ // Slash separated data. Bare values only.
+ $prefix = "/";
+ $data = $this->replaceVars($data, $parameters, "/");
+ break;
+ case "dotprefix":
+ // Dot separated data. Bare values only.
+ $prefix = ".";
+ $prefix_on_missing = \true;
+ $data = $this->replaceVars($data, $parameters, ".");
+ break;
+ case "semicolon":
+ // Semicolon prefixed and separated. Uses the key name
+ $prefix = ";";
+ $data = $this->replaceVars($data, $parameters, ";", "=", \false, \true, \false);
+ break;
+ case "form":
+ // Standard URL format. Uses the key name
+ $prefix = "?";
+ $data = $this->replaceVars($data, $parameters, "&", "=");
+ break;
+ case "continuation":
+ // Standard URL, but with leading ampersand. Uses key name.
+ $prefix = "&";
+ $data = $this->replaceVars($data, $parameters, "&", "=");
+ break;
+ }
+ // Add the initial prefix character if data is valid.
+ if ($data || $data !== \false && $prefix_on_missing) {
+ $data = $prefix . $data;
+ }
+ } else {
+ // If no operator we replace with the defaults.
+ $data = $this->replaceVars($data, $parameters);
+ }
+ // This is chops out the {...} and replaces with the new section.
+ return substr($string, 0, $start) . $data . substr($string, $end + 1);
+ }
+ private function replaceVars($section, $parameters, $sep = ",", $combine = null, $reserved = \false, $tag_empty = \false, $combine_on_empty = \true)
+ {
+ if (strpos($section, ",") === \false) {
+ // If we only have a single value, we can immediately process.
+ return $this->combine($section, $parameters, $sep, $combine, $reserved, $tag_empty, $combine_on_empty);
+ } else {
+ // If we have multiple values, we need to split and loop over them.
+ // Each is treated individually, then glued together with the
+ // separator character.
+ $vars = explode(",", $section);
+ return $this->combineList(
+ $vars,
+ $sep,
+ $parameters,
+ $combine,
+ $reserved,
+ \false,
+ // Never emit empty strings in multi-param replacements
+ $combine_on_empty
+ );
+ }
+ }
+ public function combine($key, $parameters, $sep, $combine, $reserved, $tag_empty, $combine_on_empty)
+ {
+ $length = \false;
+ $explode = \false;
+ $skip_final_combine = \false;
+ $value = \false;
+ // Check for length restriction.
+ if (strpos($key, ":") !== \false) {
+ list($key, $length) = explode(":", $key);
+ }
+ // Check for explode parameter.
+ if ($key[strlen($key) - 1] == "*") {
+ $explode = \true;
+ $key = substr($key, 0, -1);
+ $skip_final_combine = \true;
+ }
+ // Define the list separator.
+ $list_sep = $explode ? $sep : ",";
+ if (isset($parameters[$key])) {
+ $data_type = $this->getDataType($parameters[$key]);
+ switch ($data_type) {
+ case self::TYPE_SCALAR:
+ $value = $this->getValue($parameters[$key], $length);
+ break;
+ case self::TYPE_LIST:
+ $values = [];
+ foreach ($parameters[$key] as $pkey => $pvalue) {
+ $pvalue = $this->getValue($pvalue, $length);
+ if ($combine && $explode) {
+ $values[$pkey] = $key . $combine . $pvalue;
+ } else {
+ $values[$pkey] = $pvalue;
+ }
+ }
+ $value = implode($list_sep, $values);
+ if ($value == '') {
+ return '';
+ }
+ break;
+ case self::TYPE_MAP:
+ $values = [];
+ foreach ($parameters[$key] as $pkey => $pvalue) {
+ $pvalue = $this->getValue($pvalue, $length);
+ if ($explode) {
+ $pkey = $this->getValue($pkey, $length);
+ $values[] = $pkey . "=" . $pvalue;
+ // Explode triggers = combine.
+ } else {
+ $values[] = $pkey;
+ $values[] = $pvalue;
+ }
+ }
+ $value = implode($list_sep, $values);
+ if ($value == '') {
+ return \false;
+ }
+ break;
+ }
+ } elseif ($tag_empty) {
+ // If we are just indicating empty values with their key name, return that.
+ return $key;
+ } else {
+ // Otherwise we can skip this variable due to not being defined.
+ return \false;
+ }
+ if ($reserved) {
+ $value = str_replace($this->reservedEncoded, $this->reserved, $value);
+ }
+ // If we do not need to include the key name, we just return the raw
+ // value.
+ if (!$combine || $skip_final_combine) {
+ return $value;
+ }
+ // Else we combine the key name: foo=bar, if value is not the empty string.
+ return $key . ($value != '' || $combine_on_empty ? $combine . $value : '');
+ }
+ /**
+ * Return the type of a passed in value
+ */
+ private function getDataType($data)
+ {
+ if (is_array($data)) {
+ reset($data);
+ if (key($data) !== 0) {
+ return self::TYPE_MAP;
+ }
+ return self::TYPE_LIST;
+ }
+ return self::TYPE_SCALAR;
+ }
+ /**
+ * Utility function that merges multiple combine calls
+ * for multi-key templates.
+ */
+ private function combineList($vars, $sep, $parameters, $combine, $reserved, $tag_empty, $combine_on_empty)
+ {
+ $ret = [];
+ foreach ($vars as $var) {
+ $response = $this->combine($var, $parameters, $sep, $combine, $reserved, $tag_empty, $combine_on_empty);
+ if ($response === \false) {
+ continue;
+ }
+ $ret[] = $response;
+ }
+ return implode($sep, $ret);
+ }
+ /**
+ * Utility function to encode and trim values
+ */
+ private function getValue($value, $length)
+ {
+ if ($length) {
+ $value = substr($value, 0, $length);
+ }
+ $value = rawurlencode($value);
+ return $value;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/aliases.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/aliases.php
new file mode 100644
index 0000000..2591fb0
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/aliases.php
@@ -0,0 +1,80 @@
+ 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Client', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Service' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\AccessToken\\Revoke' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_AccessToken_Revoke', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\AccessToken\\Verify' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_AccessToken_Verify', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Model' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Model', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Utils\\UriTemplate' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Utils_UriTemplate', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\AuthHandler\\Guzzle6AuthHandler' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_AuthHandler_Guzzle6AuthHandler', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\AuthHandler\\Guzzle7AuthHandler' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_AuthHandler_Guzzle7AuthHandler', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\AuthHandler\\AuthHandlerFactory' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_AuthHandler_AuthHandlerFactory', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Http\\Batch' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Http_Batch', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Http\\MediaFileUpload' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Http_MediaFileUpload', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Http\\REST' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Http_REST', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Task\\Retryable' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Task_Retryable', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Task\\Exception' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Task_Exception', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Task\\Runner' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Task_Runner', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Collection' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Collection', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Service\\Exception' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Exception', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Service\\Resource' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Service_Resource', 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google\\Exception' => 'Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\Google_Exception'];
+foreach ($classMap as $class => $alias) {
+ \class_alias($class, $alias);
+}
+/**
+ * This class needs to be defined explicitly as scripts must be recognized by
+ * the autoloader.
+ */
+class Google_Task_Composer extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Task\Composer
+{
+}
+/** @phpstan-ignore-next-line */
+if (\false) {
+ class Google_AccessToken_Revoke extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\AccessToken\Revoke
+ {
+ }
+ class Google_AccessToken_Verify extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\AccessToken\Verify
+ {
+ }
+ class Google_AuthHandler_AuthHandlerFactory extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\AuthHandler\AuthHandlerFactory
+ {
+ }
+ class Google_AuthHandler_Guzzle6AuthHandler extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\AuthHandler\Guzzle6AuthHandler
+ {
+ }
+ class Google_AuthHandler_Guzzle7AuthHandler extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\AuthHandler\Guzzle7AuthHandler
+ {
+ }
+ class Google_Client extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Client
+ {
+ }
+ class Google_Collection extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Collection
+ {
+ }
+ class Google_Exception extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Exception
+ {
+ }
+ class Google_Http_Batch extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Http\Batch
+ {
+ }
+ class Google_Http_MediaFileUpload extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Http\MediaFileUpload
+ {
+ }
+ class Google_Http_REST extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Http\REST
+ {
+ }
+ class Google_Model extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Model
+ {
+ }
+ class Google_Service extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service
+ {
+ }
+ class Google_Service_Exception extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Exception
+ {
+ }
+ class Google_Service_Resource extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Service\Resource
+ {
+ }
+ class Google_Task_Exception extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Task\Exception
+ {
+ }
+ interface Google_Task_Retryable extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Task\Retryable
+ {
+ }
+ class Google_Task_Runner extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Task\Runner
+ {
+ }
+ class Google_Utils_UriTemplate extends \Matomo\Dependencies\SearchEngineKeywordsPerformance\Google\Utils\UriTemplate
+ {
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/COPYING b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/COPYING
new file mode 100644
index 0000000..b5d5055
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/COPYING
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2015 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/LICENSE b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/LICENSE
new file mode 100644
index 0000000..a148ba5
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/LICENSE
@@ -0,0 +1,203 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by
+the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity
+exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but
+not limited to compiled object code, generated documentation,
+and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or
+Object form, made available under the License, as indicated by a
+copyright notice that is included in or attached to the work
+(an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other modifications
+represent, as a whole, an original work of authorship. For the purposes
+of this License, Derivative Works shall not include works that remain
+separable from, or merely link (or bind by name) to the interfaces of,
+the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including
+the original version of the Work and any modifications or additions
+to that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright owner
+or by an individual or Legal Entity authorized to submit on behalf of
+the copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems,
+and issue tracking systems that are managed by, or on behalf of, the
+Licensor for the purpose of discussing and improving the Work, but
+excluding communication that is conspicuously marked or otherwise
+designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the
+Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+(except as stated in this section) patent license to make, have made,
+use, offer to sell, sell, import, and otherwise transfer the Work,
+where such license applies only to those patent claims licensable
+by such Contributor that are necessarily infringed by their
+Contribution(s) alone or by combination of their Contribution(s)
+with the Work to which such Contribution(s) was submitted. If You
+institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work
+or a Contribution incorporated within the Work constitutes direct
+or contributory patent infringement, then any patent licenses
+granted to You under this License for that Work shall terminate
+as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+Work or Derivative Works thereof in any medium, with or without
+modifications, and in Source or Object form, provided that You
+meet the following conditions:
+
+(a) You must give any other recipients of the Work or
+Derivative Works a copy of this License; and
+
+(b) You must cause any modified files to carry prominent notices
+stating that You changed the files; and
+
+(c) You must retain, in the Source form of any Derivative Works
+that You distribute, all copyright, patent, trademark, and
+attribution notices from the Source form of the Work,
+excluding those notices that do not pertain to any part of
+the Derivative Works; and
+
+(d) If the Work includes a "NOTICE" text file as part of its
+distribution, then any Derivative Works that You distribute must
+include a readable copy of the attribution notices contained
+within such NOTICE file, excluding those notices that do not
+pertain to any part of the Derivative Works, in at least one
+of the following places: within a NOTICE text file distributed
+as part of the Derivative Works; within the Source form or
+documentation, if provided along with the Derivative Works; or,
+within a display generated by the Derivative Works, if and
+wherever such third-party notices normally appear. The contents
+of the NOTICE file are for informational purposes only and
+do not modify the License. You may add Your own attribution
+notices within Derivative Works that You distribute, alongside
+or as an addendum to the NOTICE text from the Work, provided
+that such additional attribution notices cannot be construed
+as modifying the License.
+
+You may add Your own copyright statement to Your modifications and
+may provide additional or different license terms and conditions
+for use, reproduction, or distribution of Your modifications, or
+for any such Derivative Works as a whole, provided Your use,
+reproduction, and distribution of the Work otherwise complies with
+the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+any Contribution intentionally submitted for inclusion in the Work
+by You to the Licensor shall be under the terms and conditions of
+this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify
+the terms of any separate license agreement you may have executed
+with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+names, trademarks, service marks, or product names of the Licensor,
+except as required for reasonable and customary use in describing the
+origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+agreed to in writing, Licensor provides the Work (and each
+Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied, including, without limitation, any warranties or conditions
+of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Work and assume any
+risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise,
+unless required by applicable law (such as deliberate and grossly
+negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special,
+incidental, or consequential damages of any character arising as a
+result of this License or out of the use or inability to use the
+Work (including but not limited to damages for loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all
+other commercial damages or losses), even if such Contributor
+has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+the Work or Derivative Works thereof, You may choose to offer,
+and charge a fee for, acceptance of support, warranty, indemnity,
+or other liability obligations and/or rights consistent with this
+License. However, in accepting such obligations, You may act only
+on Your own behalf and on Your sole responsibility, not on behalf
+of any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason
+of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following
+boilerplate notice, with the fields enclosed by brackets "[]"
+replaced with your own identifying information. (Don't include
+the brackets!) The text should be enclosed in the appropriate
+comment syntax for the file format. We also recommend that a
+file or class name and description of purpose be included on the
+same "printed page" as the copyright notice for easier
+identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/VERSION b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/VERSION
new file mode 100644
index 0000000..7aa332e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/VERSION
@@ -0,0 +1 @@
+1.33.0
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/autoload.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/autoload.php
new file mode 100644
index 0000000..621f00e
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/autoload.php
@@ -0,0 +1,35 @@
+ 3) {
+ // Maximum class file path depth in this project is 3.
+ $classPath = \array_slice($classPath, 0, 3);
+ }
+ $filePath = \dirname(__FILE__) . '/src/' . \implode('/', $classPath) . '.php';
+ if (\file_exists($filePath)) {
+ require_once $filePath;
+ }
+}
+\spl_autoload_register('oauth2client_php_autoload');
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/AccessToken.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/AccessToken.php
new file mode 100644
index 0000000..d7906c3
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/AccessToken.php
@@ -0,0 +1,430 @@
+httpHandler = $httpHandler ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
+ $this->cache = $cache ?: new MemoryCacheItemPool();
+ }
+ /**
+ * Verifies an id token and returns the authenticated apiLoginTicket.
+ * Throws an exception if the id token is not valid.
+ * The audience parameter can be used to control which id tokens are
+ * accepted. By default, the id token must have been issued to this OAuth2 client.
+ *
+ * @param string $token The JSON Web Token to be verified.
+ * @param array $options [optional] {
+ * Configuration options.
+ * @type string $audience The indended recipient of the token.
+ * @type string $issuer The intended issuer of the token.
+ * @type string $cacheKey The cache key of the cached certs. Defaults to
+ * the sha1 of $certsLocation if provided, otherwise is set to
+ * "federated_signon_certs_v3".
+ * @type string $certsLocation The location (remote or local) from which
+ * to retrieve certificates, if not cached. This value should only be
+ * provided in limited circumstances in which you are sure of the
+ * behavior.
+ * @type bool $throwException Whether the function should throw an
+ * exception if the verification fails. This is useful for
+ * determining the reason verification failed.
+ * }
+ * @return array|false the token payload, if successful, or false if not.
+ * @throws InvalidArgumentException If certs could not be retrieved from a local file.
+ * @throws InvalidArgumentException If received certs are in an invalid format.
+ * @throws InvalidArgumentException If the cert alg is not supported.
+ * @throws RuntimeException If certs could not be retrieved from a remote location.
+ * @throws UnexpectedValueException If the token issuer does not match.
+ * @throws UnexpectedValueException If the token audience does not match.
+ */
+ public function verify($token, array $options = [])
+ {
+ $audience = $options['audience'] ?? null;
+ $issuer = $options['issuer'] ?? null;
+ $certsLocation = $options['certsLocation'] ?? self::FEDERATED_SIGNON_CERT_URL;
+ $cacheKey = $options['cacheKey'] ?? $this->getCacheKeyFromCertLocation($certsLocation);
+ $throwException = $options['throwException'] ?? \false;
+ // for backwards compatibility
+ // Check signature against each available cert.
+ $certs = $this->getCerts($certsLocation, $cacheKey, $options);
+ $alg = $this->determineAlg($certs);
+ if (!in_array($alg, ['RS256', 'ES256'])) {
+ throw new InvalidArgumentException('unrecognized "alg" in certs, expected ES256 or RS256');
+ }
+ try {
+ if ($alg == 'RS256') {
+ return $this->verifyRs256($token, $certs, $audience, $issuer);
+ }
+ return $this->verifyEs256($token, $certs, $audience, $issuer);
+ } catch (ExpiredException $e) {
+ // firebase/php-jwt 5+
+ } catch (SignatureInvalidException $e) {
+ // firebase/php-jwt 5+
+ } catch (InvalidTokenException $e) {
+ // simplejwt
+ } catch (InvalidArgumentException $e) {
+ } catch (UnexpectedValueException $e) {
+ }
+ if ($throwException) {
+ throw $e;
+ }
+ return \false;
+ }
+ /**
+ * Identifies the expected algorithm to verify by looking at the "alg" key
+ * of the provided certs.
+ *
+ * @param array $certs Certificate array according to the JWK spec (see
+ * https://tools.ietf.org/html/rfc7517).
+ * @return string The expected algorithm, such as "ES256" or "RS256".
+ */
+ private function determineAlg(array $certs)
+ {
+ $alg = null;
+ foreach ($certs as $cert) {
+ if (empty($cert['alg'])) {
+ throw new InvalidArgumentException('certs expects "alg" to be set');
+ }
+ $alg = $alg ?: $cert['alg'];
+ if ($alg != $cert['alg']) {
+ throw new InvalidArgumentException('More than one alg detected in certs');
+ }
+ }
+ return $alg;
+ }
+ /**
+ * Verifies an ES256-signed JWT.
+ *
+ * @param string $token The JSON Web Token to be verified.
+ * @param array $certs Certificate array according to the JWK spec (see
+ * https://tools.ietf.org/html/rfc7517).
+ * @param string|null $audience If set, returns false if the provided
+ * audience does not match the "aud" claim on the JWT.
+ * @param string|null $issuer If set, returns false if the provided
+ * issuer does not match the "iss" claim on the JWT.
+ * @return array the token payload, if successful, or false if not.
+ */
+ private function verifyEs256($token, array $certs, $audience = null, $issuer = null)
+ {
+ $this->checkSimpleJwt();
+ $jwkset = new KeySet();
+ foreach ($certs as $cert) {
+ $jwkset->add(KeyFactory::create($cert, 'php'));
+ }
+ // Validate the signature using the key set and ES256 algorithm.
+ $jwt = $this->callSimpleJwtDecode([$token, $jwkset, 'ES256']);
+ $payload = $jwt->getClaims();
+ if ($audience) {
+ if (!isset($payload['aud']) || $payload['aud'] != $audience) {
+ throw new UnexpectedValueException('Audience does not match');
+ }
+ }
+ // @see https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
+ $issuer = $issuer ?: self::IAP_ISSUER;
+ if (!isset($payload['iss']) || $payload['iss'] !== $issuer) {
+ throw new UnexpectedValueException('Issuer does not match');
+ }
+ return $payload;
+ }
+ /**
+ * Verifies an RS256-signed JWT.
+ *
+ * @param string $token The JSON Web Token to be verified.
+ * @param array $certs Certificate array according to the JWK spec (see
+ * https://tools.ietf.org/html/rfc7517).
+ * @param string|null $audience If set, returns false if the provided
+ * audience does not match the "aud" claim on the JWT.
+ * @param string|null $issuer If set, returns false if the provided
+ * issuer does not match the "iss" claim on the JWT.
+ * @return array the token payload, if successful, or false if not.
+ */
+ private function verifyRs256($token, array $certs, $audience = null, $issuer = null)
+ {
+ $this->checkAndInitializePhpsec();
+ $keys = [];
+ foreach ($certs as $cert) {
+ if (empty($cert['kid'])) {
+ throw new InvalidArgumentException('certs expects "kid" to be set');
+ }
+ if (empty($cert['n']) || empty($cert['e'])) {
+ throw new InvalidArgumentException('RSA certs expects "n" and "e" to be set');
+ }
+ $publicKey = $this->loadPhpsecPublicKey($cert['n'], $cert['e']);
+ // create an array of key IDs to certs for the JWT library
+ $keys[$cert['kid']] = new Key($publicKey, 'RS256');
+ }
+ $payload = $this->callJwtStatic('decode', [$token, $keys]);
+ if ($audience) {
+ if (!property_exists($payload, 'aud') || $payload->aud != $audience) {
+ throw new UnexpectedValueException('Audience does not match');
+ }
+ }
+ // support HTTP and HTTPS issuers
+ // @see https://developers.google.com/identity/sign-in/web/backend-auth
+ $issuers = $issuer ? [$issuer] : [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS];
+ if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) {
+ throw new UnexpectedValueException('Issuer does not match');
+ }
+ return (array) $payload;
+ }
+ /**
+ * Revoke an OAuth2 access token or refresh token. This method will revoke the current access
+ * token, if a token isn't provided.
+ *
+ * @param string|array $token The token (access token or a refresh token) that should be revoked.
+ * @param array $options [optional] Configuration options.
+ * @return bool Returns True if the revocation was successful, otherwise False.
+ */
+ public function revoke($token, array $options = [])
+ {
+ if (is_array($token)) {
+ if (isset($token['refresh_token'])) {
+ $token = $token['refresh_token'];
+ } else {
+ $token = $token['access_token'];
+ }
+ }
+ $body = Utils::streamFor(http_build_query(['token' => $token]));
+ $request = new Request('POST', self::OAUTH2_REVOKE_URI, ['Cache-Control' => 'no-store', 'Content-Type' => 'application/x-www-form-urlencoded'], $body);
+ $httpHandler = $this->httpHandler;
+ $response = $httpHandler($request, $options);
+ return $response->getStatusCode() == 200;
+ }
+ /**
+ * Gets federated sign-on certificates to use for verifying identity tokens.
+ * Returns certs as array structure, where keys are key ids, and values
+ * are PEM encoded certificates.
+ *
+ * @param string $location The location from which to retrieve certs.
+ * @param string $cacheKey The key under which to cache the retrieved certs.
+ * @param array $options [optional] Configuration options.
+ * @return array
+ * @throws InvalidArgumentException If received certs are in an invalid format.
+ */
+ private function getCerts($location, $cacheKey, array $options = [])
+ {
+ $cacheItem = $this->cache->getItem($cacheKey);
+ $certs = $cacheItem ? $cacheItem->get() : null;
+ $expireTime = null;
+ if (!$certs) {
+ list($certs, $expireTime) = $this->retrieveCertsFromLocation($location, $options);
+ }
+ if (!isset($certs['keys'])) {
+ if ($location !== self::IAP_CERT_URL) {
+ throw new InvalidArgumentException('federated sign-on certs expects "keys" to be set');
+ }
+ throw new InvalidArgumentException('certs expects "keys" to be set');
+ }
+ // Push caching off until after verifying certs are in a valid format.
+ // Don't want to cache bad data.
+ if ($expireTime) {
+ $cacheItem->expiresAt(new DateTime($expireTime));
+ $cacheItem->set($certs);
+ $this->cache->save($cacheItem);
+ }
+ return $certs['keys'];
+ }
+ /**
+ * Retrieve and cache a certificates file.
+ *
+ * @param string $url location
+ * @param array $options [optional] Configuration options.
+ * @return array{array, string}
+ * @throws InvalidArgumentException If certs could not be retrieved from a local file.
+ * @throws RuntimeException If certs could not be retrieved from a remote location.
+ */
+ private function retrieveCertsFromLocation($url, array $options = [])
+ {
+ // If we're retrieving a local file, just grab it.
+ $expireTime = '+1 hour';
+ if (strpos($url, 'http') !== 0) {
+ if (!file_exists($url)) {
+ throw new InvalidArgumentException(sprintf('Failed to retrieve verification certificates from path: %s.', $url));
+ }
+ return [json_decode((string) file_get_contents($url), \true), $expireTime];
+ }
+ $httpHandler = $this->httpHandler;
+ $response = $httpHandler(new Request('GET', $url), $options);
+ if ($response->getStatusCode() == 200) {
+ if ($cacheControl = $response->getHeaderLine('Cache-Control')) {
+ array_map(function ($value) use(&$expireTime) {
+ list($key, $value) = explode('=', $value) + [null, null];
+ if (trim($key) == 'max-age') {
+ $expireTime = '+' . $value . ' seconds';
+ }
+ }, explode(',', $cacheControl));
+ }
+ return [json_decode((string) $response->getBody(), \true), $expireTime];
+ }
+ throw new RuntimeException(sprintf('Failed to retrieve verification certificates: "%s".', $response->getBody()->getContents()), $response->getStatusCode());
+ }
+ /**
+ * @return void
+ */
+ private function checkAndInitializePhpsec()
+ {
+ if (!$this->checkAndInitializePhpsec2() && !$this->checkPhpsec3()) {
+ throw new RuntimeException('Please require phpseclib/phpseclib v2 or v3 to use this utility.');
+ }
+ }
+ /**
+ * @return string
+ * @throws TypeError If the key cannot be initialized to a string.
+ */
+ private function loadPhpsecPublicKey(string $modulus, string $exponent) : string
+ {
+ if (class_exists(RSA::class) && class_exists(BigInteger2::class)) {
+ $key = new RSA();
+ $key->loadKey(['n' => new BigInteger2($this->callJwtStatic('urlsafeB64Decode', [$modulus]), 256), 'e' => new BigInteger2($this->callJwtStatic('urlsafeB64Decode', [$exponent]), 256)]);
+ return $key->getPublicKey();
+ }
+ $key = PublicKeyLoader::load(['n' => new BigInteger3($this->callJwtStatic('urlsafeB64Decode', [$modulus]), 256), 'e' => new BigInteger3($this->callJwtStatic('urlsafeB64Decode', [$exponent]), 256)]);
+ $formattedPublicKey = $key->toString('PKCS8');
+ if (!is_string($formattedPublicKey)) {
+ throw new TypeError('Failed to initialize the key');
+ }
+ return $formattedPublicKey;
+ }
+ /**
+ * @return bool
+ */
+ private function checkAndInitializePhpsec2() : bool
+ {
+ if (!class_exists('phpseclib\\Crypt\\RSA')) {
+ return \false;
+ }
+ /**
+ * phpseclib calls "phpinfo" by default, which requires special
+ * whitelisting in the AppEngine VM environment. This function
+ * sets constants to bypass the need for phpseclib to check phpinfo
+ *
+ * @see phpseclib/Math/BigInteger
+ * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85
+ * @codeCoverageIgnore
+ */
+ if (filter_var(getenv('GAE_VM'), \FILTER_VALIDATE_BOOLEAN)) {
+ if (!defined('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\MATH_BIGINTEGER_OPENSSL_ENABLED')) {
+ define('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\MATH_BIGINTEGER_OPENSSL_ENABLED', \true);
+ }
+ if (!defined('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\CRYPT_RSA_MODE')) {
+ define('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\CRYPT_RSA_MODE', RSA::MODE_OPENSSL);
+ }
+ }
+ return \true;
+ }
+ /**
+ * @return bool
+ */
+ private function checkPhpsec3() : bool
+ {
+ return class_exists('Matomo\\Dependencies\\SearchEngineKeywordsPerformance\\phpseclib3\\Crypt\\RSA');
+ }
+ /**
+ * @return void
+ */
+ private function checkSimpleJwt()
+ {
+ // @codeCoverageIgnoreStart
+ if (!class_exists(SimpleJwt::class)) {
+ throw new RuntimeException('Please require kelvinmo/simplejwt ^0.2 to use this utility.');
+ }
+ // @codeCoverageIgnoreEnd
+ }
+ /**
+ * Provide a hook to mock calls to the JWT static methods.
+ *
+ * @param string $method
+ * @param array $args
+ * @return mixed
+ */
+ protected function callJwtStatic($method, array $args = [])
+ {
+ return call_user_func_array([JWT::class, $method], $args);
+ // @phpstan-ignore-line
+ }
+ /**
+ * Provide a hook to mock calls to the JWT static methods.
+ *
+ * @param array $args
+ * @return mixed
+ */
+ protected function callSimpleJwtDecode(array $args = [])
+ {
+ return call_user_func_array([SimpleJwt::class, 'decode'], $args);
+ }
+ /**
+ * Generate a cache key based on the cert location using sha1 with the
+ * exception of using "federated_signon_certs_v3" to preserve BC.
+ *
+ * @param string $certsLocation
+ * @return string
+ */
+ private function getCacheKeyFromCertLocation($certsLocation)
+ {
+ $key = $certsLocation === self::FEDERATED_SIGNON_CERT_URL ? 'federated_signon_certs_v3' : sha1($certsLocation);
+ return 'google_auth_certs_cache|' . $key;
+ }
+}
diff --git a/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/ApplicationDefaultCredentials.php b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/ApplicationDefaultCredentials.php
new file mode 100644
index 0000000..ab9b094
--- /dev/null
+++ b/files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/ApplicationDefaultCredentials.php
@@ -0,0 +1,301 @@
+push($middleware);
+ *
+ * $client = new Client([
+ * 'handler' => $stack,
+ * 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
+ * 'auth' => 'google_auth' // authorize all requests
+ * ]);
+ *
+ * $res = $client->get('myproject/taskqueues/myqueue');
+ * ```
+ */
+class ApplicationDefaultCredentials
+{
+ /**
+ * @deprecated
+ *
+ * Obtains an AuthTokenSubscriber that uses the default FetchAuthTokenInterface
+ * implementation to use in this environment.
+ *
+ * If supplied, $scope is used to in creating the credentials instance if
+ * this does not fallback to the compute engine defaults.
+ *
+ * @param string|string[] $scope the scope of the access request, expressed
+ * either as an Array or as a space-delimited String.
+ * @param callable $httpHandler callback which delivers psr7 request
+ * @param array $cacheConfig configuration for the cache when it's present
+ * @param CacheItemPoolInterface $cache A cache implementation, may be
+ * provided if you have one already available for use.
+ * @return AuthTokenSubscriber
+ * @throws DomainException if no implementation can be obtained.
+ */
+ public static function getSubscriber(
+ // @phpstan-ignore-line
+ $scope = null,
+ callable $httpHandler = null,
+ array $cacheConfig = null,
+ CacheItemPoolInterface $cache = null
+ )
+ {
+ $creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache);
+ /** @phpstan-ignore-next-line */
+ return new AuthTokenSubscriber($creds, $httpHandler);
+ }
+ /**
+ * Obtains an AuthTokenMiddleware that uses the default FetchAuthTokenInterface
+ * implementation to use in this environment.
+ *
+ * If supplied, $scope is used to in creating the credentials instance if
+ * this does not fallback to the compute engine defaults.
+ *
+ * @param string|string[] $scope the scope of the access request, expressed
+ * either as an Array or as a space-delimited String.
+ * @param callable $httpHandler callback which delivers psr7 request
+ * @param array $cacheConfig configuration for the cache when it's present
+ * @param CacheItemPoolInterface $cache A cache implementation, may be
+ * provided if you have one already available for use.
+ * @param string $quotaProject specifies a project to bill for access
+ * charges associated with the request.
+ * @return AuthTokenMiddleware
+ * @throws DomainException if no implementation can be obtained.
+ */
+ public static function getMiddleware($scope = null, callable $httpHandler = null, array $cacheConfig = null, CacheItemPoolInterface $cache = null, $quotaProject = null)
+ {
+ $creds = self::getCredentials($scope, $httpHandler, $cacheConfig, $cache, $quotaProject);
+ return new AuthTokenMiddleware($creds, $httpHandler);
+ }
+ /**
+ * Obtains the default FetchAuthTokenInterface implementation to use
+ * in this environment.
+ *
+ * @param string|string[] $scope the scope of the access request, expressed
+ * either as an Array or as a space-delimited String.
+ * @param callable $httpHandler callback which delivers psr7 request
+ * @param array $cacheConfig configuration for the cache when it's present
+ * @param CacheItemPoolInterface $cache A cache implementation, may be
+ * provided if you have one already available for use.
+ * @param string $quotaProject specifies a project to bill for access
+ * charges associated with the request.
+ * @param string|string[] $defaultScope The default scope to use if no
+ * user-defined scopes exist, expressed either as an Array or as a
+ * space-delimited string.
+ * @param string $universeDomain Specifies a universe domain to use for the
+ * calling client library
+ *
+ * @return FetchAuthTokenInterface
+ * @throws DomainException if no implementation can be obtained.
+ */
+ public static function getCredentials($scope = null, callable $httpHandler = null, array $cacheConfig = null, CacheItemPoolInterface $cache = null, $quotaProject = null, $defaultScope = null, string $universeDomain = null)
+ {
+ $creds = null;
+ $jsonKey = CredentialsLoader::fromEnv() ?: CredentialsLoader::fromWellKnownFile();
+ $anyScope = $scope ?: $defaultScope;
+ if (!$httpHandler) {
+ if (!($client = HttpClientCache::getHttpClient())) {
+ $client = new Client();
+ HttpClientCache::setHttpClient($client);
+ }
+ $httpHandler = HttpHandlerFactory::build($client);
+ }
+ if (is_null($quotaProject)) {
+ // if a quota project isn't specified, try to get one from the env var
+ $quotaProject = CredentialsLoader::quotaProjectFromEnv();
+ }
+ if (!is_null($jsonKey)) {
+ if ($quotaProject) {
+ $jsonKey['quota_project_id'] = $quotaProject;
+ }
+ if ($universeDomain) {
+ $jsonKey['universe_domain'] = $universeDomain;
+ }
+ $creds = CredentialsLoader::makeCredentials($scope, $jsonKey, $defaultScope);
+ } elseif (AppIdentityCredentials::onAppEngine() && !GCECredentials::onAppEngineFlexible()) {
+ $creds = new AppIdentityCredentials($anyScope);
+ } elseif (self::onGce($httpHandler, $cacheConfig, $cache)) {
+ $creds = new GCECredentials(null, $anyScope, null, $quotaProject, null, $universeDomain);
+ $creds->setIsOnGce(\true);
+ // save the credentials a trip to the metadata server
+ }
+ if (is_null($creds)) {
+ throw new DomainException(self::notFound());
+ }
+ if (!is_null($cache)) {
+ $creds = new FetchAuthTokenCache($creds, $cacheConfig, $cache);
+ }
+ return $creds;
+ }
+ /**
+ * Obtains an AuthTokenMiddleware which will fetch an ID token to use in the
+ * Authorization header. The middleware is configured with the default
+ * FetchAuthTokenInterface implementation to use in this environment.
+ *
+ * If supplied, $targetAudience is used to set the "aud" on the resulting
+ * ID token.
+ *
+ * @param string $targetAudience The audience for the ID token.
+ * @param callable $httpHandler callback which delivers psr7 request
+ * @param array $cacheConfig configuration for the cache when it's present
+ * @param CacheItemPoolInterface $cache A cache implementation, may be
+ * provided if you have one already available for use.
+ * @return AuthTokenMiddleware
+ * @throws DomainException if no implementation can be obtained.
+ */
+ public static function getIdTokenMiddleware($targetAudience, callable $httpHandler = null, array $cacheConfig = null, CacheItemPoolInterface $cache = null)
+ {
+ $creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);
+ return new AuthTokenMiddleware($creds, $httpHandler);
+ }
+ /**
+ * Obtains an ProxyAuthTokenMiddleware which will fetch an ID token to use in the
+ * Authorization header. The middleware is configured with the default
+ * FetchAuthTokenInterface implementation to use in this environment.
+ *
+ * If supplied, $targetAudience is used to set the "aud" on the resulting
+ * ID token.
+ *
+ * @param string $targetAudience The audience for the ID token.
+ * @param callable $httpHandler callback which delivers psr7 request
+ * @param array $cacheConfig configuration for the cache when it's present
+ * @param CacheItemPoolInterface $cache A cache implementation, may be
+ * provided if you have one already available for use.
+ * @return ProxyAuthTokenMiddleware
+ * @throws DomainException if no implementation can be obtained.
+ */
+ public static function getProxyIdTokenMiddleware($targetAudience, callable $httpHandler = null, array $cacheConfig = null, CacheItemPoolInterface $cache = null)
+ {
+ $creds = self::getIdTokenCredentials($targetAudience, $httpHandler, $cacheConfig, $cache);
+ return new ProxyAuthTokenMiddleware($creds, $httpHandler);
+ }
+ /**
+ * Obtains the default FetchAuthTokenInterface implementation to use
+ * in this environment, configured with a $targetAudience for fetching an ID
+ * token.
+ *
+ * @param string $targetAudience The audience for the ID token.
+ * @param callable $httpHandler callback which delivers psr7 request
+ * @param array