From e6764e9249c59523feb093149774cc74a19400d2 Mon Sep 17 00:00:00 2001
From: Christopher Butler
Date: Thu, 3 Jul 2025 13:24:32 -0400
Subject: [PATCH] Add New Premium Plugin
Why these changes are being introduced:
A third premium plugin was purchased from InnoCraft. At the same time,
one of the other premium plugins had a pending update. Per the
documentation in the HOWTO-premium-plugins.md, this pass through the
repository just installs the plugin directory and files but does not
activate the plugin.
Additionally, we revert the copying of the config.ini.php file back to
the /var/www/html folder instead into the source folder. Now that we
have established the persistent storage for the config.ini file, we
don't need to worry about forcing it into the source.
How this addresses that need:
* Add the source code for the SearchEngineKeywordsPerformance plugin to
the files/ directory
* Add the new version for the HeatmapSessionRecording plugin to the
files/ directory
* Update the Dockerfile to copy the new plugin sources to the plugins/
directory
* Update the Dockerfile to copy the config.ini.php to /var/www/html
instead of to /usr/src/matomo
* Update the backup-data.sh script to make it a little cleaner
* Update the HOWTO-premium-plugins.md documentation
* Update the HOWTO-miscellaneous.md documentation to address a TooManyRedirectsException
Relevant ticket(s):
* https://mitlibraries.atlassian.net/browse/INFRA-558
---
Dockerfile | 12 +-
docs/HowTos/HOWTO-miscellaneous.md | 9 +-
docs/HowTos/HOWTO-premium-plugins.md | 18 +-
files/backup-data.sh | 30 +-
.../API.php | 999 +++
.../Actions/ActionHsr.php | 61 +
.../Activity/BaseActivity.php | 114 +
.../Activity/HeatmapAdded.php | 41 +
.../Activity/HeatmapDeleted.php | 46 +
.../Activity/HeatmapEnded.php | 44 +
.../Activity/HeatmapPaused.php | 46 +
.../Activity/HeatmapResumed.php | 46 +
.../Activity/HeatmapScreenshotDeleted.php | 46 +
.../Activity/HeatmapUpdated.php | 42 +
.../Activity/RecordedPageviewDeleted.php | 46 +
.../Activity/RecordedSessionDeleted.php | 46 +
.../Activity/SessionRecordingAdded.php | 41 +
.../Activity/SessionRecordingDeleted.php | 46 +
.../Activity/SessionRecordingEnded.php | 44 +
.../Activity/SessionRecordingPaused.php | 46 +
.../Activity/SessionRecordingResumed.php | 46 +
.../Activity/SessionRecordingUpdated.php | 42 +
.../Archiver/Aggregator.php | 476 ++
.../CHANGELOG.md | 452 ++
.../Categories/HeatmapCategory.php | 26 +
.../Categories/ManageHeatmapSubcategory.php | 32 +
.../ManageSessionRecordingSubcategory.php | 32 +
.../Categories/SessionRecordingsCategory.php | 26 +
.../Columns/Metrics/BaseMetric.php | 27 +
.../Columns/Metrics/Browser.php | 67 +
.../Columns/Metrics/Device.php | 78 +
.../Columns/Metrics/Location.php | 86 +
.../Columns/Metrics/OperatingSystem.php | 67 +
.../Columns/Metrics/SessionTime.php | 97 +
.../Columns/Metrics/TimeOnPage.php | 65 +
.../Columns/Metrics/TimeOnSite.php | 37 +
.../Columns/Metrics/TotalEvents.php | 48 +
.../Commands/RemoveHeatmapScreenshot.php | 101 +
.../Configuration.php | 145 +
.../Controller.php | 463 ++
.../Dao/LogHsr.php | 375 ++
.../Dao/LogHsrBlob.php | 180 +
.../Dao/LogHsrEvent.php | 166 +
.../Dao/LogHsrSite.php | 141 +
.../Dao/SiteHsrDao.php | 422 ++
.../Filter/EnrichRecordedSessions.php | 73 +
.../Diagnostic/ConfigsPhpCheck.php | 112 +
.../HeatmapSessionRecording.php | 886 +++
.../Input/Breakpoint.php | 66 +
.../Input/CaptureKeystrokes.php | 40 +
.../Input/ExcludedElements.php | 50 +
.../Input/MinSessionTime.php | 57 +
.../Input/Name.php | 51 +
.../Input/PageRule.php | 80 +
.../Input/PageRules.php | 61 +
.../Input/RequiresActivity.php | 40 +
.../Input/SampleLimit.php | 57 +
.../Input/SampleRate.php | 62 +
.../Input/ScreenshotUrl.php | 58 +
.../Input/Validator.php | 176 +
.../Install/HtAccess.php | 66 +
.../Install/htaccessTemplate | 23 +
.../LEGALNOTICE | 46 +
.../LICENSE | 49 +
.../Menu.php | 50 +
.../Model/SiteHsrModel.php | 489 ++
.../MutationManipulator.php | 226 +
.../README.md | 120 +
.../Reports/GetRecordedSessions.php | 344 +
.../Settings/TrackingDisableDefault.php | 36 +
.../SystemSettings.php | 296 +
.../Tasks.php | 77 +
.../Tracker/Configs.php | 160 +
.../Tracker/HsrMatcher.php | 224 +
.../Tracker/LogTable/LogHsr.php | 42 +
.../Tracker/LogTable/LogHsrBlob.php | 37 +
.../Tracker/LogTable/LogHsrEvent.php | 37 +
.../Tracker/PageRuleMatcher.php | 305 +
.../Tracker/RequestProcessor.php | 312 +
.../Updates/3.0.10.php | 51 +
.../Updates/3.0.11.php | 51 +
.../Updates/3.0.3.php | 30 +
.../Updates/4.0.0.php | 55 +
.../Updates/5.1.0.php | 54 +
.../VisitorDetails.php | 125 +
.../Widgets/GetManageHeatmaps.php | 77 +
.../Widgets/GetManageSessionRecordings.php | 66 +
.../Widgets/GettingStartedHeatmap.php | 75 +
.../Widgets/GettingStartedSessions.php | 75 +
.../config/config.php | 21 +
.../configs.php | 161 +
.../docs/index.md | 3 +
.../javascripts/recording.js | 636 ++
.../javascripts/rowaction.js | 211 +
.../lang/bg.json | 7 +
.../lang/cs.json | 190 +
.../lang/da.json | 1 +
.../lang/de.json | 80 +
.../lang/en.json | 237 +
.../lang/es.json | 183 +
.../lang/fi.json | 1 +
.../lang/fr.json | 237 +
.../lang/hi.json | 1 +
.../lang/it.json | 190 +
.../lang/ja.json | 1 +
.../lang/nb.json | 35 +
.../lang/nl.json | 185 +
.../lang/pl.json | 185 +
.../lang/pt.json | 185 +
.../lang/ro.json | 6 +
.../lang/ru.json | 1 +
.../lang/sq.json | 237 +
.../lang/sv.json | 80 +
.../lang/tr.json | 237 +
.../lang/uk.json | 6 +
.../lang/zh-cn.json | 1 +
.../lang/zh-tw.json | 185 +
.../MutationObserver.js/MutationObserver.js | 624 ++
.../libs/MutationObserver.js/README.md | 68 +
.../libs/MutationObserver.js/dist/README.md | 7 +
.../dist/mutationobserver.min.js | 10 +
.../libs/MutationObserver.js/license | 11 +
.../libs/MutationObserver.js/package.json | 49 +
.../libs/mutation-summary/COPYING | 202 +
.../libs/mutation-summary/README.md | 59 +
.../libs/mutation-summary/package.json | 30 +
.../mutation-summary/src/mutation-summary.js | 1406 +++++
.../mutation-summary/src/mutation-summary.ts | 1750 ++++++
.../libs/mutation-summary/util/tree-mirror.js | 268 +
.../libs/mutation-summary/util/tree-mirror.ts | 375 ++
.../libs/svg.js/CHANGELOG.md | 642 ++
.../libs/svg.js/LICENSE.txt | 21 +
.../libs/svg.js/README.md | 29 +
.../libs/svg.js/dist/svg.js | 5518 +++++++++++++++++
.../libs/svg.js/dist/svg.min.js | 3 +
.../libs/svg.js/package.json | 84 +
.../package-lock.json | 71 +
.../package.json | 27 +
.../phpcs.xml | 37 +
.../plugin.json | 41 +
.../pull_request_template.md | 26 +
.../stylesheets/edit-entities.less | 55 +
.../stylesheets/list-entities.less | 35 +
.../stylesheets/recordings.less | 154 +
.../templates/_detectAdBlocker.twig | 26 +
.../templates/embedPage.twig | 43 +
.../templates/gettingStartedHeatmaps.twig | 2 +
.../templates/gettingStartedSessions.twig | 2 +
.../templates/manageHeatmap.twig | 22 +
.../templates/manageSessions.twig | 20 +
.../templates/replayRecording.twig | 60 +
.../templates/showHeatmap.twig | 21 +
.../tracker.min.js | 125 +
.../tsconfig.json | 14 +
.../vue/dist/HeatmapSessionRecording.umd.js | 5480 ++++++++++++++++
.../dist/HeatmapSessionRecording.umd.min.js | 73 +
.../vue/dist/umd.metadata.json | 6 +
.../vue/src/HeatmapVis/HeatmapVis.less | 99 +
.../vue/src/HeatmapVis/HeatmapVis.vue | 1102 ++++
.../vue/src/HeatmapVis/HeatmapVisPage.vue | 184 +
.../vue/src/HsrStore/HsrStore.store.ts | 213 +
.../vue/src/HsrTargetTest/HsrTargetTest.less | 20 +
.../vue/src/HsrTargetTest/HsrTargetTest.vue | 181 +
.../AvailableTargetPageRules.store.ts | 57 +
.../vue/src/HsrUrlTarget/HsrUrlTarget.less | 17 +
.../vue/src/HsrUrlTarget/HsrUrlTarget.vue | 199 +
.../src/ListOfPageviews/ListOfPageviews.vue | 85 +
.../vue/src/ManageHeatmap/Edit.vue | 522 ++
.../vue/src/ManageHeatmap/List.vue | 283 +
.../vue/src/ManageHeatmap/Manage.vue | 121 +
.../vue/src/ManageSessionRecording/Edit.vue | 488 ++
.../vue/src/ManageSessionRecording/List.vue | 286 +
.../vue/src/ManageSessionRecording/Manage.vue | 105 +
.../MatomoJsNotWritableAlert.vue | 47 +
.../SessionRecordingVis.less | 131 +
.../SessionRecordingVis.vue | 1222 ++++
.../vue/src/Tooltip/Tooltip.less | 59 +
.../vue/src/Tooltip/Tooltip.vue | 112 +
.../vue/src/getIframeWindow.ts | 27 +
.../vue/src/index.ts | 29 +
.../vue/src/oneAtATime.ts | 43 +
.../vue/src/types.ts | 131 +
.../API.php | 410 ++
.../Activity/AccountAdded.php | 46 +
.../Activity/AccountRemoved.php | 46 +
.../Activity/GoogleClientConfigChanged.php | 46 +
.../Activity/YandexClientConfigChanged.php | 46 +
.../Archiver.php | 35 +
.../CHANGELOG.md | 276 +
.../CrawlingOverviewSubcategory.php | 31 +
.../Categories/SearchKeywordsSubcategory.php | 26 +
.../Client/Bing.php | 310 +
.../Configuration/BaseConfiguration.php | 49 +
.../Client/Configuration/Bing.php | 77 +
.../Client/Configuration/Google.php | 178 +
.../Client/Configuration/Yandex.php | 130 +
.../Client/Google.php | 489 ++
.../Client/Yandex.php | 545 ++
.../Columns/Keyword.php | 28 +
.../Commands/ImportBing.php | 51 +
.../Commands/ImportGoogle.php | 55 +
.../Commands/ImportYandex.php | 55 +
.../Controller.php | 1125 ++++
.../Diagnostic/BingAccountDiagnostic.php | 74 +
.../Diagnostic/GoogleAccountDiagnostic.php | 71 +
.../Diagnostic/YandexAccountDiagnostic.php | 76 +
.../InvalidClientConfigException.php | 21 +
.../InvalidCredentialsException.php | 21 +
.../MissingClientConfigException.php | 21 +
.../MissingOAuthConfigException.php | 21 +
.../Exceptions/RateLimitApiException.php | 21 +
.../Exceptions/UnknownAPIException.php | 21 +
.../Importer/Bing.php | 288 +
.../Importer/Google.php | 361 ++
.../Importer/Yandex.php | 292 +
.../LICENSE | 49 +
.../MeasurableSettings.php | 182 +
.../Menu.php | 30 +
.../Metrics.php | 144 +
.../Model/Bing.php | 168 +
.../Model/Google.php | 131 +
.../Model/Yandex.php | 147 +
.../Monolog/Handler/SEKPSystemLogHandler.php | 33 +
.../Provider/Bing.php | 155 +
.../Provider/Google.php | 147 +
.../Provider/Helper/MeasurableHelper.php | 53 +
.../Provider/ProviderAbstract.php | 151 +
.../Provider/Yandex.php | 147 +
.../README.md | 87 +
.../RecordBuilders/Base.php | 32 +
.../RecordBuilders/Bing.php | 204 +
.../RecordBuilders/Google.php | 155 +
.../RecordBuilders/Yandex.php | 196 +
.../Reports/Base.php | 205 +
.../Reports/GetCrawlingErrorExamplesBing.php | 100 +
.../Reports/GetCrawlingOverviewBing.php | 132 +
.../Reports/GetCrawlingOverviewYandex.php | 107 +
.../Reports/GetKeywords.php | 53 +
.../Reports/GetKeywordsBing.php | 90 +
.../Reports/GetKeywordsGoogleImage.php | 44 +
.../Reports/GetKeywordsGoogleNews.php | 44 +
.../Reports/GetKeywordsGoogleVideo.php | 44 +
.../Reports/GetKeywordsGoogleWeb.php | 43 +
.../Reports/GetKeywordsImported.php | 58 +
.../Reports/GetKeywordsReferrers.php | 46 +
.../Reports/GetKeywordsYandex.php | 62 +
.../SearchEngineKeywordsPerformance.php | 962 +++
.../SystemSettings.php | 41 +
.../Tasks.php | 101 +
.../Updates/3.5.0.php | 45 +
.../Updates/4.1.0.php | 30 +
.../Updates/4.2.0.php | 29 +
.../config/config.php | 29 +
.../docs/index.md | 3 +
.../images/Bing.png | Bin 0 -> 14323 bytes
.../images/Google.png | Bin 0 -> 18767 bytes
.../images/Yahoo.png | Bin 0 -> 7776 bytes
.../images/Yandex.png | Bin 0 -> 18422 bytes
.../lang/bg.json | 6 +
.../lang/cs.json | 1 +
.../lang/da.json | 1 +
.../lang/de.json | 224 +
.../lang/en.json | 240 +
.../lang/es.json | 170 +
.../lang/fi.json | 1 +
.../lang/fr.json | 241 +
.../lang/hi.json | 1 +
.../lang/it.json | 225 +
.../lang/ja.json | 1 +
.../lang/nb.json | 1 +
.../lang/nl.json | 183 +
.../lang/pl.json | 178 +
.../lang/pt.json | 178 +
.../lang/ro.json | 1 +
.../lang/ru.json | 81 +
.../lang/sq.json | 240 +
.../lang/sv.json | 37 +
.../lang/tr.json | 241 +
.../lang/uk.json | 1 +
.../lang/zh-cn.json | 1 +
.../lang/zh-tw.json | 178 +
.../phpcs.xml | 41 +
.../plugin.json | 31 +
.../pull_request_template.md | 26 +
.../scoper.inc.php | 141 +
.../stylesheets/styles.less | 262 +
.../templates/bing/configuration.twig | 24 +
.../templates/google/configuration.twig | 32 +
.../templates/index.twig | 12 +
.../messageReferrerKeywordsReport.twig | 10 +
.../templates/yandex/configuration.twig | 31 +
.../vendor/autoload.php | 10 +
.../vendor/autoload_original.php | 12 +
.../vendor/composer/ClassLoader.php | 572 ++
.../vendor/composer/InstalledVersions.php | 352 ++
.../vendor/composer/LICENSE | 21 +
.../vendor/composer/autoload_classmap.php | 10 +
.../vendor/composer/autoload_files.php | 16 +
.../vendor/composer/autoload_namespaces.php | 9 +
.../vendor/composer/autoload_psr4.php | 21 +
.../vendor/composer/autoload_real.php | 55 +
.../vendor/composer/autoload_static.php | 105 +
.../vendor/composer/installed.json | 1156 ++++
.../vendor/composer/installed.php | 197 +
.../vendor/prefixed/firebase/php-jwt/LICENSE | 30 +
.../php-jwt/src/BeforeValidException.php | 19 +
.../firebase/php-jwt/src/CachedKeySet.php | 226 +
.../firebase/php-jwt/src/ExpiredException.php | 19 +
.../prefixed/firebase/php-jwt/src/JWK.php | 267 +
.../prefixed/firebase/php-jwt/src/JWT.php | 572 ++
.../src/JWTExceptionWithPayloadInterface.php | 20 +
.../prefixed/firebase/php-jwt/src/Key.php | 50 +
.../php-jwt/src/SignatureInvalidException.php | 7 +
.../google/apiclient-services/LICENSE | 203 +
.../google/apiclient-services/autoload.php | 28 +
.../google/apiclient-services/renovate.json | 7 +
.../google/apiclient-services/src/Oauth2.php | 82 +
.../src/Oauth2/Resource/Userinfo.php | 45 +
.../src/Oauth2/Resource/UserinfoV2.php | 32 +
.../src/Oauth2/Resource/UserinfoV2Me.php | 45 +
.../src/Oauth2/Tokeninfo.php | 88 +
.../src/Oauth2/Userinfo.php | 124 +
.../apiclient-services/src/SearchConsole.php | 67 +
.../src/SearchConsole/ApiDataRow.php | 70 +
.../src/SearchConsole/ApiDimensionFilter.php | 51 +
.../SearchConsole/ApiDimensionFilterGroup.php | 50 +
.../src/SearchConsole/BlockedResource.php | 33 +
.../src/SearchConsole/Image.php | 42 +
.../src/SearchConsole/MobileFriendlyIssue.php | 33 +
.../Resource/Searchanalytics.php | 54 +
.../src/SearchConsole/Resource/Sitemaps.php | 99 +
.../src/SearchConsole/Resource/Sites.php | 86 +
.../Resource/UrlTestingTools.php | 32 +
.../UrlTestingToolsMobileFriendlyTest.php | 47 +
.../src/SearchConsole/ResourceIssue.php | 40 +
.../RunMobileFriendlyTestRequest.php | 42 +
.../RunMobileFriendlyTestResponse.php | 98 +
.../SearchAnalyticsQueryRequest.php | 122 +
.../SearchAnalyticsQueryResponse.php | 50 +
.../SearchConsole/SitemapsListResponse.php | 41 +
.../src/SearchConsole/SitesListResponse.php | 41 +
.../src/SearchConsole/TestStatus.php | 42 +
.../src/SearchConsole/WmxSite.php | 42 +
.../src/SearchConsole/WmxSitemap.php | 113 +
.../src/SearchConsole/WmxSitemapContent.php | 51 +
.../google/apiclient-services/synth.metadata | 18 +
.../google/apiclient-services/synth.py | 119 +
.../vendor/prefixed/google/apiclient/LICENSE | 203 +
.../apiclient/src/AccessToken/Revoke.php | 65 +
.../apiclient/src/AccessToken/Verify.php | 217 +
.../src/AuthHandler/AuthHandlerFactory.php | 47 +
.../src/AuthHandler/Guzzle6AuthHandler.php | 76 +
.../src/AuthHandler/Guzzle7AuthHandler.php | 25 +
.../prefixed/google/apiclient/src/Client.php | 1032 +++
.../google/apiclient/src/Collection.php | 104 +
.../google/apiclient/src/Exception.php | 23 +
.../google/apiclient/src/Http/Batch.php | 190 +
.../apiclient/src/Http/MediaFileUpload.php | 273 +
.../google/apiclient/src/Http/REST.php | 153 +
.../prefixed/google/apiclient/src/Model.php | 301 +
.../prefixed/google/apiclient/src/Service.php | 63 +
.../apiclient/src/Service/Exception.php | 65 +
.../google/apiclient/src/Service/Resource.php | 214 +
.../google/apiclient/src/Task/Composer.php | 77 +
.../google/apiclient/src/Task/Exception.php | 23 +
.../google/apiclient/src/Task/Retryable.php | 26 +
.../google/apiclient/src/Task/Runner.php | 235 +
.../apiclient/src/Utils/UriTemplate.php | 264 +
.../prefixed/google/apiclient/src/aliases.php | 80 +
.../vendor/prefixed/google/auth/COPYING | 202 +
.../vendor/prefixed/google/auth/LICENSE | 203 +
.../vendor/prefixed/google/auth/VERSION | 1 +
.../vendor/prefixed/google/auth/autoload.php | 35 +
.../prefixed/google/auth/src/AccessToken.php | 430 ++
.../src/ApplicationDefaultCredentials.php | 301 +
.../src/Cache/InvalidArgumentException.php | 23 +
.../prefixed/google/auth/src/Cache/Item.php | 146 +
.../auth/src/Cache/MemoryCacheItemPool.php | 161 +
.../auth/src/Cache/SysVCacheItemPool.php | 207 +
.../google/auth/src/Cache/TypedItem.php | 152 +
.../prefixed/google/auth/src/CacheTrait.php | 96 +
.../src/CredentialSource/AwsNativeSource.php | 287 +
.../auth/src/CredentialSource/FileSource.php | 69 +
.../auth/src/CredentialSource/UrlSource.php | 83 +
.../Credentials/AppIdentityCredentials.php | 209 +
.../ExternalAccountCredentials.php | 177 +
.../auth/src/Credentials/GCECredentials.php | 493 ++
.../auth/src/Credentials/IAMCredentials.php | 77 +
.../ImpersonatedServiceAccountCredentials.php | 120 +
.../src/Credentials/InsecureCredentials.php | 62 +
.../Credentials/ServiceAccountCredentials.php | 312 +
.../ServiceAccountJwtAccessCredentials.php | 171 +
.../Credentials/UserRefreshCredentials.php | 130 +
.../google/auth/src/CredentialsLoader.php | 242 +
...ternalAccountCredentialSourceInterface.php | 23 +
.../google/auth/src/FetchAuthTokenCache.php | 238 +
.../auth/src/FetchAuthTokenInterface.php | 52 +
.../prefixed/google/auth/src/GCECache.php | 70 +
.../auth/src/GetQuotaProjectInterface.php | 32 +
.../auth/src/GetUniverseDomainInterface.php | 34 +
.../src/HttpHandler/Guzzle6HttpHandler.php | 59 +
.../src/HttpHandler/Guzzle7HttpHandler.php | 22 +
.../auth/src/HttpHandler/HttpClientCache.php | 51 +
.../src/HttpHandler/HttpHandlerFactory.php | 62 +
.../vendor/prefixed/google/auth/src/Iam.php | 79 +
.../google/auth/src/IamSignerTrait.php | 58 +
.../src/Middleware/AuthTokenMiddleware.php | 138 +
.../Middleware/ProxyAuthTokenMiddleware.php | 130 +
.../ScopedAccessTokenMiddleware.php | 140 +
.../auth/src/Middleware/SimpleMiddleware.php | 86 +
.../prefixed/google/auth/src/OAuth2.php | 1515 +++++
.../auth/src/ProjectIdProviderInterface.php | 32 +
.../auth/src/ServiceAccountSignerTrait.php | 53 +
.../google/auth/src/SignBlobInterface.php | 43 +
.../auth/src/UpdateMetadataInterface.php | 36 +
.../google/auth/src/UpdateMetadataTrait.php | 62 +
.../vendor/prefixed/guzzlehttp/guzzle/LICENSE | 27 +
.../guzzlehttp/guzzle/src/BodySummarizer.php | 23 +
.../guzzle/src/BodySummarizerInterface.php | 12 +
.../prefixed/guzzlehttp/guzzle/src/Client.php | 402 ++
.../guzzlehttp/guzzle/src/ClientInterface.php | 78 +
.../guzzlehttp/guzzle/src/ClientTrait.php | 227 +
.../guzzle/src/Cookie/CookieJar.php | 240 +
.../guzzle/src/Cookie/CookieJarInterface.php | 74 +
.../guzzle/src/Cookie/FileCookieJar.php | 92 +
.../guzzle/src/Cookie/SessionCookieJar.php | 71 +
.../guzzle/src/Cookie/SetCookie.php | 407 ++
.../src/Exception/BadResponseException.php | 31 +
.../guzzle/src/Exception/ClientException.php | 10 +
.../guzzle/src/Exception/ConnectException.php | 47 +
.../guzzle/src/Exception/GuzzleException.php | 8 +
.../Exception/InvalidArgumentException.php | 7 +
.../guzzle/src/Exception/RequestException.php | 124 +
.../guzzle/src/Exception/ServerException.php | 10 +
.../Exception/TooManyRedirectsException.php | 7 +
.../src/Exception/TransferException.php | 7 +
.../guzzle/src/Handler/CurlFactory.php | 496 ++
.../src/Handler/CurlFactoryInterface.php | 23 +
.../guzzle/src/Handler/CurlHandler.php | 43 +
.../guzzle/src/Handler/CurlMultiHandler.php | 220 +
.../guzzle/src/Handler/EasyHandle.php | 91 +
.../guzzle/src/Handler/HeaderProcessor.php | 36 +
.../guzzle/src/Handler/MockHandler.php | 174 +
.../guzzlehttp/guzzle/src/Handler/Proxy.php | 49 +
.../guzzle/src/Handler/StreamHandler.php | 455 ++
.../guzzlehttp/guzzle/src/HandlerStack.php | 238 +
.../guzzle/src/MessageFormatter.php | 168 +
.../guzzle/src/MessageFormatterInterface.php | 17 +
.../guzzlehttp/guzzle/src/Middleware.php | 227 +
.../prefixed/guzzlehttp/guzzle/src/Pool.php | 116 +
.../guzzle/src/PrepareBodyMiddleware.php | 86 +
.../guzzle/src/RedirectMiddleware.php | 162 +
.../guzzlehttp/guzzle/src/RequestOptions.php | 244 +
.../guzzlehttp/guzzle/src/RetryMiddleware.php | 91 +
.../guzzlehttp/guzzle/src/TransferStats.php | 114 +
.../prefixed/guzzlehttp/guzzle/src/Utils.php | 339 +
.../guzzlehttp/guzzle/src/functions.php | 158 +
.../guzzle/src/functions_include.php | 8 +
.../prefixed/guzzlehttp/promises/LICENSE | 24 +
.../promises/src/AggregateException.php | 14 +
.../promises/src/CancellationException.php | 10 +
.../guzzlehttp/promises/src/Coroutine.php | 151 +
.../guzzlehttp/promises/src/Create.php | 75 +
.../prefixed/guzzlehttp/promises/src/Each.php | 66 +
.../guzzlehttp/promises/src/EachPromise.php | 200 +
.../promises/src/FulfilledPromise.php | 69 +
.../prefixed/guzzlehttp/promises/src/Is.php | 43 +
.../guzzlehttp/promises/src/Promise.php | 237 +
.../promises/src/PromiseInterface.php | 87 +
.../promises/src/PromisorInterface.php | 16 +
.../promises/src/RejectedPromise.php | 75 +
.../promises/src/RejectionException.php | 40 +
.../guzzlehttp/promises/src/TaskQueue.php | 62 +
.../promises/src/TaskQueueInterface.php | 22 +
.../guzzlehttp/promises/src/Utils.php | 239 +
.../guzzlehttp/promises/src/functions.php | 334 +
.../promises/src/functions_include.php | 8 +
.../vendor/prefixed/guzzlehttp/psr7/LICENSE | 26 +
.../guzzlehttp/psr7/src/AppendStream.php | 205 +
.../guzzlehttp/psr7/src/BufferStream.php | 123 +
.../guzzlehttp/psr7/src/CachingStream.php | 125 +
.../guzzlehttp/psr7/src/DroppingStream.php | 40 +
.../src/Exception/MalformedUriException.php | 12 +
.../prefixed/guzzlehttp/psr7/src/FnStream.php | 150 +
.../prefixed/guzzlehttp/psr7/src/Header.php | 117 +
.../guzzlehttp/psr7/src/HttpFactory.php | 76 +
.../guzzlehttp/psr7/src/InflateStream.php | 33 +
.../guzzlehttp/psr7/src/LazyOpenStream.php | 41 +
.../guzzlehttp/psr7/src/LimitStream.php | 128 +
.../prefixed/guzzlehttp/psr7/src/Message.php | 189 +
.../guzzlehttp/psr7/src/MessageTrait.php | 212 +
.../prefixed/guzzlehttp/psr7/src/MimeType.php | 27 +
.../guzzlehttp/psr7/src/MultipartStream.php | 124 +
.../guzzlehttp/psr7/src/NoSeekStream.php | 23 +
.../guzzlehttp/psr7/src/PumpStream.php | 151 +
.../prefixed/guzzlehttp/psr7/src/Query.php | 104 +
.../prefixed/guzzlehttp/psr7/src/Request.php | 124 +
.../prefixed/guzzlehttp/psr7/src/Response.php | 78 +
.../prefixed/guzzlehttp/psr7/src/Rfc7230.php | 22 +
.../guzzlehttp/psr7/src/ServerRequest.php | 270 +
.../prefixed/guzzlehttp/psr7/src/Stream.php | 237 +
.../psr7/src/StreamDecoratorTrait.php | 133 +
.../guzzlehttp/psr7/src/StreamWrapper.php | 114 +
.../guzzlehttp/psr7/src/UploadedFile.php | 152 +
.../prefixed/guzzlehttp/psr7/src/Uri.php | 570 ++
.../guzzlehttp/psr7/src/UriComparator.php | 43 +
.../guzzlehttp/psr7/src/UriNormalizer.php | 175 +
.../guzzlehttp/psr7/src/UriResolver.php | 180 +
.../prefixed/guzzlehttp/psr7/src/Utils.php | 375 ++
.../constant_time_encoding/LICENSE.txt | 48 +
.../constant_time_encoding/src/Base32.php | 404 ++
.../constant_time_encoding/src/Base32Hex.php | 98 +
.../constant_time_encoding/src/Base64.php | 257 +
.../src/Base64DotSlash.php | 78 +
.../src/Base64DotSlashOrdered.php | 74 +
.../src/Base64UrlSafe.php | 82 +
.../constant_time_encoding/src/Binary.php | 85 +
.../src/EncoderInterface.php | 51 +
.../constant_time_encoding/src/Encoding.php | 244 +
.../constant_time_encoding/src/Hex.php | 124 +
.../constant_time_encoding/src/RFC4648.php | 176 +
.../prefixed/paragonie/random_compat/LICENSE | 22 +
.../paragonie/random_compat/build-phar.sh | 5 +
.../dist/random_compat.phar.pubkey | 5 +
.../dist/random_compat.phar.pubkey.asc | 11 +
.../paragonie/random_compat/lib/random.php | 34 +
.../random_compat/other/build_phar.php | 44 +
.../random_compat/psalm-autoload.php | 10 +
.../paragonie/random_compat/psalm.xml | 19 +
.../prefixed/phpseclib/phpseclib/AUTHORS | 7 +
.../prefixed/phpseclib/phpseclib/LICENSE | 20 +
.../phpseclib/Common/Functions/Strings.php | 454 ++
.../phpseclib/phpseclib/Crypt/AES.php | 112 +
.../phpseclib/phpseclib/Crypt/Blowfish.php | 660 ++
.../phpseclib/phpseclib/Crypt/ChaCha20.php | 999 +++
.../phpseclib/Crypt/Common/AsymmetricKey.php | 511 ++
.../phpseclib/Crypt/Common/BlockCipher.php | 23 +
.../Crypt/Common/Formats/Keys/JWK.php | 62 +
.../Crypt/Common/Formats/Keys/OpenSSH.php | 195 +
.../Crypt/Common/Formats/Keys/PKCS.php | 67 +
.../Crypt/Common/Formats/Keys/PKCS1.php | 187 +
.../Crypt/Common/Formats/Keys/PKCS8.php | 599 ++
.../Crypt/Common/Formats/Keys/PuTTY.php | 324 +
.../Crypt/Common/Formats/Signature/Raw.php | 53 +
.../phpseclib/Crypt/Common/PrivateKey.php | 29 +
.../phpseclib/Crypt/Common/PublicKey.php | 24 +
.../phpseclib/Crypt/Common/StreamCipher.php | 51 +
.../phpseclib/Crypt/Common/SymmetricKey.php | 3096 +++++++++
.../Crypt/Common/Traits/Fingerprint.php | 55 +
.../Crypt/Common/Traits/PasswordProtected.php | 44 +
.../phpseclib/phpseclib/Crypt/DES.php | 522 ++
.../phpseclib/phpseclib/Crypt/DH.php | 295 +
.../phpseclib/Crypt/DH/Formats/Keys/PKCS1.php | 65 +
.../phpseclib/Crypt/DH/Formats/Keys/PKCS8.php | 115 +
.../phpseclib/Crypt/DH/Parameters.php | 33 +
.../phpseclib/Crypt/DH/PrivateKey.php | 64 +
.../phpseclib/Crypt/DH/PublicKey.php | 44 +
.../phpseclib/phpseclib/Crypt/DSA.php | 292 +
.../Crypt/DSA/Formats/Keys/OpenSSH.php | 102 +
.../Crypt/DSA/Formats/Keys/PKCS1.php | 115 +
.../Crypt/DSA/Formats/Keys/PKCS8.php | 125 +
.../Crypt/DSA/Formats/Keys/PuTTY.php | 98 +
.../phpseclib/Crypt/DSA/Formats/Keys/Raw.php | 78 +
.../phpseclib/Crypt/DSA/Formats/Keys/XML.php | 123 +
.../Crypt/DSA/Formats/Signature/ASN1.php | 57 +
.../Crypt/DSA/Formats/Signature/Raw.php | 23 +
.../Crypt/DSA/Formats/Signature/SSH2.php | 61 +
.../phpseclib/Crypt/DSA/Parameters.php | 33 +
.../phpseclib/Crypt/DSA/PrivateKey.php | 131 +
.../phpseclib/Crypt/DSA/PublicKey.php | 74 +
.../phpseclib/phpseclib/Crypt/EC.php | 414 ++
.../phpseclib/Crypt/EC/BaseCurves/Base.php | 192 +
.../phpseclib/Crypt/EC/BaseCurves/Binary.php | 324 +
.../Crypt/EC/BaseCurves/KoblitzPrime.php | 273 +
.../Crypt/EC/BaseCurves/Montgomery.php | 246 +
.../phpseclib/Crypt/EC/BaseCurves/Prime.php | 695 +++
.../Crypt/EC/BaseCurves/TwistedEdwards.php | 190 +
.../phpseclib/Crypt/EC/Curves/Curve25519.php | 73 +
.../phpseclib/Crypt/EC/Curves/Curve448.php | 76 +
.../phpseclib/Crypt/EC/Curves/Ed25519.php | 295 +
.../phpseclib/Crypt/EC/Curves/Ed448.php | 222 +
.../Crypt/EC/Curves/brainpoolP160r1.php | 26 +
.../Crypt/EC/Curves/brainpoolP160t1.php | 43 +
.../Crypt/EC/Curves/brainpoolP192r1.php | 26 +
.../Crypt/EC/Curves/brainpoolP192t1.php | 30 +
.../Crypt/EC/Curves/brainpoolP224r1.php | 26 +
.../Crypt/EC/Curves/brainpoolP224t1.php | 30 +
.../Crypt/EC/Curves/brainpoolP256r1.php | 26 +
.../Crypt/EC/Curves/brainpoolP256t1.php | 30 +
.../Crypt/EC/Curves/brainpoolP320r1.php | 26 +
.../Crypt/EC/Curves/brainpoolP320t1.php | 30 +
.../Crypt/EC/Curves/brainpoolP384r1.php | 26 +
.../Crypt/EC/Curves/brainpoolP384t1.php | 30 +
.../Crypt/EC/Curves/brainpoolP512r1.php | 26 +
.../Crypt/EC/Curves/brainpoolP512t1.php | 30 +
.../phpseclib/Crypt/EC/Curves/nistb233.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistb409.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistk163.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistk233.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistk283.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistk409.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistp192.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistp224.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistp256.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistp384.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistp521.php | 17 +
.../phpseclib/Crypt/EC/Curves/nistt571.php | 17 +
.../phpseclib/Crypt/EC/Curves/prime192v1.php | 17 +
.../phpseclib/Crypt/EC/Curves/prime192v2.php | 26 +
.../phpseclib/Crypt/EC/Curves/prime192v3.php | 26 +
.../phpseclib/Crypt/EC/Curves/prime239v1.php | 26 +
.../phpseclib/Crypt/EC/Curves/prime239v2.php | 26 +
.../phpseclib/Crypt/EC/Curves/prime239v3.php | 26 +
.../phpseclib/Crypt/EC/Curves/prime256v1.php | 17 +
.../phpseclib/Crypt/EC/Curves/secp112r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/secp112r2.php | 27 +
.../phpseclib/Crypt/EC/Curves/secp128r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/secp128r2.php | 27 +
.../phpseclib/Crypt/EC/Curves/secp160k1.php | 31 +
.../phpseclib/Crypt/EC/Curves/secp160r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/secp160r2.php | 27 +
.../phpseclib/Crypt/EC/Curves/secp192k1.php | 30 +
.../phpseclib/Crypt/EC/Curves/secp192r1.php | 68 +
.../phpseclib/Crypt/EC/Curves/secp224k1.php | 30 +
.../phpseclib/Crypt/EC/Curves/secp224r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/secp256k1.php | 34 +
.../phpseclib/Crypt/EC/Curves/secp256r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/secp384r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/secp521r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect113r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect113r2.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect131r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect131r2.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect163k1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect163r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect163r2.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect193r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect193r2.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect233k1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect233r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect239k1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect283k1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect283r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect409k1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect409r1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect571k1.php | 26 +
.../phpseclib/Crypt/EC/Curves/sect571r1.php | 26 +
.../Crypt/EC/Formats/Keys/Common.php | 489 ++
.../phpseclib/Crypt/EC/Formats/Keys/JWK.php | 155 +
.../EC/Formats/Keys/MontgomeryPrivate.php | 93 +
.../EC/Formats/Keys/MontgomeryPublic.php | 65 +
.../Crypt/EC/Formats/Keys/OpenSSH.php | 163 +
.../phpseclib/Crypt/EC/Formats/Keys/PKCS1.php | 154 +
.../phpseclib/Crypt/EC/Formats/Keys/PKCS8.php | 186 +
.../phpseclib/Crypt/EC/Formats/Keys/PuTTY.php | 115 +
.../phpseclib/Crypt/EC/Formats/Keys/XML.php | 373 ++
.../Crypt/EC/Formats/Keys/libsodium.php | 106 +
.../Crypt/EC/Formats/Signature/ASN1.php | 57 +
.../Crypt/EC/Formats/Signature/IEEE.php | 60 +
.../Crypt/EC/Formats/Signature/Raw.php | 23 +
.../Crypt/EC/Formats/Signature/SSH2.php | 83 +
.../phpseclib/Crypt/EC/Parameters.php | 33 +
.../phpseclib/Crypt/EC/PrivateKey.php | 226 +
.../phpseclib/Crypt/EC/PublicKey.php | 136 +
.../phpseclib/phpseclib/Crypt/Hash.php | 1134 ++++
.../phpseclib/Crypt/PublicKeyLoader.php | 102 +
.../phpseclib/phpseclib/Crypt/RC2.php | 478 ++
.../phpseclib/phpseclib/Crypt/RC4.php | 258 +
.../phpseclib/phpseclib/Crypt/RSA.php | 824 +++
.../phpseclib/Crypt/RSA/Formats/Keys/JWK.php | 116 +
.../Crypt/RSA/Formats/Keys/MSBLOB.php | 207 +
.../Crypt/RSA/Formats/Keys/OpenSSH.php | 101 +
.../Crypt/RSA/Formats/Keys/PKCS1.php | 120 +
.../Crypt/RSA/Formats/Keys/PKCS8.php | 111 +
.../phpseclib/Crypt/RSA/Formats/Keys/PSS.php | 193 +
.../Crypt/RSA/Formats/Keys/PuTTY.php | 107 +
.../phpseclib/Crypt/RSA/Formats/Keys/Raw.php | 153 +
.../phpseclib/Crypt/RSA/Formats/Keys/XML.php | 140 +
.../phpseclib/Crypt/RSA/PrivateKey.php | 441 ++
.../phpseclib/Crypt/RSA/PublicKey.php | 439 ++
.../phpseclib/phpseclib/Crypt/Random.php | 202 +
.../phpseclib/phpseclib/Crypt/Rijndael.php | 1048 ++++
.../phpseclib/phpseclib/Crypt/Salsa20.php | 454 ++
.../phpseclib/phpseclib/Crypt/TripleDES.php | 384 ++
.../phpseclib/phpseclib/Crypt/Twofish.php | 506 ++
.../Exception/BadConfigurationException.php | 22 +
.../Exception/BadDecryptionException.php | 22 +
.../phpseclib/Exception/BadModeException.php | 22 +
.../Exception/ConnectionClosedException.php | 22 +
.../Exception/FileNotFoundException.php | 22 +
.../Exception/InconsistentSetupException.php | 22 +
.../Exception/InsufficientSetupException.php | 22 +
.../Exception/NoKeyLoadedException.php | 22 +
.../NoSupportedAlgorithmsException.php | 22 +
.../Exception/UnableToConnectException.php | 22 +
.../UnsupportedAlgorithmException.php | 22 +
.../Exception/UnsupportedCurveException.php | 22 +
.../Exception/UnsupportedFormatException.php | 22 +
.../UnsupportedOperationException.php | 22 +
.../phpseclib/phpseclib/File/ANSI.php | 553 ++
.../phpseclib/phpseclib/File/ASN1.php | 1398 +++++
.../phpseclib/phpseclib/File/ASN1/Element.php | 41 +
.../File/ASN1/Maps/AccessDescription.php | 24 +
.../ASN1/Maps/AdministrationDomainName.php | 31 +
.../File/ASN1/Maps/AlgorithmIdentifier.php | 24 +
.../phpseclib/File/ASN1/Maps/AnotherName.php | 24 +
.../phpseclib/File/ASN1/Maps/Attribute.php | 24 +
.../File/ASN1/Maps/AttributeType.php | 24 +
.../File/ASN1/Maps/AttributeTypeAndValue.php | 24 +
.../File/ASN1/Maps/AttributeValue.php | 24 +
.../phpseclib/File/ASN1/Maps/Attributes.php | 24 +
.../ASN1/Maps/AuthorityInfoAccessSyntax.php | 24 +
.../File/ASN1/Maps/AuthorityKeyIdentifier.php | 24 +
.../phpseclib/File/ASN1/Maps/BaseDistance.php | 24 +
.../File/ASN1/Maps/BasicConstraints.php | 24 +
.../Maps/BuiltInDomainDefinedAttribute.php | 24 +
.../Maps/BuiltInDomainDefinedAttributes.php | 30 +
.../ASN1/Maps/BuiltInStandardAttributes.php | 24 +
.../phpseclib/File/ASN1/Maps/CPSuri.php | 24 +
.../File/ASN1/Maps/CRLDistributionPoints.php | 24 +
.../phpseclib/File/ASN1/Maps/CRLNumber.php | 24 +
.../phpseclib/File/ASN1/Maps/CRLReason.php | 36 +
.../phpseclib/File/ASN1/Maps/CertPolicyId.php | 24 +
.../phpseclib/File/ASN1/Maps/Certificate.php | 24 +
.../File/ASN1/Maps/CertificateIssuer.php | 23 +
.../File/ASN1/Maps/CertificateList.php | 24 +
.../File/ASN1/Maps/CertificatePolicies.php | 24 +
.../ASN1/Maps/CertificateSerialNumber.php | 24 +
.../File/ASN1/Maps/CertificationRequest.php | 24 +
.../ASN1/Maps/CertificationRequestInfo.php | 24 +
.../File/ASN1/Maps/Characteristic_two.php | 29 +
.../phpseclib/File/ASN1/Maps/CountryName.php | 31 +
.../phpseclib/File/ASN1/Maps/Curve.php | 24 +
.../phpseclib/File/ASN1/Maps/DHParameter.php | 26 +
.../phpseclib/File/ASN1/Maps/DSAParams.php | 24 +
.../File/ASN1/Maps/DSAPrivateKey.php | 24 +
.../phpseclib/File/ASN1/Maps/DSAPublicKey.php | 24 +
.../phpseclib/File/ASN1/Maps/DigestInfo.php | 26 +
.../File/ASN1/Maps/DirectoryString.php | 24 +
.../phpseclib/File/ASN1/Maps/DisplayText.php | 24 +
.../File/ASN1/Maps/DistributionPoint.php | 24 +
.../File/ASN1/Maps/DistributionPointName.php | 24 +
.../phpseclib/File/ASN1/Maps/DssSigValue.php | 24 +
.../phpseclib/File/ASN1/Maps/ECParameters.php | 36 +
.../phpseclib/File/ASN1/Maps/ECPoint.php | 24 +
.../phpseclib/File/ASN1/Maps/ECPrivateKey.php | 26 +
.../phpseclib/File/ASN1/Maps/EDIPartyName.php | 29 +
.../File/ASN1/Maps/EcdsaSigValue.php | 24 +
.../File/ASN1/Maps/EncryptedData.php | 24 +
.../ASN1/Maps/EncryptedPrivateKeyInfo.php | 24 +
.../File/ASN1/Maps/ExtKeyUsageSyntax.php | 24 +
.../phpseclib/File/ASN1/Maps/Extension.php | 30 +
.../File/ASN1/Maps/ExtensionAttribute.php | 24 +
.../File/ASN1/Maps/ExtensionAttributes.php | 30 +
.../phpseclib/File/ASN1/Maps/Extensions.php | 31 +
.../phpseclib/File/ASN1/Maps/FieldElement.php | 24 +
.../phpseclib/File/ASN1/Maps/FieldID.php | 24 +
.../phpseclib/File/ASN1/Maps/GeneralName.php | 24 +
.../phpseclib/File/ASN1/Maps/GeneralNames.php | 24 +
.../File/ASN1/Maps/GeneralSubtree.php | 24 +
.../File/ASN1/Maps/GeneralSubtrees.php | 24 +
.../File/ASN1/Maps/HashAlgorithm.php | 23 +
.../File/ASN1/Maps/HoldInstructionCode.php | 24 +
.../File/ASN1/Maps/InvalidityDate.php | 24 +
.../File/ASN1/Maps/IssuerAltName.php | 23 +
.../ASN1/Maps/IssuingDistributionPoint.php | 24 +
.../File/ASN1/Maps/KeyIdentifier.php | 24 +
.../phpseclib/File/ASN1/Maps/KeyPurposeId.php | 24 +
.../phpseclib/File/ASN1/Maps/KeyUsage.php | 24 +
.../File/ASN1/Maps/MaskGenAlgorithm.php | 23 +
.../phpseclib/File/ASN1/Maps/Name.php | 24 +
.../File/ASN1/Maps/NameConstraints.php | 24 +
.../File/ASN1/Maps/NetworkAddress.php | 24 +
.../File/ASN1/Maps/NoticeReference.php | 24 +
.../File/ASN1/Maps/NumericUserIdentifier.php | 24 +
.../phpseclib/File/ASN1/Maps/ORAddress.php | 24 +
.../File/ASN1/Maps/OneAsymmetricKey.php | 26 +
.../File/ASN1/Maps/OrganizationName.php | 24 +
.../ASN1/Maps/OrganizationalUnitNames.php | 30 +
.../File/ASN1/Maps/OtherPrimeInfo.php | 31 +
.../File/ASN1/Maps/OtherPrimeInfos.php | 25 +
.../phpseclib/File/ASN1/Maps/PBEParameter.php | 26 +
.../phpseclib/File/ASN1/Maps/PBES2params.php | 26 +
.../phpseclib/File/ASN1/Maps/PBKDF2params.php | 33 +
.../phpseclib/File/ASN1/Maps/PBMAC1params.php | 26 +
.../phpseclib/File/ASN1/Maps/PKCS9String.php | 24 +
.../phpseclib/File/ASN1/Maps/Pentanomial.php | 30 +
.../phpseclib/File/ASN1/Maps/PersonalName.php | 24 +
.../File/ASN1/Maps/PolicyInformation.php | 24 +
.../File/ASN1/Maps/PolicyMappings.php | 24 +
.../File/ASN1/Maps/PolicyQualifierId.php | 24 +
.../File/ASN1/Maps/PolicyQualifierInfo.php | 24 +
.../File/ASN1/Maps/PostalAddress.php | 24 +
.../phpseclib/File/ASN1/Maps/Prime_p.php | 24 +
.../File/ASN1/Maps/PrivateDomainName.php | 24 +
.../phpseclib/File/ASN1/Maps/PrivateKey.php | 24 +
.../File/ASN1/Maps/PrivateKeyInfo.php | 24 +
.../File/ASN1/Maps/PrivateKeyUsagePeriod.php | 24 +
.../phpseclib/File/ASN1/Maps/PublicKey.php | 24 +
.../File/ASN1/Maps/PublicKeyAndChallenge.php | 24 +
.../File/ASN1/Maps/PublicKeyInfo.php | 27 +
.../File/ASN1/Maps/RC2CBCParameter.php | 26 +
.../phpseclib/File/ASN1/Maps/RDNSequence.php | 36 +
.../File/ASN1/Maps/RSAPrivateKey.php | 44 +
.../phpseclib/File/ASN1/Maps/RSAPublicKey.php | 24 +
.../File/ASN1/Maps/RSASSA_PSS_params.php | 26 +
.../phpseclib/File/ASN1/Maps/ReasonFlags.php | 24 +
.../ASN1/Maps/RelativeDistinguishedName.php | 30 +
.../File/ASN1/Maps/RevokedCertificate.php | 24 +
.../ASN1/Maps/SignedPublicKeyAndChallenge.php | 24 +
.../File/ASN1/Maps/SpecifiedECDomain.php | 26 +
.../File/ASN1/Maps/SubjectAltName.php | 23 +
.../ASN1/Maps/SubjectDirectoryAttributes.php | 24 +
.../ASN1/Maps/SubjectInfoAccessSyntax.php | 24 +
.../File/ASN1/Maps/SubjectPublicKeyInfo.php | 24 +
.../phpseclib/File/ASN1/Maps/TBSCertList.php | 24 +
.../File/ASN1/Maps/TBSCertificate.php | 41 +
.../File/ASN1/Maps/TerminalIdentifier.php | 24 +
.../phpseclib/File/ASN1/Maps/Time.php | 24 +
.../phpseclib/File/ASN1/Maps/Trinomial.php | 24 +
.../File/ASN1/Maps/UniqueIdentifier.php | 24 +
.../phpseclib/File/ASN1/Maps/UserNotice.php | 24 +
.../phpseclib/File/ASN1/Maps/Validity.php | 24 +
.../File/ASN1/Maps/netscape_ca_policy_url.php | 24 +
.../File/ASN1/Maps/netscape_cert_type.php | 26 +
.../File/ASN1/Maps/netscape_comment.php | 24 +
.../phpseclib/phpseclib/File/X509.php | 3505 +++++++++++
.../phpseclib/phpseclib/Math/BigInteger.php | 802 +++
.../Math/BigInteger/Engines/BCMath.php | 601 ++
.../Math/BigInteger/Engines/BCMath/Base.php | 102 +
.../BigInteger/Engines/BCMath/BuiltIn.php | 37 +
.../Engines/BCMath/DefaultEngine.php | 23 +
.../BigInteger/Engines/BCMath/OpenSSL.php | 23 +
.../Engines/BCMath/Reductions/Barrett.php | 157 +
.../Engines/BCMath/Reductions/EvalBarrett.php | 96 +
.../Math/BigInteger/Engines/Engine.php | 1160 ++++
.../phpseclib/Math/BigInteger/Engines/GMP.php | 612 ++
.../BigInteger/Engines/GMP/DefaultEngine.php | 37 +
.../Math/BigInteger/Engines/OpenSSL.php | 58 +
.../phpseclib/Math/BigInteger/Engines/PHP.php | 1110 ++++
.../Math/BigInteger/Engines/PHP/Base.php | 133 +
.../BigInteger/Engines/PHP/DefaultEngine.php | 23 +
.../BigInteger/Engines/PHP/Montgomery.php | 78 +
.../Math/BigInteger/Engines/PHP/OpenSSL.php | 23 +
.../Engines/PHP/Reductions/Barrett.php | 239 +
.../Engines/PHP/Reductions/Classic.php | 40 +
.../Engines/PHP/Reductions/EvalBarrett.php | 412 ++
.../Engines/PHP/Reductions/Montgomery.php | 113 +
.../Engines/PHP/Reductions/MontgomeryMult.php | 68 +
.../Engines/PHP/Reductions/PowerOfTwo.php | 54 +
.../Math/BigInteger/Engines/PHP32.php | 341 +
.../Math/BigInteger/Engines/PHP64.php | 342 +
.../phpseclib/phpseclib/Math/BinaryField.php | 183 +
.../phpseclib/Math/BinaryField/Integer.php | 442 ++
.../phpseclib/Math/Common/FiniteField.php | 21 +
.../Math/Common/FiniteField/Integer.php | 42 +
.../phpseclib/phpseclib/Math/PrimeField.php | 106 +
.../phpseclib/Math/PrimeField/Integer.php | 370 ++
.../phpseclib/phpseclib/Net/SFTP.php | 3058 +++++++++
.../phpseclib/phpseclib/Net/SFTP/Stream.php | 697 +++
.../phpseclib/phpseclib/Net/SSH2.php | 4594 ++++++++++++++
.../phpseclib/phpseclib/System/SSH/Agent.php | 253 +
.../phpseclib/System/SSH/Agent/Identity.php | 280 +
.../System/SSH/Common/Traits/ReadBytes.php | 36 +
.../phpseclib/phpseclib/bootstrap.php | 20 +
.../phpseclib/phpseclib/phpseclib/openssl.cnf | 6 +
.../vendor/prefixed/psr/cache/LICENSE.txt | 19 +
.../prefixed/psr/cache/src/CacheException.php | 10 +
.../psr/cache/src/CacheItemInterface.php | 100 +
.../psr/cache/src/CacheItemPoolInterface.php | 129 +
.../cache/src/InvalidArgumentException.php | 13 +
.../vendor/prefixed/psr/http-client/LICENSE | 19 +
.../src/ClientExceptionInterface.php | 10 +
.../psr/http-client/src/ClientInterface.php | 19 +
.../src/NetworkExceptionInterface.php | 23 +
.../src/RequestExceptionInterface.php | 23 +
.../vendor/prefixed/psr/http-factory/LICENSE | 21 +
.../src/RequestFactoryInterface.php | 18 +
.../src/ResponseFactoryInterface.php | 18 +
.../src/ServerRequestFactoryInterface.php | 24 +
.../src/StreamFactoryInterface.php | 43 +
.../src/UploadedFileFactoryInterface.php | 28 +
.../http-factory/src/UriFactoryInterface.php | 17 +
.../vendor/prefixed/psr/http-message/LICENSE | 19 +
.../psr/http-message/src/MessageInterface.php | 177 +
.../psr/http-message/src/RequestInterface.php | 124 +
.../http-message/src/ResponseInterface.php | 66 +
.../src/ServerRequestInterface.php | 249 +
.../psr/http-message/src/StreamInterface.php | 144 +
.../src/UploadedFileInterface.php | 118 +
.../psr/http-message/src/UriInterface.php | 309 +
.../prefixed/ralouphie/getallheaders/LICENSE | 21 +
.../getallheaders/src/getallheaders.php | 38 +
.../symfony/deprecation-contracts/LICENSE | 19 +
.../deprecation-contracts/function.php | 28 +
.../vendor/prefixed/vendor/autoload.php | 12 +
.../prefixed/vendor/composer/ClassLoader.php | 572 ++
.../vendor/prefixed/vendor/composer/LICENSE | 21 +
.../vendor/composer/autoload_classmap.php | 581 ++
.../vendor/composer/autoload_files.php | 16 +
.../vendor/composer/autoload_namespaces.php | 9 +
.../vendor/composer/autoload_psr4.php | 9 +
.../vendor/composer/autoload_real.php | 55 +
.../vendor/composer/autoload_static.php | 601 ++
.../vendor/scoper-autoload.php | 26 +
.../vendor/yiisoft/extensions.php | 3 +
.../SearchEngineKeywordsPerformance.umd.js | 2336 +++++++
...SearchEngineKeywordsPerformance.umd.js.map | 1 +
...SearchEngineKeywordsPerformance.umd.min.js | 30 +
...chEngineKeywordsPerformance.umd.min.js.map | 1 +
.../vue/dist/umd.metadata.json | 6 +
.../vue/src/Admin/AdminPage.vue | 54 +
.../vue/src/Admin/Provider.vue | 103 +
.../vue/src/Bing/Configuration.vue | 431 ++
.../src/Configure/ConfigureConnection.less | 3 +
.../vue/src/Configure/ConfigureConnection.vue | 125 +
.../vue/src/Google/Configuration.vue | 637 ++
.../vue/src/Yandex/Configuration.vue | 578 ++
.../vue/src/index.ts | 20 +
.../vue/src/utilities.ts | 18 +
920 files changed, 139711 insertions(+), 22 deletions(-)
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/API.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Actions/ActionHsr.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/BaseActivity.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapAdded.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapDeleted.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapEnded.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapPaused.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapResumed.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapScreenshotDeleted.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/HeatmapUpdated.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/RecordedPageviewDeleted.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/RecordedSessionDeleted.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingAdded.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingDeleted.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingEnded.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingPaused.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingResumed.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Activity/SessionRecordingUpdated.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Archiver/Aggregator.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/CHANGELOG.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Categories/HeatmapCategory.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Categories/ManageHeatmapSubcategory.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Categories/ManageSessionRecordingSubcategory.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Categories/SessionRecordingsCategory.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/BaseMetric.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Browser.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Device.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/Location.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/OperatingSystem.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/SessionTime.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TimeOnPage.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TimeOnSite.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Columns/Metrics/TotalEvents.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Commands/RemoveHeatmapScreenshot.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Configuration.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Controller.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsr.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrBlob.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrEvent.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Dao/LogHsrSite.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Dao/SiteHsrDao.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/DataTable/Filter/EnrichRecordedSessions.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Diagnostic/ConfigsPhpCheck.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/HeatmapSessionRecording.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/Breakpoint.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/CaptureKeystrokes.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/ExcludedElements.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/MinSessionTime.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/Name.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/PageRule.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/PageRules.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/RequiresActivity.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/SampleLimit.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/SampleRate.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/ScreenshotUrl.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Input/Validator.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Install/HtAccess.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Install/htaccessTemplate
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/LEGALNOTICE
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/LICENSE
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Menu.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Model/SiteHsrModel.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/MutationManipulator.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/README.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Reports/GetRecordedSessions.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Settings/TrackingDisableDefault.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/SystemSettings.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Tasks.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Tracker/Configs.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Tracker/HsrMatcher.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Tracker/LogTable/LogHsr.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Tracker/LogTable/LogHsrBlob.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Tracker/LogTable/LogHsrEvent.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Tracker/PageRuleMatcher.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Tracker/RequestProcessor.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Updates/3.0.10.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Updates/3.0.11.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Updates/3.0.3.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Updates/4.0.0.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Updates/5.1.0.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/VisitorDetails.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Widgets/GetManageHeatmaps.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Widgets/GetManageSessionRecordings.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Widgets/GettingStartedHeatmap.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/Widgets/GettingStartedSessions.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/config/config.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/configs.php
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/docs/index.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/javascripts/recording.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/javascripts/rowaction.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/bg.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/cs.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/da.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/de.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/en.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/es.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/fi.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/fr.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/hi.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/it.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/ja.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/nb.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/nl.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/pl.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/pt.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/ro.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/ru.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/sq.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/sv.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/tr.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/uk.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/zh-cn.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/lang/zh-tw.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/MutationObserver.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/README.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/dist/README.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/dist/mutationobserver.min.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/license
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/MutationObserver.js/package.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/COPYING
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/README.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/package.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/src/mutation-summary.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/src/mutation-summary.ts
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/util/tree-mirror.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/mutation-summary/util/tree-mirror.ts
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/CHANGELOG.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/LICENSE.txt
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/README.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/dist/svg.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/dist/svg.min.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/libs/svg.js/package.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/package-lock.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/package.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/phpcs.xml
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/plugin.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/pull_request_template.md
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/edit-entities.less
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/list-entities.less
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/stylesheets/recordings.less
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/templates/_detectAdBlocker.twig
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/templates/embedPage.twig
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/templates/gettingStartedHeatmaps.twig
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/templates/gettingStartedSessions.twig
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/templates/manageHeatmap.twig
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/templates/manageSessions.twig
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/templates/replayRecording.twig
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/templates/showHeatmap.twig
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/tracker.min.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/tsconfig.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/HeatmapSessionRecording.umd.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/HeatmapSessionRecording.umd.min.js
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/dist/umd.metadata.json
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVis.less
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVis.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HeatmapVis/HeatmapVisPage.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrStore/HsrStore.store.ts
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrTargetTest/HsrTargetTest.less
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrTargetTest/HsrTargetTest.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/AvailableTargetPageRules.store.ts
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/HsrUrlTarget.less
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/HsrUrlTarget/HsrUrlTarget.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ListOfPageviews/ListOfPageviews.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/Edit.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/List.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageHeatmap/Manage.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/Edit.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/List.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/ManageSessionRecording/Manage.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/MatomoJsNotWritable/MatomoJsNotWritableAlert.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/SessionRecordingVis/SessionRecordingVis.less
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/SessionRecordingVis/SessionRecordingVis.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/Tooltip/Tooltip.less
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/Tooltip/Tooltip.vue
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/getIframeWindow.ts
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/index.ts
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/oneAtATime.ts
create mode 100644 files/plugin-HeatmapSessionRecording-5.2.4/vue/src/types.ts
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/API.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Activity/AccountAdded.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Activity/AccountRemoved.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Activity/GoogleClientConfigChanged.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Activity/YandexClientConfigChanged.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Archiver.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/CHANGELOG.md
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Categories/CrawlingOverviewSubcategory.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Categories/SearchKeywordsSubcategory.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Bing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/BaseConfiguration.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Bing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Google.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Configuration/Yandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Google.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Client/Yandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Columns/Keyword.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportBing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportGoogle.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Commands/ImportYandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Controller.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/BingAccountDiagnostic.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/GoogleAccountDiagnostic.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Diagnostic/YandexAccountDiagnostic.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/InvalidClientConfigException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/InvalidCredentialsException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/MissingClientConfigException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/MissingOAuthConfigException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/RateLimitApiException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Exceptions/UnknownAPIException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Bing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Google.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Importer/Yandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/MeasurableSettings.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Menu.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Metrics.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Bing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Google.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Model/Yandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Monolog/Handler/SEKPSystemLogHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Bing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Google.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Helper/MeasurableHelper.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/ProviderAbstract.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Provider/Yandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/README.md
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Base.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Bing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Google.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/RecordBuilders/Yandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/Base.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingErrorExamplesBing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingOverviewBing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetCrawlingOverviewYandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywords.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsBing.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleImage.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleNews.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleVideo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsGoogleWeb.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsImported.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsReferrers.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Reports/GetKeywordsYandex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/SearchEngineKeywordsPerformance.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/SystemSettings.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Tasks.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/3.5.0.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/4.1.0.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/Updates/4.2.0.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/config/config.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/docs/index.md
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Bing.png
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Google.png
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Yahoo.png
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/images/Yandex.png
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/bg.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/cs.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/da.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/de.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/en.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/es.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/fi.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/fr.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/hi.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/it.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ja.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/nb.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/nl.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/pl.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/pt.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ro.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/ru.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/sq.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/sv.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/tr.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/uk.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/zh-cn.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/lang/zh-tw.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/phpcs.xml
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/plugin.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/pull_request_template.md
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/scoper.inc.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/stylesheets/styles.less
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/bing/configuration.twig
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/google/configuration.twig
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/index.twig
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/messageReferrerKeywordsReport.twig
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/templates/yandex/configuration.twig
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/autoload.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/autoload_original.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/ClassLoader.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/InstalledVersions.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_classmap.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_files.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_namespaces.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_psr4.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_real.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/autoload_static.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/installed.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/composer/installed.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/BeforeValidException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/CachedKeySet.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/ExpiredException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWK.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWT.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/Key.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/firebase/php-jwt/src/SignatureInvalidException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/autoload.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/renovate.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/Userinfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/UserinfoV2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Resource/UserinfoV2Me.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Tokeninfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/Oauth2/Userinfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDataRow.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDimensionFilter.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ApiDimensionFilterGroup.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/BlockedResource.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Image.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/MobileFriendlyIssue.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Searchanalytics.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Sitemaps.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/Sites.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/UrlTestingTools.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/Resource/UrlTestingToolsMobileFriendlyTest.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/ResourceIssue.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/RunMobileFriendlyTestRequest.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/RunMobileFriendlyTestResponse.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SearchAnalyticsQueryRequest.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SearchAnalyticsQueryResponse.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SitemapsListResponse.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/SitesListResponse.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/TestStatus.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSite.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSitemap.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/src/SearchConsole/WmxSitemapContent.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/synth.metadata
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient-services/synth.py
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AccessToken/Revoke.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AccessToken/Verify.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/AuthHandlerFactory.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/Guzzle6AuthHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/AuthHandler/Guzzle7AuthHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Client.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Collection.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Exception.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/Batch.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/MediaFileUpload.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Http/REST.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Model.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service/Exception.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Service/Resource.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Composer.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Exception.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Retryable.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Task/Runner.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/Utils/UriTemplate.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/apiclient/src/aliases.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/COPYING
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/VERSION
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/autoload.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/AccessToken.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/ApplicationDefaultCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Cache/InvalidArgumentException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Cache/Item.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Cache/MemoryCacheItemPool.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Cache/SysVCacheItemPool.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Cache/TypedItem.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/CacheTrait.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/CredentialSource/AwsNativeSource.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/CredentialSource/FileSource.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/CredentialSource/UrlSource.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/AppIdentityCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/ExternalAccountCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/GCECredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/IAMCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/ImpersonatedServiceAccountCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/InsecureCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/ServiceAccountCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/ServiceAccountJwtAccessCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Credentials/UserRefreshCredentials.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/CredentialsLoader.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/ExternalAccountCredentialSourceInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/FetchAuthTokenCache.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/FetchAuthTokenInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/GCECache.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/GetQuotaProjectInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/GetUniverseDomainInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/HttpHandler/Guzzle6HttpHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/HttpHandler/Guzzle7HttpHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/HttpHandler/HttpClientCache.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/HttpHandler/HttpHandlerFactory.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Iam.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/IamSignerTrait.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Middleware/AuthTokenMiddleware.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Middleware/ProxyAuthTokenMiddleware.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Middleware/ScopedAccessTokenMiddleware.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/Middleware/SimpleMiddleware.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/OAuth2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/ProjectIdProviderInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/ServiceAccountSignerTrait.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/SignBlobInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/UpdateMetadataInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/google/auth/src/UpdateMetadataTrait.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/BodySummarizer.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/BodySummarizerInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Client.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/ClientInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/ClientTrait.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Cookie/CookieJar.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Cookie/SetCookie.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/BadResponseException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/ClientException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/ConnectException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/GuzzleException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/RequestException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/ServerException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Exception/TransferException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/CurlFactory.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/CurlHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/EasyHandle.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/MockHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/Proxy.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Handler/StreamHandler.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/HandlerStack.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/MessageFormatter.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/MessageFormatterInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Middleware.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Pool.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/RedirectMiddleware.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/RequestOptions.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/RetryMiddleware.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/TransferStats.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/Utils.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/functions.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/guzzle/src/functions_include.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/AggregateException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/CancellationException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/Coroutine.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/Create.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/Each.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/EachPromise.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/FulfilledPromise.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/Is.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/Promise.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/PromiseInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/PromisorInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/RejectedPromise.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/RejectionException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/TaskQueue.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/TaskQueueInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/Utils.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/functions.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/promises/src/functions_include.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/AppendStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/BufferStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/CachingStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/DroppingStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Exception/MalformedUriException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/FnStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Header.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/HttpFactory.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/InflateStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/LazyOpenStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/LimitStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Message.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/MessageTrait.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/MimeType.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/MultipartStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/NoSeekStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/PumpStream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Query.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Request.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Response.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Rfc7230.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/ServerRequest.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Stream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/StreamDecoratorTrait.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/StreamWrapper.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/UploadedFile.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Uri.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/UriComparator.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/UriNormalizer.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/UriResolver.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/guzzlehttp/psr7/src/Utils.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/LICENSE.txt
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Base32.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Base32Hex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Base64.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Base64DotSlash.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Base64UrlSafe.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Binary.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/EncoderInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Encoding.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/Hex.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/constant_time_encoding/src/RFC4648.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/random_compat/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/random_compat/build-phar.sh
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/random_compat/dist/random_compat.phar.pubkey
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/random_compat/dist/random_compat.phar.pubkey.asc
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/random_compat/lib/random.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/random_compat/other/build_phar.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/random_compat/psalm-autoload.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/paragonie/random_compat/psalm.xml
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/AUTHORS
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Common/Functions/Strings.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/AES.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/ChaCha20.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/AsymmetricKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/BlockCipher.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/JWK.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/OpenSSH.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS8.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PuTTY.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Signature/Raw.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/PrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/PublicKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/StreamCipher.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/SymmetricKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Traits/Fingerprint.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Common/Traits/PasswordProtected.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DES.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DH.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DH/Formats/Keys/PKCS1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DH/Formats/Keys/PKCS8.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DH/Parameters.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DH/PrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DH/PublicKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/OpenSSH.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PKCS1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PKCS8.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/PuTTY.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/Raw.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Keys/XML.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/ASN1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/Raw.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Formats/Signature/SSH2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/Parameters.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/PrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/DSA/PublicKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Base.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Binary.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/KoblitzPrime.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Montgomery.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/Prime.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/BaseCurves/TwistedEdwards.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Curve25519.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Curve448.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Ed25519.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/Ed448.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP160r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP160t1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP192r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP192t1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP224r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP224t1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP256r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP256t1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP320r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP320t1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP384r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP384t1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP512r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/brainpoolP512t1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistb233.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistb409.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk163.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk233.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk283.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistk409.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp192.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp224.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp256.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp384.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistp521.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/nistt571.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime192v3.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime239v3.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/prime256v1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp112r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp112r2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp128r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp128r2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp160r2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp192k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp192r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp224k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp224r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp256k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp256r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp384r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/secp521r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect113r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect113r2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect131r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect131r2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect163r2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect193r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect193r2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect233k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect233r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect239k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect283k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect283r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect409k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect409r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect571k1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Curves/sect571r1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/Common.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/JWK.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/MontgomeryPrivate.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/MontgomeryPublic.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/OpenSSH.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PKCS1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PKCS8.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/PuTTY.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/XML.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Keys/libsodium.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/ASN1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/IEEE.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/Raw.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/SSH2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/Parameters.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/EC/PublicKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Hash.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/PublicKeyLoader.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RC2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RC4.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/JWK.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/MSBLOB.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/OpenSSH.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PKCS1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PKCS8.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PSS.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/PuTTY.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/Raw.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/Formats/Keys/XML.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/PrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/RSA/PublicKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Random.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Salsa20.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Crypt/Twofish.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/BadConfigurationException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/BadDecryptionException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/BadModeException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/ConnectionClosedException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/FileNotFoundException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/InconsistentSetupException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/InsufficientSetupException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/NoKeyLoadedException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/NoSupportedAlgorithmsException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/UnableToConnectException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/UnsupportedAlgorithmException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/UnsupportedCurveException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/UnsupportedFormatException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Exception/UnsupportedOperationException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ANSI.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Element.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AccessDescription.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AdministrationDomainName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AlgorithmIdentifier.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AnotherName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Attribute.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeType.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeTypeAndValue.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AttributeValue.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Attributes.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AuthorityInfoAccessSyntax.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/AuthorityKeyIdentifier.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BaseDistance.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BasicConstraints.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInDomainDefinedAttribute.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInDomainDefinedAttributes.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/BuiltInStandardAttributes.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CPSuri.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLDistributionPoints.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLNumber.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CRLReason.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertPolicyId.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Certificate.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateIssuer.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateList.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificatePolicies.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificateSerialNumber.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificationRequest.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CertificationRequestInfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Characteristic_two.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/CountryName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Curve.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DHParameter.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAParams.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAPrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DSAPublicKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DigestInfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DirectoryString.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DisplayText.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DistributionPoint.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DistributionPointName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/DssSigValue.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECParameters.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECPoint.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ECPrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EDIPartyName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EcdsaSigValue.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EncryptedData.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/EncryptedPrivateKeyInfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtKeyUsageSyntax.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Extension.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtensionAttribute.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ExtensionAttributes.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Extensions.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/FieldElement.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/FieldID.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralNames.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralSubtree.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/GeneralSubtrees.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/HashAlgorithm.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/HoldInstructionCode.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/InvalidityDate.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/IssuerAltName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/IssuingDistributionPoint.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyIdentifier.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyPurposeId.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/KeyUsage.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/MaskGenAlgorithm.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Name.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NameConstraints.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NetworkAddress.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NoticeReference.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/NumericUserIdentifier.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ORAddress.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OneAsymmetricKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OrganizationName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OrganizationalUnitNames.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OtherPrimeInfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/OtherPrimeInfos.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBEParameter.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBES2params.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBKDF2params.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PBMAC1params.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PKCS9String.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Pentanomial.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PersonalName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyInformation.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyMappings.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyQualifierId.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PolicyQualifierInfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PostalAddress.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Prime_p.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateDomainName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKeyInfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PrivateKeyUsagePeriod.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKeyAndChallenge.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/PublicKeyInfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RC2CBCParameter.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RDNSequence.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSAPrivateKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSAPublicKey.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RSASSA_PSS_params.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/ReasonFlags.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RelativeDistinguishedName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/RevokedCertificate.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SignedPublicKeyAndChallenge.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SpecifiedECDomain.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectAltName.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectDirectoryAttributes.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectInfoAccessSyntax.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/SubjectPublicKeyInfo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TBSCertList.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TBSCertificate.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/TerminalIdentifier.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Time.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Trinomial.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/UniqueIdentifier.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/UserNotice.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/Validity.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_ca_policy_url.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_cert_type.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/ASN1/Maps/netscape_comment.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/File/X509.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Base.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/BuiltIn.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/DefaultEngine.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/OpenSSL.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Reductions/Barrett.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/BCMath/Reductions/EvalBarrett.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/Engine.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/GMP.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/GMP/DefaultEngine.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/OpenSSL.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Base.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/DefaultEngine.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Montgomery.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/OpenSSL.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Barrett.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Classic.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/EvalBarrett.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/Montgomery.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/MontgomeryMult.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP/Reductions/PowerOfTwo.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP32.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP64.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BinaryField.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/BinaryField/Integer.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/Common/FiniteField.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/Common/FiniteField/Integer.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/PrimeField.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Math/PrimeField/Integer.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Net/SFTP.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/Net/SSH2.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/System/SSH/Agent.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/System/SSH/Common/Traits/ReadBytes.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/bootstrap.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/phpseclib/phpseclib/phpseclib/openssl.cnf
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/cache/LICENSE.txt
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/cache/src/CacheException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/cache/src/CacheItemInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/cache/src/CacheItemPoolInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/cache/src/InvalidArgumentException.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-client/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-client/src/ClientExceptionInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-client/src/ClientInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-client/src/NetworkExceptionInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-client/src/RequestExceptionInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-factory/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-factory/src/RequestFactoryInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-factory/src/ResponseFactoryInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-factory/src/ServerRequestFactoryInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-factory/src/StreamFactoryInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-factory/src/UploadedFileFactoryInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-factory/src/UriFactoryInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-message/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-message/src/MessageInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-message/src/RequestInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-message/src/ResponseInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-message/src/ServerRequestInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-message/src/StreamInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-message/src/UploadedFileInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/psr/http-message/src/UriInterface.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/ralouphie/getallheaders/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/ralouphie/getallheaders/src/getallheaders.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/symfony/deprecation-contracts/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/symfony/deprecation-contracts/function.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/autoload.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/composer/ClassLoader.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/composer/LICENSE
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/composer/autoload_classmap.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/composer/autoload_files.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/composer/autoload_namespaces.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/composer/autoload_psr4.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/composer/autoload_real.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/prefixed/vendor/composer/autoload_static.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/scoper-autoload.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vendor/yiisoft/extensions.php
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/dist/SearchEngineKeywordsPerformance.umd.js
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/dist/SearchEngineKeywordsPerformance.umd.js.map
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/dist/SearchEngineKeywordsPerformance.umd.min.js
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/dist/SearchEngineKeywordsPerformance.umd.min.js.map
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/dist/umd.metadata.json
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/Admin/AdminPage.vue
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/Admin/Provider.vue
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/Bing/Configuration.vue
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/Configure/ConfigureConnection.less
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/Configure/ConfigureConnection.vue
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/Google/Configuration.vue
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/Yandex/Configuration.vue
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/index.ts
create mode 100644 files/plugin-SearchEngineKeywordsPerformance-5.0.22/vue/src/utilities.ts
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 = ''
+
+ // 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(/^