diff --git a/plugins/ExampleVisualization/ExampleVisualization.php b/plugins/ExampleVisualization/ExampleVisualization.php new file mode 100644 index 0000000000000000000000000000000000000000..1a03abab57b35c096b7c907a60d5a6bf0456cb3c --- /dev/null +++ b/plugins/ExampleVisualization/ExampleVisualization.php @@ -0,0 +1,14 @@ +requestConfig->filter_sort_order = 'desc'; + } + + public function beforeGenericFiltersAreAppliedToLoadedDataTable() + { + // this hook is executed before generic filters like "filter_limit" and "filter_offset" are applied + // Usage: + // $this->dateTable->filter($nameOrClosure); + } + + public function afterGenericFiltersAreAppliedToLoadedDataTable() + { + // this hook is executed after generic filters like "filter_limit" and "filter_offset" are applied + // Usage: + // $this->dateTable->filter($nameOrClosure, $parameters); + } + + public function afterAllFiltersAreApplied() + { + // this hook is executed after the data table is loaded and after all filteres are applied. + // format your data here that you want to pass to the view + + $this->assignTemplateVar('vizTitle', 'MyAwesomeTitle'); + } + + public function beforeRender() + { + // Configure how your visualization should look like, for instance you can disable search + // By defining the config properties shortly before rendering you make sure the config properties have a certain + // value because they could be changed by a report or by request parameters ($_GET / $_POST) before. + // $this->config->show_search = false + } + + public static function canDisplayViewDataTable(ViewDataTable $view) + { + // You usually do not need to implement this method. Here you can define whether your visualization can display + // a specific data table or not. For instance you may only display your visualization in case a single data + // table is requested. Example: + // return $view->isRequestingSingleDataTable(); + + return parent::canDisplayViewDataTable($view); + } +} diff --git a/plugins/ExampleVisualization/config/config.php b/plugins/ExampleVisualization/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/ExampleVisualization/config/config.php @@ -0,0 +1,3 @@ + +

{{ vizTitle }}

+ + + + + {% for name in properties.columns_to_display %} + {% if name in properties.translations|keys %} + + {% else %} + + {% endif %} + {% endfor %} + + + + {% for tableRow in dataTable.getRows %} + + {% for column in properties.columns_to_display %} + + {% endfor %} + + {% endfor %} + +
{{ properties.translations[name]|translate }}{{ name }}
{{ tableRow.getColumn(column)|default('-')|truncate(50)|rawSafeDecoded }}
+ \ No newline at end of file diff --git a/plugins/Resolution/Reports/Base.php b/plugins/Resolution/Reports/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..6d4a19baedc5c3625334dfb7ede94170034fbc7e --- /dev/null +++ b/plugins/Resolution/Reports/Base.php @@ -0,0 +1,33 @@ +categoryId = 'General_Visitors'; + } + + protected function getBasicResolutionDisplayProperties(ViewDataTable $view) + { + $view->config->show_search = false; + $view->config->show_exclude_low_population = false; + + $view->requestConfig->filter_limit = 5; + + if ($view->isViewDataTableId(Graph::ID)) { + $view->config->max_graph_elements = 5; + } + } +} diff --git a/plugins/Resolution/Reports/GetConfiguration.php b/plugins/Resolution/Reports/GetConfiguration.php new file mode 100644 index 0000000000000000000000000000000000000000..bb315185c4e80addf7ae8866937aadb988dd0ad6 --- /dev/null +++ b/plugins/Resolution/Reports/GetConfiguration.php @@ -0,0 +1,46 @@ +dimension = new Configuration(); + $this->name = Piwik::translate('Resolution_Configurations'); + $this->documentation = Piwik::translate('Resolution_WidgetGlobalVisitorsDocumentation', '
'); + $this->order = 7; + + $this->subcategoryId = 'DevicesDetection_Software'; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $this->getBasicResolutionDisplayProperties($view); + + $view->requestConfig->filter_limit = 3; + $view->config->show_search = true; + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Resolution', 'getResolution'), + ); + } +} diff --git a/plugins/Resolution/Reports/GetResolution.php b/plugins/Resolution/Reports/GetResolution.php new file mode 100644 index 0000000000000000000000000000000000000000..4764f0cc72c652d3c83a8a0d408f38a152b22c57 --- /dev/null +++ b/plugins/Resolution/Reports/GetResolution.php @@ -0,0 +1,49 @@ +dimension = new Resolution(); + $this->name = Piwik::translate('Resolution_WidgetResolutions'); + $this->documentation = Piwik::translate('Resolution_WidgetResolutionsDocumentation'); + $this->order = 8; + + $this->subcategoryId = 'DevicesDetection_Devices'; + } + + public function configureView(ViewDataTable $view) + { + $this->getBasicResolutionDisplayProperties($view); + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Resolution', 'getConfiguration'), + ); + } + + public function isEnabled() + { + // Metadata visibility is global-only here, so check the policy state directly. + return !PolicyManager::isPolicyActive(CnilPolicy::class, $idSite = null); + } +} diff --git a/plugins/Resolution/lang/am.json b/plugins/Resolution/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..a28aa67e0b897476859085dc5ff6377d145e885c --- /dev/null +++ b/plugins/Resolution/lang/am.json @@ -0,0 +1,10 @@ +{ + "Resolution": { + "ColumnConfiguration": "ውቅረት", + "ColumnResolution": "ጥራት", + "Configurations": "ውቅረቶች", + "Resolutions": "ጥራቶች", + "WidgetGlobalVisitors": "የሁሉም ጎብኚዎች ውቅረት", + "WidgetResolutions": "የማያ ጥራቶች" + } +} diff --git a/plugins/Resolution/lang/ar.json b/plugins/Resolution/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..8f861cdfc14de6fbd90300a06f150d77e32adaff --- /dev/null +++ b/plugins/Resolution/lang/ar.json @@ -0,0 +1,12 @@ +{ + "Resolution": { + "ColumnConfiguration": "الإعداد", + "ColumnResolution": "الكثافة النقطية", + "Configurations": "الإعدادات", + "PluginDescription": "يوضّح كثافة شاشة زوارك.", + "Resolutions": "الكثافات النقطية", + "WidgetGlobalVisitors": "الإعدادات العامة للزوار", + "WidgetGlobalVisitorsDocumentation": "يعرض هذا التقرير الإعدادات الأكثر شيوعاً لمجمل زوارك. يتضمن الإعداد نظام التشغيل، ونوع المتصفّح وكثافة الشاشة.", + "WidgetResolutions": "كثافات الشاشة النقطية" + } +} diff --git a/plugins/Resolution/lang/az.json b/plugins/Resolution/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Resolution/lang/az.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Resolution/lang/bg.json b/plugins/Resolution/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..e91a18110559419799731da0c5be8b13bbfc12ed --- /dev/null +++ b/plugins/Resolution/lang/bg.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Обобщена конфигурация", + "ColumnResolution": "Разделителна способност на екрана", + "Configurations": "Конфигурации", + "PluginDescription": "Отчита разделителната способност на екрана на Вашите посетители.", + "Resolutions": "Разделителна способност", + "WidgetGlobalVisitors": "Конфигурация на гло", + "WidgetGlobalVisitorsDocumentation": "Този отчет показва повечето общопознати цялостни конфигурации, които вашите посетители са имали. Конфигурация е комбинацията от операционна система, тип на браузера и резолюция на екрана.", + "WidgetResolutions": "Разделителна способност", + "WidgetResolutionsDocumentation": "Този отчет показва разделителната способност на екрана, използвана от посетителите, когато разглеждат Вашия уебсайт." + } +} diff --git a/plugins/Resolution/lang/bs.json b/plugins/Resolution/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Resolution/lang/bs.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Resolution/lang/ca.json b/plugins/Resolution/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..ca11d0a2c05572aa8ffe0fb87ac47d9e53928246 --- /dev/null +++ b/plugins/Resolution/lang/ca.json @@ -0,0 +1,16 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuració", + "ColumnResolution": "Resolució", + "Configurations": "Configuracions", + "PluginDescription": "Informa de les resolucions de pantalla dels vostres visitants.", + "Resolutions": "Resolucions", + "ScreenResolutionDetectionDisabled": "Detecció de resolució de pantalla desactivada", + "ScreenResolutionDetectionDisabledRequirementNote": "La detecció de resolució de pantalla i l'informe de resolució de pantalla han d'estar desactivats.", + "ScreenResolutionReportDisabledByCompliancePolicy": "L'informe de resolució de pantalla està desactivat per la política de compliment.", + "WidgetGlobalVisitors": "Configuracions globals dels visitants", + "WidgetGlobalVisitorsDocumentation": "Aquest informe mostra les configuracions més comuns que tenen els vostres visitants. Una configuració es la combinació de Sistema Operatiu, tipus de navegador i resolució de pantalla.", + "WidgetResolutions": "Resolucions", + "WidgetResolutionsDocumentation": "Aquest informe mostra les resolucions de pantalla que han utilitzat els vostres visitants per veure el vostre lloc web." + } +} diff --git a/plugins/Resolution/lang/cy.json b/plugins/Resolution/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Resolution/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Resolution/lang/da.json b/plugins/Resolution/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..40111562e1b17bcbf35039fa185a92adad1d1fce --- /dev/null +++ b/plugins/Resolution/lang/da.json @@ -0,0 +1,12 @@ +{ + "Resolution": { + "ColumnConfiguration": "Konfiguration", + "ColumnResolution": "Opløsning", + "Configurations": "Indstillinger", + "PluginDescription": "Rapporter de besøgendes skærmopløsninger.", + "Resolutions": "Opløsninger", + "WidgetGlobalVisitors": "Besøgendes konfiguration", + "WidgetGlobalVisitorsDocumentation": "Rapporten viser de mest almindelige samlede konfigurationer, som de besøgende havde. En konfiguration er en kombination af et styresystem, en browsertype og en skærmopløsning.", + "WidgetResolutions": "Skærmopløsninger" + } +} diff --git a/plugins/Resolution/lang/de.json b/plugins/Resolution/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..b3245dfbe780f9b1f34985fc691c0cb79b1f1ff4 --- /dev/null +++ b/plugins/Resolution/lang/de.json @@ -0,0 +1,16 @@ +{ + "Resolution": { + "ColumnConfiguration": "Konfiguration", + "ColumnResolution": "Auflösung", + "Configurations": "Konfigurationen", + "PluginDescription": "Berichte über Bildschirmauflösungen Ihrer Besucher.", + "Resolutions": "Auflösungen", + "ScreenResolutionDetectionDisabled": "Erkennung der Bildschirmauflösung deaktiviert", + "ScreenResolutionDetectionDisabledRequirementNote": "Die Erkennung der Bildschirmauflösung und der Bericht über die Bildschirmauflösung müssen deaktiviert werden.", + "ScreenResolutionReportDisabledByCompliancePolicy": "Der Bericht zur Bildschirmauflösung ist durch die Compliance-Richtlinie deaktiviert.", + "WidgetGlobalVisitors": "Globale Besucherkonfiguration", + "WidgetGlobalVisitorsDocumentation": "Dieser Bericht zeigt Ihnen die häufigsten Gesamtkonfigurationen der Besucher. Eine Konfiguration ist die Kombination aus Betriebssystem, Browsertyp und Bildschirmauflösung.", + "WidgetResolutions": "Bildschirmauflösungen", + "WidgetResolutionsDocumentation": "Dieser Bericht zeigt die Bildschirmauflösung, die Ihre Besucher verwendet haben, als sie Ihre Webseite besucht hatten." + } +} diff --git a/plugins/Resolution/lang/el.json b/plugins/Resolution/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..e072454220bfa2d62510a0e9692e6f57aac9cb59 --- /dev/null +++ b/plugins/Resolution/lang/el.json @@ -0,0 +1,16 @@ +{ + "Resolution": { + "ColumnConfiguration": "Ρύθμιση", + "ColumnResolution": "Ανάλυση", + "Configurations": "Ρυθμίσεις", + "PluginDescription": "Αναφέρει τις αναλύσεις οθόνης των επισκεπτών σας.", + "Resolutions": "Αναλύσεις οθόνης", + "ScreenResolutionDetectionDisabled": "Εντοπισμός ανάλυσης οθόνης ανενεργός", + "ScreenResolutionDetectionDisabledRequirementNote": "Ο εντοπισμός ανάλυσης οθόνης και η αναφορά ανάλυσης οθόνης πρέπει να είναι ανενεργά.", + "ScreenResolutionReportDisabledByCompliancePolicy": "Η αναφορά ανάλυσης οθόνης είναι ανενεργή για λόγους συμμόρφωσης για την ιδιωτικότητα.", + "WidgetGlobalVisitors": "Ρύθμιση Επισκέπτη", + "WidgetGlobalVisitorsDocumentation": "Αυτή η αναφορά δείχνει τις πιο συχνές καθολικές ρυθμίσεις που έχουν οι επισκέπτες σας. Μια ρύθμιση είναι ο συνδυασμός του λειτουργικού συστήματος, του τύπου του προγράμματος πλοήγησης και της ανάλυσης οθόνης.", + "WidgetResolutions": "Ανάλυση οθόνης", + "WidgetResolutionsDocumentation": "Αυτή η αναφορά δείχνει τις αναλύσεις οθόνης που χρησιμοποιούσαν οι επισκέπτες σας κατά την επίσκεψή τους στον ιστοτόπο σας." + } +} diff --git a/plugins/Resolution/lang/en.json b/plugins/Resolution/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..73895006df541af2207923803a7c300e456e5908 --- /dev/null +++ b/plugins/Resolution/lang/en.json @@ -0,0 +1,16 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuration", + "ColumnResolution": "Resolution", + "Configurations": "Configurations", + "PluginDescription": "Reports the screen resolutions of your visitors.", + "Resolutions": "Resolutions", + "WidgetGlobalVisitors": "Visitor Configuration", + "WidgetGlobalVisitorsDocumentation": "This report shows the most common overall configurations that your visitors had. A configuration is the combination of an operating system, a browser type and a screen resolution.", + "WidgetResolutions": "Screen Resolution", + "WidgetResolutionsDocumentation": "This report shows the screen resolutions your visitors used when viewing your website.", + "ScreenResolutionDetectionDisabled": "Screen resolution detection disabled", + "ScreenResolutionDetectionDisabledRequirementNote": "Screen resolution detection and screen resolution report must be disabled.", + "ScreenResolutionReportDisabledByCompliancePolicy": "Screen resolution report is disabled by compliance policy." + } +} diff --git a/plugins/Resolution/lang/es-ar.json b/plugins/Resolution/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..1e9b9b98b7c8b986809cc44448b5b563bec73364 --- /dev/null +++ b/plugins/Resolution/lang/es-ar.json @@ -0,0 +1,12 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuración", + "ColumnResolution": "Resolución", + "Configurations": "Configuración", + "PluginDescription": "Informa las resoluciones de pantalla de tus visitantes.", + "Resolutions": "Resoluciones", + "WidgetGlobalVisitors": "Configuración de visitante", + "WidgetGlobalVisitorsDocumentation": "Este informe muestra las configuraciones más comunes que tenían tus visitantes. Una configuración es una combinación de un sistema operativo, un tipo de navegador y una resolución de pantalla.", + "WidgetResolutions": "Resolución de pantalla" + } +} diff --git a/plugins/Resolution/lang/es.json b/plugins/Resolution/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..414b1b687fdd204c72f4b03c7d852a5bbcf417af --- /dev/null +++ b/plugins/Resolution/lang/es.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuración", + "ColumnResolution": "Resolución", + "Configurations": "Configuraciones", + "PluginDescription": "Informa las resoluciones de pantalla de sus visitantes.", + "Resolutions": "Resoluciones", + "WidgetGlobalVisitors": "Configuración de visitantes", + "WidgetGlobalVisitorsDocumentation": "Este informe muestra las configuraciones más usuales que poseen sus visitantes. Una configuración es una combinación de un sistema operativo, un tipo de navegador de internet y una resolución de pantalla.", + "WidgetResolutions": "Resolución de pantalla", + "WidgetResolutionsDocumentation": "Este informe muestra las resoluciones de pantalla que usaron sus visitantes al ver su sitio web." + } +} diff --git a/plugins/Resolution/lang/et.json b/plugins/Resolution/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..ce8e233ecf4234cc0cd1a8a2bdf3c9f0f9694a47 --- /dev/null +++ b/plugins/Resolution/lang/et.json @@ -0,0 +1,10 @@ +{ + "Resolution": { + "ColumnConfiguration": "Konfiguratsioon", + "ColumnResolution": "Resolutsioon", + "Configurations": "Konfiguratsioonid", + "Resolutions": "Ekraani resolutsioonid", + "WidgetGlobalVisitors": "Kohalike külastajate seaded", + "WidgetResolutions": "Ekraani resolutsioonid" + } +} diff --git a/plugins/Resolution/lang/eu.json b/plugins/Resolution/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..68a5934bd2af2ff9868ab9cf1dc6cf6e7ad05c5d --- /dev/null +++ b/plugins/Resolution/lang/eu.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Konfigurazioa", + "ColumnResolution": "Bereizmena", + "Configurations": "Konfigurazioak", + "PluginDescription": "Zure bisitarien pantailen bereizmenaren berri ematen du.", + "Resolutions": "Bereizmenak", + "WidgetGlobalVisitors": "Bisitarien konfigurazioa", + "WidgetGlobalVisitorsDocumentation": "Txosten honek erakusten du zein diren zure bisitarien konfigurazio ohikoenak. Konfigurazio bat sistema eragilearen, nabigatzaile-motaren eta pantailaren bereizmenaren konbinazioa da.", + "WidgetResolutions": "Pantailaren bereizmenak", + "WidgetResolutionsDocumentation": "Txosten honek erakusten du zure bisitariek zer pantaila bereizmen erabili zituzten zure webgunea bisitatzean." + } +} diff --git a/plugins/Resolution/lang/fr.json b/plugins/Resolution/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..c01c598ea2a02335bd32a2b2f06fc04db17aaaf8 --- /dev/null +++ b/plugins/Resolution/lang/fr.json @@ -0,0 +1,16 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuration", + "ColumnResolution": "Résolution", + "Configurations": "Configurations", + "PluginDescription": "Rapporte les résolutions d'écran de vos visiteurs.", + "Resolutions": "Résolutions", + "ScreenResolutionDetectionDisabled": "Détection de la résolution d'écran désactivée", + "ScreenResolutionDetectionDisabledRequirementNote": "La détection et le rapport de résolution d'écran doivent être désactivés.", + "ScreenResolutionReportDisabledByCompliancePolicy": "Le rapport de résolution d'écran est désactivé par la politique de conformité.", + "WidgetGlobalVisitors": "Configuration globale des visiteurs", + "WidgetGlobalVisitorsDocumentation": "Ce rapport montre les configurations globales les plus communes de vos visiteurs. Une configuration est la combinaison d'un système d'exploitation, d'un type de navigateur et d'une résolution d'écran.", + "WidgetResolutions": "Résolutions d'écran", + "WidgetResolutionsDocumentation": "Ce rapport indique les résolutions d'écran utilisées par vos visiteurs lors de la consultation de votre site web." + } +} diff --git a/plugins/Resolution/lang/ga.json b/plugins/Resolution/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..34b9833d4ae94aa330bf1e3f60ecfeb21c2aa04e --- /dev/null +++ b/plugins/Resolution/lang/ga.json @@ -0,0 +1,16 @@ +{ + "Resolution": { + "ColumnConfiguration": "Cumraíocht", + "ColumnResolution": "Rún", + "Configurations": "Cumraíochtaí", + "PluginDescription": "Tuairiscíonn sé rúin scáileáin do chuairteoirí.", + "Resolutions": "Rúin", + "ScreenResolutionDetectionDisabled": "Braiteadh réitigh scáileáin díchumasaithe", + "ScreenResolutionDetectionDisabledRequirementNote": "Ní mór braiteadh réitigh scáileáin agus tuarascáil réitigh scáileáin a dhíchumasú.", + "ScreenResolutionReportDisabledByCompliancePolicy": "Tá tuarascáil réitigh scáileáin díchumasaithe ag an mbeartas comhlíontachta.", + "WidgetGlobalVisitors": "Cumraíocht Cuairteoirí", + "WidgetGlobalVisitorsDocumentation": "Léiríonn an tuarascáil seo na cumraíochtaí foriomlána is coitianta a bhí ag do chuairteoirí. Is éard is cumraíocht ann ná meascán de chóras oibriúcháin, cineál brabhsálaí agus taifeach scáileáin.", + "WidgetResolutions": "Taifeach Scáileáin", + "WidgetResolutionsDocumentation": "Taispeánann an tuarascáil seo na réitigh scáileáin a d’úsáid do chuairteoirí agus iad ag féachaint ar do shuíomh Gréasáin." + } +} diff --git a/plugins/Resolution/lang/gl.json b/plugins/Resolution/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..69a165d539577e30c258f1cfd4a00b61b2f39dc4 --- /dev/null +++ b/plugins/Resolution/lang/gl.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuración", + "ColumnResolution": "Resolución", + "Configurations": "Configuracións", + "PluginDescription": "Informa sobre as resolucións de pantalla das visitas.", + "Resolutions": "Resolucións", + "WidgetGlobalVisitors": "Configuración global de visitantes", + "WidgetGlobalVisitorsDocumentation": "Este informa mostra as configuracións xerais máis frecuentes utilizadas polas visitas. Unha configuración é a combinación de sistema operativo, tipo de navegador e resolución de pantalla.", + "WidgetResolutions": "Resolución de pantalla", + "WidgetResolutionsDocumentation": "Este informe mostra as resolucións da pantalla que usaron os teus visitantes ao velo teu sitio web." + } +} diff --git a/plugins/Resolution/lang/gu.json b/plugins/Resolution/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..9ac6e7edfa42d7a42f669f1aabb3eb15acaf62d0 --- /dev/null +++ b/plugins/Resolution/lang/gu.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "કન્ફિગરેશન", + "ColumnResolution": "રિઝોલ્યુશન", + "Configurations": "કન્ફિગરેશન્સ", + "PluginDescription": "તમારા મુલાકાતીઓના સ્ક્રીન રીઝોલ્યુશનની જાણ કરે છે.", + "Resolutions": "રિઝોલ્યુશન્સ", + "WidgetGlobalVisitors": "મુલાકાતી કન્ફિગરેશન", + "WidgetGlobalVisitorsDocumentation": "આ રિપોર્ટ તમારા મુલાકાતીઓની સૌથી સામાન્ય એકંદર કન્ફિગરેશન દર્શાવે છે. કન્ફિગરેશન એ ઓપરેટિંગ સિસ્ટમ, બ્રાઉઝર પ્રકાર અને સ્ક્રીન રીઝોલ્યુશનનું સંયોજન છે.", + "WidgetResolutions": "સ્ક્રીન રીઝોલ્યુશન", + "WidgetResolutionsDocumentation": "આ રિપોર્ટ તમારી વેબસાઇટ જોતી વખતે તમારા મુલાકાતીઓ દ્વારા ઉપયોગમાં લેવાતા સ્ક્રીન રીઝોલ્યુશન બતાવે છે." + } +} diff --git a/plugins/Resolution/lang/hi.json b/plugins/Resolution/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..85358807f2c04da0c4dd2203f1abee75063f828e --- /dev/null +++ b/plugins/Resolution/lang/hi.json @@ -0,0 +1,12 @@ +{ + "Resolution": { + "ColumnConfiguration": "विन्यास", + "ColumnResolution": "संकल्प", + "Configurations": "विन्यास", + "PluginDescription": "अपने दर्शकों के स्क्रीन संकल्प रिपोर्ट।", + "Resolutions": "संकल्प", + "WidgetGlobalVisitors": "आगंतुक विन्यास", + "WidgetGlobalVisitorsDocumentation": "यह रिपोर्ट आपकी आगंतुकों था कि सबसे आम समग्र विन्यास को दर्शाता है। एक विन्यास एक ऑपरेटिंग सिस्टम, एक ब्राउज़र का प्रकार और एक स्क्रीन संकल्प का संयोजन है।", + "WidgetResolutions": "स्क्रीन संकल्प" + } +} diff --git a/plugins/Resolution/lang/hy.json b/plugins/Resolution/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Resolution/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Resolution/lang/id.json b/plugins/Resolution/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..53f53b7275b266149e01e581ad5f302937a57310 --- /dev/null +++ b/plugins/Resolution/lang/id.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Pengaturan", + "ColumnResolution": "Resolusi", + "Configurations": "Pengaturan", + "PluginDescription": "Melaporkan resolusi layar dari pengunjung Anda.", + "Resolutions": "Resolusi", + "WidgetGlobalVisitors": "Konfigurasi Pengunjung", + "WidgetGlobalVisitorsDocumentation": "Laporan ini menunjukkan pengaturan paling umum yang pengunjung miliki. Subuah pengaturan terdiri atas sistem operasi, jenis peramban, dan resolusi layar.", + "WidgetResolutions": "Resolusi layar", + "WidgetResolutionsDocumentation": "Laporan ini menunjukkan resolusi layar yang digunakan pengunjung Anda saat melihat situs web Anda." + } +} diff --git a/plugins/Resolution/lang/is.json b/plugins/Resolution/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..395237c48d1f24cc81dbfa5d9dd5cd8d267612cc --- /dev/null +++ b/plugins/Resolution/lang/is.json @@ -0,0 +1,10 @@ +{ + "Resolution": { + "ColumnConfiguration": "Uppsetning", + "ColumnResolution": "Skjáupplausn", + "Configurations": "Uppsetningar", + "Resolutions": "Skjáupplausnir", + "WidgetGlobalVisitors": "Altæk gestastilling", + "WidgetResolutions": "Skjáupplausnir" + } +} diff --git a/plugins/Resolution/lang/it.json b/plugins/Resolution/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..09e01ec7d4e066f2811689fa3e594f6786128c75 --- /dev/null +++ b/plugins/Resolution/lang/it.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configurazione", + "ColumnResolution": "Risoluzione", + "Configurations": "Configurazioni", + "PluginDescription": "Restituisce la risoluzione di schermo dei tuoi visitatori.", + "Resolutions": "Risoluzioni", + "WidgetGlobalVisitors": "Configurazione visitatori", + "WidgetGlobalVisitorsDocumentation": "Questo report mostra le configurazioni globali più comuni che i visitatori avevano. Una configurazione è la combinazione di un sistema operativo, un tipo browser e una risoluzione di schermo.", + "WidgetResolutions": "Risoluzione schermo", + "WidgetResolutionsDocumentation": "Questo report mostra le risoluzioni dello schermo utilizzate dai visitatori durante la visualizzazione del tuo sito web." + } +} diff --git a/plugins/Resolution/lang/ja.json b/plugins/Resolution/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..5a73940019c603878b7c6a2e9fb375fbe925b9fd --- /dev/null +++ b/plugins/Resolution/lang/ja.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "環境設定", + "ColumnResolution": "解像度", + "Configurations": "環境設定", + "PluginDescription": "ビジターの画面解像度をレポートします。", + "Resolutions": "解像度", + "WidgetGlobalVisitors": "ビジターの全般的な環境設定", + "WidgetGlobalVisitorsDocumentation": "ビジターの最も一般的な利用環境についてのリポートです。オペレーティングシステム、ブラウザの種類と画面の解像度の組合せで表示します。", + "WidgetResolutions": "画面解像度", + "WidgetResolutionsDocumentation": "このレポートは、訪問者がウェブサイトを閲覧する際に使用した画面解像度を示します。" + } +} diff --git a/plugins/Resolution/lang/ka.json b/plugins/Resolution/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..fadd4379d79154479cda95041e0c44ae288f73dd --- /dev/null +++ b/plugins/Resolution/lang/ka.json @@ -0,0 +1,10 @@ +{ + "Resolution": { + "ColumnConfiguration": "კონფიგურაცია", + "ColumnResolution": "რეზოლუცია", + "Configurations": "გონფიგურაციები", + "Resolutions": "რეზოლუციები", + "WidgetGlobalVisitors": "ვიზიტორების გლობალური კონფიგურაცია", + "WidgetResolutions": "ეკრანის რეზოლუვიები" + } +} diff --git a/plugins/Resolution/lang/ko.json b/plugins/Resolution/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..a571bbf4cb345f4df8ba6961894a2d403df71083 --- /dev/null +++ b/plugins/Resolution/lang/ko.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "구성", + "ColumnResolution": "해상도", + "Configurations": "구성", + "PluginDescription": "방문자의 화면 해상도를 기록/보고합니다.", + "Resolutions": "해상도", + "WidgetGlobalVisitors": "글로벌 방문자 구성", + "WidgetGlobalVisitorsDocumentation": "방문자의 가장 일반적인 사용 환경에 대한 보고서입니다. 운영 체제, 브라우저 종류와 화면 해상도의 조합으로 표시합니다.", + "WidgetResolutions": "스크린 해상도", + "WidgetResolutionsDocumentation": "이 보고서는 방문자가 웹사이트를 볼 때 사용한 화면 해상도를 보여줍니다." + } +} diff --git a/plugins/Resolution/lang/ku.json b/plugins/Resolution/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..9bb897b6b2bd00cabe6bbd9a2db6a3e3066ea6b1 --- /dev/null +++ b/plugins/Resolution/lang/ku.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "شێوەبەندی", + "ColumnResolution": "ڕێزوولووشن", + "Configurations": "شێوەبەندییەکان", + "PluginDescription": "ڕێزووڵووشنی دیمەندەری سەردانکەران ڕاپۆرت دەکا", + "Resolutions": "ڕێزووڵووشنەکان", + "WidgetGlobalVisitors": "شێوەبەندیی سەردانکەر", + "WidgetGlobalVisitorsDocumentation": "ئەم ڕاپۆرتە باوترین شێوەبەندیی گشتی نیشان دەدات کە سەردانکەرانت هەیانبووە. شێوەپێدان بریتیە لە تێکەڵکردنی سیستمی بەکارخەری، جۆری وێبگەڕەکە و ڕێزووڵووشنی دیمەندەر.", + "WidgetResolutions": "ڕێزووڵووشنی دیمەندەر", + "WidgetResolutionsDocumentation": "ئەم ڕاپۆرتە ڕێزووڵووشنی دیمەندەری بەکارهێنەران نیشان دەدا لەو کاتەدا کە وێبسایتەکەتیان بینیوە." + } +} diff --git a/plugins/Resolution/lang/lb.json b/plugins/Resolution/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..864eb552b807caac98d74aee6335b6b8c70564a8 --- /dev/null +++ b/plugins/Resolution/lang/lb.json @@ -0,0 +1,6 @@ +{ + "Resolution": { + "ColumnResolution": "Opléisung", + "Resolutions": "Opléisungen" + } +} diff --git a/plugins/Resolution/lang/lt.json b/plugins/Resolution/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..d91158e795acb9a39fba0175c515812c57e3af9c --- /dev/null +++ b/plugins/Resolution/lang/lt.json @@ -0,0 +1,12 @@ +{ + "Resolution": { + "ColumnConfiguration": "Konfigūracija", + "ColumnResolution": "Ekrano raiška", + "Configurations": "Konfigūracijos", + "PluginDescription": "Praneša apie jūsų lankytojų ekrano raiškas.", + "Resolutions": "Ekranų raiškos", + "WidgetGlobalVisitors": "Lankytojų konfigūracija", + "WidgetGlobalVisitorsDocumentation": "Ši ataskaita rodo visas dažniausias jūsų lankytojų naudotas konfigūracijas. Konfigūracija - tai operacinės sistemos, naršyklės tipo ir ekrano raiškos kombinacija.", + "WidgetResolutions": "Ekrano raiška" + } +} diff --git a/plugins/Resolution/lang/nn.json b/plugins/Resolution/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..45b2c8133aa5cc118c9b7aef7d47841ddea5b324 --- /dev/null +++ b/plugins/Resolution/lang/nn.json @@ -0,0 +1,10 @@ +{ + "Resolution": { + "ColumnConfiguration": "Konfigurasjon", + "ColumnResolution": "Oppløysing", + "Configurations": "Konfigurasjonar", + "Resolutions": "Oppløysingar", + "WidgetGlobalVisitors": "Global vitjarkonfigurasjon", + "WidgetResolutions": "Skjermoppløysing" + } +} diff --git a/plugins/Resolution/lang/pt-br.json b/plugins/Resolution/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..8b2f433c56d0038653291d4b9b840dd195151a31 --- /dev/null +++ b/plugins/Resolution/lang/pt-br.json @@ -0,0 +1,16 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuração", + "ColumnResolution": "Resolução", + "Configurations": "Configurações", + "PluginDescription": "Informa as resoluções de tela dos seus visitantes.", + "Resolutions": "Resoluções", + "ScreenResolutionDetectionDisabled": "Detecção de resolução de tela desativada", + "ScreenResolutionDetectionDisabledRequirementNote": "A detecção e o relatório de resolução de tela devem ser desativados.", + "ScreenResolutionReportDisabledByCompliancePolicy": "O relatório de resolução de tela está desabilitado pela política de conformidade.", + "WidgetGlobalVisitors": "Configuração do visitante", + "WidgetGlobalVisitorsDocumentation": "Este relatório mostra as configurações gerais mais comuns que os seus visitantes tinham. Uma configuração é a combinação de um sistema operacional, um tipo de navegador e uma resolução de tela.", + "WidgetResolutions": "Resolução de tela", + "WidgetResolutionsDocumentation": "Este relatório mostra as resoluções de tela usadas pelos visitantes ao visualizar seu site." + } +} diff --git a/plugins/Resolution/lang/pt.json b/plugins/Resolution/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..47cb2a33ed3b264c1c2e306f4b94eda3d8209053 --- /dev/null +++ b/plugins/Resolution/lang/pt.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuração", + "ColumnResolution": "Resolução", + "Configurations": "Configurações", + "PluginDescription": "Relatórios das resoluções de ecrã dos seus visitantes.", + "Resolutions": "Resoluções", + "WidgetGlobalVisitors": "Configuração dos visitantes", + "WidgetGlobalVisitorsDocumentation": "Este relatório mostra as configurações gerais mais comuns que os visitantes tinham. Uma configuração é a combinação de um sistema operativo, um tipo de navegador e uma resolução de ecrã.", + "WidgetResolutions": "Resoluções de ecrã", + "WidgetResolutionsDocumentation": "Este relatório mostra as resoluções de ecrã que os seus visitantes utilizaram quando visualizaram o seu site da Web." + } +} diff --git a/plugins/Resolution/lang/ru.json b/plugins/Resolution/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..d2a8b97cfd39f61079b87e2a86fc9cbb5c3b92ba --- /dev/null +++ b/plugins/Resolution/lang/ru.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Конфигурация", + "ColumnResolution": "Разрешение", + "Configurations": "По конфигурации", + "PluginDescription": "Сообщает о разрешениии экранов у посетителей.", + "Resolutions": "По разрешению мониторов", + "WidgetGlobalVisitors": "Глобальная конфигурация", + "WidgetGlobalVisitorsDocumentation": "Этот отчет показывает общую информацию по наиболее популяоным конфигурациям системы ваших посетителей. Конфигурация - это комбинация операционной системы, браузера и разрешения экрана.", + "WidgetResolutions": "По разрешению мониторов", + "WidgetResolutionsDocumentation": "В этом отчете показаны разрешения экрана, которые ваши посетители использовали при просмотре вашего веб-сайта." + } +} diff --git a/plugins/Resolution/lang/si.json b/plugins/Resolution/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Resolution/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Resolution/lang/sl.json b/plugins/Resolution/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..3180769f0591a42e7670d9dab9a4b0c7b47bd1f7 --- /dev/null +++ b/plugins/Resolution/lang/sl.json @@ -0,0 +1,9 @@ +{ + "Resolution": { + "ColumnConfiguration": "Nastavitev", + "ColumnResolution": "Resolucija", + "Configurations": "Nastavitve", + "Resolutions": "Resolucije", + "WidgetResolutions": "Resolucija zaslona" + } +} diff --git a/plugins/Resolution/lang/sr.json b/plugins/Resolution/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..0f4b5cf31791fc058c9779600147fc23484dc563 --- /dev/null +++ b/plugins/Resolution/lang/sr.json @@ -0,0 +1,12 @@ +{ + "Resolution": { + "ColumnConfiguration": "Podešavanja", + "ColumnResolution": "Rezolucija", + "Configurations": "Podešavanja", + "PluginDescription": "Prikazuje ekransku rezoluciju posetilaca.", + "Resolutions": "Rezolucije", + "WidgetGlobalVisitors": "Globalna podešavanja posetilaca", + "WidgetGlobalVisitorsDocumentation": "Ovaj izveštaj prikazuje najčešća podešavanja vaših posetilaca. Pod podešavanjem podrazumevamo kombinaciju operativnog sistema, tipa brauzera i ekranske rezolucije.", + "WidgetResolutions": "Rezolucije ekrana" + } +} diff --git a/plugins/Resolution/lang/ta.json b/plugins/Resolution/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..b41f41a7b71f8da69f4ad9c867c5ac9333a29614 --- /dev/null +++ b/plugins/Resolution/lang/ta.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "உள்ளமைவு", + "ColumnResolution": "பகுத்தல்", + "Configurations": "உள்ளமைவுகள்", + "PluginDescription": "உங்கள் பார்வையாளர்களின் திரைத் தீர்மானங்களை தெரிவிக்கிறது.", + "Resolutions": "தீர்மானங்கள்", + "WidgetGlobalVisitors": "பார்வையாளர் உள்ளமைவு", + "WidgetGlobalVisitorsDocumentation": "இந்த அறிக்கை உங்கள் பார்வையாளர்களிடம் இருந்த பொதுவான ஒட்டுமொத்த உள்ளமைவுகளைக் காட்டுகிறது. ஒரு உள்ளமைவு என்பது ஒரு இயக்க முறைமை, உலாவி வகை மற்றும் திரை தெளிவுத்திறன் ஆகியவற்றின் கலவையாகும்.", + "WidgetResolutions": "திரை தெளிவுத்திறன்", + "WidgetResolutionsDocumentation": "உங்கள் வலைத்தளத்தைப் பார்க்கும்போது உங்கள் பார்வையாளர்கள் பயன்படுத்திய திரைத் தீர்மானங்களை இந்த அறிக்கை காட்டுகிறது." + } +} diff --git a/plugins/Resolution/lang/te.json b/plugins/Resolution/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..64c7ad580529366847ba9940aa497cc850e4818e --- /dev/null +++ b/plugins/Resolution/lang/te.json @@ -0,0 +1,6 @@ +{ + "Resolution": { + "ColumnConfiguration": "స్వరూపణం", + "Configurations": "స్వరూపణలు" + } +} diff --git a/plugins/Resolution/lang/th.json b/plugins/Resolution/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..6a8831d49da42340afc051d316befc14815df1ee --- /dev/null +++ b/plugins/Resolution/lang/th.json @@ -0,0 +1,10 @@ +{ + "Resolution": { + "ColumnConfiguration": "การตั้งค่า", + "ColumnResolution": "ความละเอียด", + "Configurations": "การกำหนดค่า", + "Resolutions": "ความละเอียด", + "WidgetGlobalVisitors": "การตั้งค่าผู้เข้าชมโดยรวม", + "WidgetResolutions": "ความละเอียดจอภาพ" + } +} diff --git a/plugins/Resolution/lang/tl.json b/plugins/Resolution/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..438e3b8ec5ed845d3842a5d175fd2da3534ab148 --- /dev/null +++ b/plugins/Resolution/lang/tl.json @@ -0,0 +1,11 @@ +{ + "Resolution": { + "ColumnConfiguration": "Configuration", + "ColumnResolution": "Resolusyon", + "Configurations": "Configurations", + "Resolutions": "Mga Resolution", + "WidgetGlobalVisitors": "Configuration ng bisita", + "WidgetGlobalVisitorsDocumentation": "Ang ulat na ito ay nagpapakita ng karaniwang pangkalahatang configuration na meron ang iyong bisita. Ang configuration ay binubuo ng operating system uri ng mga browser at screen resolution.", + "WidgetResolutions": "Screen Resolution" + } +} diff --git a/plugins/Resolution/lang/uk.json b/plugins/Resolution/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..ec8decb92751e9cfea0bbffa5dcc3d0be1f91bf7 --- /dev/null +++ b/plugins/Resolution/lang/uk.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "Конфігурація", + "ColumnResolution": "Роздільна здатність", + "Configurations": "По конфігурації", + "PluginDescription": "Повідомляє про роздільну здатність дисплея відвідувачів.", + "Resolutions": "По роздільній здатності моніторів", + "WidgetGlobalVisitors": "Глобальна конфігурація", + "WidgetGlobalVisitorsDocumentation": "Цей звіт показує загальну інформацію з найбільш популярними конфігураціями системи ваших відвідувачів. Конфігурація - це комбінація операційної системи, браузера і роздільної здатності дисплею.", + "WidgetResolutions": "По роздільній здатності моніторів", + "WidgetResolutionsDocumentation": "Цей звіт показує роздільну здатність екрана, яку використовували відвідувачі під час перегляду вашого вебсайту." + } +} diff --git a/plugins/Resolution/lang/zh-cn.json b/plugins/Resolution/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..eb28a285cdc4aef923ad8e2e49448d181b3e461f --- /dev/null +++ b/plugins/Resolution/lang/zh-cn.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "客户端配置", + "ColumnResolution": "分辨率", + "Configurations": "客户端配置", + "PluginDescription": "报告访客的屏幕分辨率。", + "Resolutions": "分辨率", + "WidgetGlobalVisitors": "访客设置", + "WidgetGlobalVisitorsDocumentation": "本报表显示您的访客最常用的系统配置。系统配置是操作系统、浏览器类型及显示器分辨率的组合。", + "WidgetResolutions": "画面分辨率", + "WidgetResolutionsDocumentation": "此报告显示访问者在查看您的网站时使用的屏幕分辨率。" + } +} diff --git a/plugins/Resolution/lang/zh-tw.json b/plugins/Resolution/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..c9a5ab11f63ec310ca4028e383d0540e2c408ac6 --- /dev/null +++ b/plugins/Resolution/lang/zh-tw.json @@ -0,0 +1,13 @@ +{ + "Resolution": { + "ColumnConfiguration": "裝置配置", + "ColumnResolution": "解析度", + "Configurations": "裝置配置", + "PluginDescription": "顯示訪客的螢幕解析度。", + "Resolutions": "解析度", + "WidgetGlobalVisitors": "訪客裝置配置", + "WidgetGlobalVisitorsDocumentation": "此報表顯示訪客中最常見的裝置配置。裝置配置包含作業系統、瀏覽器以及螢幕解析度。", + "WidgetResolutions": "螢幕解析度", + "WidgetResolutionsDocumentation": "這份報表顯示訪客瀏覽你的網站時使用的螢幕解析度。" + } +} diff --git a/plugins/TasksTimetable/CHANGELOG.md b/plugins/TasksTimetable/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..b8fe726960385b58c03da57e4a195da742a21ec4 --- /dev/null +++ b/plugins/TasksTimetable/CHANGELOG.md @@ -0,0 +1,14 @@ +## Changelog + +* 5.0.2 - 2025-07-07 Textual changes +* 5.0.1 Added plugin category for Marketplace +* 5.0.0 Compatibility with Matomo 5.0 +* 4.0.2 Better unserialise +* 4.0.1 Compatibility with Matomo 4.0 +* 4.0.0 Compatibility with Matomo 4.0 +* 3.0.2 translation updates +* 3.0.0 Compatibility with Piwik 3.0 +* 0.2.2 Display time of day in table and renamed column to "Date in UTC timezone" +* 0.2.1 Minor translation updates. +* 0.2.0 Compatibility w/ Piwik 2.15. +* 0.1.0 Initial release diff --git a/plugins/TasksTimetable/Controller.php b/plugins/TasksTimetable/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..8294d558255dac85023ce4a9ac39ccd46a7b77fe --- /dev/null +++ b/plugins/TasksTimetable/Controller.php @@ -0,0 +1,73 @@ +setGeneralVariablesView($view); + + $tasks = Option::get('TaskScheduler.timetable'); + + if (!empty($tasks)) { + $tasks = Common::safe_unserialize($tasks); + } + + if (empty($tasks)) { + $tasks = array(); + } else { + asort($tasks); + } + + $tsNow = Date::now()->getTimestamp(); + + $formatter = new Formatter(); + + $tasksFormatted = array(); + foreach ($tasks as $name => $timestamp) { + $tasksFormatted[] = array( + 'name' => $name, + 'executionDate' => $this->getFormattedDatetime($timestamp), + 'ts_difference' => $formatter->getPrettyTimeFromSeconds($timestamp - $tsNow) + ); + } + + $view->currentTime = $this->getFormattedDatetime(Date::now()->getTimestampUTC()); + $view->tasks = $tasksFormatted; + + return $view->render(); + } + + /** + * @param $timestamp + * @return string + * @throws \Exception + */ + protected function getFormattedDatetime($timestamp) + { + $dateFormat = Piwik::translate(Date::DATE_FORMAT_LONG); + return Date::factory($timestamp)->getLocalized($dateFormat) . ' ' . Date::factory($timestamp)->getLocalized(' h:mm:ss'); + } +} diff --git a/plugins/TasksTimetable/LICENSE b/plugins/TasksTimetable/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7 --- /dev/null +++ b/plugins/TasksTimetable/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/plugins/TasksTimetable/Menu.php b/plugins/TasksTimetable/Menu.php new file mode 100644 index 0000000000000000000000000000000000000000..24165130411aa1df2018dc106d62b010bb7faa8a --- /dev/null +++ b/plugins/TasksTimetable/Menu.php @@ -0,0 +1,28 @@ +addDiagnosticItem( + 'TasksTimetable_ScheduledTasks', + array('module' => 'TasksTimetable', 'action' => 'index'), + $order = 6 + ); + } + } +} diff --git a/plugins/TasksTimetable/README.md b/plugins/TasksTimetable/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ffc10811f383affbc1b0e0ac4ad2feadc52e2fc3 --- /dev/null +++ b/plugins/TasksTimetable/README.md @@ -0,0 +1,7 @@ +# Matomo TasksTimetable Plugin + +[![Build Status](https://github.com/matomo-org/plugin-TasksTimetable/actions/workflows/matomo-tests.yml/badge.svg?branch=4.x-dev)](https://github.com/matomo-org/plugin-TasksTimetable/actions/workflows/matomo-tests.yml) + +## Description + +Plugin to list all scheduled tasks: Name of the tasks and next execution time displayed in a table. diff --git a/plugins/TasksTimetable/TasksTimetable.php b/plugins/TasksTimetable/TasksTimetable.php new file mode 100644 index 0000000000000000000000000000000000000000..a7dd0a25b21da6001cc3dbe23be3e1595733fa9a --- /dev/null +++ b/plugins/TasksTimetable/TasksTimetable.php @@ -0,0 +1,33 @@ + 'getClientSideTranslationKeys', + ]; + } + + public function getClientSideTranslationKeys(&$translations) + { + $translations[] = 'TasksTimetable_ScheduledTasks'; + $translations[] = 'TasksTimetable_Introduction'; + $translations[] = 'General_Name'; + $translations[] = 'General_Date'; + $translations[] = 'TasksTimetable_NothingScheduled'; + } +} diff --git a/plugins/TasksTimetable/docs/faq.md b/plugins/TasksTimetable/docs/faq.md new file mode 100644 index 0000000000000000000000000000000000000000..68ac8d8b703d9aeaf92d5b7232d5037947a0f60c --- /dev/null +++ b/plugins/TasksTimetable/docs/faq.md @@ -0,0 +1,5 @@ +## FAQ + +__Where can I find the timetable?__ + +It is located as a menu item "Scheduled Tasks" under diagnostics on the settings page. diff --git a/plugins/TasksTimetable/lang/am.json b/plugins/TasksTimetable/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/am.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/ar.json b/plugins/TasksTimetable/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/ar.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/az.json b/plugins/TasksTimetable/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..38540252570452e13ad3786421cb8e773a85164b --- /dev/null +++ b/plugins/TasksTimetable/lang/az.json @@ -0,0 +1,5 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Planlaşdırılmış Tapşırıqlar" + } +} diff --git a/plugins/TasksTimetable/lang/be.json b/plugins/TasksTimetable/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/be.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/bg.json b/plugins/TasksTimetable/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..3a5cf511923db7f6d544fa36a60cbb23e5eb88ce --- /dev/null +++ b/plugins/TasksTimetable/lang/bg.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Следните задачи са планирани да бъдат изпълнени въз основа на текущото UTC време: %s.", + "NothingScheduled": "Няма планирани задачи", + "ScheduledTasks": "Планирани задачи" + } +} diff --git a/plugins/TasksTimetable/lang/bn.json b/plugins/TasksTimetable/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/bn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/bs.json b/plugins/TasksTimetable/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/bs.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/ca.json b/plugins/TasksTimetable/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..f4863460f4bd63a0f025a74ccd672d07d49397de --- /dev/null +++ b/plugins/TasksTimetable/lang/ca.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Tasques programades", + "Introduction": "Les següents tasques estan programades per ser executades basades amb l'hora UTC actual : %s.", + "NothingScheduled": "No hi ha tasques programades" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/cs.json b/plugins/TasksTimetable/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..19439d884a084dac334862f8a355d2a03ff74688 --- /dev/null +++ b/plugins/TasksTimetable/lang/cs.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Naplánované úlohy", + "Introduction": "Na základě aktuálního UTC času %s jsou naplánovány ke spuštění následující úlohy.", + "NothingScheduled": "Žádné naplánované úlohy" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/cy.json b/plugins/TasksTimetable/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/da.json b/plugins/TasksTimetable/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..d3b751ad05e42dc5640dc5e74c0ef20a8c7d0088 --- /dev/null +++ b/plugins/TasksTimetable/lang/da.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Planlagte opgaver", + "Introduction": "Følgende opgaver er planlagt til at blive udført baseret på den aktuelle UTC-tid: %s.", + "NothingScheduled": "Ingen planlagte opgaver" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/de.json b/plugins/TasksTimetable/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..26bdbf84f8995ac9ff5e35f42fa8d6b2cfbfed42 --- /dev/null +++ b/plugins/TasksTimetable/lang/de.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Geplante Aufgaben", + "Introduction": "Auf Basis der aktuellen UTC-Zeit (%s) sind folgende Aufgaben zur Ausführung geplant.", + "NothingScheduled": "Keine geplanten Aufgaben" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/dv.json b/plugins/TasksTimetable/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/el.json b/plugins/TasksTimetable/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..df0a638adcacb0086b7518581cb017772c62cee4 --- /dev/null +++ b/plugins/TasksTimetable/lang/el.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Οι παρακάτω εργασίες είναι προγραμματισμένες να εκτελεστούν βάσει της τρέχουσας ώρας UTC: %s.", + "NothingScheduled": "Δεν υπάρχουν προγραμματισμένες εργασίες", + "ScheduledTasks": "Προγραμματισμένες Εργασίες" + } +} diff --git a/plugins/TasksTimetable/lang/en.json b/plugins/TasksTimetable/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..549c8eccc74a415f67473cb17189ed7ea1d6efbe --- /dev/null +++ b/plugins/TasksTimetable/lang/en.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Scheduled Tasks", + "Introduction": "The following tasks are scheduled to be executed based on the current UTC time: %s.", + "NothingScheduled": "No scheduled tasks" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/eo.json b/plugins/TasksTimetable/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/eo.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/es-ar.json b/plugins/TasksTimetable/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..0df1776bfc2dc00f4fa70f844d61dcde51913ef4 --- /dev/null +++ b/plugins/TasksTimetable/lang/es-ar.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Tareas programadas", + "Introduction": "Las siguientes tareas están programadas para ejecutarse en el hora UTC actual: %s.", + "NothingScheduled": "No hay tareas programadas" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/es.json b/plugins/TasksTimetable/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..418325191a721125fdd0fc5484a8e4a4c51b28fc --- /dev/null +++ b/plugins/TasksTimetable/lang/es.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Tareas programadas", + "Introduction": "Las siguientes tareas están agendadas para ser ejecutadas basadas en el actual horario UTC: %s.", + "NothingScheduled": "No hay tareas programadas" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/et.json b/plugins/TasksTimetable/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..cc64d48188f14b893f1238753c49dd472e9c6943 --- /dev/null +++ b/plugins/TasksTimetable/lang/et.json @@ -0,0 +1,6 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Ajastatud tegevused", + "NothingScheduled": "Ajastatud tegevusi pole" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/eu.json b/plugins/TasksTimetable/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..493ccf29f6a46cb3d015e8a2a09c201466820990 --- /dev/null +++ b/plugins/TasksTimetable/lang/eu.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Ondorengo atazak uneko UTC orduaren arabera exekutatzeko programatuta daude: %s.", + "NothingScheduled": "Ez dago programatutako atazarik", + "ScheduledTasks": "Programatutako atazak" + } +} diff --git a/plugins/TasksTimetable/lang/fa.json b/plugins/TasksTimetable/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..d79ec41feac6647ae3870bba00e84ac2bebe5d28 --- /dev/null +++ b/plugins/TasksTimetable/lang/fa.json @@ -0,0 +1,6 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "وظایف برنامه ریزی شده", + "NothingScheduled": "بدون وظایف برنامه ریزی شده" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/fi.json b/plugins/TasksTimetable/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..c738dcd8bc153a5157905d105aaf523a1d98e720 --- /dev/null +++ b/plugins/TasksTimetable/lang/fi.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Ajastetut tehtävät", + "Introduction": "Seuraavat tehtävät on ajastettu alkamaan nykyisen UTC-ajan mukaan: %s.", + "NothingScheduled": "Ei ajastettuja tehtäviä" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/fr.json b/plugins/TasksTimetable/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..0af09d2e17d79832798649f91745b1d9afcdb7cc --- /dev/null +++ b/plugins/TasksTimetable/lang/fr.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Les tâches suivantes sont planifiées pour être exécutées en fonction de l'heure actuelle en UTC : %s.", + "NothingScheduled": "Aucune tâche planifiée", + "ScheduledTasks": "Tâches planifiées" + } +} diff --git a/plugins/TasksTimetable/lang/ga.json b/plugins/TasksTimetable/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..d5e702bba34c2083bb00a3e4540489609795410b --- /dev/null +++ b/plugins/TasksTimetable/lang/ga.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Tá na tascanna seo a leanas sceidealaithe le cur i gcrích bunaithe ar an am UTC reatha: %s.", + "NothingScheduled": "Uimh tascanna sceidealta", + "ScheduledTasks": "Tascanna Sceidealta" + } +} diff --git a/plugins/TasksTimetable/lang/gl.json b/plugins/TasksTimetable/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..2fb92e1ae2c07ea5e69ed97dc1f1efca35b1c733 --- /dev/null +++ b/plugins/TasksTimetable/lang/gl.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "As seguintes tarefas están programadas para executarse en función da hora UTC actual: %s.", + "NothingScheduled": "Non hai tarefas programadas", + "ScheduledTasks": "Tarefas programadas" + } +} diff --git a/plugins/TasksTimetable/lang/gu.json b/plugins/TasksTimetable/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..fe9b3dbafd7d16a9dff2727d072129ddca0a78a3 --- /dev/null +++ b/plugins/TasksTimetable/lang/gu.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "નીચેના કાર્યો વર્તમાન UTC સમયના આધારે ચલાવવા માટે સુનિશ્ચિત થયેલ છે: %s.", + "NothingScheduled": "કોઈ સુનિશ્ચિત કાર્યો નથી", + "ScheduledTasks": "સુનિશ્ચિત કાર્યો" + } +} diff --git a/plugins/TasksTimetable/lang/he.json b/plugins/TasksTimetable/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..b7ea1fad682ce8bde5447bb185dd548e7654747d --- /dev/null +++ b/plugins/TasksTimetable/lang/he.json @@ -0,0 +1,5 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "משימות מתוזמנות" + } +} diff --git a/plugins/TasksTimetable/lang/hi.json b/plugins/TasksTimetable/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..eb558d5c52fdcb422ac50820571663a2605d50a9 --- /dev/null +++ b/plugins/TasksTimetable/lang/hi.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "नियत कार्य", + "Introduction": "निम्न कार्य वर्तमान UTC समय के आधार पर निष्पादित किया जाना निर्धारित किया जाता है: %s।", + "NothingScheduled": "कोई शेड्यूल किए गए कार्यों" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/hr.json b/plugins/TasksTimetable/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..969f5219d8f685dddbe58e191aed9039ace93ce5 --- /dev/null +++ b/plugins/TasksTimetable/lang/hr.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Sljedeći zadaci su zakazani za izvršavanje na temelju trenutačnog UTC vremena: %s.", + "NothingScheduled": "Nema zakazanih zadataka", + "ScheduledTasks": "Zakazani zadaci" + } +} diff --git a/plugins/TasksTimetable/lang/hu.json b/plugins/TasksTimetable/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/hu.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/hy.json b/plugins/TasksTimetable/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/id.json b/plugins/TasksTimetable/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..d32e1cc6cb0b0e809f46a0dd7f8a9c1d7311219a --- /dev/null +++ b/plugins/TasksTimetable/lang/id.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Tugas-tugas berikut ini dijadwalkan untuk dieksekusi berdasarkan waktu UTC saat ini: %s.", + "NothingScheduled": "Tidak ada tugas terjadwal", + "ScheduledTasks": "Tugas Terjadwal" + } +} diff --git a/plugins/TasksTimetable/lang/is.json b/plugins/TasksTimetable/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/is.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/it.json b/plugins/TasksTimetable/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..e660ca3da7b4f817124ad9d613e23cf4059aa876 --- /dev/null +++ b/plugins/TasksTimetable/lang/it.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Attività pianificate", + "Introduction": "I seguenti task sono programmati per essere eseguiti in base all'ora corrente UTC: %s.", + "NothingScheduled": "Non ci sono attività pianificate" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/ja.json b/plugins/TasksTimetable/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..003f0790466a1694f5d4642338e932b67f431844 --- /dev/null +++ b/plugins/TasksTimetable/lang/ja.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "スケジュールタスク", + "Introduction": "現在の UTC 時刻に基づいて、次のタスクが実行されるスケジュール: %s", + "NothingScheduled": "スケジュールされたタスクはありません" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/ka.json b/plugins/TasksTimetable/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/ka.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/ko.json b/plugins/TasksTimetable/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..8a3871f2bc151dcd4b0f4b04afed4f685bcb8626 --- /dev/null +++ b/plugins/TasksTimetable/lang/ko.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "다음 작업은 현재 UTC 시간을 기준으로 실행되도록 예약되어 있습니다: %s.", + "NothingScheduled": "예약된 작업 없음", + "ScheduledTasks": "예약된 작업" + } +} diff --git a/plugins/TasksTimetable/lang/ku.json b/plugins/TasksTimetable/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/ku.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/lb.json b/plugins/TasksTimetable/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/lb.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/lt.json b/plugins/TasksTimetable/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..6d343899198b179c9f226fec99ee69f55caf192a --- /dev/null +++ b/plugins/TasksTimetable/lang/lt.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Suplanuotos užduotys", + "Introduction": "Šios užduotys yra suplanuotos įvykdyti, remiantis esamu pasauliniu laiku (UTC): %s.", + "NothingScheduled": "Nėra suplanuotų užduočių" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/lv.json b/plugins/TasksTimetable/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/lv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/ms.json b/plugins/TasksTimetable/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..f4276b60ab79f74566b40e43c19b3885d4b8683a --- /dev/null +++ b/plugins/TasksTimetable/lang/ms.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Tugas berikut dijadualkan untuk dilaksanakan berdasarkan masa UTC semasa: %s.", + "NothingScheduled": "Tiada tugas yang dijadualkan", + "ScheduledTasks": "Tugas Berjadual" + } +} diff --git a/plugins/TasksTimetable/lang/nb.json b/plugins/TasksTimetable/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..5ef529ab6d36679003bd9fddbdbcfb07e2b79ed9 --- /dev/null +++ b/plugins/TasksTimetable/lang/nb.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Planlagte oppgaver", + "Introduction": "De følgende oppgavene er planlagt utført basert på gjeldende UTC-tid: %s.", + "NothingScheduled": "Ingen planlagte oppgaver" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/nl.json b/plugins/TasksTimetable/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..df847ca2778db17a527916ddaaccf7b9aec9498c --- /dev/null +++ b/plugins/TasksTimetable/lang/nl.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Geplande taken", + "Introduction": "De volgende taken zijn gepland om uitgevoerd te worden gebaseerd op de huidige UTC-tijd: %s.", + "NothingScheduled": "Geen geplande taken" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/nn.json b/plugins/TasksTimetable/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/nn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/pl.json b/plugins/TasksTimetable/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..824329b6c58eae8447a5a2655645d18a29899933 --- /dev/null +++ b/plugins/TasksTimetable/lang/pl.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Zaplanowane zadania", + "Introduction": "Następujące zadania zostały zaplanowane do wykonania (czas UTC): %s.", + "NothingScheduled": "Nie zaplanowano zadań" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/pt-br.json b/plugins/TasksTimetable/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..ad9ffb18b6fe84cad5bdcd459a6521228cb6795d --- /dev/null +++ b/plugins/TasksTimetable/lang/pt-br.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Tarefas agendadas", + "Introduction": "As seguintes tarefas estão agendadas para serem executadas com base no atual horário UTC: %s.", + "NothingScheduled": "Sem tarefas agendadas" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/pt.json b/plugins/TasksTimetable/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..20be3f119c7e9a4886ba1557c37189ba2f5cd69d --- /dev/null +++ b/plugins/TasksTimetable/lang/pt.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Tarefas agendadas", + "Introduction": "As seguintes tarefas foram agendadas para serem executadas baseadas no fuso horário UTC: %s.", + "NothingScheduled": "Sem tarefas agendadas" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/ro.json b/plugins/TasksTimetable/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..c2b107e287c317f3e98ec878636bc68fa244b953 --- /dev/null +++ b/plugins/TasksTimetable/lang/ro.json @@ -0,0 +1,6 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Activități programate", + "NothingScheduled": "Nu sunt programate sarcini" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/ru.json b/plugins/TasksTimetable/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..c184862607a9aaffebc756d381b7b1206aae2eb5 --- /dev/null +++ b/plugins/TasksTimetable/lang/ru.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Запланированные задачи", + "Introduction": "Следующие задачи запланированы для выполнения в текущее UTC время: %s.", + "NothingScheduled": "Нет запланированных задач" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/si.json b/plugins/TasksTimetable/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/sk.json b/plugins/TasksTimetable/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/sk.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/sl.json b/plugins/TasksTimetable/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/sl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/sq.json b/plugins/TasksTimetable/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..7d0c6c890d73c631f5f26d7ea76e928a5e8af62c --- /dev/null +++ b/plugins/TasksTimetable/lang/sq.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Akte të Planifikuar", + "Introduction": "Aktet vijues janë planifikuar për t’u kryer bazuar në formatin e tanishëm UTC për kohën: %s.", + "NothingScheduled": "Pa akte të planifikuar" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/sr.json b/plugins/TasksTimetable/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..5f027ecc7d8059c82dda86a171b83f59520c8428 --- /dev/null +++ b/plugins/TasksTimetable/lang/sr.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Zakazani zadaci", + "Introduction": "Sledeći zadaci su zakazani za izvršenje bazirano na tekućem UTC vremenu: %s.", + "NothingScheduled": "Nema zakazanih zadataka" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/sv.json b/plugins/TasksTimetable/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..eb7aae086bad2b66a1f2b5fad05616af6c855c27 --- /dev/null +++ b/plugins/TasksTimetable/lang/sv.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Planerade uppdrag", + "Introduction": "Följande uppdrag är schemalagda för exekvering baserat på nuvarande UTC-tiden: %s.", + "NothingScheduled": "Inga schemalagda aktiviteter" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/ta.json b/plugins/TasksTimetable/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..7becdccf3ea95349582254796bb5328ec41a9e39 --- /dev/null +++ b/plugins/TasksTimetable/lang/ta.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "தற்போதைய UTC நேரத்தின் அடிப்படையில் பின்வரும் பணிகள் செயல்படுத்த திட்டமிடப்பட்டுள்ளன: %s.", + "NothingScheduled": "திட்டமிடப்பட்ட பணிகள் இல்லை", + "ScheduledTasks": "திட்டமிடப்பட்ட பணிகள்" + } +} diff --git a/plugins/TasksTimetable/lang/te.json b/plugins/TasksTimetable/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/te.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/th.json b/plugins/TasksTimetable/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/th.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/tl.json b/plugins/TasksTimetable/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..6cfd8b8af1a8b2801e9cee12ee72bb5cd6e045c0 --- /dev/null +++ b/plugins/TasksTimetable/lang/tl.json @@ -0,0 +1,6 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "Naka-iskedyul na mga Gawain", + "NothingScheduled": "Walang mga naka-iskedyul na mga gawain" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/tr.json b/plugins/TasksTimetable/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..a71366f3d8d51adc6f397dfb1cceeeef1cec6611 --- /dev/null +++ b/plugins/TasksTimetable/lang/tr.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Şu görevler güncel UTC saatine göre çalışacak şekilde zamanlanmış: %s.", + "NothingScheduled": "Henüz bir zamanlanmış görev yok", + "ScheduledTasks": "Zamanlanmış görevler" + } +} diff --git a/plugins/TasksTimetable/lang/tzm.json b/plugins/TasksTimetable/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/tzm.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/uk.json b/plugins/TasksTimetable/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..23b7c91c6f160009257f60a30fbd74f5ec63e99a --- /dev/null +++ b/plugins/TasksTimetable/lang/uk.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "Introduction": "Наступні завдання заплановано виконувати на основі поточного часу UTC: %s.", + "NothingScheduled": "Запланованих завдань немає", + "ScheduledTasks": "Заплановані завдання" + } +} diff --git a/plugins/TasksTimetable/lang/ur.json b/plugins/TasksTimetable/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/ur.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/vi.json b/plugins/TasksTimetable/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/TasksTimetable/lang/vi.json @@ -0,0 +1 @@ +{} diff --git a/plugins/TasksTimetable/lang/zh-cn.json b/plugins/TasksTimetable/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..e3e8d6905f8cb6d55f0beb99789cef325c04b0e4 --- /dev/null +++ b/plugins/TasksTimetable/lang/zh-cn.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "计划任务", + "Introduction": "下面的任务在安排在UTC时间:%s执行。", + "NothingScheduled": "没有计划任务" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/lang/zh-tw.json b/plugins/TasksTimetable/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..58979174f161c598befbd7a6cc3ff8e639afd30c --- /dev/null +++ b/plugins/TasksTimetable/lang/zh-tw.json @@ -0,0 +1,7 @@ +{ + "TasksTimetable": { + "ScheduledTasks": "已排定任務", + "Introduction": "下列任務已排定執行於:%s。", + "NothingScheduled": "沒有排定的任務" + } +} \ No newline at end of file diff --git a/plugins/TasksTimetable/plugin.json b/plugins/TasksTimetable/plugin.json new file mode 100644 index 0000000000000000000000000000000000000000..311ccd45c38a9462617e610c4f4d9d60824d8707 --- /dev/null +++ b/plugins/TasksTimetable/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "TasksTimetable", + "version": "5.0.2", + "description": "List all maintenance tasks that are scheduled to run. Displays the task names and next execution time in a table.", + "theme": false, + "authors": [ + { + "name": "Megan Liang", + "email": "", + "homepage": "" + }, + { + "name": "Jay Deshpande", + "email": "", + "homepage": "" + }, + { + "name": "Matomo", + "email": "hello@matomo.org", + "homepage": "http://matomo.org" + } + ], + "require": { + "matomo": ">=5.0.0-b1,<6.0.0-b1" + }, + "support": { + "issues": "https://github.com/matomo-org/plugin-TasksTimetable/issues", + "forum": "https://forum.matomo.org", + "source": "https://github.com/matomo-org/plugin-TasksTimetable" + }, + "license": "GPL v3+", + "keywords": ["tasks", "scheduled", "timetable", "monitoring"], + "homepage": "https://matomo.org", + "category": "development" +} diff --git a/plugins/TasksTimetable/pull_request_template.md b/plugins/TasksTimetable/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..211089bb4ec77dcc05db341c8d1be45bae811eea --- /dev/null +++ b/plugins/TasksTimetable/pull_request_template.md @@ -0,0 +1,29 @@ +## Description + + +## Issue No + + +## Steps to Replicate the Issue +1. +2. +3. + + + +## Checklist +- [✔/✖] Tested locally or on demo2/demo3? +- [✔/✖/NA] New test case added/updated? +- [✔/✖/NA] Are all newly added texts included via translation? +- [✔/✖/NA] Are text sanitized properly? (Eg use of v-text v/s v-html for vue) +- [✔/✖/NA] Version bumped? +- [✔/✖/NA] I have understood, reviewed, and tested all AI outputs before use +- [✔/✖/NA] All AI instructions respect security, IP, and privacy rules +- [✔/✖/NA] Documentation updated? diff --git a/plugins/TasksTimetable/templates/index.twig b/plugins/TasksTimetable/templates/index.twig new file mode 100644 index 0000000000000000000000000000000000000000..d489777a6514230974ae8864c3c119d82ac9b70b --- /dev/null +++ b/plugins/TasksTimetable/templates/index.twig @@ -0,0 +1,11 @@ +{% extends 'admin.twig' %} + +{% set title %}{{ 'TasksTimetable_ScheduledTasks'|translate }}{% endset %} + +{% block content %} +
+{% endblock %} diff --git a/plugins/TasksTimetable/vue/dist/TasksTimetable.umd.js b/plugins/TasksTimetable/vue/dist/TasksTimetable.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..5f62351f0a8700fea3274bad0c42af13d15e5a6e --- /dev/null +++ b/plugins/TasksTimetable/vue/dist/TasksTimetable.umd.js @@ -0,0 +1,252 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("vue")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", ], factory); + else if(typeof exports === 'object') + exports["TasksTimetable"] = factory(require("CoreHome"), require("vue")); + else + root["TasksTimetable"] = factory(root["CoreHome"], root["Vue"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/TasksTimetable/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "TasksTable", function() { return /* reexport */ TasksTable; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--0-1!./plugins/TasksTimetable/vue/src/TasksTable/TasksTable.vue?vue&type=template&id=e30c0c60 + +var _hoisted_1 = ["innerHTML"]; + +var _hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); + +var _hoisted_3 = { + style: { + "color": "#999" + } +}; + +var _hoisted_4 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" (in "); + +var _hoisted_5 = ["innerHTML"]; + +var _hoisted_6 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(") "); + +var _hoisted_7 = { + key: 0 +}; +var _hoisted_8 = { + colspan: "2" +}; + +var _hoisted_9 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); + +var _hoisted_10 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); + +var _hoisted_11 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); + +function render(_ctx, _cache, $props, $setup, $data, $options) { + var _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + + var _directive_content_table = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("content-table"); + + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, { + "content-title": _ctx.translate('TasksTimetable_ScheduledTasks') + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(function () { + return [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", { + innerHTML: _ctx.$sanitize(_ctx.introduction) + }, null, 8, _hoisted_1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("table", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("thead", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tr", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Name')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("th", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Date')) + " in UTC timezone", 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("tbody", null, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.tasks, function (task, index) { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("tr", { + key: index + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(task.name), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(task.executionDate), 1), _hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", _hoisted_3, [_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(task.ts_difference) + }, null, 8, _hoisted_5), _hoisted_6])])]); + }), 128)), !_ctx.tasks.length ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("tr", _hoisted_7, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("td", _hoisted_8, [_hoisted_9, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TasksTimetable_NothingScheduled')), 1), _hoisted_10, _hoisted_11])])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)])], 512), [[_directive_content_table]])]; + }), + _: 1 + }, 8, ["content-title"]); +} +// CONCATENATED MODULE: ./plugins/TasksTimetable/vue/src/TasksTable/TasksTable.vue?vue&type=template&id=e30c0c60 + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--14-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--14-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--0-1!./plugins/TasksTimetable/vue/src/TasksTable/TasksTable.vue?vue&type=script&lang=ts + + +/* harmony default export */ var TasksTablevue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + currentTime: { + type: String, + required: true + }, + tasks: { + type: Array, + required: true + } + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"] + }, + directives: { + ContentTable: external_CoreHome_["ContentTable"] + }, + computed: { + introduction: function introduction() { + return Object(external_CoreHome_["translate"])('TasksTimetable_Introduction', "".concat(this.currentTime, "")); + } + } +})); +// CONCATENATED MODULE: ./plugins/TasksTimetable/vue/src/TasksTable/TasksTable.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/TasksTimetable/vue/src/TasksTable/TasksTable.vue + + + +TasksTablevue_type_script_lang_ts.render = render + +/* harmony default export */ var TasksTable = (TasksTablevue_type_script_lang_ts); +// CONCATENATED MODULE: ./plugins/TasksTimetable/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=TasksTimetable.umd.js.map \ No newline at end of file diff --git a/plugins/TasksTimetable/vue/dist/TasksTimetable.umd.min.js b/plugins/TasksTimetable/vue/dist/TasksTimetable.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..67b92d1a81331f813961886892921dbd9efc7035 --- /dev/null +++ b/plugins/TasksTimetable/vue/dist/TasksTimetable.umd.min.js @@ -0,0 +1,8 @@ +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue")):"function"===typeof define&&define.amd?define(["CoreHome"],t):"object"===typeof exports?exports["TasksTimetable"]=t(require("CoreHome"),require("vue")):e["TasksTimetable"]=t(e["CoreHome"],e["Vue"])})("undefined"!==typeof self?self:this,(function(e,t){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="plugins/TasksTimetable/vue/dist/",n(n.s="fae3")}({"19dc":function(t,n){t.exports=e},"8bbf":function(e,n){e.exports=t},fae3:function(e,t,n){"use strict";if(n.r(t),n.d(t,"TasksTable",(function(){return k})),"undefined"!==typeof window){var r=window.document.currentScript,o=r&&r.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);o&&(n.p=o[1])}var c=n("8bbf"),l=["innerHTML"],a=Object(c["createElementVNode"])("br",null,null,-1),i={style:{color:"#999"}},u=Object(c["createTextVNode"])(" (in "),s=["innerHTML"],b=Object(c["createTextVNode"])(") "),d={key:0},f={colspan:"2"},p=Object(c["createElementVNode"])("br",null,null,-1),m=Object(c["createElementVNode"])("br",null,null,-1),j=Object(c["createElementVNode"])("br",null,null,-1);function O(e,t,n,r,o,O){var y=Object(c["resolveComponent"])("ContentBlock"),T=Object(c["resolveDirective"])("content-table");return Object(c["openBlock"])(),Object(c["createBlock"])(y,{"content-title":e.translate("TasksTimetable_ScheduledTasks")},{default:Object(c["withCtx"])((function(){return[Object(c["createElementVNode"])("p",{innerHTML:e.$sanitize(e.introduction)},null,8,l),Object(c["withDirectives"])(Object(c["createElementVNode"])("table",null,[Object(c["createElementVNode"])("thead",null,[Object(c["createElementVNode"])("tr",null,[Object(c["createElementVNode"])("th",null,Object(c["toDisplayString"])(e.translate("General_Name")),1),Object(c["createElementVNode"])("th",null,Object(c["toDisplayString"])(e.translate("General_Date"))+" in UTC timezone",1)])]),Object(c["createElementVNode"])("tbody",null,[(Object(c["openBlock"])(!0),Object(c["createElementBlock"])(c["Fragment"],null,Object(c["renderList"])(e.tasks,(function(t,n){return Object(c["openBlock"])(),Object(c["createElementBlock"])("tr",{key:n},[Object(c["createElementVNode"])("td",null,Object(c["toDisplayString"])(t.name),1),Object(c["createElementVNode"])("td",null,[Object(c["createTextVNode"])(Object(c["toDisplayString"])(t.executionDate),1),a,Object(c["createElementVNode"])("span",i,[u,Object(c["createElementVNode"])("span",{innerHTML:e.$sanitize(t.ts_difference)},null,8,s),b])])])})),128)),e.tasks.length?Object(c["createCommentVNode"])("",!0):(Object(c["openBlock"])(),Object(c["createElementBlock"])("tr",d,[Object(c["createElementVNode"])("td",f,[p,Object(c["createTextVNode"])(Object(c["toDisplayString"])(e.translate("TasksTimetable_NothingScheduled")),1),m,j])]))])],512),[[T]])]})),_:1},8,["content-title"])}var y=n("19dc"),T=Object(c["defineComponent"])({props:{currentTime:{type:String,required:!0},tasks:{type:Array,required:!0}},components:{ContentBlock:y["ContentBlock"]},directives:{ContentTable:y["ContentTable"]},computed:{introduction:function(){return Object(y["translate"])("TasksTimetable_Introduction",''.concat(this.currentTime,""))}}});T.render=O;var k=T; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */}})})); +//# sourceMappingURL=TasksTimetable.umd.min.js.map \ No newline at end of file diff --git a/plugins/TasksTimetable/vue/dist/umd.metadata.json b/plugins/TasksTimetable/vue/dist/umd.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..9ecfcc0456204cf8e8e80416ebc95ef42d4fbd28 --- /dev/null +++ b/plugins/TasksTimetable/vue/dist/umd.metadata.json @@ -0,0 +1,5 @@ +{ + "dependsOn": [ + "CoreHome" + ] +} \ No newline at end of file diff --git a/plugins/TasksTimetable/vue/src/TasksTable/TasksTable.vue b/plugins/TasksTimetable/vue/src/TasksTable/TasksTable.vue new file mode 100644 index 0000000000000000000000000000000000000000..55c09ae1869074f6671d607c93d9281a23966a2a --- /dev/null +++ b/plugins/TasksTimetable/vue/src/TasksTable/TasksTable.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/plugins/TasksTimetable/vue/src/index.ts b/plugins/TasksTimetable/vue/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..39f13c339c718d578c7d84d3c48456b4a0a50fae --- /dev/null +++ b/plugins/TasksTimetable/vue/src/index.ts @@ -0,0 +1,8 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +export { default as TasksTable } from './TasksTable/TasksTable.vue'; diff --git a/plugins/TwoFactorAuth/API.php b/plugins/TwoFactorAuth/API.php new file mode 100644 index 0000000000000000000000000000000000000000..8bf38be8d2289587fa40152ef5a5db141bbe9f7c --- /dev/null +++ b/plugins/TwoFactorAuth/API.php @@ -0,0 +1,48 @@ +twoFa = $twoFa; + } + + /** + * Disables two-factor authentication for the specified user. + * + * @param string $userLogin The login of the user whose two-factor authentication should be reset. + * @param string $passwordConfirmation The current superuser's password confirmation. + * @return void + */ + public function resetTwoFactorAuth( + string $userLogin, + #[\SensitiveParameter] + string $passwordConfirmation = '' + ) { + Piwik::checkUserHasSuperUserAccess(); + + $this->confirmCurrentUserPassword($passwordConfirmation); + $this->twoFa->disable2FAforUser($userLogin); + } +} diff --git a/plugins/TwoFactorAuth/Commands/Disable2FAForUser.php b/plugins/TwoFactorAuth/Commands/Disable2FAForUser.php new file mode 100644 index 0000000000000000000000000000000000000000..8cbe5787fd0a9006e592e7cb6303f7750137abaa --- /dev/null +++ b/plugins/TwoFactorAuth/Commands/Disable2FAForUser.php @@ -0,0 +1,44 @@ +setName('twofactorauth:disable-2fa-for-user'); + $this->setDescription( + 'Disable two-factor authentication for a user. Useful if a user loses the device that was used for' + . ' two-factor authentication. After it was disabled, the user will be able to set it up again.' + ); + $this->addRequiredValueOption('login', null, 'Login of an existing user'); + } + + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + $this->checkAllRequiredOptionsAreNotEmpty(); + $login = $input->getOption('login'); + + // Note: We can't use API here, as the API method would require a password confirmation + $t2f = StaticContainer::get(TwoFactorAuthentication::class); + $t2f->disable2FAforUser($login); + + $message = sprintf('Disabled two-factor authentication for user: %s', $login); + $output->writeln($message); + + return self::SUCCESS; + } +} diff --git a/plugins/TwoFactorAuth/Controller.php b/plugins/TwoFactorAuth/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..d27e98c638c13a9fcf2660a13a29e64c84292cf8 --- /dev/null +++ b/plugins/TwoFactorAuth/Controller.php @@ -0,0 +1,377 @@ +settings = $systemSettings; + $this->recoveryCodeDao = $recoveryCodeDao; + $this->passwordVerify = $passwordVerify; + $this->twoFa = $twoFa; + $this->validator = $validator; + + parent::__construct(); + } + + public function loginTwoFactorAuth() + { + $this->validator->checkCanUseTwoFa(); + + if (!$this->validator->isCurrentUserMatchingSessionUser()) { + return $this->renderFreshLoginAfterResettingPendingTwoFactorSession(); + } + + $this->validator->check2FaEnabled(); + $this->validator->checkNotVerified2FAYet(); + + $messageNoAccess = null; + + $view = new View('@TwoFactorAuth/loginTwoFactorAuth'); + $form = new FormTwoFactorAuthCode(); + $form->removeAttribute('action'); // remove action attribute, otherwise hash part will be lost + if ($form->validate()) { + $nonce = $form->getSubmitValue('form_nonce'); + $messageNoAccess = Nonce::verifyNonceWithErrorMessage(self::LOGIN_2FA_NONCE, $nonce); + if ($nonce && $messageNoAccess === "" && $form->validate()) { + $authCode = $form->getSubmitValue('form_authcode'); + if ($authCode && is_string($authCode)) { + $authCode = str_replace('-', '', $authCode); + $authCode = strtoupper($authCode); // recovery codes are stored upper case, app codes are only numbers + $authCode = trim($authCode); + } + + if ($this->twoFa->validateAuthCode(Piwik::getCurrentUserLogin(), $authCode)) { + $sessionFingerprint = new SessionFingerprint(); + $sessionFingerprint->setTwoFactorAuthenticationVerified(Piwik::getCurrentUserLogin()); + Url::redirectToUrl(Url::getCurrentUrl()); + } else { + $messageNoAccess = Piwik::translate('TwoFactorAuth_InvalidAuthCode'); + try { + $bruteForce = StaticContainer::get('Piwik\Plugins\Login\Security\BruteForceDetection'); + if ($bruteForce->isEnabled()) { + $bruteForce->addFailedAttempt(IP::getIpFromHeader(), Piwik::getCurrentUserLogin()); + } + } catch (Exception $e) { + // ignore error eg if login plugin is disabled + } + } + } + } + $view->contactEmail = implode(',', Piwik::getContactEmailAddresses()); + $view->loginModule = Piwik::getLoginPluginName(); + $view->AccessErrorString = $messageNoAccess; + $view->addForm($form); + $this->setBasicVariablesView($view); + $view->nonce = Nonce::getNonce(self::LOGIN_2FA_NONCE); + + return $view->render(); + } + + private function renderFreshLoginAfterResettingPendingTwoFactorSession() + { + \Piwik\Plugins\Login\Controller::clearSession(); + Access::getInstance()->setSessionExpired(true); + + return StaticContainer::get(\Piwik\Plugins\Login\Controller::class)->login( + null, + Piwik::translate('General_YourSessionHasExpired') + ); + } + + public function userSettings() + { + $this->validator->checkCanUseTwoFa(); + + return $this->renderTemplate('userSettings', array( + 'isEnabled' => TwoFactorAuthentication::isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin()), + 'isForced' => $this->twoFa->isUserRequiredToHaveTwoFactorEnabled(), + 'disableNonce' => Nonce::getNonce(self::DISABLE_2FA_NONCE), + )); + } + + public function disableTwoFactorAuth() + { + $this->validator->checkCanUseTwoFa(); + $this->validator->check2FaEnabled(); + $this->validator->checkVerified2FA(); + + if ($this->twoFa->isUserRequiredToHaveTwoFactorEnabled()) { + throw new Exception('Two-factor authentication cannot be disabled as it is enforced'); + } + + $nonce = Common::getRequestVar('disableNonce', null, 'string'); + $params = array('module' => 'TwoFactorAuth', 'action' => 'disableTwoFactorAuth', 'disableNonce' => $nonce); + + if ($this->passwordVerify->requirePasswordVerifiedRecently($params)) { + Nonce::checkNonce(self::DISABLE_2FA_NONCE, $nonce); + + $this->twoFa->disable2FAforUser(Piwik::getCurrentUserLogin()); + $this->passwordVerify->forgetVerifiedPassword(); + + $container = StaticContainer::getContainer(); + $email = $container->make(TwoFactorAuthDisabledEmail::class, array( + 'login' => Piwik::getCurrentUserLogin(), + 'emailAddress' => Piwik::getCurrentUserEmail(), + )); + $email->safeSend(); + + $this->redirectToIndex('UsersManager', 'userSecurity', null, null, null, array( + 'disableNonce' => false, + )); + } + } + + private function make2faSession() + { + return new SessionNamespace('TwoFactorAuthenticator'); + } + + public function onLoginSetupTwoFactorAuth() + { + // called when 2fa is required, but user has not yet set up 2fa + + return $this->setupTwoFactorAuth($standalone = true); + } + + /** + * Action to setup two factor authentication + * + * @return string + * @throws \Exception + */ + public function setupTwoFactorAuth($standalone = false) + { + $this->validator->checkCanUseTwoFa(); + + if ($standalone) { + $view = new View('@TwoFactorAuth/setupTwoFactorAuthStandalone'); + $this->setBasicVariablesView($view); + $view->submitAction = 'onLoginSetupTwoFactorAuth'; + } else { + $view = new View('@TwoFactorAuth/setupTwoFactorAuth'); + $this->setGeneralVariablesView($view); + $view->submitAction = 'setupTwoFactorAuth'; + + $redirectParams = array('module' => 'TwoFactorAuth', 'action' => 'setupTwoFactorAuth'); + if (!$this->passwordVerify->requirePasswordVerified($redirectParams)) { + // should usually not go in here but redirect instead + throw new Exception('You have to verify your password first.'); + } + } + + $session = $this->make2faSession(); + + if (empty($session->secret)) { + $session->secret = $this->twoFa->generateSecret(); + } + + $secret = $session->secret; + $session->setExpirationSeconds(60 * 15, 'secret'); + + $authCode = Common::getRequestVar('authCode', '', 'string'); + $authCodeNonce = Common::getRequestVar('authCodeNonce', '', 'string'); + $hasSubmittedForm = !empty($authCodeNonce) || !empty($authCode); + $accessErrorString = ''; + $login = Piwik::getCurrentUserLogin(); + + if ( + !empty($secret) && !empty($authCode) + && Nonce::verifyNonce(self::AUTH_CODE_NONCE, $authCodeNonce) + ) { + if ($this->twoFa->validateAuthCodeDuringSetup(trim($authCode), $secret)) { + $this->twoFa->saveSecret($login, $secret); + $fingerprint = new SessionFingerprint(); + $fingerprint->setTwoFactorAuthenticationVerified($login); + unset($session->secret); + $this->passwordVerify->forgetVerifiedPassword(); + + Piwik::postEvent('TwoFactorAuth.enabled', array($login)); + + $container = StaticContainer::getContainer(); + $email = $container->make(TwoFactorAuthEnabledEmail::class, array( + 'login' => Piwik::getCurrentUserLogin(), + 'emailAddress' => Piwik::getCurrentUserEmail(), + )); + $email->safeSend(); + + if ($standalone) { + $this->redirectToIndex('CoreHome', 'index'); + return; + } + + $view = new View('@TwoFactorAuth/setupFinished'); + $this->setGeneralVariablesView($view); + return $view->render(); + } else { + $accessErrorString = Piwik::translate('TwoFactorAuth_WrongAuthCodeTryAgain'); + } + } elseif (!$standalone) { + // the user has not posted the form... at least not with a valid nonce... we make sure the password verify + // is valid for at least another 15 minutes and if not, ask for another password confirmation to avoid + // the user may be posting a valid auth code after rendering this screen but the password verify is invalid + // by then. + $redirectParams = array('module' => 'TwoFactorAuth', 'action' => 'setupTwoFactorAuth'); + if (!$this->passwordVerify->requirePasswordVerifiedRecently($redirectParams)) { + throw new Exception('You have to verify your password first.'); + } + } + + if ( + !$this->recoveryCodeDao->getAllRecoveryCodesForLogin($login) + || (!$hasSubmittedForm && !TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($login)) + ) { + // we cannot generate new codes after form has been submitted and user is not yet using 2fa cause we would + // change recovery codes in the background without the user noticing... we cannot simply do this: + // if !getAllRecoveryCodesForLogin => createRecoveryCodesForLogin. Because it could be a security issue that + // user might start the setup but never finishes. Before setting up 2fa the first time we have to change + // the recovery codes + $this->recoveryCodeDao->createRecoveryCodesForLogin($login); + } + + $view->title = $this->settings->twoFactorAuthTitle->getValue(); + $view->description = $login; + $view->authCodeNonce = Nonce::getNonce(self::AUTH_CODE_NONCE); + $view->AccessErrorString = $accessErrorString; + $view->isAlreadyUsing2fa = TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($login); + $view->newSecret = $secret; + $view->twoFaBarCodeSetupUrl = $this->getTwoFaBarCodeSetupUrl($secret); + $view->codes = $this->recoveryCodeDao->getAllRecoveryCodesForLogin($login); + $view->standalone = $standalone; + + return $view->render(); + } + + public function showRecoveryCodes() + { + $this->validator->checkCanUseTwoFa(); + $this->validator->checkVerified2FA(); + $this->validator->check2FaEnabled(); + + $regenerateNonce = Common::getRequestVar('regenerateNonce', '', 'string', $_POST); + $postedValidNonce = !empty($regenerateNonce) && Nonce::verifyNonce( + self::REGENERATE_CODES_2FA_NONCE, + $regenerateNonce + ); + + $regenerateSuccess = false; + $regenerateError = false; + $container = StaticContainer::getContainer(); + + if ($postedValidNonce && $this->passwordVerify->hasBeenVerified()) { + $this->passwordVerify->forgetVerifiedPassword(); + $this->recoveryCodeDao->createRecoveryCodesForLogin(Piwik::getCurrentUserLogin()); + $regenerateSuccess = true; + + $email = $container->make(RecoveryCodesRegeneratedEmail::class, array( + 'login' => Piwik::getCurrentUserLogin(), + 'emailAddress' => Piwik::getCurrentUserEmail(), + )); + $email->safeSend(); + // no need to redirect as password was verified nonce + // if user has posted a valid nonce, we do not need to require password again as nonce must have been generated recent + // avoids use case where eg password verify is only valid for one more minute when opening the page but user regenerates 2min later + } elseif (!$this->passwordVerify->requirePasswordVerifiedRecently(array('module' => 'TwoFactorAuth', 'action' => 'showRecoveryCodes'))) { + // should usually not go in here but redirect instead + throw new Exception('You have to verify your password first.'); + } + + if (!$postedValidNonce && !empty($regenerateNonce)) { + $regenerateError = true; + } + + $recoveryCodes = $this->recoveryCodeDao->getAllRecoveryCodesForLogin(Piwik::getCurrentUserLogin()); + + if (!$regenerateSuccess && !$regenerateError) { + $email = $container->make(RecoveryCodesShowedEmail::class, array( + 'login' => Piwik::getCurrentUserLogin(), + 'emailAddress' => Piwik::getCurrentUserEmail(), + )); + $email->safeSend(); + } + + return $this->renderTemplate('showRecoveryCodes', array( + 'codes' => $recoveryCodes, + 'regenerateNonce' => Nonce::getNonce(self::REGENERATE_CODES_2FA_NONCE), + 'regenerateError' => $regenerateError, + 'regenerateSuccess' => $regenerateSuccess, + )); + } + + private function getTwoFaBarCodeSetupUrl( + #[\SensitiveParameter] + $secret + ) { + $title = $this->settings->twoFactorAuthTitle->getValue(); + $descr = Piwik::getCurrentUserLogin(); + + $url = 'otpauth://totp/' . urlencode($descr) . '?secret=' . $secret; + if (isset($title)) { + $url .= '&issuer=' . urlencode($title); + } + + return $url; + } + + protected function getQRUrl($description, $title) + { + return sprintf('index.php?module=TwoFactorAuth&action=showQrCode&cb=%s&title=%s&descr=%s', Common::getRandomString(8), urlencode($title), urlencode($description)); + } +} diff --git a/plugins/TwoFactorAuth/Dao/RecoveryCodeDao.php b/plugins/TwoFactorAuth/Dao/RecoveryCodeDao.php new file mode 100644 index 0000000000000000000000000000000000000000..39ff046ef31579f1d65152cedaeb51dea81385a4 --- /dev/null +++ b/plugins/TwoFactorAuth/Dao/RecoveryCodeDao.php @@ -0,0 +1,87 @@ +tablePrefixed = Common::prefixTable($this->table); + $this->generator = $generator; + } + + public function getPrefixedTableName() + { + return $this->tablePrefixed; + } + + public function createRecoveryCodesForLogin($login) + { + $codes = array(); + $this->deleteAllRecoveryCodesForLogin($login); + + for ($i = 0; $i < 10; $i++) { + $code = $this->generator->generateCode(); + $code = mb_strtoupper($code); + $this->insertRecoveryCode($login, $code); + $codes[] = $code; + } + return $codes; + } + + public function insertRecoveryCode($login, $recoveryCode) + { + // we do not really care about duplicates as it is very unlikely to happen, that's why we don't even use a + // unique login,recovery_code index + $sql = sprintf('INSERT INTO `%s` (`login`, `recovery_code`) VALUES(?,?)', $this->tablePrefixed); + Db::query($sql, array($login, $recoveryCode)); + } + + public function useRecoveryCode($login, $recoveryCode) + { + if ($this->deleteRecoveryCode($login, $recoveryCode)) { + return true; + } + return false; + } + + public function getAllRecoveryCodesForLogin($login) + { + $sql = sprintf('SELECT recovery_code FROM `%s` WHERE login = ?', $this->tablePrefixed); + $rows = Db::fetchAll($sql, array($login)); + $codes = array_column($rows, 'recovery_code'); + return $codes; + } + + public function deleteRecoveryCode($login, $recoveryCode) + { + $sql = sprintf('DELETE FROM `%s` WHERE login = ? and recovery_code = ?', $this->tablePrefixed); + $query = Db::query($sql, array($login, $recoveryCode)); + return $query->rowCount(); + } + + public function deleteAllRecoveryCodesForLogin($login) + { + $query = sprintf('DELETE FROM `%s` WHERE login = ?', $this->tablePrefixed); + + Db::query($query, array($login)); + } +} diff --git a/plugins/TwoFactorAuth/Dao/RecoveryCodeRandomGenerator.php b/plugins/TwoFactorAuth/Dao/RecoveryCodeRandomGenerator.php new file mode 100644 index 0000000000000000000000000000000000000000..d71bd11f8b8fda6ef12b8ba98a0525bd246c72ed --- /dev/null +++ b/plugins/TwoFactorAuth/Dao/RecoveryCodeRandomGenerator.php @@ -0,0 +1,20 @@ +index++; + return str_pad($this->index, 16, '0'); + } +} diff --git a/plugins/TwoFactorAuth/Dao/TwoFaSecretRandomGenerator.php b/plugins/TwoFactorAuth/Dao/TwoFaSecretRandomGenerator.php new file mode 100644 index 0000000000000000000000000000000000000000..caec8610555cac85eb768f7facad42dcda106e97 --- /dev/null +++ b/plugins/TwoFactorAuth/Dao/TwoFaSecretRandomGenerator.php @@ -0,0 +1,21 @@ +createSecret(16); + } +} diff --git a/plugins/TwoFactorAuth/Dao/TwoFaSecretStaticGenerator.php b/plugins/TwoFactorAuth/Dao/TwoFaSecretStaticGenerator.php new file mode 100644 index 0000000000000000000000000000000000000000..55eeee98982c4b41e46a112f96c3ad6e25d47143 --- /dev/null +++ b/plugins/TwoFactorAuth/Dao/TwoFaSecretStaticGenerator.php @@ -0,0 +1,18 @@ +addElement('text', 'form_authcode') + ->addRule( + 'required', + Piwik::translate('General_Required', 'Authentication code') + ); + + $this->addElement('hidden', 'form_nonce'); + + $this->addElement('submit', 'submit'); + } +} diff --git a/plugins/TwoFactorAuth/SystemSettings.php b/plugins/TwoFactorAuth/SystemSettings.php new file mode 100644 index 0000000000000000000000000000000000000000..16cdee8a071347b1b0ae0aedfc9e5eaeadee2a2d --- /dev/null +++ b/plugins/TwoFactorAuth/SystemSettings.php @@ -0,0 +1,61 @@ +twoFactorAuthRequired = $this->createRequire2FA(); + $this->twoFactorAuthTitle = $this->create2FATitle(); + } + + private function createRequire2FA() + { + $setting = $this->makeSetting('twoFactorAuthRequired', $default = false, FieldConfig::TYPE_BOOL, function (FieldConfig $field) { + $field->title = Piwik::translate('TwoFactorAuth_RequireTwoFAForAll'); + $field->description = Piwik::translate('TwoFactorAuth_RequireTwoFAForAllInformation'); + $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX; + + $isWritable = defined('PIWIK_TEST_MODE') || TwoFactorAuthentication::isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin()); + if (!$isWritable) { + $field->uiControlAttributes = ['disabled' => 'disabled']; + } + }); + + return $setting; + } + + private function create2FATitle() + { + $default = 'Analytics - ' . Url::getCurrentHost(''); + if (Plugin\Manager::getInstance()->isPluginActivated('WhiteLabel')) { + $default = 'Matomo ' . $default; + } + return $this->makeSetting('twoFactorAuthName', $default, FieldConfig::TYPE_STRING, function (FieldConfig $field) { + $field->title = 'Two-factor authentication title'; + $field->uiControl = FieldConfig::UI_CONTROL_TEXT; + $field->description = 'The name of the title to display that will be displayed in the Authenticator app.'; + }); + } +} diff --git a/plugins/TwoFactorAuth/Tasks.php b/plugins/TwoFactorAuth/Tasks.php new file mode 100644 index 0000000000000000000000000000000000000000..84bad73b097feffcdef8ecc84843ce36a77f7da9 --- /dev/null +++ b/plugins/TwoFactorAuth/Tasks.php @@ -0,0 +1,33 @@ +twoFa = $twoFa; + } + + public function schedule() + { + $this->daily('cleanupTwoFaCodesUsedRecently'); + } + + public function cleanupTwoFaCodesUsedRecently() + { + $this->twoFa->cleanupTwoFaCodesUsedRecently(); + } +} diff --git a/plugins/TwoFactorAuth/TwoFactorAuth.php b/plugins/TwoFactorAuth/TwoFactorAuth.php new file mode 100644 index 0000000000000000000000000000000000000000..878799235742a979134c5a094349cf36a162b3cb --- /dev/null +++ b/plugins/TwoFactorAuth/TwoFactorAuth.php @@ -0,0 +1,354 @@ + array('function' => 'onRequestDispatch', 'after' => true), + 'AssetManager.getJavaScriptFiles' => 'getJsFiles', + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'API.UsersManager.deleteUser.end' => 'deleteRecoveryCodes', + 'API.UsersManager.createAppSpecificTokenAuth.end' => 'onCreateAppSpecificTokenAuth', + 'Request.dispatch.end' => array('function' => 'onRequestDispatchEnd', 'after' => true), + 'Template.userSecurity.afterPassword' => 'render2FaUserSettings', + 'Login.authenticate.processSuccessfulSession.end' => 'onSuccessfulSession', + 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + ); + } + + public function getClientSideTranslationKeys(&$translations) + { + $translations[] = 'TwoFactorAuth_WarningChangingConfiguredDevice'; + $translations[] = 'TwoFactorAuth_SetupIntroFollowSteps'; + $translations[] = 'TwoFactorAuth_StepX'; + $translations[] = 'TwoFactorAuth_RecoveryCodes'; + $translations[] = 'TwoFactorAuth_RecoveryCodesExplanation'; + $translations[] = 'TwoFactorAuth_RecoveryCodesSecurity'; + $translations[] = 'TwoFactorAuth_RecoveryCodesAllUsed'; + $translations[] = 'General_Download'; + $translations[] = 'General_Print'; + $translations[] = 'General_Copy'; + $translations[] = 'General_Continue'; + $translations[] = 'TwoFactorAuth_SetupBackupRecoveryCodes'; + $translations[] = 'General_Next'; + $translations[] = 'TwoFactorAuth_SetupAuthenticatorOnDeviceStep1'; + $translations[] = 'General_Or'; + $translations[] = 'TwoFactorAuth_ConfirmSetup'; + $translations[] = 'TwoFactorAuth_VerifyAuthCodeIntro'; + $translations[] = 'TwoFactorAuth_AuthenticationCode'; + $translations[] = 'TwoFactorAuth_VerifyAuthCodeHelp'; + $translations[] = 'General_Confirm'; + $translations[] = 'TwoFactorAuth_SetupAuthenticatorOnDeviceStep2ShowCodes'; + $translations[] = 'TwoFactorAuth_ShowCodes'; + $translations[] = 'TwoFactorAuth_DontHaveOTPApp'; + $translations[] = 'TwoFactorAuth_ShowCodeModalInstructions1'; + $translations[] = 'TwoFactorAuth_ShowCodeModalInstructions2'; + $translations[] = 'TwoFactorAuth_ShowCodeModalInstructions3'; + $translations[] = 'TwoFactorAuth_SetupAuthenticatorOnDevice'; + $translations[] = 'TwoFactorAuth_TwoFactorAuthentication'; + $translations[] = 'General_Error'; + $translations[] = 'TwoFactorAuth_VerifyIdentifyExplanation'; + $translations[] = 'TwoFactorAuth_DontHaveYourMobileDevice'; + $translations[] = 'TwoFactorAuth_EnterRecoveryCodeInstead'; + $translations[] = 'TwoFactorAuth_NotPossibleToLogIn'; + $translations[] = 'TwoFactorAuth_LostAuthenticationDevice'; + $translations[] = 'TwoFactorAuth_AskSuperUserResetAuthenticationCode'; + $translations[] = 'General_Logout'; + $translations[] = 'TwoFactorAuth_Your2FaAuthSecret'; + $translations[] = 'TwoFactorAuth_GenerateNewRecoveryCodes'; + $translations[] = 'TwoFactorAuth_GenerateNewRecoveryCodesInfo'; + $translations[] = 'TwoFactorAuth_RecoveryCodesRegenerated'; + $translations[] = 'General_ExceptionSecurityCheckFailed'; + $translations[] = 'TwoFactorAuth_TwoFAShort'; + $translations[] = 'TwoFactorAuth_TwoFactorAuthenticationIntro'; + $translations[] = 'TwoFactorAuth_TwoFactorAuthenticationIsEnabled'; + $translations[] = 'TwoFactorAuth_TwoFactorAuthenticationRequired'; + $translations[] = 'TwoFactorAuth_ConfigureDifferentDevice'; + $translations[] = 'TwoFactorAuth_DisableTwoFA'; + $translations[] = 'TwoFactorAuth_ShowRecoveryCodes'; + $translations[] = 'TwoFactorAuth_TwoFactorAuthenticationIsDisabled'; + $translations[] = 'TwoFactorAuth_EnableTwoFA'; + $translations[] = 'TwoFactorAuth_ConfirmDisableTwoFA'; + $translations[] = 'General_Yes'; + $translations[] = 'General_No'; + $translations[] = 'TwoFactorAuth_RequiredToSetUpTwoFactorAuthentication'; + $translations[] = 'TwoFactorAuth_SetUpTwoFactorAuthentication'; + $translations[] = 'TwoFactorAuth_SetupFinishedTitle'; + $translations[] = 'TwoFactorAuth_SetupFinishedSubtitle'; + $translations[] = 'General_Continue'; + $translations[] = 'TwoFactorAuth_Verify'; + } + + public function getStylesheetFiles(&$stylesheets) + { + $stylesheets[] = "plugins/TwoFactorAuth/stylesheets/twofactorauth.less"; + } + + public function getJsFiles(&$jsFiles) + { + $jsFiles[] = "node_modules/qrcodejs2/qrcode.min.js"; + } + + public function deleteRecoveryCodes($returnedValue, $params) + { + $model = new Model(); + if ( + !empty($params['parameters']['userLogin']) + && !$model->userExists($params['parameters']['userLogin']) + ) { + // we delete only if the deletion was really successful + $dao = StaticContainer::get(RecoveryCodeDao::class); + $dao->deleteAllRecoveryCodesForLogin($params['parameters']['userLogin']); + } + } + + public function render2FaUserSettings(&$out) + { + $validator = $this->getValidator(); + if ($validator->canUseTwoFa()) { + $content = FrontController::getInstance()->dispatch('TwoFactorAuth', 'userSettings'); + if (!empty($content)) { + $out .= $content; + } + } + } + + public function onSuccessfulSession($login) + { + if (Piwik::getModule() === 'Login' && Piwik::getAction() === 'logme' && $login) { + // we allow user to send an "authCode" along logme to directly log in... if not, user will see the + // auth code verification screen after logme + $authCode = Common::getRequestVar('authCode', '', 'string'); + $twoFa = $this->getTwoFa(); + + if ($authCode && TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($login)) { + if ($twoFa->validateAuthCode($login, $authCode)) { + $sessionFingerprint = new SessionFingerprint(); + $sessionFingerprint->setTwoFactorAuthenticationVerified($login); + } else { + $this->recordFailedTwoFactorAttempt($login); + } + } + } + } + + private function getTwoFa() + { + return StaticContainer::get(TwoFactorAuthentication::class); + } + + private function getValidator() + { + return StaticContainer::get(Validator::class); + } + + private function isValidTokenAuth( + #[\SensitiveParameter] + $tokenAuth + ) { + $model = new Model(); + $user = $model->getUserByTokenAuth($tokenAuth, true); + return !empty($user); + } + + public function onCreateAppSpecificTokenAuth($returnedValue, array $params): void + { + if (!SettingsPiwik::isMatomoInstalled()) { + return; + } + + if (!empty($returnedValue) && !empty($params['parameters']['userLogin'])) { + $login = UserLoginHelper::findCanonicalLoginByLoginOrEmail($params['parameters']['userLogin']); + $twoFa = $this->getTwoFa(); + + if (!empty($login) && TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($login) && $this->isValidTokenAuth($returnedValue)) { + $authCode = \Piwik\Request::fromRequest()->getStringParameter('authCode', ''); + // we only return an error when the login/password combo was correct. otherwise you could brute force + // auth tokens + if (!$authCode) { + $this->recordFailedTwoFactorAttempt($login); + throw new NoPrivilegesException(Piwik::translate('TwoFactorAuth_MissingAuthCodeAPI')); + } + if (!$twoFa->validateAuthCode($login, $authCode)) { + $this->recordFailedTwoFactorAttempt($login); + throw new NoPrivilegesException(Piwik::translate('TwoFactorAuth_InvalidAuthCode')); + } + } elseif ( + $twoFa->isUserRequiredToHaveTwoFactorEnabled() + && (empty($login) || !TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($login)) + ) { + throw new Exception(Piwik::translate('TwoFactorAuth_RequiredAuthCodeNotConfiguredAPI')); + } + } + } + + /** + * Records a failed login attempt so the existing brute-force protection can engage. + */ + private function recordFailedTwoFactorAttempt(string $login): void + { + try { + $bruteForce = StaticContainer::get('Piwik\Plugins\Login\Security\BruteForceDetection'); + if ($bruteForce->isEnabled()) { + $bruteForce->addFailedAttempt(IP::getIpFromHeader(), $login); + } + } catch (Exception $e) { + // ignore error eg if login plugin is disabled + } + } + + public function onRequestDispatch(&$module, &$action, $parameters) + { + $validator = $this->getValidator(); + if (!$validator->canUseTwoFa()) { + return; + } + + if ($module === 'Proxy') { + return false; + } + + if (!$this->requiresAuth($module, $action, $parameters)) { + return; + } + + if ( + $validator->hasPendingSessionTwoFactorAuthentication() + && !$validator->isCurrentUserMatchingSessionUser() + ) { + if (!Request::isRootRequestApiRequest()) { + $this->resetPendingTwoFactorSessionAndRequireFreshLogin($module, $action); + } elseif (StaticContainer::get(AuthenticationToken::class)->isSessionToken()) { + throw new Exception(Piwik::translate('General_YourSessionHasExpired')); + } + return; + } + + $twoFa = $this->getTwoFa(); + + $isUsing2FA = TwoFactorAuthentication::isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin()); + if ($isUsing2FA && Session::isStarted()) { + $sessionFingerprint = new SessionFingerprint(); + if (!$sessionFingerprint->hasVerifiedTwoFactor()) { + if (!Request::isRootRequestApiRequest()) { + $module = 'TwoFactorAuth'; + $action = 'loginTwoFactorAuth'; + } elseif (StaticContainer::get(AuthenticationToken::class)->isSessionToken()) { + // don't allow API requests with session auth if 2fa code hasn't been verified. + throw new Exception(Piwik::translate('General_YourSessionHasExpired')); + } + } + } elseif (!$isUsing2FA && $twoFa->isUserRequiredToHaveTwoFactorEnabled()) { + $module = 'TwoFactorAuth'; + $action = 'onLoginSetupTwoFactorAuth'; + } + } + + private function resetPendingTwoFactorSessionAndRequireFreshLogin(&$module, &$action) + { + LoginController::clearSession(); + Access::getInstance()->setSessionExpired(true); + + $module = Piwik::getLoginPluginName(); + $action = 'login'; + } + + private function requiresAuth($module, $action, $parameters) + { + if ($module === 'TwoFactorAuth' && $action === 'showQrCode') { + return false; + } + + if ($module === 'CorePluginsAdmin' && strtolower($action) === 'safemode') { + return false; + } + + if ($module === 'CoreUpdater' && $action !== 'newVersionAvailable' && $action !== 'oneClickUpdate') { + return false; + } + + if ($module === Piwik::getLoginPluginName() && $action === 'logout') { + return false; + } + + $auth = StaticContainer::get('Piwik\Auth'); + if (!$auth->getLogin() && method_exists($auth, 'getTokenAuth') && $auth->getTokenAuth()) { + // when authenticated by token only, we do not require 2fa + // needed eg for rendering exported widgets authenticated by token + return false; + } + + $requiresAuth = true; + Piwik::postEvent('TwoFactorAuth.requiresTwoFactorAuthentication', array(&$requiresAuth, $module, $action, $parameters)); + + return $requiresAuth; + } + + public function onRequestDispatchEnd(&$result, $module, $action, $parameters) + { + $validator = $this->getValidator(); + if (!$validator->canUseTwoFa()) { + return; + } + + if (!$this->requiresAuth($module, $action, $parameters)) { + return; + } + + $twoFa = $this->getTwoFa(); + + $isUsing2FA = TwoFactorAuthentication::isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin()); + if ($isUsing2FA && !Request::isRootRequestApiRequest()) { + $sessionFingerprint = new SessionFingerprint(); + if (!$sessionFingerprint->hasVerifiedTwoFactor()) { + $result = $this->removeTokenFromOutput($result); + } + } elseif (!$isUsing2FA && $twoFa->isUserRequiredToHaveTwoFactorEnabled()) { + $result = $this->removeTokenFromOutput($result); + } + } + + private function removeTokenFromOutput($output) + { + if (empty($output)) { + return $output; + } + + $token = Piwik::getCurrentUserTokenAuth(); + // make sure to not leak the token... otherwise someone could log in using someone's credentials... + // and then maybe in the auth screen look into the DOM to find the token... and then bypass the + // auth code using API + return str_replace($token, md5('') . '2fareplaced', $output); + } +} diff --git a/plugins/TwoFactorAuth/TwoFactorAuthentication.php b/plugins/TwoFactorAuth/TwoFactorAuthentication.php new file mode 100644 index 0000000000000000000000000000000000000000..a33b581e78dfa78e930790ee01a9b2a19d431ad7 --- /dev/null +++ b/plugins/TwoFactorAuth/TwoFactorAuthentication.php @@ -0,0 +1,277 @@ +settings = $systemSettings; + $this->recoveryCodeDao = $recoveryCodeDao; + $this->secretGenerator = $twoFaSecretRandomGenerator; + } + + private static function getUserModel(): Model + { + return new Model(); + } + + /** + * @return string + */ + public function generateSecret() + { + return $this->secretGenerator->generateSecret(); + } + + /** + * @param string $login + * @return void + */ + public function disable2FAforUser($login) + { + $this->saveSecret($login, ''); + $this->recoveryCodeDao->deleteAllRecoveryCodesForLogin($login); + + Piwik::postEvent('TwoFactorAuth.disabled', array($login)); + } + + private static function isAnonymous(string $login): bool + { + return strtolower($login) === 'anonymous'; + } + + /** + * @param string $login + * @param string $secret + * @return void + */ + public function saveSecret( + $login, + #[\SensitiveParameter] + $secret + ) { + if (self::isAnonymous($login)) { + throw new Exception('Anonymous cannot use two-factor authentication'); + } + + if (!empty($secret) && !$this->recoveryCodeDao->getAllRecoveryCodesForLogin($login)) { + // ensures the user has seen and ideally backuped the recovery codes... we don't create them here on demand + throw new Exception('Cannot enable two-factor authentication, no recovery codes have been created'); + } + + $model = self::getUserModel(); + $model->updateUserFields($login, array('twofactor_secret' => $secret)); + } + + /** + * @return bool + */ + public function isUserRequiredToHaveTwoFactorEnabled() + { + return !!$this->settings->twoFactorAuthRequired->getValue(); + } + + /** + * @param string $login + * @return bool + */ + public static function isUserUsingTwoFactorAuthentication($login) + { + if (self::isAnonymous($login)) { + return false; // not possible to use auth code with anonymous + } + + $user = self::getUser($login); + return !empty($user['twofactor_secret']); + } + + private static function getUser(string $login): array + { + $model = self::getUserModel(); + return $model->getUser($login); + } + + private function wasTwoFaCodeUsedRecently( + string $login, + #[\SensitiveParameter] + string $authCode + ): bool { + $time = Option::get($this->gettwoFaCodeUsedKey($login, $authCode)); + if (empty($time)) { + return false; + } + $blockWindowSeconds = 60 * self::BLOCK_TWOFA_CODE_MINUTES; + return (int)$time >= time() - $blockWindowSeconds; + } + + private function gettwoFaCodeUsedKey( + string $login, + #[\SensitiveParameter] + string $authCode + ): string { + return self::OPTION_PREFIX_TWO_FA_CODE_USED . md5($login . $authCode . SettingsPiwik::getSalt()); + } + + private function setTwoFaCodeWasUsed( + string $login, + #[\SensitiveParameter] + string $authCode + ): bool { + $table = Common::prefixTable('option'); + $optionName = $this->gettwoFaCodeUsedKey($login, $authCode); + $currentTime = time(); + $bind = [$optionName, $currentTime, 0]; + try { + Db::query('INSERT INTO `' . $table . '` (option_name, option_value, autoload) VALUES (?, ?, ?) ', $bind); + Option::clearCachedOption($optionName); + return true; + } catch (Exception $e) { + // when 2 processes try to insert at same time this can fail with duplicate key. + // if the record is older than the block window, refresh the timestamp and allow usage again. + $blockWindowSeconds = 60 * self::BLOCK_TWOFA_CODE_MINUTES; + $staleThreshold = $currentTime - $blockWindowSeconds; + $updateBind = [$currentTime, $optionName, $staleThreshold]; + + $result = Db::query( + 'UPDATE `' . $table . '` SET option_value = ?, autoload = 0 WHERE option_name = ? AND CAST(option_value AS UNSIGNED) <= ?', + $updateBind + ); + + $didUpdateRecord = (bool) Db::get()->rowCount($result); + if ($didUpdateRecord) { + Option::clearCachedOption($optionName); + } + + return $didUpdateRecord; + } + } + + /** + * @return void + */ + public function cleanupTwoFaCodesUsedRecently() + { + $values = Option::getLike(TwoFactorAuthentication::OPTION_PREFIX_TWO_FA_CODE_USED . '%'); + if (!empty($values)) { + foreach ($values as $optionName => $timeCodeWasUsed) { + $fiveMinutesAgo = time() - (60 * self::BLOCK_TWOFA_CODE_MINUTES); + if ($timeCodeWasUsed < $fiveMinutesAgo) { + // delete any entry created more than 5 min ago + Option::delete($optionName); + } + } + } + } + + /** + * @param string $login + * @param string $authCode + * @return bool + */ + public function validateAuthCode( + $login, + #[\SensitiveParameter] + $authCode + ) { + if (!self::isUserUsingTwoFactorAuthentication($login)) { + return false; + } + + $user = self::getUser($login); + + if (!is_string($authCode)) { + return false; + } + + if ($this->wasTwoFaCodeUsedRecently($user['login'], $authCode)) { + return false; + } + + if (!$this->setTwoFaCodeWasUsed($user['login'], $authCode)) { + return false; + } + + if ( + !empty($user['twofactor_secret']) + && $this->validateAuthCodeDuringSetup($authCode, $user['twofactor_secret']) + ) { + return true; + } + + if ($this->recoveryCodeDao->useRecoveryCode($user['login'], $authCode)) { + return true; + } + + return false; + } + + /** + * @param string $authCode + * @param string $secret + * @return bool + */ + public function validateAuthCodeDuringSetup( + #[\SensitiveParameter] + $authCode, + #[\SensitiveParameter] + $secret + ) { + $twoFactorAuth = $this->makeAuthenticator(); + + if (!empty($secret) && $twoFactorAuth->verifyCode($secret, $authCode, 2)) { + return true; + } + return false; + } + + private function makeAuthenticator(): \TwoFactorAuthenticator + { + return new \TwoFactorAuthenticator(); + } +} diff --git a/plugins/TwoFactorAuth/Validator.php b/plugins/TwoFactorAuth/Validator.php new file mode 100644 index 0000000000000000000000000000000000000000..035dba2e35cb7677c1365f6f69e9e9ab76c52454 --- /dev/null +++ b/plugins/TwoFactorAuth/Validator.php @@ -0,0 +1,135 @@ +twoFa = $twoFactorAuthentication; + } + + public function canUseTwoFa() + { + if (Common::isPhpCliMode() && (!defined('PIWIK_TEST_MODE') || !PIWIK_TEST_MODE)) { + // eg when archiving or executing other commands + return false; + } + + if (!SettingsPiwik::isMatomoInstalled()) { + return false; + } + + return !Piwik::isUserIsAnonymous(); + } + + public function checkCanUseTwoFa() + { + Piwik::checkUserIsNotAnonymous(); + + if (!SettingsPiwik::isMatomoInstalled()) { + throw new NotYetInstalledException('Matomo is not set up yet'); + } + } + + public function check2FaIsRequired() + { + if (!$this->twoFa->isUserRequiredToHaveTwoFactorEnabled()) { + throw new Exception('not available'); + } + } + + public function check2FaEnabled() + { + if (!TwoFactorAuthentication::isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin())) { + throw new Exception('not available'); + } + } + + public function check2FaNotEnabled() + { + if (TwoFactorAuthentication::isUserUsingTwoFactorAuthentication(Piwik::getCurrentUserLogin())) { + throw new Exception('not available'); + } + } + + public function checkVerified2FA() + { + $sessionFingerprint = $this->getSessionFingerPrint(); + if (!$sessionFingerprint->hasVerifiedTwoFactor()) { + throw new Exception('not available'); + } + } + + public function checkNotVerified2FAYet() + { + $sessionFingerprint = $this->getSessionFingerPrint(); + if ($sessionFingerprint->hasVerifiedTwoFactor()) { + throw new Exception('not available'); + } + } + + public function getSessionUser() + { + $sessionFingerprint = $this->getSessionFingerPrint(); + return $sessionFingerprint->getUser(); + } + + public function isCurrentUserMatchingSessionUser() + { + $sessionUser = $this->getSessionUser(); + if (empty($sessionUser)) { + return false; + } + + return $sessionUser === Piwik::getCurrentUserLogin(); + } + + public function checkCurrentUserMatchesSessionUser() + { + if (!$this->isCurrentUserMatchingSessionUser()) { + throw new Exception('not available'); + } + } + + public function hasPendingSessionTwoFactorAuthentication() + { + $sessionUser = $this->getSessionUser(); + if (empty($sessionUser)) { + return false; + } + + $needsTwoFactorAuthentication = TwoFactorAuthentication::isUserUsingTwoFactorAuthentication($sessionUser) + || $this->twoFa->isUserRequiredToHaveTwoFactorEnabled(); + if (!$needsTwoFactorAuthentication) { + return false; + } + + $sessionFingerprint = $this->getSessionFingerPrint(); + return !$sessionFingerprint->hasVerifiedTwoFactor(); + } + + private function getSessionFingerPrint() + { + return new SessionFingerprint(); + } +} diff --git a/plugins/TwoFactorAuth/config/config.php b/plugins/TwoFactorAuth/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/TwoFactorAuth/config/config.php @@ -0,0 +1,3 @@ + Piwik\DI::autowire('Piwik\Plugins\TwoFactorAuth\Dao\TwoFaSecretStaticGenerator'), + 'Piwik\Plugins\TwoFactorAuth\Dao\RecoveryCodeRandomGenerator' => Piwik\DI::autowire('Piwik\Plugins\TwoFactorAuth\Dao\RecoveryCodeStaticGenerator'), + 'Piwik\Plugins\TwoFactorAuth\TwoFactorAuthentication' => Piwik\DI::decorate(function ($previous) { + /** @var Piwik\Plugins\TwoFactorAuth\TwoFactorAuthentication $previous */ + + if (!\Piwik\SettingsPiwik::isMatomoInstalled()) { + return $previous; + } + + $fakeCorrectAuthCode = \Piwik\Container\StaticContainer::get('test.vars.fakeCorrectAuthCode'); + if (!empty($fakeCorrectAuthCode) && !\Piwik\Common::isPhpCliMode()) { + $staticSecret = new \Piwik\Plugins\TwoFactorAuth\Dao\TwoFaSecretStaticGenerator(); + $secret = $staticSecret->generateSecret(); + + require_once PIWIK_DOCUMENT_ROOT . '/libs/Authenticator/TwoFactorAuthenticator.php'; + $authenticator = new \TwoFactorAuthenticator(); + $_GET['authcode'] = $authenticator->getCode($secret); + $_GET['authCode'] = $_GET['authcode']; + $_POST['authCode'] = $_GET['authcode']; + $_POST['authcode'] = $_GET['authcode']; + $_REQUEST['authcode'] = $_GET['authcode']; + $_REQUEST['authCode'] = $_GET['authcode']; + } + + return $previous; + }), + 'Piwik\Plugins\TwoFactorAuth\Dao\RecoveryCodeDao' => Piwik\DI::decorate(function ($previous) { + /** @var Piwik\Plugins\TwoFactorAuth\Dao\RecoveryCodeDao $previous */ + + if (!\Piwik\SettingsPiwik::isMatomoInstalled()) { + return $previous; + } + + $restoreCodes = \Piwik\Container\StaticContainer::get('test.vars.restoreRecoveryCodes'); + if (!empty($restoreCodes)) { + // we ensure this recovery code always works for those users + foreach (array('with2FA', 'with2FADisable') as $user) { + $previous->useRecoveryCode($user, '123456'); // we are using it first to make sure there is no duplicate + $previous->insertRecoveryCode($user, '123456'); + \Piwik\Option::deleteLike(\Piwik\Plugins\TwoFactorAuth\TwoFactorAuthentication::OPTION_PREFIX_TWO_FA_CODE_USED . '%'); + } + } + + return $previous; + }), + 'Piwik\Plugins\TwoFactorAuth\SystemSettings' => Piwik\DI::decorate(function ($previous) { + /** @var Piwik\Plugins\TwoFactorAuth\SystemSettings $previous */ + if (!\Piwik\SettingsPiwik::isMatomoInstalled()) { + return $previous; + } + + Piwik\Access::doAsSuperUser(function () use ($previous) { + $requireTwoFa = \Piwik\Container\StaticContainer::get('test.vars.requireTwoFa'); + if (!empty($requireTwoFa)) { + $previous->twoFactorAuthRequired->setValue(1); + } else { + try { + $previous->twoFactorAuthRequired->setValue(0); + } catch (Exception $e) { + // may fail when matomo is trying to update or so + } + } + }); + + return $previous; + }), +); diff --git a/plugins/TwoFactorAuth/config/tracker.php b/plugins/TwoFactorAuth/config/tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/TwoFactorAuth/config/tracker.php @@ -0,0 +1,3 @@ + +{% endblock %} \ No newline at end of file diff --git a/plugins/TwoFactorAuth/templates/setupFinished.twig b/plugins/TwoFactorAuth/templates/setupFinished.twig new file mode 100644 index 0000000000000000000000000000000000000000..b9d25c8dfea1d0ef558204b89d1b4cb18b4baf53 --- /dev/null +++ b/plugins/TwoFactorAuth/templates/setupFinished.twig @@ -0,0 +1,4 @@ +{% extends 'admin.twig' %} +{% block content %} +
+{% endblock %} diff --git a/plugins/TwoFactorAuth/templates/setupTwoFactorAuth.twig b/plugins/TwoFactorAuth/templates/setupTwoFactorAuth.twig new file mode 100644 index 0000000000000000000000000000000000000000..0ca28affb2b03fc0fdd4c9aabb0b9d0357b49444 --- /dev/null +++ b/plugins/TwoFactorAuth/templates/setupTwoFactorAuth.twig @@ -0,0 +1,15 @@ +{% extends 'admin.twig' %} + +{% block content %} +
+{% endblock %} \ No newline at end of file diff --git a/plugins/TwoFactorAuth/templates/setupTwoFactorAuthStandalone.twig b/plugins/TwoFactorAuth/templates/setupTwoFactorAuthStandalone.twig new file mode 100644 index 0000000000000000000000000000000000000000..282d042bbd36cbc3fea4d1919672b16da199a461 --- /dev/null +++ b/plugins/TwoFactorAuth/templates/setupTwoFactorAuthStandalone.twig @@ -0,0 +1,15 @@ +{% extends '@Login/loginLayout.twig' %} + +{% block loginContent %} +
+{% endblock %} \ No newline at end of file diff --git a/plugins/TwoFactorAuth/templates/showRecoveryCodes.twig b/plugins/TwoFactorAuth/templates/showRecoveryCodes.twig new file mode 100644 index 0000000000000000000000000000000000000000..fed91b49fa954f9fd0b93debffcb7c542eb19a02 --- /dev/null +++ b/plugins/TwoFactorAuth/templates/showRecoveryCodes.twig @@ -0,0 +1,11 @@ +{% extends 'admin.twig' %} + +{% block content %} +
+{% endblock %} \ No newline at end of file diff --git a/plugins/TwoFactorAuth/templates/userSettings.twig b/plugins/TwoFactorAuth/templates/userSettings.twig new file mode 100644 index 0000000000000000000000000000000000000000..0018e41908d7387f6907ad23209bf08c0c1b76f1 --- /dev/null +++ b/plugins/TwoFactorAuth/templates/userSettings.twig @@ -0,0 +1,6 @@ +
diff --git a/plugins/TwoFactorAuth/vue/dist/TwoFactorAuth.umd.js b/plugins/TwoFactorAuth/vue/dist/TwoFactorAuth.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..4cea36253b67e463f09bc91d5d92329dd8bb1209 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/dist/TwoFactorAuth.umd.js @@ -0,0 +1,1072 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("Login"), require("vue"), require("CorePluginsAdmin")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", "Login", , "CorePluginsAdmin"], factory); + else if(typeof exports === 'object') + exports["TwoFactorAuth"] = factory(require("CoreHome"), require("Login"), require("vue"), require("CorePluginsAdmin")); + else + root["TwoFactorAuth"] = factory(root["CoreHome"], root["Login"], root["Vue"], root["CorePluginsAdmin"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__5b81__, __WEBPACK_EXTERNAL_MODULE__8bbf__, __WEBPACK_EXTERNAL_MODULE_a5a2__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/TwoFactorAuth/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "5b81": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__5b81__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "a5a2": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE_a5a2__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "ShowRecoveryCodes", function() { return /* reexport */ ShowRecoveryCodes; }); +__webpack_require__.d(__webpack_exports__, "ShowRecoveryCodesPage", function() { return /* reexport */ ShowRecoveryCodesPage; }); +__webpack_require__.d(__webpack_exports__, "SetupTwoFactorAuth", function() { return /* reexport */ SetupTwoFactorAuth; }); +__webpack_require__.d(__webpack_exports__, "LoginTwoFactorAuth", function() { return /* reexport */ LoginTwoFactorAuth; }); +__webpack_require__.d(__webpack_exports__, "SetupFinished", function() { return /* reexport */ SetupFinished; }); +__webpack_require__.d(__webpack_exports__, "UserSettings", function() { return /* reexport */ UserSettings; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodes.vue?vue&type=template&id=43bd6978 + +const _hoisted_1 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_3 = { + class: "alert alert-warning" +}; +const _hoisted_4 = { + key: 0, + class: "twoFactorRecoveryCodes browser-default" +}; +const _hoisted_5 = { + key: 1, + class: "alert alert-danger" +}; +const _hoisted_6 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_7 = ["value"]; +const _hoisted_8 = ["value"]; +const _hoisted_9 = ["value"]; +function render(_ctx, _cache, $props, $setup, $data, $options) { + var _ctx$codes; + const _directive_select_on_focus = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("select-on-focus"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_RecoveryCodesExplanation')), 1), _hoisted_1, _hoisted_2]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_3, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_RecoveryCodesSecurity')), 1), (_ctx$codes = _ctx.codes) !== null && _ctx$codes !== void 0 && _ctx$codes.length ? Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("ul", _hoisted_4, [(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.codes, (code, index) => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("li", { + key: index + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(code.toUpperCase().match(/.{1,4}/g).join('-')), 1); + }), 128))])), [[_directive_select_on_focus, {}]]) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_5, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_RecoveryCodesAllUsed')), 1)), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [_hoisted_6, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "button", + class: "btn backupRecoveryCode", + onClick: _cache[0] || (_cache[0] = $event => { + _ctx.downloadRecoveryCodes(); + _ctx.$emit('downloaded'); + }), + value: _ctx.translate('General_Download'), + style: { + "margin-right": "3.5px" + } + }, null, 8, _hoisted_7), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "button", + class: "btn backupRecoveryCode", + onClick: _cache[1] || (_cache[1] = $event => { + _ctx.print(); + _ctx.$emit('downloaded'); + }), + value: _ctx.translate('General_Print'), + style: { + "margin-right": "3.5px" + } + }, null, 8, _hoisted_8), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "button", + class: "btn backupRecoveryCode", + onClick: _cache[2] || (_cache[2] = $event => { + _ctx.copyRecoveryCodesToClipboard(); + _ctx.$emit('downloaded'); + }), + value: _ctx.translate('General_Copy') + }, null, 8, _hoisted_9)])]); +} +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodes.vue?vue&type=template&id=43bd6978 + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodes.vue?vue&type=script&lang=ts + + +/* harmony default export */ var ShowRecoveryCodesvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + codes: { + type: Array, + default() { + return []; + } + } + }, + directives: { + SelectOnFocus: external_CoreHome_["SelectOnFocus"] + }, + emits: ['downloaded'], + methods: { + copyRecoveryCodesToClipboard() { + const textarea = document.createElement('textarea'); + textarea.value = this.codes.join('\n'); + textarea.setAttribute('readonly', ''); + textarea.style.position = 'absolute'; + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + }, + downloadRecoveryCodes() { + external_CoreHome_["Matomo"].helper.sendContentAsDownload('analytics_recovery_codes.txt', this.codes.join('\n')); + }, + print() { + window.print(); + } + } +})); +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodes.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodes.vue + + + +ShowRecoveryCodesvue_type_script_lang_ts.render = render + +/* harmony default export */ var ShowRecoveryCodes = (ShowRecoveryCodesvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodesPage.vue?vue&type=template&id=13e8e57f + +const ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_1 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_3 = { + key: 0, + class: "alert alert-success" +}; +const ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_4 = { + key: 1, + class: "alert alert-danger" +}; +const ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_5 = ["action"]; +const ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_6 = ["value"]; +const ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_7 = ["value"]; +function ShowRecoveryCodesPagevue_type_template_id_13e8e57f_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_ShowRecoveryCodes = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ShowRecoveryCodes"); + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, { + "content-title": _ctx.contentTitle + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ShowRecoveryCodes, { + codes: _ctx.codes + }, null, 8, ["codes"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_GenerateNewRecoveryCodes')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_GenerateNewRecoveryCodesInfo')), 1), ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_1, ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_2]), _ctx.regenerateSuccess ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_3, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_RecoveryCodesRegenerated')), 1)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.regenerateError ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_ExceptionSecurityCheckFailed')), 1)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("form", { + method: "post", + action: _ctx.showRecoveryCodesLink + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "hidden", + name: "regenerateNonce", + value: _ctx.regenerateNonce + }, null, 8, ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_6), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "submit", + class: "btn", + value: _ctx.translate('TwoFactorAuth_GenerateNewRecoveryCodes') + }, null, 8, ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_7)], 8, ShowRecoveryCodesPagevue_type_template_id_13e8e57f_hoisted_5)]), + _: 1 + }, 8, ["content-title"]); +} +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodesPage.vue?vue&type=template&id=13e8e57f + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodesPage.vue?vue&type=script&lang=ts + + + +/* harmony default export */ var ShowRecoveryCodesPagevue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + codes: Array, + regenerateSuccess: Boolean, + regenerateError: Boolean, + regenerateNonce: { + type: String, + required: true + } + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"], + ShowRecoveryCodes: ShowRecoveryCodes + }, + computed: { + contentTitle() { + const part1 = Object(external_CoreHome_["translate"])('TwoFactorAuth_TwoFactorAuthentication'); + const part2 = Object(external_CoreHome_["translate"])('TwoFactorAuth_RecoveryCodes'); + return `${part1} - ${part2}`; + }, + showRecoveryCodesLink() { + return `?${external_CoreHome_["MatomoUrl"].stringify(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value), {}, { + module: 'TwoFactorAuth', + action: 'showRecoveryCodes' + }))}`; + } + } +})); +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodesPage.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodesPage.vue + + + +ShowRecoveryCodesPagevue_type_script_lang_ts.render = ShowRecoveryCodesPagevue_type_template_id_13e8e57f_render + +/* harmony default export */ var ShowRecoveryCodesPage = (ShowRecoveryCodesPagevue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/SetupTwoFactorAuth.vue?vue&type=template&id=2690bbc8 + +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_1 = { + class: "setupTwoFactorAuthentication", + ref: "root" +}; +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_2 = { + key: 0, + class: "alert alert-warning" +}; +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_3 = ["disabled"]; +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_4 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + name: "twoFactorStep2", + id: "twoFactorStep2", + style: { + "opacity": "0" + } +}, null, -1); +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_5 = ["innerHTML"]; +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_6 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_7 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + name: "twoFactorStep3", + id: "twoFactorStep3", + style: { + "opacity": "0" + } +}, null, -1); +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_8 = { + key: 0, + class: "message_container" +}; +const SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_9 = ["innerHTML"]; +const _hoisted_10 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_11 = ["action"]; +const _hoisted_12 = ["value"]; +const _hoisted_13 = ["disabled", "value"]; +const _hoisted_14 = { + class: "ui-confirm two-fa-qr-code-dialog" +}; +const _hoisted_15 = { + class: "row" +}; +const _hoisted_16 = { + class: "col l8 offset-l2 m10 offset-m1 s12 center-align" +}; +const _hoisted_17 = { + id: "qrcode", + ref: "qrcode", + title: "" +}; +const _hoisted_18 = { + class: "text-code" +}; +const _hoisted_19 = ["innerHTML"]; +const _hoisted_20 = { + class: "row" +}; +const _hoisted_21 = { + class: "col l8 offset-l2 m10 offset-m1 s12" +}; +const _hoisted_22 = ["value"]; +const _hoisted_23 = ["value"]; +function SetupTwoFactorAuthvue_type_template_id_2690bbc8_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_ShowRecoveryCodes = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ShowRecoveryCodes"); + const _component_InstallOTPApp = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("InstallOTPApp"); + const _component_Notification = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Notification"); + const _component_Field = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Field"); + const _component_MatomoDialog = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("MatomoDialog"); + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + const _directive_copy_to_clipboard = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("copy-to-clipboard"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, { + "content-title": _ctx.standalone ? _ctx.translate('TwoFactorAuth_RequiredToSetUpTwoFactorAuthentication') : _ctx.translate('TwoFactorAuth_SetUpTwoFactorAuthentication') + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_1, [_ctx.isAlreadyUsing2fa ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_WarningChangingConfiguredDevice')), 1)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_SetupIntroFollowSteps')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_StepX', 1)) + " - " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_RecoveryCodes')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ShowRecoveryCodes, { + codes: _ctx.codes, + onDownloaded: _cache[0] || (_cache[0] = $event => this.hasDownloadedRecoveryCode = true) + }, null, 8, ["codes"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", { + class: "alert alert-info backupRecoveryCodesAlert" + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_SetupBackupRecoveryCodes')), 513), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.step === 1]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + class: "btn goToStep2", + onClick: _cache[1] || (_cache[1] = $event => _ctx.nextStep()), + disabled: !_ctx.hasDownloadedRecoveryCode + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Next')), 9, SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_3), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.step === 1]])]), SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_StepX', 2)) + " - " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_SetupAuthenticatorOnDevice')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_InstallOTPApp), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", { + innerHTML: _ctx.$sanitize(_ctx.setupAuthenticatorOnDeviceStep2ShowCodes) + }, null, 8, SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_5), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_6, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("button", { + class: "btn showOtpCodes", + onClick: _cache[2] || (_cache[2] = $event => _ctx.showQrCodeModal()) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_ShowCodes')), 513), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.step >= 2]])])], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.step >= 2]]), SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_7, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_StepX', 3)) + " - " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_ConfirmSetup')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_VerifyAuthCodeIntro')), 1), _ctx.accessErrorString ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_8, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Notification, { + noclear: true, + context: "error" + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("strong", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Error')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(": "), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.accessErrorString) + }, null, 8, SetupTwoFactorAuthvue_type_template_id_2690bbc8_hoisted_9), _hoisted_10]), + _: 1 + })])])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("form", { + method: "post", + class: "setupConfirmAuthCodeForm", + autocorrect: "off", + autocapitalize: "none", + autocomplete: "off", + action: _ctx.linkTo({ + 'module': 'TwoFactorAuth', + 'action': _ctx.submitAction + }) + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Field, { + uicontrol: "text", + name: "authCode", + title: _ctx.translate('TwoFactorAuth_AuthenticationCode'), + modelValue: _ctx.authCode, + "onUpdate:modelValue": _cache[3] || (_cache[3] = $event => _ctx.authCode = $event), + maxlength: 6, + placeholder: '123456', + autocomplete: "one-time-code", + "inline-help": _ctx.translate('TwoFactorAuth_VerifyAuthCodeHelp') + }, null, 8, ["title", "modelValue", "inline-help"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "hidden", + name: "authCodeNonce", + value: _ctx.authCodeNonce + }, null, 8, _hoisted_12), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "submit", + class: "btn confirmAuthCode", + disabled: _ctx.authCode.length !== 6, + value: _ctx.translate('General_Confirm') + }, null, 8, _hoisted_13)], 8, _hoisted_11)], 512), [[external_commonjs_vue_commonjs2_vue_root_Vue_["vShow"], _ctx.step >= 3]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_MatomoDialog, { + modelValue: _ctx.qrCodeDialogVisible, + "onUpdate:modelValue": _cache[4] || (_cache[4] = $event => _ctx.qrCodeDialogVisible = $event), + onValidation: _cache[5] || (_cache[5] = $event => { + _ctx.closeQrCodeModal(); + _ctx.nextStep(); + }), + options: { + focusSelector: '.modal-action.btn' + } + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_14, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_Your2FaAuthSecret')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_15, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_16, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_ShowCodeModalInstructions1')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", _hoisted_17, null, 512)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_ShowCodeModalInstructions2')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_18, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("pre", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.newSecret), 1)])), [[_directive_copy_to_clipboard, {}]])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", { + innerHTML: _ctx.$sanitize(_ctx.showCodeModalInstructions3) + }, null, 8, _hoisted_19)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_20, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_21, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_DontHaveOTPApp')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_InstallOTPApp)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + role: "validation", + type: "button", + value: _ctx.translate('General_Continue') + }, null, 8, _hoisted_22), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + role: "no", + type: "button", + value: _ctx.translate('General_Cancel') + }, null, 8, _hoisted_23)])]), + _: 1 + }, 8, ["modelValue", "options"])], 512)]), + _: 1 + }, 8, ["content-title"]); +} +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/SetupTwoFactorAuth.vue?vue&type=template&id=2690bbc8 + +// EXTERNAL MODULE: external "CorePluginsAdmin" +var external_CorePluginsAdmin_ = __webpack_require__("a5a2"); + +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/types.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/InstallOTPApp.vue?vue&type=template&id=a1258308 + +const InstallOTPAppvue_type_template_id_a1258308_hoisted_1 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + target: "_blank", + rel: "noreferrer noopener", + href: "https://github.com/andOTP/andOTP#downloads" +}, "andOTP", -1); +const InstallOTPAppvue_type_template_id_a1258308_hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + target: "_blank", + rel: "noreferrer noopener", + href: "https://authy.com/guides/github/" +}, "Authy", -1); +const InstallOTPAppvue_type_template_id_a1258308_hoisted_3 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + target: "_blank", + rel: "noreferrer noopener", + href: "https://support.1password.com/one-time-passwords/" +}, "1Password", -1); +const InstallOTPAppvue_type_template_id_a1258308_hoisted_4 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + target: "_blank", + rel: "noreferrer noopener", + href: "https://helpdesk.lastpass.com/multifactor-authentication-options/lastpass-authenticator/" +}, "LastPass Authenticator", -1); +const InstallOTPAppvue_type_template_id_a1258308_hoisted_5 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + target: "_blank", + rel: "noreferrer noopener", + href: "https://support.google.com/accounts/answer/1066447" +}, "Google Authenticator", -1); +function InstallOTPAppvue_type_template_id_a1258308_render(_ctx, _cache, $props, $setup, $data, $options) { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_SetupAuthenticatorOnDeviceStep1')) + " ", 1), InstallOTPAppvue_type_template_id_a1258308_hoisted_1, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(", "), InstallOTPAppvue_type_template_id_a1258308_hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(", "), InstallOTPAppvue_type_template_id_a1258308_hoisted_3, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(", "), InstallOTPAppvue_type_template_id_a1258308_hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(", " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Or')) + " ", 1), InstallOTPAppvue_type_template_id_a1258308_hoisted_5, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(". ")]); +} +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/InstallOTPApp.vue?vue&type=template&id=a1258308 + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/InstallOTPApp.vue?vue&type=script&lang=ts + +/* harmony default export */ var InstallOTPAppvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({})); +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/InstallOTPApp.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/InstallOTPApp.vue + + + +InstallOTPAppvue_type_script_lang_ts.render = InstallOTPAppvue_type_template_id_a1258308_render + +/* harmony default export */ var InstallOTPApp = (InstallOTPAppvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/SetupTwoFactorAuth.vue?vue&type=script&lang=ts + + + + + + +const { + QRCode, + $ +} = window; +/* harmony default export */ var SetupTwoFactorAuthvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + isAlreadyUsing2fa: Boolean, + accessErrorString: String, + submitAction: { + type: String, + required: true + }, + authCodeNonce: { + type: String, + required: true + }, + newSecret: { + type: String, + required: true + }, + codes: Array, + twoFaBarCodeSetupUrl: { + type: String, + required: true + }, + standalone: Boolean + }, + components: { + InstallOTPApp: InstallOTPApp, + MatomoDialog: external_CoreHome_["MatomoDialog"], + ShowRecoveryCodes: ShowRecoveryCodes, + Notification: external_CoreHome_["Notification"], + Field: external_CorePluginsAdmin_["Field"], + ContentBlock: external_CoreHome_["ContentBlock"] + }, + directives: { + CopyToClipboard: external_CoreHome_["CopyToClipboard"] + }, + data() { + return { + step: 1, + hasDownloadedRecoveryCode: false, + authCode: '', + qrCodeDialogVisible: false + }; + }, + mounted() { + setTimeout(() => { + const qrcode = this.$refs.qrcode; + // eslint-disable-next-line no-new + new QRCode(qrcode, { + text: this.twoFaBarCodeSetupUrl, + width: 200, + height: 200 + }); + $(qrcode).attr('title', ''); // do not show secret on hover + if (this.accessErrorString) { + // user entered something wrong + this.step = 3; + this.scrollToEnd(); + } + }); + }, + methods: { + scrollToEnd() { + setTimeout(() => { + let id = ''; + if (this.step === 2) { + id = '#twoFactorStep2'; + } else if (this.step === 3) { + id = '#twoFactorStep3'; + } + if (id) { + external_CoreHome_["Matomo"].helper.lazyScrollTo(id, 50, true); + } + }, 50); + }, + showQrCodeModal() { + this.qrCodeDialogVisible = true; + }, + closeQrCodeModal() { + this.qrCodeDialogVisible = false; + }, + nextStep() { + this.step += 1; + if (this.step > 3) { + this.step = 3; + } + this.scrollToEnd(); + }, + linkTo(params) { + return `?${external_CoreHome_["MatomoUrl"].stringify(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value), params))}`; + } + }, + computed: { + setupAuthenticatorOnDeviceStep2ShowCodes() { + return Object(external_CoreHome_["translate"])('TwoFactorAuth_SetupAuthenticatorOnDeviceStep2ShowCodes', Object(external_CoreHome_["translate"])('TwoFactorAuth_ShowCodes')); + }, + showCodeModalInstructions3() { + return Object(external_CoreHome_["translate"])('TwoFactorAuth_ShowCodeModalInstructions3', Object(external_CoreHome_["translate"])('General_Continue')); + } + } +})); +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/SetupTwoFactorAuth.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/SetupTwoFactorAuth.vue + + + +SetupTwoFactorAuthvue_type_script_lang_ts.render = SetupTwoFactorAuthvue_type_template_id_2690bbc8_render + +/* harmony default export */ var SetupTwoFactorAuth = (SetupTwoFactorAuthvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/LoginTwoFactorAuth/LoginTwoFactorAuth.vue?vue&type=template&id=43d88a20 + +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_1 = { + class: "message_container" +}; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_2 = ["innerHTML"]; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_3 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_4 = { + class: "row" +}; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_5 = { + class: "col s12 input-field" +}; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_6 = ["value"]; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_7 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "text", + name: "form_authcode", + placeholder: "", + id: "form_authcode", + class: "input", + value: "", + size: "20", + autocorrect: "off", + autocapitalize: "none", + autocomplete: "one-time-code", + tabindex: "10", + autofocus: "autofocus" +}, null, -1); +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_8 = { + for: "form_authcode" +}; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_9 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("i", { + class: "icon-user icon" +}, null, -1); +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_10 = { + class: "row actions" +}; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_11 = { + class: "col s12" +}; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_12 = ["value"]; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_13 = ["innerHTML"]; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_14 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_15 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_16 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_17 = ["href"]; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_18 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_19 = ["href"]; +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_20 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_21 = ["href"]; +function LoginTwoFactorAuthvue_type_template_id_43d88a20_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_FormErrors = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("FormErrors"); + const _component_Notification = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Notification"); + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, { + "content-title": _ctx.translate('TwoFactorAuth_TwoFactorAuthentication') + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_FormErrors, { + "form-errors": _ctx.formData.errors + }, null, 8, ["form-errors"]), _ctx.accessErrorString ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_Notification, { + key: 0, + noclear: true, + context: "error" + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("strong", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Error')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(": "), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.accessErrorString) + }, null, 8, LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_2), LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_3]), + _: 1 + })) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("form", Object(external_commonjs_vue_commonjs2_vue_root_Vue_["mergeProps"])(_ctx.formDataAttributes, { + class: "loginTwoFaForm" + }), [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_4, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_5, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "hidden", + name: "form_nonce", + id: "login_form_nonce", + value: _ctx.formNonce + }, null, 8, LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_6), LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_7, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("label", LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_8, [LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_9, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(" " + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_AuthenticationCode')), 1)])])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_10, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_11, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + class: "submit btn btn-block", + id: "login_form_submit", + type: "submit", + value: _ctx.translate('TwoFactorAuth_Verify'), + tabindex: "100" + }, null, 8, LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_12)])])], 16), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_VerifyIdentifyExplanation')) + " ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.learnMoreText) + }, null, 8, LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_13), LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_14, LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_15, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("strong", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_DontHaveYourMobileDevice')), 1), LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_16, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: _ctx.externalRawLink('https://matomo.org/faq/how-to/faq_27248'), + rel: "noreferrer noopener", + target: "_blank" + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_EnterRecoveryCodeInstead')), 9, LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_17), LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_18, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: _ctx.mailToLink, + rel: "noreferrer noopener" + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_AskSuperUserResetAuthenticationCode')), 9, LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_19), LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_20, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: _ctx.logoutLink, + rel: "noreferrer noopener" + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Logout')), 9, LoginTwoFactorAuthvue_type_template_id_43d88a20_hoisted_21)])]), + _: 1 + }, 8, ["content-title"]); +} +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/LoginTwoFactorAuth/LoginTwoFactorAuth.vue?vue&type=template&id=43d88a20 + +// EXTERNAL MODULE: external "Login" +var external_Login_ = __webpack_require__("5b81"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/LoginTwoFactorAuth/LoginTwoFactorAuth.vue?vue&type=script&lang=ts + + + +/* harmony default export */ var LoginTwoFactorAuthvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + formData: { + type: Object, + required: true + }, + accessErrorString: String, + formNonce: { + type: String, + required: true + }, + loginModule: { + type: String, + required: true + }, + piwikUrl: String, + userLogin: { + type: String, + required: true + }, + contactEmail: { + type: String, + required: true + } + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"], + Notification: external_CoreHome_["Notification"], + FormErrors: external_Login_["FormErrors"] + }, + computed: { + learnMoreText() { + return Object(external_CoreHome_["translate"])('General_LearnMore', Object(external_CoreHome_["externalLink"])('https://matomo.org/faq/general/faq_27245'), ''); + }, + mailToLink() { + return `mailto:${this.contactEmail}?${external_CoreHome_["MatomoUrl"].stringify({ + subject: Object(external_CoreHome_["translate"])('TwoFactorAuth_NotPossibleToLogIn'), + body: Object(external_CoreHome_["translate"])('TwoFactorAuth_LostAuthenticationDevice', '\n\n', '\n\n', this.piwikUrl || '', '\n\n', this.userLogin, Object(external_CoreHome_["externalRawLink"])('https://matomo.org/faq/how-to/faq_27248')) + })}`; + }, + logoutLink() { + return `?${external_CoreHome_["MatomoUrl"].stringify({ + module: this.loginModule, + action: 'logout' + })}`; + }, + formDataAttributes() { + // convert html attribute string (ie 'a="b" d="f"') to JS object {a: "b", d: "f"} + return Object.fromEntries(this.formData.attributes.split(/\s+/g).filter(s => s).map(pair => pair.split('=')).map(([name, value]) => [name, external_CoreHome_["Matomo"].helper.htmlDecode(value.substr(1, value.length - 2))])); + } + } +})); +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/LoginTwoFactorAuth/LoginTwoFactorAuth.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/LoginTwoFactorAuth/LoginTwoFactorAuth.vue + + + +LoginTwoFactorAuthvue_type_script_lang_ts.render = LoginTwoFactorAuthvue_type_template_id_43d88a20_render + +/* harmony default export */ var LoginTwoFactorAuth = (LoginTwoFactorAuthvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/SetupFinished/SetupFinished.vue?vue&type=template&id=631d8678 + +const SetupFinishedvue_type_template_id_631d8678_hoisted_1 = { + class: "successMessage" +}; +const SetupFinishedvue_type_template_id_631d8678_hoisted_2 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const SetupFinishedvue_type_template_id_631d8678_hoisted_3 = ["href"]; +function SetupFinishedvue_type_template_id_631d8678_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, { + class: "twoFactorSetupFinished" + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", SetupFinishedvue_type_template_id_631d8678_hoisted_1, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_SetupFinishedTitle')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h3", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_SetupFinishedSubtitle')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [SetupFinishedvue_type_template_id_631d8678_hoisted_2, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + class: "btn", + href: _ctx.userSecurityLink + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Continue')), 9, SetupFinishedvue_type_template_id_631d8678_hoisted_3)])]), + _: 1 + }); +} +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupFinished/SetupFinished.vue?vue&type=template&id=631d8678 + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/SetupFinished/SetupFinished.vue?vue&type=script&lang=ts + + +/* harmony default export */ var SetupFinishedvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + components: { + ContentBlock: external_CoreHome_["ContentBlock"] + }, + computed: { + userSecurityLink() { + return `?${external_CoreHome_["MatomoUrl"].stringify(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value), {}, { + module: 'UsersManager', + action: 'userSecurity' + }))}`; + } + } +})); +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupFinished/SetupFinished.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/SetupFinished/SetupFinished.vue + + + +SetupFinishedvue_type_script_lang_ts.render = SetupFinishedvue_type_template_id_631d8678_render + +/* harmony default export */ var SetupFinished = (SetupFinishedvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/UserSettings/UserSettings.vue?vue&type=template&id=790c613d + +const UserSettingsvue_type_template_id_790c613d_hoisted_1 = ["innerHTML"]; +const UserSettingsvue_type_template_id_790c613d_hoisted_2 = { + key: 0 +}; +const UserSettingsvue_type_template_id_790c613d_hoisted_3 = { + class: "twoFaStatusEnabled" +}; +const UserSettingsvue_type_template_id_790c613d_hoisted_4 = { + key: 1 +}; +const UserSettingsvue_type_template_id_790c613d_hoisted_5 = { + key: 0 +}; +const UserSettingsvue_type_template_id_790c613d_hoisted_6 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const UserSettingsvue_type_template_id_790c613d_hoisted_7 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const UserSettingsvue_type_template_id_790c613d_hoisted_8 = ["href"]; +const UserSettingsvue_type_template_id_790c613d_hoisted_9 = { + key: 1 +}; +const UserSettingsvue_type_template_id_790c613d_hoisted_10 = ["href"]; +const UserSettingsvue_type_template_id_790c613d_hoisted_11 = ["href"]; +const UserSettingsvue_type_template_id_790c613d_hoisted_12 = ["value"]; +const UserSettingsvue_type_template_id_790c613d_hoisted_13 = ["href"]; +const UserSettingsvue_type_template_id_790c613d_hoisted_14 = { + key: 2 +}; +const UserSettingsvue_type_template_id_790c613d_hoisted_15 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const UserSettingsvue_type_template_id_790c613d_hoisted_16 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const UserSettingsvue_type_template_id_790c613d_hoisted_17 = ["href"]; +const UserSettingsvue_type_template_id_790c613d_hoisted_18 = { + id: "confirmDisable2FA", + class: "ui-confirm", + ref: "confirmDisable2FA" +}; +const UserSettingsvue_type_template_id_790c613d_hoisted_19 = ["value"]; +const UserSettingsvue_type_template_id_790c613d_hoisted_20 = ["value"]; +function UserSettingsvue_type_template_id_790c613d_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, { + "content-title": _ctx.contentTitle, + class: "userSettings2FA" + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", { + innerHTML: _ctx.$sanitize(_ctx.twoFactorAuthIntro) + }, null, 8, UserSettingsvue_type_template_id_790c613d_hoisted_1), _ctx.isEnabled ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("p", UserSettingsvue_type_template_id_790c613d_hoisted_2, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("strong", UserSettingsvue_type_template_id_790c613d_hoisted_3, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_TwoFactorAuthenticationIsEnabled')), 1)])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), _ctx.isEnabled ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("p", UserSettingsvue_type_template_id_790c613d_hoisted_4, [_ctx.isForced ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", UserSettingsvue_type_template_id_790c613d_hoisted_5, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_TwoFactorAuthenticationRequired')) + " ", 1), UserSettingsvue_type_template_id_790c613d_hoisted_6, UserSettingsvue_type_template_id_790c613d_hoisted_7, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + class: "btn btn-link enable2FaLink", + href: _ctx.setupTwoFactorAuthLink, + style: { + "margin-right": "3.5px" + } + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_ConfigureDifferentDevice')), 9, UserSettingsvue_type_template_id_790c613d_hoisted_8)])) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", UserSettingsvue_type_template_id_790c613d_hoisted_9, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + class: "btn btn-link enable2FaLink", + href: _ctx.setupTwoFactorAuthLink, + style: { + "margin-right": "3.5px" + } + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_ConfigureDifferentDevice')), 9, UserSettingsvue_type_template_id_790c613d_hoisted_10), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + href: _ctx.disableTwoFactorAuthLink, + style: { + "display": "none" + }, + id: "disable2fa" + }, "disable2fa", 8, UserSettingsvue_type_template_id_790c613d_hoisted_11), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + type: "button", + class: "btn btn-link disable2FaLink", + onClick: _cache[0] || (_cache[0] = $event => _ctx.onDisable2FaLinkClick()), + value: _ctx.translate('TwoFactorAuth_DisableTwoFA'), + style: { + "margin-right": "3.5px" + } + }, null, 8, UserSettingsvue_type_template_id_790c613d_hoisted_12)])), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + class: "btn btn-link showRecoveryCodesLink", + href: _ctx.showRecoveryCodesLink + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_ShowRecoveryCodes')), 9, UserSettingsvue_type_template_id_790c613d_hoisted_13)])) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("p", UserSettingsvue_type_template_id_790c613d_hoisted_14, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("strong", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_TwoFactorAuthenticationIsDisabled')), 1), UserSettingsvue_type_template_id_790c613d_hoisted_15, UserSettingsvue_type_template_id_790c613d_hoisted_16, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + class: "btn btn-link enable2FaLink", + href: _ctx.setupTwoFactorAuthLink + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_EnableTwoFA')), 9, UserSettingsvue_type_template_id_790c613d_hoisted_17)])), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", UserSettingsvue_type_template_id_790c613d_hoisted_18, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('TwoFactorAuth_ConfirmDisableTwoFA')), 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + role: "yes", + type: "button", + value: _ctx.translate('General_Yes') + }, null, 8, UserSettingsvue_type_template_id_790c613d_hoisted_19), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + role: "no", + type: "button", + value: _ctx.translate('General_No') + }, null, 8, UserSettingsvue_type_template_id_790c613d_hoisted_20)], 512)]), + _: 1 + }, 8, ["content-title"]); +} +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/UserSettings/UserSettings.vue?vue&type=template&id=790c613d + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/TwoFactorAuth/vue/src/UserSettings/UserSettings.vue?vue&type=script&lang=ts + + +/* harmony default export */ var UserSettingsvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + isEnabled: Boolean, + isForced: Boolean, + disableNonce: { + type: String, + required: true + } + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"] + }, + computed: { + contentTitle() { + const part1 = Object(external_CoreHome_["translate"])('TwoFactorAuth_TwoFactorAuthentication'); + const part2 = Object(external_CoreHome_["translate"])('TwoFactorAuth_TwoFAShort'); + return `${part1} (${part2})`; + }, + twoFactorAuthIntro() { + return Object(external_CoreHome_["translate"])('TwoFactorAuth_TwoFactorAuthenticationIntro', Object(external_CoreHome_["externalLink"])('https://matomo.org/faq/general/faq_27245'), ''); + }, + setupTwoFactorAuthLink() { + return `?${external_CoreHome_["MatomoUrl"].stringify(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value), {}, { + module: 'TwoFactorAuth', + action: 'setupTwoFactorAuth' + }))}`; + }, + disableTwoFactorAuthLink() { + return `?${external_CoreHome_["MatomoUrl"].stringify(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value), {}, { + module: 'TwoFactorAuth', + action: 'disableTwoFactorAuth', + disableNonce: this.disableNonce + }))}`; + }, + showRecoveryCodesLink() { + return `?${external_CoreHome_["MatomoUrl"].stringify(Object.assign(Object.assign({}, external_CoreHome_["MatomoUrl"].urlParsed.value), {}, { + module: 'TwoFactorAuth', + action: 'showRecoveryCodes' + }))}`; + } + }, + methods: { + onDisable2FaLinkClick() { + const nonce = this.disableNonce; + external_CoreHome_["Matomo"].helper.modalConfirm(this.$refs.confirmDisable2FA, { + yes() { + external_CoreHome_["MatomoUrl"].updateUrl({ + module: 'TwoFactorAuth', + action: 'disableTwoFactorAuth', + disableNonce: nonce + }); + } + }); + } + } +})); +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/UserSettings/UserSettings.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/UserSettings/UserSettings.vue + + + +UserSettingsvue_type_script_lang_ts.render = UserSettingsvue_type_template_id_790c613d_render + +/* harmony default export */ var UserSettings = (UserSettingsvue_type_script_lang_ts); +// CONCATENATED MODULE: ./plugins/TwoFactorAuth/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + + + + + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=TwoFactorAuth.umd.js.map \ No newline at end of file diff --git a/plugins/TwoFactorAuth/vue/dist/TwoFactorAuth.umd.min.js b/plugins/TwoFactorAuth/vue/dist/TwoFactorAuth.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..03d94fb5a507e6f60caec390bca5109b75666af5 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/dist/TwoFactorAuth.umd.min.js @@ -0,0 +1,14 @@ +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("Login"),require("vue"),require("CorePluginsAdmin")):"function"===typeof define&&define.amd?define(["CoreHome","Login",,"CorePluginsAdmin"],t):"object"===typeof exports?exports["TwoFactorAuth"]=t(require("CoreHome"),require("Login"),require("vue"),require("CorePluginsAdmin")):e["TwoFactorAuth"]=t(e["CoreHome"],e["Login"],e["Vue"],e["CorePluginsAdmin"])})("undefined"!==typeof self?self:this,(function(e,t,o,n){return function(e){var t={};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}return o.m=e,o.c=t,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)o.d(n,r,function(t){return e[t]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="plugins/TwoFactorAuth/vue/dist/",o(o.s="fae3")}({"19dc":function(t,o){t.exports=e},"5b81":function(e,o){e.exports=t},"8bbf":function(e,t){e.exports=o},a5a2:function(e,t){e.exports=n},fae3:function(e,t,o){"use strict";if(o.r(t),o.d(t,"ShowRecoveryCodes",(function(){return f})),o.d(t,"ShowRecoveryCodesPage",(function(){return S})),o.d(t,"SetupTwoFactorAuth",(function(){return de})),o.d(t,"LoginTwoFactorAuth",(function(){return Me})),o.d(t,"SetupFinished",(function(){return Ge})),o.d(t,"UserSettings",(function(){return dt})),"undefined"!==typeof window){var n=window.document.currentScript,r=n&&n.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);r&&(o.p=r[1])}var a=o("8bbf");const c=Object(a["createElementVNode"])("br",null,null,-1),l=Object(a["createElementVNode"])("br",null,null,-1),i={class:"alert alert-warning"},s={key:0,class:"twoFactorRecoveryCodes browser-default"},u={key:1,class:"alert alert-danger"},d=Object(a["createElementVNode"])("br",null,null,-1),b=["value"],p=["value"],m=["value"];function O(e,t,o,n,r,O){var j;const h=Object(a["resolveDirective"])("select-on-focus");return Object(a["openBlock"])(),Object(a["createElementBlock"])("div",null,[Object(a["createElementVNode"])("p",null,[Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.translate("TwoFactorAuth_RecoveryCodesExplanation")),1),c,l]),Object(a["createElementVNode"])("div",i,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_RecoveryCodesSecurity")),1),null!==(j=e.codes)&&void 0!==j&&j.length?Object(a["withDirectives"])((Object(a["openBlock"])(),Object(a["createElementBlock"])("ul",s,[(Object(a["openBlock"])(!0),Object(a["createElementBlock"])(a["Fragment"],null,Object(a["renderList"])(e.codes,(e,t)=>(Object(a["openBlock"])(),Object(a["createElementBlock"])("li",{key:t},Object(a["toDisplayString"])(e.toUpperCase().match(/.{1,4}/g).join("-")),1))),128))])),[[h,{}]]):(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",u,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_RecoveryCodesAllUsed")),1)),Object(a["createElementVNode"])("p",null,[d,Object(a["createElementVNode"])("input",{type:"button",class:"btn backupRecoveryCode",onClick:t[0]||(t[0]=t=>{e.downloadRecoveryCodes(),e.$emit("downloaded")}),value:e.translate("General_Download"),style:{"margin-right":"3.5px"}},null,8,b),Object(a["createElementVNode"])("input",{type:"button",class:"btn backupRecoveryCode",onClick:t[1]||(t[1]=t=>{e.print(),e.$emit("downloaded")}),value:e.translate("General_Print"),style:{"margin-right":"3.5px"}},null,8,p),Object(a["createElementVNode"])("input",{type:"button",class:"btn backupRecoveryCode",onClick:t[2]||(t[2]=t=>{e.copyRecoveryCodesToClipboard(),e.$emit("downloaded")}),value:e.translate("General_Copy")},null,8,m)])])}var j=o("19dc"),h=Object(a["defineComponent"])({props:{codes:{type:Array,default(){return[]}}},directives:{SelectOnFocus:j["SelectOnFocus"]},emits:["downloaded"],methods:{copyRecoveryCodesToClipboard(){const e=document.createElement("textarea");e.value=this.codes.join("\n"),e.setAttribute("readonly",""),e.style.position="absolute",e.style.left="-9999px",document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)},downloadRecoveryCodes(){j["Matomo"].helper.sendContentAsDownload("analytics_recovery_codes.txt",this.codes.join("\n"))},print(){window.print()}}});h.render=O;var f=h;const w=Object(a["createElementVNode"])("br",null,null,-1),y=Object(a["createElementVNode"])("br",null,null,-1),g={key:0,class:"alert alert-success"},v={key:1,class:"alert alert-danger"},C=["action"],N=["value"],E=["value"];function V(e,t,o,n,r,c){const l=Object(a["resolveComponent"])("ShowRecoveryCodes"),i=Object(a["resolveComponent"])("ContentBlock");return Object(a["openBlock"])(),Object(a["createBlock"])(i,{"content-title":e.contentTitle},{default:Object(a["withCtx"])(()=>[Object(a["createVNode"])(l,{codes:e.codes},null,8,["codes"]),Object(a["createElementVNode"])("h2",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_GenerateNewRecoveryCodes")),1),Object(a["createElementVNode"])("p",null,[Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.translate("TwoFactorAuth_GenerateNewRecoveryCodesInfo")),1),w,y]),e.regenerateSuccess?(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",g,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_RecoveryCodesRegenerated")),1)):Object(a["createCommentVNode"])("",!0),e.regenerateError?(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",v,Object(a["toDisplayString"])(e.translate("General_ExceptionSecurityCheckFailed")),1)):Object(a["createCommentVNode"])("",!0),Object(a["createElementVNode"])("form",{method:"post",action:e.showRecoveryCodesLink},[Object(a["createElementVNode"])("input",{type:"hidden",name:"regenerateNonce",value:e.regenerateNonce},null,8,N),Object(a["createElementVNode"])("input",{type:"submit",class:"btn",value:e.translate("TwoFactorAuth_GenerateNewRecoveryCodes")},null,8,E)],8,C)]),_:1},8,["content-title"])}var T=Object(a["defineComponent"])({props:{codes:Array,regenerateSuccess:Boolean,regenerateError:Boolean,regenerateNonce:{type:String,required:!0}},components:{ContentBlock:j["ContentBlock"],ShowRecoveryCodes:f},computed:{contentTitle(){const e=Object(j["translate"])("TwoFactorAuth_TwoFactorAuthentication"),t=Object(j["translate"])("TwoFactorAuth_RecoveryCodes");return`${e} - ${t}`},showRecoveryCodesLink(){return"?"+j["MatomoUrl"].stringify(Object.assign(Object.assign({},j["MatomoUrl"].urlParsed.value),{},{module:"TwoFactorAuth",action:"showRecoveryCodes"}))}}});T.render=V;var S=T;const A={class:"setupTwoFactorAuthentication",ref:"root"},F={key:0,class:"alert alert-warning"},k=["disabled"],_=Object(a["createElementVNode"])("a",{name:"twoFactorStep2",id:"twoFactorStep2",style:{opacity:"0"}},null,-1),D=["innerHTML"],B=Object(a["createElementVNode"])("br",null,null,-1),x=Object(a["createElementVNode"])("a",{name:"twoFactorStep3",id:"twoFactorStep3",style:{opacity:"0"}},null,-1),M={key:0,class:"message_container"},L=["innerHTML"],R=Object(a["createElementVNode"])("br",null,null,-1),q=["action"],U=["value"],P=["disabled","value"],G={class:"ui-confirm two-fa-qr-code-dialog"},H={class:"row"},$={class:"col l8 offset-l2 m10 offset-m1 s12 center-align"},I={id:"qrcode",ref:"qrcode",title:""},z={class:"text-code"},Q=["innerHTML"],X={class:"row"},Y={class:"col l8 offset-l2 m10 offset-m1 s12"},W=["value"],J=["value"];function K(e,t,o,n,r,c){const l=Object(a["resolveComponent"])("ShowRecoveryCodes"),i=Object(a["resolveComponent"])("InstallOTPApp"),s=Object(a["resolveComponent"])("Notification"),u=Object(a["resolveComponent"])("Field"),d=Object(a["resolveComponent"])("MatomoDialog"),b=Object(a["resolveComponent"])("ContentBlock"),p=Object(a["resolveDirective"])("copy-to-clipboard");return Object(a["openBlock"])(),Object(a["createBlock"])(b,{"content-title":e.standalone?e.translate("TwoFactorAuth_RequiredToSetUpTwoFactorAuthentication"):e.translate("TwoFactorAuth_SetUpTwoFactorAuthentication")},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("div",A,[e.isAlreadyUsing2fa?(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",F,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_WarningChangingConfiguredDevice")),1)):Object(a["createCommentVNode"])("",!0),Object(a["createElementVNode"])("p",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_SetupIntroFollowSteps")),1),Object(a["createElementVNode"])("h2",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_StepX",1))+" - "+Object(a["toDisplayString"])(e.translate("TwoFactorAuth_RecoveryCodes")),1),Object(a["createVNode"])(l,{codes:e.codes,onDownloaded:t[0]||(t[0]=e=>this.hasDownloadedRecoveryCode=!0)},null,8,["codes"]),Object(a["withDirectives"])(Object(a["createElementVNode"])("div",{class:"alert alert-info backupRecoveryCodesAlert"},Object(a["toDisplayString"])(e.translate("TwoFactorAuth_SetupBackupRecoveryCodes")),513),[[a["vShow"],1===e.step]]),Object(a["createElementVNode"])("p",null,[Object(a["withDirectives"])(Object(a["createElementVNode"])("button",{class:"btn goToStep2",onClick:t[1]||(t[1]=t=>e.nextStep()),disabled:!e.hasDownloadedRecoveryCode},Object(a["toDisplayString"])(e.translate("General_Next")),9,k),[[a["vShow"],1===e.step]])]),_,Object(a["withDirectives"])(Object(a["createElementVNode"])("div",null,[Object(a["createElementVNode"])("h2",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_StepX",2))+" - "+Object(a["toDisplayString"])(e.translate("TwoFactorAuth_SetupAuthenticatorOnDevice")),1),Object(a["createVNode"])(i),Object(a["createElementVNode"])("p",{innerHTML:e.$sanitize(e.setupAuthenticatorOnDeviceStep2ShowCodes)},null,8,D),Object(a["createElementVNode"])("p",null,[B,Object(a["withDirectives"])(Object(a["createElementVNode"])("button",{class:"btn showOtpCodes",onClick:t[2]||(t[2]=t=>e.showQrCodeModal())},Object(a["toDisplayString"])(e.translate("TwoFactorAuth_ShowCodes")),513),[[a["vShow"],e.step>=2]])])],512),[[a["vShow"],e.step>=2]]),x,Object(a["withDirectives"])(Object(a["createElementVNode"])("div",null,[Object(a["createElementVNode"])("h2",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_StepX",3))+" - "+Object(a["toDisplayString"])(e.translate("TwoFactorAuth_ConfirmSetup")),1),Object(a["createElementVNode"])("p",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_VerifyAuthCodeIntro")),1),e.accessErrorString?(Object(a["openBlock"])(),Object(a["createElementBlock"])("div",M,[Object(a["createElementVNode"])("div",null,[Object(a["createVNode"])(s,{noclear:!0,context:"error"},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("strong",null,Object(a["toDisplayString"])(e.translate("General_Error")),1),Object(a["createTextVNode"])(": "),Object(a["createElementVNode"])("span",{innerHTML:e.$sanitize(e.accessErrorString)},null,8,L),R]),_:1})])])):Object(a["createCommentVNode"])("",!0),Object(a["createElementVNode"])("form",{method:"post",class:"setupConfirmAuthCodeForm",autocorrect:"off",autocapitalize:"none",autocomplete:"off",action:e.linkTo({module:"TwoFactorAuth",action:e.submitAction})},[Object(a["createElementVNode"])("div",null,[Object(a["createVNode"])(u,{uicontrol:"text",name:"authCode",title:e.translate("TwoFactorAuth_AuthenticationCode"),modelValue:e.authCode,"onUpdate:modelValue":t[3]||(t[3]=t=>e.authCode=t),maxlength:6,placeholder:"123456",autocomplete:"one-time-code","inline-help":e.translate("TwoFactorAuth_VerifyAuthCodeHelp")},null,8,["title","modelValue","inline-help"])]),Object(a["createElementVNode"])("input",{type:"hidden",name:"authCodeNonce",value:e.authCodeNonce},null,8,U),Object(a["createElementVNode"])("input",{type:"submit",class:"btn confirmAuthCode",disabled:6!==e.authCode.length,value:e.translate("General_Confirm")},null,8,P)],8,q)],512),[[a["vShow"],e.step>=3]]),Object(a["createVNode"])(d,{modelValue:e.qrCodeDialogVisible,"onUpdate:modelValue":t[4]||(t[4]=t=>e.qrCodeDialogVisible=t),onValidation:t[5]||(t[5]=t=>{e.closeQrCodeModal(),e.nextStep()}),options:{focusSelector:".modal-action.btn"}},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("div",G,[Object(a["createElementVNode"])("h2",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_Your2FaAuthSecret")),1),Object(a["createElementVNode"])("div",H,[Object(a["createElementVNode"])("div",$,[Object(a["createElementVNode"])("p",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_ShowCodeModalInstructions1")),1),Object(a["createElementVNode"])("p",null,[Object(a["createElementVNode"])("span",I,null,512)]),Object(a["createElementVNode"])("p",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_ShowCodeModalInstructions2")),1),Object(a["createElementVNode"])("div",z,[Object(a["withDirectives"])((Object(a["openBlock"])(),Object(a["createElementBlock"])("pre",null,[Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.newSecret),1)])),[[p,{}]])]),Object(a["createElementVNode"])("p",{innerHTML:e.$sanitize(e.showCodeModalInstructions3)},null,8,Q)])]),Object(a["createElementVNode"])("div",X,[Object(a["createElementVNode"])("div",Y,[Object(a["createElementVNode"])("h3",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_DontHaveOTPApp")),1),Object(a["createVNode"])(i)])]),Object(a["createElementVNode"])("input",{role:"validation",type:"button",value:e.translate("General_Continue")},null,8,W),Object(a["createElementVNode"])("input",{role:"no",type:"button",value:e.translate("General_Cancel")},null,8,J)])]),_:1},8,["modelValue","options"])],512)]),_:1},8,["content-title"])}var Z=o("a5a2"); +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */const ee=Object(a["createElementVNode"])("a",{target:"_blank",rel:"noreferrer noopener",href:"https://github.com/andOTP/andOTP#downloads"},"andOTP",-1),te=Object(a["createElementVNode"])("a",{target:"_blank",rel:"noreferrer noopener",href:"https://authy.com/guides/github/"},"Authy",-1),oe=Object(a["createElementVNode"])("a",{target:"_blank",rel:"noreferrer noopener",href:"https://support.1password.com/one-time-passwords/"},"1Password",-1),ne=Object(a["createElementVNode"])("a",{target:"_blank",rel:"noreferrer noopener",href:"https://helpdesk.lastpass.com/multifactor-authentication-options/lastpass-authenticator/"},"LastPass Authenticator",-1),re=Object(a["createElementVNode"])("a",{target:"_blank",rel:"noreferrer noopener",href:"https://support.google.com/accounts/answer/1066447"},"Google Authenticator",-1);function ae(e,t,o,n,r,c){return Object(a["openBlock"])(),Object(a["createElementBlock"])("p",null,[Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.translate("TwoFactorAuth_SetupAuthenticatorOnDeviceStep1"))+" ",1),ee,Object(a["createTextVNode"])(", "),te,Object(a["createTextVNode"])(", "),oe,Object(a["createTextVNode"])(", "),ne,Object(a["createTextVNode"])(", "+Object(a["toDisplayString"])(e.translate("General_Or"))+" ",1),re,Object(a["createTextVNode"])(". ")])}var ce=Object(a["defineComponent"])({});ce.render=ae;var le=ce;const{QRCode:ie,$:se}=window;var ue=Object(a["defineComponent"])({props:{isAlreadyUsing2fa:Boolean,accessErrorString:String,submitAction:{type:String,required:!0},authCodeNonce:{type:String,required:!0},newSecret:{type:String,required:!0},codes:Array,twoFaBarCodeSetupUrl:{type:String,required:!0},standalone:Boolean},components:{InstallOTPApp:le,MatomoDialog:j["MatomoDialog"],ShowRecoveryCodes:f,Notification:j["Notification"],Field:Z["Field"],ContentBlock:j["ContentBlock"]},directives:{CopyToClipboard:j["CopyToClipboard"]},data(){return{step:1,hasDownloadedRecoveryCode:!1,authCode:"",qrCodeDialogVisible:!1}},mounted(){setTimeout(()=>{const e=this.$refs.qrcode;new ie(e,{text:this.twoFaBarCodeSetupUrl,width:200,height:200}),se(e).attr("title",""),this.accessErrorString&&(this.step=3,this.scrollToEnd())})},methods:{scrollToEnd(){setTimeout(()=>{let e="";2===this.step?e="#twoFactorStep2":3===this.step&&(e="#twoFactorStep3"),e&&j["Matomo"].helper.lazyScrollTo(e,50,!0)},50)},showQrCodeModal(){this.qrCodeDialogVisible=!0},closeQrCodeModal(){this.qrCodeDialogVisible=!1},nextStep(){this.step+=1,this.step>3&&(this.step=3),this.scrollToEnd()},linkTo(e){return"?"+j["MatomoUrl"].stringify(Object.assign(Object.assign({},j["MatomoUrl"].urlParsed.value),e))}},computed:{setupAuthenticatorOnDeviceStep2ShowCodes(){return Object(j["translate"])("TwoFactorAuth_SetupAuthenticatorOnDeviceStep2ShowCodes",Object(j["translate"])("TwoFactorAuth_ShowCodes"))},showCodeModalInstructions3(){return Object(j["translate"])("TwoFactorAuth_ShowCodeModalInstructions3",Object(j["translate"])("General_Continue"))}}});ue.render=K;var de=ue;const be={class:"message_container"},pe=["innerHTML"],me=Object(a["createElementVNode"])("br",null,null,-1),Oe={class:"row"},je={class:"col s12 input-field"},he=["value"],fe=Object(a["createElementVNode"])("input",{type:"text",name:"form_authcode",placeholder:"",id:"form_authcode",class:"input",value:"",size:"20",autocorrect:"off",autocapitalize:"none",autocomplete:"one-time-code",tabindex:"10",autofocus:"autofocus"},null,-1),we={for:"form_authcode"},ye=Object(a["createElementVNode"])("i",{class:"icon-user icon"},null,-1),ge={class:"row actions"},ve={class:"col s12"},Ce=["value"],Ne=["innerHTML"],Ee=Object(a["createElementVNode"])("br",null,null,-1),Ve=Object(a["createElementVNode"])("br",null,null,-1),Te=Object(a["createElementVNode"])("br",null,null,-1),Se=["href"],Ae=Object(a["createElementVNode"])("br",null,null,-1),Fe=["href"],ke=Object(a["createElementVNode"])("br",null,null,-1),_e=["href"];function De(e,t,o,n,r,c){const l=Object(a["resolveComponent"])("FormErrors"),i=Object(a["resolveComponent"])("Notification"),s=Object(a["resolveComponent"])("ContentBlock");return Object(a["openBlock"])(),Object(a["createBlock"])(s,{"content-title":e.translate("TwoFactorAuth_TwoFactorAuthentication")},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("div",be,[Object(a["createVNode"])(l,{"form-errors":e.formData.errors},null,8,["form-errors"]),e.accessErrorString?(Object(a["openBlock"])(),Object(a["createBlock"])(i,{key:0,noclear:!0,context:"error"},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("strong",null,Object(a["toDisplayString"])(e.translate("General_Error")),1),Object(a["createTextVNode"])(": "),Object(a["createElementVNode"])("span",{innerHTML:e.$sanitize(e.accessErrorString)},null,8,pe),me]),_:1})):Object(a["createCommentVNode"])("",!0)]),Object(a["createElementVNode"])("form",Object(a["mergeProps"])(e.formDataAttributes,{class:"loginTwoFaForm"}),[Object(a["createElementVNode"])("div",Oe,[Object(a["createElementVNode"])("div",je,[Object(a["createElementVNode"])("input",{type:"hidden",name:"form_nonce",id:"login_form_nonce",value:e.formNonce},null,8,he),fe,Object(a["createElementVNode"])("label",we,[ye,Object(a["createTextVNode"])(" "+Object(a["toDisplayString"])(e.translate("TwoFactorAuth_AuthenticationCode")),1)])])]),Object(a["createElementVNode"])("div",ge,[Object(a["createElementVNode"])("div",ve,[Object(a["createElementVNode"])("input",{class:"submit btn btn-block",id:"login_form_submit",type:"submit",value:e.translate("TwoFactorAuth_Verify"),tabindex:"100"},null,8,Ce)])])],16),Object(a["createElementVNode"])("p",null,[Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.translate("TwoFactorAuth_VerifyIdentifyExplanation"))+" ",1),Object(a["createElementVNode"])("span",{innerHTML:e.$sanitize(e.learnMoreText)},null,8,Ne),Ee,Ve,Object(a["createElementVNode"])("strong",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_DontHaveYourMobileDevice")),1),Te,Object(a["createElementVNode"])("a",{href:e.externalRawLink("https://matomo.org/faq/how-to/faq_27248"),rel:"noreferrer noopener",target:"_blank"},Object(a["toDisplayString"])(e.translate("TwoFactorAuth_EnterRecoveryCodeInstead")),9,Se),Ae,Object(a["createElementVNode"])("a",{href:e.mailToLink,rel:"noreferrer noopener"},Object(a["toDisplayString"])(e.translate("TwoFactorAuth_AskSuperUserResetAuthenticationCode")),9,Fe),ke,Object(a["createElementVNode"])("a",{href:e.logoutLink,rel:"noreferrer noopener"},Object(a["toDisplayString"])(e.translate("General_Logout")),9,_e)])]),_:1},8,["content-title"])}var Be=o("5b81"),xe=Object(a["defineComponent"])({props:{formData:{type:Object,required:!0},accessErrorString:String,formNonce:{type:String,required:!0},loginModule:{type:String,required:!0},piwikUrl:String,userLogin:{type:String,required:!0},contactEmail:{type:String,required:!0}},components:{ContentBlock:j["ContentBlock"],Notification:j["Notification"],FormErrors:Be["FormErrors"]},computed:{learnMoreText(){return Object(j["translate"])("General_LearnMore",Object(j["externalLink"])("https://matomo.org/faq/general/faq_27245"),"")},mailToLink(){return`mailto:${this.contactEmail}?${j["MatomoUrl"].stringify({subject:Object(j["translate"])("TwoFactorAuth_NotPossibleToLogIn"),body:Object(j["translate"])("TwoFactorAuth_LostAuthenticationDevice","\n\n","\n\n",this.piwikUrl||"","\n\n",this.userLogin,Object(j["externalRawLink"])("https://matomo.org/faq/how-to/faq_27248"))})}`},logoutLink(){return"?"+j["MatomoUrl"].stringify({module:this.loginModule,action:"logout"})},formDataAttributes(){return Object.fromEntries(this.formData.attributes.split(/\s+/g).filter(e=>e).map(e=>e.split("=")).map(([e,t])=>[e,j["Matomo"].helper.htmlDecode(t.substr(1,t.length-2))]))}}});xe.render=De;var Me=xe;const Le={class:"successMessage"},Re=Object(a["createElementVNode"])("br",null,null,-1),qe=["href"];function Ue(e,t,o,n,r,c){const l=Object(a["resolveComponent"])("ContentBlock");return Object(a["openBlock"])(),Object(a["createBlock"])(l,{class:"twoFactorSetupFinished"},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("h2",Le,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_SetupFinishedTitle")),1),Object(a["createElementVNode"])("h3",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_SetupFinishedSubtitle")),1),Object(a["createElementVNode"])("p",null,[Re,Object(a["createElementVNode"])("a",{class:"btn",href:e.userSecurityLink},Object(a["toDisplayString"])(e.translate("General_Continue")),9,qe)])]),_:1})}var Pe=Object(a["defineComponent"])({components:{ContentBlock:j["ContentBlock"]},computed:{userSecurityLink(){return"?"+j["MatomoUrl"].stringify(Object.assign(Object.assign({},j["MatomoUrl"].urlParsed.value),{},{module:"UsersManager",action:"userSecurity"}))}}});Pe.render=Ue;var Ge=Pe;const He=["innerHTML"],$e={key:0},Ie={class:"twoFaStatusEnabled"},ze={key:1},Qe={key:0},Xe=Object(a["createElementVNode"])("br",null,null,-1),Ye=Object(a["createElementVNode"])("br",null,null,-1),We=["href"],Je={key:1},Ke=["href"],Ze=["href"],et=["value"],tt=["href"],ot={key:2},nt=Object(a["createElementVNode"])("br",null,null,-1),rt=Object(a["createElementVNode"])("br",null,null,-1),at=["href"],ct={id:"confirmDisable2FA",class:"ui-confirm",ref:"confirmDisable2FA"},lt=["value"],it=["value"];function st(e,t,o,n,r,c){const l=Object(a["resolveComponent"])("ContentBlock");return Object(a["openBlock"])(),Object(a["createBlock"])(l,{"content-title":e.contentTitle,class:"userSettings2FA"},{default:Object(a["withCtx"])(()=>[Object(a["createElementVNode"])("p",{innerHTML:e.$sanitize(e.twoFactorAuthIntro)},null,8,He),e.isEnabled?(Object(a["openBlock"])(),Object(a["createElementBlock"])("p",$e,[Object(a["createElementVNode"])("strong",Ie,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_TwoFactorAuthenticationIsEnabled")),1)])):Object(a["createCommentVNode"])("",!0),e.isEnabled?(Object(a["openBlock"])(),Object(a["createElementBlock"])("p",ze,[e.isForced?(Object(a["openBlock"])(),Object(a["createElementBlock"])("span",Qe,[Object(a["createTextVNode"])(Object(a["toDisplayString"])(e.translate("TwoFactorAuth_TwoFactorAuthenticationRequired"))+" ",1),Xe,Ye,Object(a["createElementVNode"])("a",{class:"btn btn-link enable2FaLink",href:e.setupTwoFactorAuthLink,style:{"margin-right":"3.5px"}},Object(a["toDisplayString"])(e.translate("TwoFactorAuth_ConfigureDifferentDevice")),9,We)])):(Object(a["openBlock"])(),Object(a["createElementBlock"])("span",Je,[Object(a["createElementVNode"])("a",{class:"btn btn-link enable2FaLink",href:e.setupTwoFactorAuthLink,style:{"margin-right":"3.5px"}},Object(a["toDisplayString"])(e.translate("TwoFactorAuth_ConfigureDifferentDevice")),9,Ke),Object(a["createElementVNode"])("a",{href:e.disableTwoFactorAuthLink,style:{display:"none"},id:"disable2fa"},"disable2fa",8,Ze),Object(a["createElementVNode"])("input",{type:"button",class:"btn btn-link disable2FaLink",onClick:t[0]||(t[0]=t=>e.onDisable2FaLinkClick()),value:e.translate("TwoFactorAuth_DisableTwoFA"),style:{"margin-right":"3.5px"}},null,8,et)])),Object(a["createElementVNode"])("a",{class:"btn btn-link showRecoveryCodesLink",href:e.showRecoveryCodesLink},Object(a["toDisplayString"])(e.translate("TwoFactorAuth_ShowRecoveryCodes")),9,tt)])):(Object(a["openBlock"])(),Object(a["createElementBlock"])("p",ot,[Object(a["createElementVNode"])("strong",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_TwoFactorAuthenticationIsDisabled")),1),nt,rt,Object(a["createElementVNode"])("a",{class:"btn btn-link enable2FaLink",href:e.setupTwoFactorAuthLink},Object(a["toDisplayString"])(e.translate("TwoFactorAuth_EnableTwoFA")),9,at)])),Object(a["createElementVNode"])("div",ct,[Object(a["createElementVNode"])("h2",null,Object(a["toDisplayString"])(e.translate("TwoFactorAuth_ConfirmDisableTwoFA")),1),Object(a["createElementVNode"])("input",{role:"yes",type:"button",value:e.translate("General_Yes")},null,8,lt),Object(a["createElementVNode"])("input",{role:"no",type:"button",value:e.translate("General_No")},null,8,it)],512)]),_:1},8,["content-title"])}var ut=Object(a["defineComponent"])({props:{isEnabled:Boolean,isForced:Boolean,disableNonce:{type:String,required:!0}},components:{ContentBlock:j["ContentBlock"]},computed:{contentTitle(){const e=Object(j["translate"])("TwoFactorAuth_TwoFactorAuthentication"),t=Object(j["translate"])("TwoFactorAuth_TwoFAShort");return`${e} (${t})`},twoFactorAuthIntro(){return Object(j["translate"])("TwoFactorAuth_TwoFactorAuthenticationIntro",Object(j["externalLink"])("https://matomo.org/faq/general/faq_27245"),"")},setupTwoFactorAuthLink(){return"?"+j["MatomoUrl"].stringify(Object.assign(Object.assign({},j["MatomoUrl"].urlParsed.value),{},{module:"TwoFactorAuth",action:"setupTwoFactorAuth"}))},disableTwoFactorAuthLink(){return"?"+j["MatomoUrl"].stringify(Object.assign(Object.assign({},j["MatomoUrl"].urlParsed.value),{},{module:"TwoFactorAuth",action:"disableTwoFactorAuth",disableNonce:this.disableNonce}))},showRecoveryCodesLink(){return"?"+j["MatomoUrl"].stringify(Object.assign(Object.assign({},j["MatomoUrl"].urlParsed.value),{},{module:"TwoFactorAuth",action:"showRecoveryCodes"}))}},methods:{onDisable2FaLinkClick(){const e=this.disableNonce;j["Matomo"].helper.modalConfirm(this.$refs.confirmDisable2FA,{yes(){j["MatomoUrl"].updateUrl({module:"TwoFactorAuth",action:"disableTwoFactorAuth",disableNonce:e})}})}}});ut.render=st;var dt=ut; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */}})})); +//# sourceMappingURL=TwoFactorAuth.umd.min.js.map \ No newline at end of file diff --git a/plugins/TwoFactorAuth/vue/dist/umd.metadata.json b/plugins/TwoFactorAuth/vue/dist/umd.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..ed3e9b188b06343a88f0147f11d5c80238e81463 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/dist/umd.metadata.json @@ -0,0 +1,7 @@ +{ + "dependsOn": [ + "CoreHome", + "CorePluginsAdmin", + "Login" + ] +} \ No newline at end of file diff --git a/plugins/TwoFactorAuth/vue/src/LoginTwoFactorAuth/LoginTwoFactorAuth.vue b/plugins/TwoFactorAuth/vue/src/LoginTwoFactorAuth/LoginTwoFactorAuth.vue new file mode 100644 index 0000000000000000000000000000000000000000..1155bf9797a15fabb1b8f7e0a547453bd87c69f1 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/LoginTwoFactorAuth/LoginTwoFactorAuth.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/plugins/TwoFactorAuth/vue/src/SetupFinished/SetupFinished.vue b/plugins/TwoFactorAuth/vue/src/SetupFinished/SetupFinished.vue new file mode 100644 index 0000000000000000000000000000000000000000..19e30ef59aadcbe67a8637a4cc258517d708cc30 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/SetupFinished/SetupFinished.vue @@ -0,0 +1,39 @@ + + + + + diff --git a/plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/InstallOTPApp.vue b/plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/InstallOTPApp.vue new file mode 100644 index 0000000000000000000000000000000000000000..d05687bb54d2227dcbd598e6b1211e4168c1f194 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/InstallOTPApp.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/SetupTwoFactorAuth.vue b/plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/SetupTwoFactorAuth.vue new file mode 100644 index 0000000000000000000000000000000000000000..885da51adb733e7075a6e07213203888bdfc26ed --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/SetupTwoFactorAuth/SetupTwoFactorAuth.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodes.vue b/plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodes.vue new file mode 100644 index 0000000000000000000000000000000000000000..23a3f7cbc58f411991ef21c498814a9e765986dc --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodes.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodesPage.vue b/plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodesPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..ec0ee558774a5d0961c13d2e0c33209e7391ad50 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/ShowRecoveryCodes/ShowRecoveryCodesPage.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/plugins/TwoFactorAuth/vue/src/UserSettings/UserSettings.vue b/plugins/TwoFactorAuth/vue/src/UserSettings/UserSettings.vue new file mode 100644 index 0000000000000000000000000000000000000000..23bb9fe6f324ec944cbd6c450590c4640b0847fe --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/UserSettings/UserSettings.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/plugins/TwoFactorAuth/vue/src/index.ts b/plugins/TwoFactorAuth/vue/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4165a1b99a065e1cbbc71c908ac438d1788884e1 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/index.ts @@ -0,0 +1,13 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +export { default as ShowRecoveryCodes } from './ShowRecoveryCodes/ShowRecoveryCodes.vue'; +export { default as ShowRecoveryCodesPage } from './ShowRecoveryCodes/ShowRecoveryCodesPage.vue'; +export { default as SetupTwoFactorAuth } from './SetupTwoFactorAuth/SetupTwoFactorAuth.vue'; +export { default as LoginTwoFactorAuth } from './LoginTwoFactorAuth/LoginTwoFactorAuth.vue'; +export { default as SetupFinished } from './SetupFinished/SetupFinished.vue'; +export { default as UserSettings } from './UserSettings/UserSettings.vue'; diff --git a/plugins/TwoFactorAuth/vue/src/types.ts b/plugins/TwoFactorAuth/vue/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ccfb39804b2be51be2aed7abf7d74f6a79acb38 --- /dev/null +++ b/plugins/TwoFactorAuth/vue/src/types.ts @@ -0,0 +1,16 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +export interface QRCodeConstructor { + new (e: HTMLElement, options: unknown): unknown; +} + +declare global { + interface Window { + QRCode: QRCodeConstructor; + } +} diff --git a/plugins/UserCountry/API.php b/plugins/UserCountry/API.php new file mode 100644 index 0000000000000000000000000000000000000000..443c96ceb23f0c2431e52aff17d897a1ee3ed778 --- /dev/null +++ b/plugins/UserCountry/API.php @@ -0,0 +1,508 @@ +getDataTable(Archiver::COUNTRY_RECORD_NAME, $idSite, $period, $date, $segment); + + $dataTable->filter(function (DataTable $dt) { + if ($dt->getRowFromLabel('ti')) { + $dt->filter('GroupBy', array( + 'label', + function ($label) { + if ($label == 'ti') { + return 'cn'; + } + return $label; + }, + )); + } + }); + + // apply filter on the whole datatable in order the inline search to work (searches are done on "beautiful" label) + $dataTable->filter('AddSegmentValue'); + $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'code')); + $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'logo', __NAMESPACE__ . '\getFlagFromCode')); + $dataTable->filter('ColumnCallbackReplace', array('label', __NAMESPACE__ . '\countryTranslate')); + + $dataTable->queueFilter('ColumnCallbackAddMetadata', array( + array(), + 'logoHeight', + function () { + return 16; + }, + )); + + return $dataTable; + } + + /** + * Returns visit information grouped by continent. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|false|null $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Continent rows with translated labels and continent codes. + */ + public function getContinent($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable(Archiver::COUNTRY_RECORD_NAME, $idSite, $period, $date, $segment); + + $getContinent = array('Piwik\Common', 'getContinent'); + $dataTable->filter('GroupBy', array('label', $getContinent)); + + $dataTable->filter('ColumnCallbackReplace', array('label', __NAMESPACE__ . '\continentTranslate')); + $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'code')); + + return $dataTable; + } + + /** + * Returns visit information for every region with at least one visit. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|false|null $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Region rows with country, region, and flag metadata. + */ + public function getRegion($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable(Archiver::REGION_RECORD_NAME, $idSite, $period, $date, $segment); + + $separator = Archiver::LOCATION_SEPARATOR; + $unk = Visit::UNKNOWN_CODE; + + $dataTable->filter(function (DataTable $dt) use ($period, $date, $separator) { + /** @var string $archiveDate */ + $archiveDate = $dt->getMetadata(DataTable::ARCHIVED_DATE_METADATA_NAME); + + // convert fips region codes to iso if required + if ($this->shouldRegionCodesBeConvertedToIso($archiveDate, $date, $period)) { + $dt->filter('GroupBy', array( + 'label', + function ($label) use ($separator) { + $regionCode = (string)getElementFromStringArray($label, $separator, 0, ''); + $countryCode = (string)getElementFromStringArray($label, $separator, 1, ''); + + list($countryCode, $regionCode) = GeoIp2::convertRegionCodeToIso( + $countryCode, + $regionCode, + true + ); + + $splitLabel = explode($separator, $label); + + if (isset($splitLabel[0])) { + $splitLabel[0] = $regionCode; + } + + if (isset($splitLabel[1])) { + $splitLabel[1] = strtolower($countryCode); + } + + return implode($separator, $splitLabel); + }, + )); + } elseif ($dt->getRowFromLabel('1|ti')) { + $dt->filter('GroupBy', array( + 'label', + function ($label) { + if ($label == '1|ti') { + return '14|cn'; + } + return $label; + }, + )); + } + }); + + $segments = array('regionCode', 'countryCode'); + $dataTable->filter('AddSegmentByLabel', array($segments, Archiver::LOCATION_SEPARATOR)); + + // split the label and put the elements into the 'region' and 'country' metadata fields + $dataTable->filter( + 'ColumnCallbackAddMetadata', + array('label', 'region', __NAMESPACE__ . '\getElementFromStringArray', array($separator, 0, $unk)) + ); + $dataTable->filter( + 'ColumnCallbackAddMetadata', + array('label', 'country', __NAMESPACE__ . '\getElementFromStringArray', array($separator, 1, $unk)) + ); + + // add country name metadata + $dataTable->filter( + 'MetadataCallbackAddMetadata', + array('country', 'country_name', __NAMESPACE__ . '\CountryTranslate', $applyToSummaryRow = false) + ); + + // get the region name of each row and put it into the 'region_name' metadata + $dataTable->filter( + 'ColumnCallbackAddMetadata', + array('label', 'region_name', __NAMESPACE__ . '\getRegionName', $params = null, + $applyToSummaryRow = false) + ); + + // add the country flag as a url to the 'logo' metadata field + $dataTable->filter('MetadataCallbackAddMetadata', array('country', 'logo', __NAMESPACE__ . '\getFlagFromCode')); + + // prettify the region label + $dataTable->filter('ColumnCallbackReplace', array('label', __NAMESPACE__ . '\getPrettyRegionName')); + + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + + return $dataTable; + } + + /** + * Returns visit information for every city with at least one visit. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|false|null $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map City rows with city, region, country, and flag metadata. + */ + public function getCity($idSite, string $period, string $date, $segment = false) + { + $dataTable = $this->getDataTable(Archiver::CITY_RECORD_NAME, $idSite, $period, $date, $segment); + + $separator = Archiver::LOCATION_SEPARATOR; + $unk = Visit::UNKNOWN_CODE; + + $dataTable->filter(function (DataTable $dt) use ($period, $date, $separator) { + /** @var string $archiveDate */ + $archiveDate = $dt->getMetadata(DataTable::ARCHIVED_DATE_METADATA_NAME); + + // convert fips region codes to iso if required + if ($this->shouldRegionCodesBeConvertedToIso($archiveDate, $date, $period)) { + $dt->filter('GroupBy', array( + 'label', + function ($label) use ($separator) { + $regionCode = (string)getElementFromStringArray($label, $separator, 1, ''); + $countryCode = (string)getElementFromStringArray($label, $separator, 2, ''); + + list($countryCode, $regionCode) = GeoIp2::convertRegionCodeToIso( + $countryCode, + $regionCode, + true + ); + + $splitLabel = explode($separator, $label); + + if (isset($splitLabel[1])) { + $splitLabel[1] = $regionCode; + } + + if (isset($splitLabel[2])) { + $splitLabel[2] = strtolower($countryCode); + } + + return implode($separator, $splitLabel); + }, + )); + } else { + $dt->filter('GroupBy', array( + 'label', + function ($label) { + if (substr($label, -5) == '|1|ti') { + return substr($label, 0, -5) . '|14|cn'; + } + return $label; + }, + )); + } + }); + + $segments = array('city', 'regionCode', 'countryCode'); + $dataTable->filter('AddSegmentByLabel', array($segments, Archiver::LOCATION_SEPARATOR)); + + // split the label and put the elements into the 'city_name', 'region', 'country', + // 'lat' & 'long' metadata fields + $strUnknown = Piwik::translate('General_Unknown'); + $dataTable->filter( + 'ColumnCallbackAddMetadata', + array('label', 'city_name', __NAMESPACE__ . '\getElementFromStringArray', + array($separator, 0, $strUnknown)) + ); + $dataTable->filter( + 'MetadataCallbackAddMetadata', + array('city_name', 'city', function ($city) use ($strUnknown) { + if ($city == $strUnknown) { + return "xx"; + } else { + return false; + } + }) + ); + $dataTable->filter( + 'ColumnCallbackAddMetadata', + array('label', 'region', __NAMESPACE__ . '\getElementFromStringArray', array($separator, 1, $unk)) + ); + $dataTable->filter( + 'ColumnCallbackAddMetadata', + array('label', 'country', __NAMESPACE__ . '\getElementFromStringArray', array($separator, 2, $unk)) + ); + + // backwards compatibility: for reports that have lat|long in label + $dataTable->filter( + 'ColumnCallbackAddMetadata', + array('label', 'lat', __NAMESPACE__ . '\getElementFromStringArray', array($separator, 3, false)) + ); + $dataTable->filter( + 'ColumnCallbackAddMetadata', + array('label', 'long', __NAMESPACE__ . '\getElementFromStringArray', array($separator, 4, false)) + ); + + // add country name & region name metadata + $dataTable->filter( + 'MetadataCallbackAddMetadata', + array('country', 'country_name', __NAMESPACE__ . '\countryTranslate', $applyToSummaryRow = false) + ); + + $getRegionName = '\\Piwik\\Plugins\\UserCountry\\getRegionNameFromCodes'; + $dataTable->filter('MetadataCallbackAddMetadata', array( + array('country', 'region'), 'region_name', $getRegionName, $applyToSummaryRow = false)); + + // add the country flag as a url to the 'logo' metadata field + $dataTable->filter('MetadataCallbackAddMetadata', array('country', 'logo', __NAMESPACE__ . '\getFlagFromCode')); + + // prettify the label + $dataTable->filter('ColumnCallbackReplace', array('label', __NAMESPACE__ . '\getPrettyCityName')); + + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + + return $dataTable; + } + + /** + * if no switch to ISO was done --> no conversion as only FIPS codes are in use and handled correctly + * if there has been a switch to ISO, we need to check the date: + * - if the start date of the period is after the date we switched to ISO: no conversion needed + * - if not we need to convert the codes to ISO, if the code is mappable + * Note: as all old codes are mapped, not mappable codes need to be iso codes already, so we leave them + * + * @param string $archiveDate + */ + private function shouldRegionCodesBeConvertedToIso($archiveDate, string $date, string $period): bool + { + $timeOfSwitch = Option::get(GeoIp2::SWITCH_TO_ISO_REGIONS_OPTION_NAME); + + if (empty($timeOfSwitch)) { + return false; // if option was not set, all codes are fips codes, so leave them + } + + try { + $dateOfSwitch = Date::factory((int)$timeOfSwitch); + $period = Period\Factory::build($period, $date); + $periodStart = $period->getDateStart(); + } catch (Exception $e) { + return false; + } + + // if all region codes in log tables have been converted, check if archiving date was after the date of switch to iso + // this check might not be fully correct in cases were only periods > day get recreated, but it should avoid some + // double conversion if all archives have been recreated after converting all region codes + $codesConverted = Option::get(ConvertRegionCodesToIso::OPTION_NAME); + + if ($codesConverted && $archiveDate) { + try { + $dateOfArchive = Date::factory($archiveDate); + + if ($dateOfArchive->isLater($dateOfSwitch)) { + return false; + } + } catch (Exception $e) { + } + } + + if ($dateOfSwitch->isEarlier($periodStart)) { + return false; + } + + return true; + } + + /** + * Returns a mapping from ISO country code to translated country name. + * + * @return array Country names keyed by lowercase ISO country code. + */ + public function getCountryCodeMapping(): array + { + $regionDataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider'); + + $countryCodeList = $regionDataProvider->getCountryList(); + + array_walk($countryCodeList, function (&$item, $key) { + $item = Piwik::translate('Intl_Country_' . strtoupper($key)); + }); + + return $countryCodeList; + } + + /** + * Uses a location provider to find/guess the location of an IP address. + * + * See LocationProvider::getLocation to see the details + * of the result of this function. + * + * @param string|false $ip The IP address to geolocate, or `false` to use the current request IP. + * @param string|false $provider The provider ID to use, or `false` to use the currently configured provider. + * @return array Location data returned by the selected provider. + */ + public function getLocationFromIP($ip = false, $provider = false): array + { + Piwik::checkUserHasSomeViewAccess(); + + if (empty($ip)) { + $ip = IP::getIpFromHeader(); + } + + if (empty($provider)) { + $provider = LocationProvider::getCurrentProviderId(); + } + + $oProvider = LocationProvider::getProviderById($provider); + if (empty($oProvider)) { + throw new Exception("Cannot find the '$provider' provider. It is either an invalid provider " + . "ID or the ID of a provider that is not working."); + } + + $location = $oProvider->getLocation(array('ip' => $ip)); + if (empty($location)) { + throw new Exception("Could not geolocate '$ip'!"); + } + $location['ip'] = $ip; + return $location; + } + + /** + * Sets the active geolocation provider. + * + * @param string $providerId The provider ID to activate, for example `default` or `geoip2_php`. + * @return void + */ + public function setLocationProvider($providerId) + { + Piwik::checkUserHasSuperUserAccess(); + + if (!UserCountry::isGeoLocationAdminEnabled()) { + throw new \Exception('Setting geo location has been disabled in config.'); + } + + LocationProvider::setCurrentProvider($providerId); + } + + /** + * @param int|string|int[] $idSite + * @param string|false|null $segment + * @return DataTable|DataTable\Map + */ + protected function getDataTable(string $name, $idSite, string $period, string $date, $segment) + { + Piwik::checkUserHasViewAccess($idSite); + $archive = Archive::build($idSite, $period, $date, $segment); + $dataTable = $archive->getDataTable($name); + $dataTable->queueFilter('ReplaceColumnNames'); + return $dataTable; + } + + /** + * Returns the number of distinct countries in the requested period. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|false|null $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @return DataTable|DataTable\Map Numeric archive result containing the number of distinct countries. + */ + public function getNumberOfDistinctCountries($idSite, string $period, string $date, $segment = false) + { + Piwik::checkUserHasViewAccess($idSite); + $archive = Archive::build($idSite, $period, $date, $segment); + return $archive->getDataTableFromNumeric(Archiver::DISTINCT_COUNTRIES_METRIC); + } +} diff --git a/plugins/UserCountry/Archiver.php b/plugins/UserCountry/Archiver.php new file mode 100644 index 0000000000000000000000000000000000000000..0cd3800b42bf221b751c6f298bd3a1b3ee4bc19e --- /dev/null +++ b/plugins/UserCountry/Archiver.php @@ -0,0 +1,30 @@ +' . Piwik::translate('UserCountry_LocationsSubcategoryHelp') . '

'; + } +} diff --git a/plugins/UserCountry/Columns/Base.php b/plugins/UserCountry/Columns/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..756bafa7bb08bfe132e70970623776a793d00140 --- /dev/null +++ b/plugins/UserCountry/Columns/Base.php @@ -0,0 +1,98 @@ +getParams()); + + if (!empty($value)) { + if (!$request->isAuthenticated()) { + Common::printDebug("WARN: Tracker API '$urlParamToOverride' was used with invalid token_auth"); + throw new InvalidRequestParameterException("Tracker API '$urlParamToOverride' was used, requires valid token_auth"); + } + return $value; + } + + return false; + } + + public function getRequiredVisitFields() + { + return array('location_ip', 'location_browser_lang'); + } + + protected function getLocationDetail($userInfo, $locationKey) + { + $useLocationCache = empty($GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']); + $location = $this->getVisitorGeolocator()->getLocation($userInfo, $useLocationCache); + + if (!isset($location[$locationKey])) { + return false; + } + + return $location[$locationKey]; + } + + protected function getVisitorGeolocator() + { + if ($this->visitorGeolocator === null) { + $this->visitorGeolocator = new VisitorGeolocator(); + } + + return $this->visitorGeolocator; + } + + protected function getUserInfo(Request $request, Visitor $visitor) + { + $ipAddress = $this->getIpAddress($visitor->getVisitorColumn('location_ip'), $request); + $language = $request->getBrowserLanguage(); + + $userInfo = array('lang' => $language, 'ip' => $ipAddress); + + return $userInfo; + } + + private function getIpAddress($anonymizedIp, \Piwik\Tracker\Request $request) + { + $privacyConfig = new PrivacyManagerConfig(); + + $ip = $request->getIp(); + + if ($privacyConfig->useAnonymizedIpForVisitEnrichment) { + $ip = $anonymizedIp; + } + + $ipAddress = IPUtils::binaryToStringIP($ip); + + return $ipAddress; + } +} diff --git a/plugins/UserCountry/Columns/City.php b/plugins/UserCountry/Columns/City.php new file mode 100644 index 0000000000000000000000000000000000000000..7891a31aa5ea00f5a7ddee17611ad9d614c02df4 --- /dev/null +++ b/plugins/UserCountry/Columns/City.php @@ -0,0 +1,62 @@ +getUrlOverrideValueIfAllowed('city', $request); + if ($value !== false) { + $value = substr($value, 0, 255); + return $value; + } + + $userInfo = $this->getUserInfo($request, $visitor); + + return $this->getLocationDetail($userInfo, LocationProvider::CITY_NAME_KEY); + } + + /** + * @param Action|null $action + * @return string|false + */ + public function onExistingVisit(Request $request, Visitor $visitor, $action) + { + return $this->getUrlOverrideValueIfAllowed('city', $request); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } +} diff --git a/plugins/UserCountry/Columns/Continent.php b/plugins/UserCountry/Columns/Continent.php new file mode 100644 index 0000000000000000000000000000000000000000..748e77427543f866c160ce8076048efd78c04a42 --- /dev/null +++ b/plugins/UserCountry/Columns/Continent.php @@ -0,0 +1,37 @@ +setName('UserCountry_CountryCode'); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + + $segment = new Segment(); + $segment->setSegment('countryName'); + $segment->setName('UserCountry_Country'); + $segment->setAcceptedValues('Germany, France, Spain, ...'); + $segment->setNeedsMostFrequentValues(false); + $regionDataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider'); + $countryList = $regionDataProvider->getCountryList(); + array_walk($countryList, function (&$item, $key) { + $item = Piwik::translate('Intl_Country_' . strtoupper($key), [], 'en'); + }); + + $segment->setSqlFilterValue(function ($val) use ($countryList) { + $countryList = array_map(function ($countryName) { + return mb_strtolower($countryName); + }, $countryList); + $result = array_search(mb_strtolower($val), $countryList); + + if ($result === false) { + $result = 'UNK'; + } + return $result; + }); + $segment->setSuggestedValuesCallback(function ($idSite, $maxValuesToReturn, $table) use ($countryList) { + return $this->sortStaticListByUsage($countryList, $table, 'countryCode', $maxValuesToReturn); + }); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + + public function formatValue($value, $idSite, Formatter $formatter) + { + return \Piwik\Plugins\UserCountry\countryTranslate($value); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + $value = $this->getUrlOverrideValueIfAllowed('country', $request); + if ($value !== false) { + $value = substr($value, 0, 3); + return $value; + } + + $userInfo = $this->getUserInfo($request, $visitor); + $country = $this->getLocationDetail($userInfo, LocationProvider::COUNTRY_CODE_KEY); + + if (!empty($country) && $country != Visit::UNKNOWN_CODE) { + return strtolower($country); + } + + return Visit::UNKNOWN_CODE; + } + + /** + * @param Action|null $action + * @return string|false + */ + public function onExistingVisit(Request $request, Visitor $visitor, $action) + { + return $this->getUrlOverrideValueIfAllowed('country', $request); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + $country = $visitor->getVisitorColumn($this->columnName); + + if (isset($country) && false !== $country) { + return $country; + } + + $browserLanguage = $request->getBrowserLanguage(); + $enableLanguageToCountryGuess = Config::getInstance()->Tracker['enable_language_to_country_guess']; + $locationIp = $visitor->getVisitorColumn('location_ip'); + + return Common::getCountry($browserLanguage, $enableLanguageToCountryGuess, $locationIp); + } +} diff --git a/plugins/UserCountry/Columns/Latitude.php b/plugins/UserCountry/Columns/Latitude.php new file mode 100644 index 0000000000000000000000000000000000000000..333d017c92082cf9b0ed3ed92db8bd2e86d9b264 --- /dev/null +++ b/plugins/UserCountry/Columns/Latitude.php @@ -0,0 +1,69 @@ +getUrlOverrideValueIfAllowed('lat', $request); + + if ($value !== false) { + return $value; + } + + $userInfo = $this->getUserInfo($request, $visitor); + + $latitude = $this->getLocationDetail($userInfo, LocationProvider::LATITUDE_KEY); + + return $latitude; + } + + /** + * @param Action|null $action + * @return string|false + */ + public function onExistingVisit(Request $request, Visitor $visitor, $action) + { + return $this->getUrlOverrideValueIfAllowed('lat', $request); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } +} diff --git a/plugins/UserCountry/Columns/Longitude.php b/plugins/UserCountry/Columns/Longitude.php new file mode 100644 index 0000000000000000000000000000000000000000..4af172f4c926ff1b4796bf009c8617079687c8eb --- /dev/null +++ b/plugins/UserCountry/Columns/Longitude.php @@ -0,0 +1,64 @@ +getUrlOverrideValueIfAllowed('long', $request); + + if ($value !== false) { + return $value; + } + + $userInfo = $this->getUserInfo($request, $visitor); + + $longitude = $this->getLocationDetail($userInfo, LocationProvider::LONGITUDE_KEY); + + return $longitude; + } + + /** + * @param Action|null $action + * @return string|false + */ + public function onExistingVisit(Request $request, Visitor $visitor, $action) + { + return $this->getUrlOverrideValueIfAllowed('long', $request); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } +} diff --git a/plugins/UserCountry/Columns/Region.php b/plugins/UserCountry/Columns/Region.php new file mode 100644 index 0000000000000000000000000000000000000000..c0c8d37eb704918050f676d5af196362bed568ab --- /dev/null +++ b/plugins/UserCountry/Columns/Region.php @@ -0,0 +1,67 @@ +' . Piwik::translate('General_ForExampleShort') . ' region=BFC;country=fr'; + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onNewVisit(Request $request, Visitor $visitor, $action) + { + $value = $this->getUrlOverrideValueIfAllowed('region', $request); + if ($value !== false) { + $value = substr($value, 0, 3); + return $value; + } + + $userInfo = $this->getUserInfo($request, $visitor); + + return $this->getLocationDetail($userInfo, LocationProvider::REGION_CODE_KEY); + } + + /** + * @param Action|null $action + * @return string|false + */ + public function onExistingVisit(Request $request, Visitor $visitor, $action) + { + return $this->getUrlOverrideValueIfAllowed('region', $request); + } + + /** + * @param Action|null $action + * @return mixed + */ + public function onAnyGoalConversion(Request $request, Visitor $visitor, $action) + { + return $visitor->getVisitorColumn($this->columnName); + } +} diff --git a/plugins/UserCountry/Commands/AttributeHistoricalDataWithLocations.php b/plugins/UserCountry/Commands/AttributeHistoricalDataWithLocations.php new file mode 100644 index 0000000000000000000000000000000000000000..042087a799234f73a69aa8ebcf9054adeeddc6ec --- /dev/null +++ b/plugins/UserCountry/Commands/AttributeHistoricalDataWithLocations.php @@ -0,0 +1,206 @@ +dao = $dao ?: new RawLogDao(); + } + + protected function configure() + { + $this->setName('usercountry:attribute'); + $this->setDescription("Re-attribute existing raw data (visits & conversions) with geolocated location data, using the specified or configured location provider."); + $this->addRequiredArgument(self::DATES_RANGE_ARGUMENT, 'Attribute visits in this date range. Eg, 2012-01-01,2013-01-01'); + $this->addOptionalValueOption( + self::PERCENT_STEP_ARGUMENT, + null, + 'How often to display the command progress. A status update will be printed after N percent of visits are processed, ' + . 'where N is the value of this option.', + self::PERCENT_STEP_ARGUMENT_DEFAULT + ); + $this->addRequiredValueOption(self::PROVIDER_ARGUMENT, null, 'Provider id which should be used to attribute visits. If empty then' + . ' Piwik will use the currently configured provider. If no provider is configured, the default provider is used.'); + $this->addOptionalValueOption(self::SEGMENT_LIMIT_OPTION, null, 'Number of visits to process at a time.', self::SEGMENT_LIMIT_OPTION_DEFAULT); + $this->addNoValueOption(self::FORCE_OPTION, null, "Force geolocation, even if the requested provider does not appear to work. It is not " + . "recommended to use this option."); + } + + protected function doExecute(): int + { + $input = $this->getInput(); + $output = $this->getOutput(); + [$from, $to] = $this->getDateRangeToAttribute(); + + $this->visitorGeolocator = $this->createGeolocator(); + + $this->percentStep = $this->getPercentStep(); + $this->amountOfVisits = $this->dao->countVisitsWithDatesLimit($from, $to); + + $output->writeln( + sprintf( + 'Re-attribution for date range: %s to %s. %d visits to process with provider "%s".', + $from, + $to, + $this->amountOfVisits, + $this->visitorGeolocator->getProvider()->getId() + ) + ); + + $this->timer = new Timer(); + + $this->processSpecifiedLogsInChunks($from, $to, $input->getOption(self::SEGMENT_LIMIT_OPTION)); + + $output->writeln("Completed. " . $this->timer->__toString() . ""); + + return self::SUCCESS; + } + + protected function processSpecifiedLogsInChunks($from, $to, $segmentLimit) + { + $self = $this; + $this->visitorGeolocator->reattributeVisitLogs($from, $to, $idSite = null, $segmentLimit, function () use ($self) { + $self->onVisitProcessed(); + }); + } + + /** + * @return int + */ + protected function getPercentStep() + { + $percentStep = $this->getInput()->getOption(self::PERCENT_STEP_ARGUMENT); + + if (!is_numeric($percentStep)) { + return self::PERCENT_STEP_ARGUMENT_DEFAULT; + } + + // Percent step should be between maximum percent value and minimum percent value (1-100) + if ($percentStep > 99 || $percentStep < 1) { + return 100; + } + + return $percentStep; + } + + /** + * Print information about progress. + */ + public function onVisitProcessed() + { + ++$this->processed; + + $percent = ceil($this->processed / $this->amountOfVisits * 100); + + if ( + $percent > $this->processedPercent + && $percent % $this->percentStep === 0 + ) { + $this->getOutput()->writeln(sprintf('%d%% processed. %s', $percent, $this->timer->__toString())); + + $this->processedPercent = $percent; + } + } + + private function getDateRangeToAttribute() + { + $dateRangeString = $this->getInput()->getArgument(self::DATES_RANGE_ARGUMENT); + + $dates = explode(',', $dateRangeString); + $dates = array_map(array('Piwik\Date', 'factory'), $dates); + + if (count($dates) != 2) { + throw new \InvalidArgumentException('Invalid date range supplied: ' . $dateRangeString); + } + + return $dates; + } + + private function createGeolocator() + { + $input = $this->getInput(); + $output = $this->getOutput(); + $providerId = $input->getOption(self::PROVIDER_ARGUMENT); + $geolocator = new VisitorGeolocator(LocationProvider::getProviderById($providerId) ?: null); + + $usedProvider = $geolocator->getProvider(); + if (!$usedProvider->isAvailable()) { + throw new \InvalidArgumentException("The provider '$providerId' is not currently available, please make sure it is configured correctly."); + } + + $isWorkingOrErrorMessage = $usedProvider->isWorking(); + if ($isWorkingOrErrorMessage !== true) { + $errorMessage = "The provider '$providerId' does not appear to be working correctly. Details: $isWorkingOrErrorMessage"; + + $forceGeolocation = $input->getOption(self::FORCE_OPTION); + if ($forceGeolocation) { + $output->writeln("$errorMessage"); + $output->writeln("Ignoring location provider issue, geolocating anyway due to --force option."); + } else { + throw new \InvalidArgumentException($errorMessage); + } + } + + return $geolocator; + } +} diff --git a/plugins/UserCountry/Controller.php b/plugins/UserCountry/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..d5a692469497cea9d6a2dd86a82ff1ec3b45b302 --- /dev/null +++ b/plugins/UserCountry/Controller.php @@ -0,0 +1,152 @@ +urlSparklineCountries = $this->getUrlSparkline('getLastDistinctCountriesGraph'); + $view->numberDistinctCountries = $this->getNumberOfDistinctCountries(); + + return $view->render(); + } + + public function adminIndex() + { + $this->dieIfGeolocationAdminIsDisabled(); + Piwik::checkUserHasSuperUserAccess(); + $view = new View('@UserCountry/adminIndex'); + + $allProviderInfo = LocationProvider::getAllProviderInfo($newline = '
', $includeExtra = true); + $view->locationProviders = $allProviderInfo; + $view->currentProviderId = LocationProvider::getCurrentProviderId(); + $view->thisIP = IP::getIpFromHeader(); + $view->hasGeoIp2Provider = Manager::getInstance()->isPluginActivated('GeoIp2'); + + if (!LocationProvider::getCurrentProvider()) { + $provider = LocationProvider::getProviderById(LocationProvider::getCurrentProviderId()); + + if ($provider) { + $message = Piwik::translate('UserCountry_GeolocationProviderBroken', '' . $provider->getInfo()['title'] . ''); + } else { + $message = Piwik::translate('UserCountry_GeolocationProviderUnavailable', '' . LocationProvider::getCurrentProviderId() . ''); + } + + $notification = new Notification($message); + $notification->context = Notification::CONTEXT_ERROR; + $notification->raw = true; + Notification\Manager::notify('UserCountry_GeoLocationProviderBroken', $notification); + } else { + $isWorking = LocationProvider::getCurrentProvider()->isWorking(); + if (true !== $isWorking) { + $message = Piwik::translate('UserCountry_GeolocationProviderBroken', '' . LocationProvider::getCurrentProvider()->getInfo()['title'] . ''); + + if ($isWorking) { + $message .= '

' . $isWorking; + } + + $notification = new Notification($message); + $notification->context = Notification::CONTEXT_ERROR; + $notification->raw = true; + Notification\Manager::notify('UserCountry_GeoLocationProviderBroken', $notification); + } + } + + // check if there is a working provider (that isn't the default one) + $isThereWorkingProvider = false; + foreach ($allProviderInfo as $id => $provider) { + if ( + $id != DefaultProvider::ID && $id != DisabledProvider::ID + && $provider['status'] == LocationProvider::INSTALLED + ) { + $isThereWorkingProvider = true; + break; + } + } + $view->isThereWorkingProvider = $isThereWorkingProvider; + + $configurations = $setUpGuides = ''; + foreach (LocationProvider::getAllProviders() as $provider) { + $configurations .= $provider->renderConfiguration(); + $setUpGuides .= $provider->renderSetUpGuide(); + } + + $view->configurations = $configurations; + $view->setUpGuides = $setUpGuides; + $this->setBasicVariablesView($view); + $this->setBasicVariablesAdminView($view); + + return $view->render(); + } + + /** + * Echo's a pretty formatted location using a specific LocationProvider. + * + * Input: + * The 'id' query parameter must be set to the ID of the LocationProvider to use. + * + * Output: + * The pretty formatted location that was obtained. Will be HTML. + */ + public function getLocationUsingProvider() + { + $this->dieIfGeolocationAdminIsDisabled(); + Piwik::checkUserHasSuperUserAccess(); + + $providerId = Common::getRequestVar('id'); + $provider = LocationProvider::getProviderById($providerId); + if (empty($provider)) { + throw new Exception("Invalid provider ID: '$providerId'."); + } + + $location = $provider->getLocation(array('ip' => IP::getIpFromHeader(), + 'lang' => Common::getBrowserLanguage(), + 'disable_fallbacks' => true)); + $location = LocationProvider::prettyFormatLocation( + $location, + $newline = '
', + $includeExtra = true + ); + + return $location; + } + + public function getNumberOfDistinctCountries() + { + return $this->getNumericValue('UserCountry.getNumberOfDistinctCountries'); + } + + public function getLastDistinctCountriesGraph() + { + $view = $this->getLastUnitGraph('UserCountry', __FUNCTION__, "UserCountry.getNumberOfDistinctCountries"); + $view->config->columns_to_display = array('UserCountry_distinctCountries'); + return $this->renderView($view); + } + + private function dieIfGeolocationAdminIsDisabled() + { + if (!UserCountry::isGeoLocationAdminEnabled()) { + throw new \Exception('Geo location setting page has been disabled.'); + } + } +} diff --git a/plugins/UserCountry/Diagnostic/GeolocationDiagnostic.php b/plugins/UserCountry/Diagnostic/GeolocationDiagnostic.php new file mode 100644 index 0000000000000000000000000000000000000000..39eb99d961c9840d82930cc81b17c0c531fe96de --- /dev/null +++ b/plugins/UserCountry/Diagnostic/GeolocationDiagnostic.php @@ -0,0 +1,80 @@ +translator = $translator; + } + + public function execute() + { + if (!SettingsPiwik::isMatomoInstalled()) { + // Skip the diagnostic if Matomo is being installed + return []; + } + + $label = $this->translator->translate('UserCountry_Geolocation'); + + $currentProviderId = LocationProvider::getCurrentProviderId(); + $allProviders = LocationProvider::getAllProviderInfo(); + + $providerStatus = $allProviders[$currentProviderId]['status'] ?? LocationProvider::NOT_INSTALLED; + + $providerWarning = $allProviders[$currentProviderId]['usageWarning'] ?? null; + $statusMessage = $allProviders[$currentProviderId]['statusMessage'] ?? null; + + if ($providerStatus === LocationProvider::BROKEN) { + $message = Piwik::translate('UserCountry_GeolocationProviderBroken', '' . $allProviders[$currentProviderId]['title'] . ''); + if ($statusMessage) { + $message .= '

' . $statusMessage; + } + return [DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_ERROR, $message)]; + } + + if ($providerStatus === LocationProvider::NOT_INSTALLED) { + $provider = $allProviders[$currentProviderId] ?? null; + + if ($provider) { + $message = Piwik::translate('UserCountry_GeolocationProviderBroken', '' . $allProviders[$currentProviderId]['title'] . ''); + } else { + $message = Piwik::translate('UserCountry_GeolocationProviderUnavailable', '' . LocationProvider::getCurrentProviderId() . ''); + } + + return [DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_ERROR, $message)]; + } + + if (!empty($providerWarning)) { + return [DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_WARNING, $providerWarning)]; + } + + $availableInfo = LocationProvider::getProviderById($currentProviderId)->getSupportedLocationInfo(); + $message = sprintf("%s (%s)", $currentProviderId, implode(', ', array_keys(array_filter($availableInfo)))); + + return [DiagnosticResult::singleResult($label, DiagnosticResult::STATUS_OK, $message)]; + } +} diff --git a/plugins/UserCountry/LocationProvider.php b/plugins/UserCountry/LocationProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..c8949feaef1328494e5350eddecfcc3778ba4d29 --- /dev/null +++ b/plugins/UserCountry/LocationProvider.php @@ -0,0 +1,567 @@ + 'geoip2php', + * 'title' => '...', + * 'description' => '...' + * ); + * + * @return array + */ + abstract public function getInfo(); + + /** + * Returns an array mapping location result keys w/ bool values indicating whether + * that information is supported by this provider. If it is not supported, that means + * this provider either cannot get this information, or is not configured to get it. + * + * @return array eg. array(self::CONTINENT_CODE_KEY => true, + * self::CONTINENT_NAME_KEY => true, + * self::ORG_KEY => false) + * The result is not guaranteed to have keys for every type of location + * info. + */ + abstract public function getSupportedLocationInfo(); + + /** + * Renders Configuration or Setup View to be attached to the provider list + * + * @return string + */ + public function renderConfiguration() + { + return ''; + } + + /** + * Renders SetUp Guide, which will be shown above Geolocation admin, if there is no working provider + * + * @return string + */ + public function renderSetUpGuide() + { + return ''; + } + + /** + * Method called when a provider gets activated. + */ + public function activate() + { + } + + /** + * Returns if location provider should be shown. + */ + public function isVisible() + { + return true; + } + + /** + * Returns a message that should be shown as diagnostics warning if provider is used + */ + public function getUsageWarning(): ?string + { + return null; + } + + /** + * Returns every available provider instance. + * + * @return LocationProvider[] + */ + public static function getAllProviders() + { + if (is_null(self::$providers)) { + self::$providers = array(); + $plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated(); + foreach ($plugins as $plugin) { + foreach (self::getLocationProviders($plugin) as $instance) { + self::$providers[] = $instance; + } + } + } + + return self::$providers; + } + + /** + * Get all location providers that are defined by the given plugin. + * + * @return LocationProvider[] + */ + protected static function getLocationProviders(Plugin $plugin) + { + $locationProviders = $plugin->findMultipleComponents('LocationProvider', 'Piwik\\Plugins\\UserCountry\\LocationProvider'); + $instances = []; + + foreach ($locationProviders as $locationProvider) { + $instances[] = new $locationProvider(); + } + + return $instances; + } + + /** + * Returns all provider instances that are 'available'. An 'available' provider + * is one that is available for use. They may not necessarily be working. + * + * @return array + */ + public static function getAvailableProviders() + { + $result = array(); + foreach (self::getAllProviders() as $provider) { + if ($provider->isAvailable()) { + $result[] = $provider; + } + } + return $result; + } + + /** + * Returns an array mapping provider IDs w/ information about the provider, + * for each location provider. + * + * The following information is provided for each provider: + * 'id' - The provider's unique string ID. + * 'title' - The provider's title. + * 'description' - A description of how the location provider works. + * 'status' - Either self::NOT_INSTALLED, self::INSTALLED or self::BROKEN. + * 'statusMessage' - If the status is self::BROKEN, then the message describes why. + * 'location' - A pretty formatted location of the current IP address + * (IP::getIpFromHeader()). + * + * An example result: + * array( + * 'geoip2php' => array('id' => 'geoip2php', + * 'title' => '...', + * 'description' => '...', + * 'status' => self::BROKEN, + * 'statusMessage' => '...', + * 'location' => '...') + * 'geoip_serverbased' => array(...) + * ) + * + * @param string $newline What to separate lines with in the pretty locations. + * @param bool $includeExtra Whether to include ISP/Org info in formatted location. + * @return array + */ + public static function getAllProviderInfo($newline = "\n", $includeExtra = false) + { + $allInfo = array(); + foreach (self::getAllProviders() as $provider) { + $info = $provider->getInfo(); + + $status = self::INSTALLED; + $location = false; + $statusMessage = false; + + $availableOrMessage = $provider->isAvailable(); + if ($availableOrMessage !== true) { + $status = self::NOT_INSTALLED; + if (is_string($availableOrMessage)) { + $statusMessage = $availableOrMessage; + } + } else { + $workingOrError = $provider->isWorking(); + if ($workingOrError === true) { // if the implementation is configured correctly, get the location + $locInfo = array('ip' => IP::getIpFromHeader(), + 'lang' => Common::getBrowserLanguage(), + 'disable_fallbacks' => true); + + $location = $provider->getLocation($locInfo); + $location = self::prettyFormatLocation($location, $newline, $includeExtra); + } else // otherwise set an error message describing why + { + $status = self::BROKEN; + $statusMessage = $workingOrError; + } + } + + $info['status'] = $status; + $info['statusMessage'] = $statusMessage; + $info['location'] = $location; + $info['isVisible'] = $provider->isVisible(); + $info['usageWarning'] = $provider->getUsageWarning(); + + $allInfo[$info['id']] = $info; + } + + uasort($allInfo, function ($a, $b) { + if ($a['order'] == $b['order']) { + return strcmp($a['id'], $b['id']); + } + return $a['order'] - $b['order']; + }); + + return $allInfo; + } + + /** + * Returns the ID of the currently used location provider. + * + * The used provider is stored in the 'usercountry.location_provider' option. + * + * This function should not be called by the Tracker. + * + * @return string + */ + public static function getCurrentProviderId() + { + try { + $optionValue = Option::get(self::CURRENT_PROVIDER_OPTION_NAME); + } catch (\Exception $e) { + $optionValue = false; + } + return $optionValue === false ? self::getDefaultProviderId() : $optionValue; + } + + /** + * Returns the provider instance of the current location provider. + * + * This function should not be called by the Tracker. + * + * @return \Piwik\Plugins\UserCountry\LocationProvider|null + */ + public static function getCurrentProvider() + { + return self::getProviderById(self::getCurrentProviderId()); + } + + /** + * Sets the provider to use when tracking. + * + * @param string $providerId The ID of the provider to use. + * @return LocationProvider The new current provider. + * @throws Exception If the provider ID is invalid. + */ + public static function setCurrentProvider($providerId) + { + $provider = self::getProviderById($providerId); + if (empty($provider)) { + throw new Exception( + "Invalid provider ID '$providerId'. The provider either does not exist or is not available" + ); + } + + $provider->activate(); + + Option::set(self::CURRENT_PROVIDER_OPTION_NAME, $providerId); + Cache::clearCacheGeneral(); + return $provider; + } + + /** + * Returns the default provider id to use + * + * @return string + */ + public static function getDefaultProviderId() + { + if (TrackerConfig::getBoolConfigValue('enable_default_location_provider', false)) { + return DefaultProvider::ID; + } + + return DisabledProvider::ID; + } + + /** + * Returns a provider instance by ID or false if the ID is invalid or unavailable. + * + * @param string $providerId + * @return \Piwik\Plugins\UserCountry\LocationProvider|null + */ + public static function getProviderById($providerId) + { + foreach (self::getAllProviders() as $provider) { + if ($provider->getId() == $providerId && $provider->isAvailable()) { + return $provider; + } + } + + return null; + } + + public function getId() + { + $info = $this->getInfo(); + + return $info['id']; + } + + /** + * Tries to fill in any missing information in a location result. + * + * This method will try to set the continent code, continent name and country code + * using other information. + * + * Note: This function must always be called by location providers in getLocation. + * + * @param array $location The location information to modify. + */ + public function completeLocationResult(&$location) + { + // fill in continent code if country code is present + if ( + empty($location[self::CONTINENT_CODE_KEY]) + && !empty($location[self::COUNTRY_CODE_KEY]) + ) { + $countryCode = strtolower($location[self::COUNTRY_CODE_KEY]); + $location[self::CONTINENT_CODE_KEY] = Common::getContinent($countryCode); + } + + // fill in continent name if continent code is present + if ( + empty($location[self::CONTINENT_NAME_KEY]) + && !empty($location[self::CONTINENT_CODE_KEY]) + ) { + $continentCode = strtolower($location[self::CONTINENT_CODE_KEY]); + $location[self::CONTINENT_NAME_KEY] = continentTranslate($continentCode); + } + + // fill in country name if country code is present + if ( + empty($location[self::COUNTRY_NAME_KEY]) + && !empty($location[self::COUNTRY_CODE_KEY]) + ) { + $countryCode = strtolower($location[self::COUNTRY_CODE_KEY]); + $location[self::COUNTRY_NAME_KEY] = countryTranslate($countryCode); + } + + // deal w/ improper latitude/longitude & round proper values + if (!empty($location[self::LATITUDE_KEY])) { + if (is_numeric($location[self::LATITUDE_KEY])) { + $location[self::LATITUDE_KEY] = round($location[self::LATITUDE_KEY], self::GEOGRAPHIC_COORD_PRECISION); + } else { + unset($location[self::LATITUDE_KEY]); + } + } + + if (!empty($location[self::LONGITUDE_KEY])) { + if (is_numeric($location[self::LONGITUDE_KEY])) { + $location[self::LONGITUDE_KEY] = round($location[self::LONGITUDE_KEY], self::GEOGRAPHIC_COORD_PRECISION); + } else { + unset($location[self::LONGITUDE_KEY]); + } + } + } + + /** + * Returns a prettified location result. + * + * @param array|false $locationInfo + * @param string $newline The line separator (ie, \n or
). + * @param bool $includeExtra Whether to include ISP/Organization info. + * @return string + */ + public static function prettyFormatLocation($locationInfo, $newline = "\n", $includeExtra = false) + { + if ($locationInfo === false) { + return Piwik::translate('General_Unknown'); + } + + // add latitude/longitude line + $lines = array(); + if ( + !empty($locationInfo[self::LATITUDE_KEY]) + && !empty($locationInfo[self::LONGITUDE_KEY]) + ) { + $lines[] = '(' . $locationInfo[self::LATITUDE_KEY] . ', ' . $locationInfo[self::LONGITUDE_KEY] . ')'; + } + + // add city/state line + $cityState = array(); + if (!empty($locationInfo[self::CITY_NAME_KEY])) { + $cityState[] = $locationInfo[self::CITY_NAME_KEY]; + } + + if (!empty($locationInfo[self::REGION_CODE_KEY])) { + $cityState[] = $locationInfo[self::REGION_CODE_KEY]; + } elseif (!empty($locationInfo[self::REGION_NAME_KEY])) { + $cityState[] = $locationInfo[self::REGION_NAME_KEY]; + } + + if (!empty($cityState)) { + $lines[] = implode(', ', $cityState); + } + + // add postal code line + if (!empty($locationInfo[self::POSTAL_CODE_KEY])) { + $lines[] = $locationInfo[self::POSTAL_CODE_KEY]; + } + + // add country line + if (!empty($locationInfo[self::COUNTRY_NAME_KEY])) { + $lines[] = $locationInfo[self::COUNTRY_NAME_KEY]; + } elseif (!empty($locationInfo[self::COUNTRY_CODE_KEY])) { + $lines[] = $locationInfo[self::COUNTRY_CODE_KEY]; + } + + // add extra information (ISP/Organization) + if ($includeExtra) { + $lines[] = ''; + + $unknown = Piwik::translate('General_Unknown'); + + $org = !empty($locationInfo[self::ORG_KEY]) ? $locationInfo[self::ORG_KEY] : $unknown; + $lines[] = "Org: $org"; + + $isp = !empty($locationInfo[self::ISP_KEY]) ? $locationInfo[self::ISP_KEY] : $unknown; + $lines[] = "ISP: $isp"; + } + + return implode($newline, $lines); + } + + /** + * Returns an IP address from an array that was passed into getLocation. This + * will return an IPv4 address or IPv6 address. + * + * @param array $info Must have 'ip' key. + * @return string + */ + protected function getIpFromInfo($info) + { + $ip = \Matomo\Network\IP::fromStringIP($info['ip']); + + if ($ip instanceof \Matomo\Network\IPv6 && $ip->isMappedIPv4()) { + return $ip->toIPv4String(); + } else { + return $ip->toString(); + } + } + + /** + * Returns true if the location provider can be used for security checks based + * on location, such as determining the current country where the user logs in from. + */ + public function canBeUsedForLocationBasedSecurityChecks(): bool + { + return false; + } +} diff --git a/plugins/UserCountry/LocationProvider/DefaultProvider.php b/plugins/UserCountry/LocationProvider/DefaultProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..952619857dff83fb6017aad2942924eca787788c --- /dev/null +++ b/plugins/UserCountry/LocationProvider/DefaultProvider.php @@ -0,0 +1,198 @@ +getCountryUsingProviderExtensionIfAvailable($info['ip']); + + if (empty($country)) { + $enableLanguageToCountryGuess = Config::getInstance()->Tracker['enable_language_to_country_guess']; + + if (empty($info['lang'])) { + $info['lang'] = Common::getBrowserLanguage(); + } + $country = Common::getCountry($info['lang'], $enableLanguageToCountryGuess, $info['ip']); + } + + $location = [parent::COUNTRY_CODE_KEY => $country]; + $this->completeLocationResult($location); + + return $location; + } + + + private function getCountryUsingProviderExtensionIfAvailable($ipAddress) + { + if ( + !Manager::getInstance()->isPluginInstalled('Provider') + || !class_exists('Piwik\Plugins\Provider\Provider') + || Common::getRequestVar('dp', 0, 'int') === 1 + ) { + return false; + } + + $privacyConfig = new PrivacyManagerConfig(); + + // when using anonymized ip for enrichment we skip this check + if ($privacyConfig->useAnonymizedIpForVisitEnrichment) { + return false; + } + + $hostname = $this->getHost($ipAddress); + $hostnameExtension = ProviderProvider::getCleanHostname($hostname); + + $hostnameDomain = substr($hostnameExtension, 1 + strrpos($hostnameExtension, '.')); + if ($hostnameDomain == 'uk') { + $hostnameDomain = 'gb'; + } + + /** @var RegionDataProvider $regionDataProvider */ + $regionDataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider'); + + if (array_key_exists($hostnameDomain, $regionDataProvider->getCountryList())) { + return $hostnameDomain; + } + + return false; + } + + /** + * Returns the hostname given the IP address string + * + * @param string $ipStr IP Address + * @return string hostname (or human-readable IP address) + */ + protected function getHost($ipStr) + { + $ip = IP::fromStringIP($ipStr); + + $host = $ip->getHostname(); + $host = ($host === null ? $ipStr : $host); + + return trim(strtolower($host)); + } + + /** + * Returns whether this location provider is available. + * + * @return bool + */ + public function isAvailable() + { + return TrackerConfig::getBoolConfigValue('enable_default_location_provider', false); + } + + + /** + * Returns whether this location provider is visible. + * + * @return bool + */ + public function isVisible() + { + return TrackerConfig::getBoolConfigValue('enable_default_location_provider', false); + } + + /** + * Returns whether this location provider is working correctly. + * + * This implementation is always working correctly. + * + * @return bool always true + */ + public function isWorking() + { + return true; + } + + /** + * Returns an array describing the types of location information this provider will + * return. + * + * This provider supports the following types of location info: + * - continent code + * - continent name + * - country code + * - country name + * + * @return array + */ + public function getSupportedLocationInfo() + { + return [self::CONTINENT_CODE_KEY => true, + self::CONTINENT_NAME_KEY => true, + self::COUNTRY_CODE_KEY => true, + self::COUNTRY_NAME_KEY => true]; + } + + /** + * Returns information about this location provider. Contains an id, title & description: + * + * array( + * 'id' => 'default', + * 'title' => '...', + * 'description' => '...' + * ); + * + * @return array + */ + public function getInfo() + { + $desc = '

' . Piwik::translate('UserCountry_DefaultLocationProviderDesc1') . ' ' + . Piwik::translate( + 'UserCountry_DefaultLocationProviderDesc2', + ['', '', '', ''] + ) + . '

' . Url::getExternalLinkTag('https://matomo.org/faq/how-to/faq_163') + . Piwik::translate('UserCountry_HowToInstallGeoIPDatabases') + . '

'; + return ['id' => self::ID, 'title' => self::TITLE, 'description' => $desc, 'order' => 1]; + } + + public function getUsageWarning(): ?string + { + $comment = Piwik::translate('UserCountry_DefaultLocationProviderDesc1') . ' '; + $comment .= Piwik::translate('UserCountry_DefaultLocationProviderDesc2', [ + Url::getExternalLinkTag('https://matomo.org/docs/geo-locate/'), '', '', '', + ]); + + return $comment; + } +} diff --git a/plugins/UserCountry/LocationProvider/DisabledProvider.php b/plugins/UserCountry/LocationProvider/DisabledProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..f0e2016f0998e25308aa3e2b9d246e02f12b04fb --- /dev/null +++ b/plugins/UserCountry/LocationProvider/DisabledProvider.php @@ -0,0 +1,97 @@ + 'default', + * 'title' => '...', + * 'description' => '...' + * ); + * + * @return array + */ + public function getInfo() + { + $desc = Piwik::translate('UserCountry_DisabledLocationProvider'); + return array('id' => self::ID, 'title' => self::TITLE, 'description' => $desc, 'order' => 0); + } + + public function getUsageWarning(): ?string + { + $comment = Piwik::translate('UserCountry_DefaultLocationProviderDesc1') . ' '; + $comment .= Piwik::translate('UserCountry_DefaultLocationProviderDesc2', array( + Url::getExternalLinkTag('https://matomo.org/docs/geo-locate/'), '', '', '', + )); + + return $comment; + } +} diff --git a/plugins/UserCountry/Menu.php b/plugins/UserCountry/Menu.php new file mode 100644 index 0000000000000000000000000000000000000000..8c6e6e787d01bf49fa389564629cc5e9c3d7c67a --- /dev/null +++ b/plugins/UserCountry/Menu.php @@ -0,0 +1,27 @@ +addSystemItem( + 'UserCountry_Geolocation', + $this->urlForAction('adminIndex'), + $order = 30 + ); + } + } +} diff --git a/plugins/UserCountry/ProfileSummary/LocationSummary.php b/plugins/UserCountry/ProfileSummary/LocationSummary.php new file mode 100644 index 0000000000000000000000000000000000000000..38180270a0e2517972de615d0f72f2bb512ade04 --- /dev/null +++ b/plugins/UserCountry/ProfileSummary/LocationSummary.php @@ -0,0 +1,79 @@ +profile['countries'])) { + return ''; + } + + $view = new View('@UserCountry/_profileSummary.twig'); + $view->visitorData = $this->profile; + + if ( + Common::getRequestVar('showMap', 1) == 1 + && !empty($view->visitorData['hasLatLong']) + && \Piwik\Plugin\Manager::getInstance()->isPluginLoaded('UserCountryMap') + ) { + $view->userCountryMapUrl = $this->getUserCountryMapUrlForVisitorProfile(); + } + + return $view->render(); + } + + private function getUserCountryMapUrlForVisitorProfile() + { + $params = array( + 'module' => 'UserCountryMap', + 'action' => 'realtimeMap', + 'segment' => Live\Live::getSegmentWithVisitorId(), + 'visitorId' => false, + 'changeVisitAlpha' => 0, + 'removeOldVisits' => 0, + 'realtimeWindow' => 'false', + 'showFooterMessage' => 0, + 'showDateTime' => 0, + 'doNotRefreshVisits' => 1, + ); + return Url::getCurrentQueryStringWithParametersModified($params); + } + + /** + * @inheritdoc + */ + public function getOrder() + { + return 100; + } +} diff --git a/plugins/UserCountry/RecordBuilders/Locations.php b/plugins/UserCountry/RecordBuilders/Locations.php new file mode 100644 index 0000000000000000000000000000000000000000..b764dfd96b760e4077dfa98d385f8c7558d36446 --- /dev/null +++ b/plugins/UserCountry/RecordBuilders/Locations.php @@ -0,0 +1,195 @@ + Archiver::COUNTRY_FIELD, + Archiver::REGION_RECORD_NAME => Archiver::REGION_FIELD, + Archiver::CITY_RECORD_NAME => Archiver::CITY_FIELD, + ]; + + private $latLongForCities = []; + + public function __construct() + { + parent::__construct(); + + $this->columnToSortByBeforeTruncation = Metrics::INDEX_NB_VISITS; + } + + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + $maxRowsInTable = PiwikConfig::getInstance()->General['datatable_archiving_maximum_rows_standard']; + + return [ + Record::make(Record::TYPE_BLOB, Archiver::COUNTRY_RECORD_NAME), + Record::make(Record::TYPE_BLOB, Archiver::REGION_RECORD_NAME) + ->setMaxRowsInTable($maxRowsInTable), + Record::make(Record::TYPE_BLOB, Archiver::CITY_RECORD_NAME) + ->setMaxRowsInTable($maxRowsInTable), + + Record::make(Record::TYPE_NUMERIC, Archiver::DISTINCT_COUNTRIES_METRIC) + ->setIsCountOfBlobRecordRows(Archiver::COUNTRY_RECORD_NAME), + ]; + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + try { + /** @var DataTable[] $records */ + $records = []; + foreach ($this->dimensions as $recordName => $dimension) { + $records[$recordName] = new DataTable(); + } + + $logAggregator = $archiveProcessor->getLogAggregator(); + + $this->aggregateFromVisits($records, $logAggregator); + $this->aggregateFromConversions($records, $logAggregator); + + $this->setLatitudeLongitude($records[Archiver::CITY_RECORD_NAME]); + + $records[Archiver::DISTINCT_COUNTRIES_METRIC] = $records[Archiver::COUNTRY_RECORD_NAME]->getRowsCount(); + + return $records; + } finally { + unset($this->latLongForCities); + } + } + + /** + * @param DataTable[] $records + */ + protected function aggregateFromVisits(array $records, LogAggregator $logAggregator): void + { + $additionalSelects = array('MAX(log_visit.location_latitude) as location_latitude', + 'MAX(log_visit.location_longitude) as location_longitude'); + $query = $logAggregator->queryVisitsByDimension(array_values($this->dimensions), $where = false, $additionalSelects); + if ($query === false) { + return; + } + + while ($row = $query->fetch()) { + $this->makeRegionCityLabelsUnique($row); + $this->rememberCityLatLong($row); + + $columns = [ + Metrics::INDEX_NB_UNIQ_VISITORS => $row[Metrics::INDEX_NB_UNIQ_VISITORS], + Metrics::INDEX_NB_VISITS => $row[Metrics::INDEX_NB_VISITS], + Metrics::INDEX_NB_ACTIONS => $row[Metrics::INDEX_NB_ACTIONS], + Metrics::INDEX_NB_USERS => $row[Metrics::INDEX_NB_USERS], + Metrics::INDEX_MAX_ACTIONS => $row[Metrics::INDEX_MAX_ACTIONS], + Metrics::INDEX_SUM_VISIT_LENGTH => $row[Metrics::INDEX_SUM_VISIT_LENGTH], + Metrics::INDEX_BOUNCE_COUNT => $row[Metrics::INDEX_BOUNCE_COUNT], + Metrics::INDEX_NB_VISITS_CONVERTED => $row[Metrics::INDEX_NB_VISITS_CONVERTED], + ]; + + foreach ($records as $recordName => $dataTable) { + $dimension = $this->dimensions[$recordName]; + $dataTable->sumRowWithLabel($row[$dimension], $columns); + } + } + } + + /** + * Makes sure the region and city of a query row are unique. + * + * @param array $row + */ + private function makeRegionCityLabelsUnique(array &$row): void + { + // remove the location separator from the region/city/country we get from the query + foreach ($this->dimensions as $column) { + $row[$column] = str_replace(Archiver::LOCATION_SEPARATOR, '', $row[$column] ?? ''); + } + + // set city first, as containing region might be manipulated afterwards if not empty + if (!empty($row[Archiver::CITY_FIELD])) { + $row[Archiver::CITY_FIELD] = $row[Archiver::CITY_FIELD] . Archiver::LOCATION_SEPARATOR . $row[Archiver::REGION_FIELD] . Archiver::LOCATION_SEPARATOR . $row[Archiver::COUNTRY_FIELD]; + } + + if (!empty($row[Archiver::REGION_FIELD])) { + $row[Archiver::REGION_FIELD] = $row[Archiver::REGION_FIELD] . Archiver::LOCATION_SEPARATOR . $row[Archiver::COUNTRY_FIELD]; + } + } + + protected function rememberCityLatLong(array $row): void + { + if ( + !empty($row[Archiver::CITY_FIELD]) + && !empty($row[Archiver::LATITUDE_FIELD]) + && !empty($row[Archiver::LONGITUDE_FIELD]) + && empty($this->latLongForCities[$row[Archiver::CITY_FIELD]]) + ) { + $this->latLongForCities[$row[Archiver::CITY_FIELD]] = array($row[Archiver::LATITUDE_FIELD], $row[Archiver::LONGITUDE_FIELD]); + } + } + + /** + * @param DataTable[] $records + */ + protected function aggregateFromConversions(array $records, LogAggregator $logAggregator): void + { + $query = $logAggregator->queryConversionsByDimension(array_values($this->dimensions)); + + while ($row = $query->fetch()) { + $this->makeRegionCityLabelsUnique($row); + + foreach ($records as $recordName => $table) { + $idGoal = (int) $row['idgoal']; + $columns = [ + Metrics::INDEX_GOALS => [ + $idGoal => Metrics::makeGoalColumnsRow($idGoal, $row), + ], + ]; + + $dimension = $this->dimensions[$recordName]; + $table->sumRowWithLabel($row[$dimension], $columns); + } + } + + foreach ($records as $table) { + $table->filter(DataTable\Filter\EnrichRecordWithGoalMetricSums::class); + } + } + + /** + * Utility method, appends latitude/longitude pairs to city table labels, if that data + * exists for the city. + */ + private function setLatitudeLongitude(DataTable $tableCity): void + { + foreach ($tableCity->getRows() as $row) { + $label = $row->getColumn('label'); + if (isset($this->latLongForCities[$label])) { + // get lat/long for city + list($lat, $long) = $this->latLongForCities[$label]; + $lat = round($lat, LocationProvider::GEOGRAPHIC_COORD_PRECISION); + $long = round($long, LocationProvider::GEOGRAPHIC_COORD_PRECISION); + + // set latitude + longitude metadata + $row->setMetadata('lat', $lat); + $row->setMetadata('long', $long); + } + } + } +} diff --git a/plugins/UserCountry/Reports/Base.php b/plugins/UserCountry/Reports/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..8e453f2563dd8d196c6c2e36530d6295bfe5272a --- /dev/null +++ b/plugins/UserCountry/Reports/Base.php @@ -0,0 +1,77 @@ +categoryId = 'General_Visitors'; + $this->subcategoryId = 'UserCountry_SubmenuLocations'; + $this->hasGoalMetrics = true; + } + + protected function getGeoIPReportDocSuffix() + { + return Piwik::translate( + 'UserCountry_GeoIPDocumentationSuffix', + [ + Url::getExternalLinkTag('http://www.maxmind.com/?rId=piwik'), + '', + Url::getExternalLinkTag('http://www.maxmind.com/en/city_accuracy?rId=piwik'), + '', + ] + ); + } + + /** + * Checks if a datatable for a view is empty and if so, displays a message in the footer + * telling users to configure GeoIP. + */ + protected function checkIfNoDataForGeoIpReport(ViewDataTable $view) + { + $view->config->filters[] = function ($dataTable) use ($view) { + // if there's only one row whose label is 'Unknown', display a message saying there's no data + if ( + $dataTable->getRowsCount() == 1 + && $dataTable->getFirstRow()->getColumn('label') == Piwik::translate('General_Unknown') + ) { + $footerMessage = Piwik::translate('UserCountry_NoDataForGeoIPReport1'); + + $userCountry = new UserCountry(); + // if GeoIP is working, don't display this part of the message + if (!$userCountry->isGeoIPWorking()) { + $params = ['module' => 'UserCountry', 'action' => 'adminIndex']; + $footerMessage .= ' ' . Piwik::translate( + 'UserCountry_NoDataForGeoIPReport2', + [ + '', + '', + Url::getExternalLinkTag('https://db-ip.com/?refid=mtm'), + '', + ] + ); + } else { + $footerMessage .= ' ' . Piwik::translate( + 'UserCountry_ToGeolocateOldVisits', + [Url::getExternalLinkTag('https://matomo.org/faq/how-to/faq_167'), ''] + ); + } + + $view->config->show_footer_message = $footerMessage; + } + }; + } +} diff --git a/plugins/UserCountry/Reports/GetCity.php b/plugins/UserCountry/Reports/GetCity.php new file mode 100644 index 0000000000000000000000000000000000000000..bd6b2b067e1112b6351b49c99143b8c5d9f33779 --- /dev/null +++ b/plugins/UserCountry/Reports/GetCity.php @@ -0,0 +1,38 @@ +dimension = new City(); + $this->name = Piwik::translate('UserCountry_City'); + $this->documentation = Piwik::translate('UserCountry_getCityDocumentation') . '
' . $this->getGeoIPReportDocSuffix(); + $this->order = 10; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $view->config->show_exclude_low_population = false; + $view->config->documentation = $this->documentation; + + $view->requestConfig->filter_limit = 5; + + $this->checkIfNoDataForGeoIpReport($view); + } +} diff --git a/plugins/UserCountry/Reports/GetContinent.php b/plugins/UserCountry/Reports/GetContinent.php new file mode 100644 index 0000000000000000000000000000000000000000..13016a7059cae5f16bc97e952c6d4e8fcf49929f --- /dev/null +++ b/plugins/UserCountry/Reports/GetContinent.php @@ -0,0 +1,54 @@ +dimension = new Continent(); + $this->name = Piwik::translate('UserCountry_Continent'); + $this->documentation = Piwik::translate('UserCountry_getContinentDocumentation'); + $this->order = 6; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + $widgetsList->addWidgetConfig($factory->createContainerWidget('Continent')); + + $widgetsList->addToContainerWidget('Continent', $factory->createWidget()); + + $widget = $factory->createWidget()->setAction('getDistinctCountries')->setName(''); + $widgetsList->addToContainerWidget('Continent', $widget); + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $view->config->show_exclude_low_population = false; + $view->config->show_search = false; + $view->config->show_offset_information = false; + $view->config->show_pagination_control = false; + $view->config->documentation = $this->documentation; + + if (!$view->isViewDataTableId(Evolution::ID)) { + $view->config->show_limit_control = false; + } + } +} diff --git a/plugins/UserCountry/Reports/GetCountry.php b/plugins/UserCountry/Reports/GetCountry.php new file mode 100644 index 0000000000000000000000000000000000000000..aeaba6c9f83dc81d19e21a5a6ddcadf97191fad4 --- /dev/null +++ b/plugins/UserCountry/Reports/GetCountry.php @@ -0,0 +1,49 @@ +dimension = new Country(); + $this->name = Piwik::translate('UserCountry_Country'); + $this->documentation = Piwik::translate('UserCountry_getCountryDocumentation'); + $this->order = 5; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $view->config->show_exclude_low_population = false; + $view->config->documentation = $this->documentation; + + $view->requestConfig->filter_limit = 5; + + if (LocationProvider::getCurrentProviderId() == LocationProvider\DefaultProvider::ID) { + // if we're using the default location provider, add a note explaining how it works + $footerMessage = Piwik::translate("General_Note") . ': ' + . Piwik::translate( + 'UserCountry_DefaultLocationProviderExplanation', + [Url::getExternalLinkTag('https://matomo.org/docs/geo-locate/'), ''] + ); + + $view->config->show_footer_message = $footerMessage; + } + } +} diff --git a/plugins/UserCountry/Reports/GetRegion.php b/plugins/UserCountry/Reports/GetRegion.php new file mode 100644 index 0000000000000000000000000000000000000000..d40113715b0aee34835b1840b67418b83960a289 --- /dev/null +++ b/plugins/UserCountry/Reports/GetRegion.php @@ -0,0 +1,38 @@ +dimension = new Region(); + $this->name = Piwik::translate('UserCountry_Region'); + $this->documentation = Piwik::translate('UserCountry_getRegionDocumentation') . '
' . $this->getGeoIPReportDocSuffix(); + $this->order = 7; + } + + public function configureView(ViewDataTable $view) + { + $view->config->show_flatten_table = false; + $view->config->show_flatten_table_export = false; + $view->config->show_exclude_low_population = false; + $view->config->documentation = $this->documentation; + + $view->requestConfig->filter_limit = 5; + + $this->checkIfNoDataForGeoIpReport($view); + } +} diff --git a/plugins/UserCountry/UserCountry.php b/plugins/UserCountry/UserCountry.php new file mode 100644 index 0000000000000000000000000000000000000000..668de2ec72ec82e79cb83019e38592467cb598e9 --- /dev/null +++ b/plugins/UserCountry/UserCountry.php @@ -0,0 +1,108 @@ + 'getClientSideTranslationKeys', + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'AssetManager.getJavaScriptFiles' => 'getJsFiles', + 'Tracker.setTrackerCacheGeneral' => 'setTrackerCacheGeneral', + 'Insights.addReportToOverview' => 'addReportToInsightsOverview', + ); + } + + public function getClientSideTranslationKeys(&$translations) + { + $translations[] = 'General_InfoFor'; + $translations[] = 'General_NotInstalled'; + $translations[] = 'General_Installed'; + $translations[] = 'General_Broken'; + $translations[] = 'UserCountry_CurrentLocationIntro'; + $translations[] = 'General_Refresh'; + $translations[] = 'UserCountry_CannotLocalizeLocalIP'; + $translations[] = 'UserCountry_NoProviders'; + $translations[] = 'General_Disabled'; + $translations[] = 'UserCountry_GeolocationPageDesc'; + $translations[] = 'UserCountry_LocationProvider'; + $translations[] = 'UserCountry_Geolocation'; + $translations[] = 'UserCountry_DistinctCountries'; + } + + public function addReportToInsightsOverview(&$reports) + { + $reports['UserCountry_getCountry'] = array(); + } + + public function setTrackerCacheGeneral(&$cache) + { + $cache['currentLocationProviderId'] = LocationProvider::getCurrentProviderId(); + } + + public function getStylesheetFiles(&$stylesheets) + { + $stylesheets[] = "plugins/UserCountry/stylesheets/userCountry.less"; + } + + public function getJsFiles(&$jsFiles) + { + } + + /** + * Returns a list of country codes for a given continent code. + * + * @param string $continent The continent code. + * @return array + */ + public static function getCountriesForContinent($continent) + { + /** @var RegionDataProvider $regionDataProvider */ + $regionDataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider'); + + $result = array(); + $continent = strtolower($continent); + foreach ($regionDataProvider->getCountryList() as $countryCode => $continentCode) { + if ($continent == $continentCode) { + $result[] = $countryCode; + } + } + return array('SQL' => "'" . implode("', '", $result) . "', ?", + 'bind' => '-'); // HACK: SegmentExpression requires a $bind, even if there's nothing to bind + } + + /** + * Returns true if a GeoIP provider is installed & working, false if otherwise. + * + * @return bool + */ + public function isGeoIPWorking() + { + $provider = LocationProvider::getCurrentProvider(); + return $provider instanceof GeoIp2 + && $provider->isAvailable() === true + && $provider->isWorking() === true; + } + + public static function isGeoLocationAdminEnabled() + { + return (bool) Config::getInstance()->General['enable_geolocation_admin']; + } +} diff --git a/plugins/UserCountry/VisitorDetails.php b/plugins/UserCountry/VisitorDetails.php new file mode 100644 index 0000000000000000000000000000000000000000..feedcd0ca88544ed0d5d51f9a83489b120b9c217 --- /dev/null +++ b/plugins/UserCountry/VisitorDetails.php @@ -0,0 +1,197 @@ +getContinent(); + $visitor['continentCode'] = $this->getContinentCode(); + $visitor['country'] = $this->getCountryName(); + $visitor['countryCode'] = $this->getCountryCode(); + $visitor['countryFlag'] = $this->getCountryFlag(); + $visitor['region'] = $this->getRegionName(); + $visitor['regionCode'] = $this->getRegionCode(); + $visitor['city'] = $this->getCityName(); + $visitor['location'] = $this->getPrettyLocation(); + $visitor['latitude'] = $this->getLatitude(); + $visitor['longitude'] = $this->getLongitude(); + } + + protected function getCountryCode() + { + return $this->details['location_country']; + } + + protected function getCountryName() + { + return countryTranslate($this->getCountryCode()); + } + + protected function getCountryFlag() + { + return getFlagFromCode($this->getCountryCode()); + } + + protected function getContinent() + { + return continentTranslate($this->getContinentCode()); + } + + protected function getContinentCode() + { + return Common::getContinent($this->details['location_country']); + } + + protected function getCityName() + { + if (!empty($this->details['location_city'])) { + return $this->details['location_city']; + } + + return null; + } + + protected function getRegionName() + { + $region = $this->getRegionCode(); + if ($region != '' && $region != Visit::UNKNOWN_CODE) { + return getRegionNameFromCodes( + $this->details['location_country'], + $region + ); + } + + return null; + } + + protected function getRegionCode() + { + return $this->details['location_region']; + } + + protected function getPrettyLocation() + { + $parts = array(); + + $city = $this->getCityName(); + if (!empty($city)) { + $parts[] = $city; + } + $region = $this->getRegionName(); + if (!empty($region)) { + $parts[] = $region; + } + + // add country & return concatenated result + $parts[] = $this->getCountryName(); + + return implode(', ', $parts); + } + + protected function getLatitude() + { + if (!empty($this->details['location_latitude'])) { + return $this->details['location_latitude']; + } + + return null; + } + + protected function getLongitude() + { + if (!empty($this->details['location_longitude'])) { + return $this->details['location_longitude']; + } + + return null; + } + + + private $cities = array(); + private $countries = array(); + private $continents = array(); + + public function initProfile($visits, &$profile) + { + $this->cities = array(); + $this->continents = array(); + $this->countries = array(); + $profile['hasLatLong'] = false; + } + + public function handleProfileVisit($visit, &$profile) + { + // realtime map only checks for latitude + $hasLatitude = $visit->getColumn('latitude') !== false; + if ($hasLatitude) { + $profile['hasLatLong'] = true; + } + + $countryCode = $visit->getColumn('countryCode'); + if (!isset($this->countries[$countryCode])) { + $this->countries[$countryCode] = 0; + } + ++$this->countries[$countryCode]; + + $continentCode = $visit->getColumn('continentCode'); + if (!isset($this->continents[$continentCode])) { + $this->continents[$continentCode] = 0; + } + ++$this->continents[$continentCode]; + + if ($countryCode && !array_key_exists($countryCode, $this->cities)) { + $this->cities[$countryCode] = array(); + } + $city = $visit->getColumn('city'); + if (!empty($city)) { + $this->cities[$countryCode][] = $city; + } + } + + public function finalizeProfile($visits, &$profile) + { + // transform country/continents/search keywords into something that will look good in XML + $profile['countries'] = $profile['continents'] = array(); + + // sort by visit/action + asort($this->continents); + foreach ($this->continents as $continentCode => $nbVisits) { + $profile['continents'][] = array( + 'continent' => $continentCode, + 'nb_visits' => $nbVisits, + 'prettyName' => \Piwik\Plugins\UserCountry\continentTranslate($continentCode), + ); + } + + // sort by visit/action + asort($this->countries); + + foreach ($this->countries as $countryCode => $nbVisits) { + $countryInfo = array( + 'country' => $countryCode, + 'nb_visits' => $nbVisits, + 'flag' => \Piwik\Plugins\UserCountry\getFlagFromCode($countryCode), + 'prettyName' => \Piwik\Plugins\UserCountry\countryTranslate($countryCode), + ); + if (!empty($this->cities[$countryCode])) { + $countryInfo['cities'] = array_unique($this->cities[$countryCode]); + } + $profile['countries'][] = $countryInfo; + } + } +} diff --git a/plugins/UserCountry/VisitorGeolocator.php b/plugins/UserCountry/VisitorGeolocator.php new file mode 100644 index 0000000000000000000000000000000000000000..a4148cc2a2a772702e850a16a5e4afcecb2af31f --- /dev/null +++ b/plugins/UserCountry/VisitorGeolocator.php @@ -0,0 +1,324 @@ + LocationProvider::COUNTRY_CODE_KEY, + 'location_region' => LocationProvider::REGION_CODE_KEY, + 'location_city' => LocationProvider::CITY_NAME_KEY, + 'location_latitude' => LocationProvider::LATITUDE_KEY, + 'location_longitude' => LocationProvider::LONGITUDE_KEY, + ); + + /** + * @var Cache + */ + protected static $defaultLocationCache = null; + + /** + * @var LocationProvider + */ + private $provider; + + /** + * @var LocationProvider + */ + private $backupProvider; + + /** + * @var Cache + */ + private $locationCache; + + /** + * @var RawLogDao + */ + protected $dao; + + /** + * @var LoggerInterface + */ + protected $logger; + + public function __construct( + ?LocationProvider $provider = null, + ?LocationProvider $backupProvider = null, + ?Cache $locationCache = null, + ?RawLogDao $dao = null, + ?LoggerInterface $logger = null + ) { + if ($provider === null) { + // note: Common::getCurrentLocationProviderId() uses the tracker cache, which is why it's used here instead + // of accessing the option table + $provider = LocationProvider::getProviderById(Common::getCurrentLocationProviderId()); + + if (empty($provider)) { + Common::printDebug("GEO: no current location provider sent, falling back to '" . LocationProvider::getDefaultProviderId() . "' one."); + + $provider = $this->getDefaultProvider(); + } + } + $this->provider = $provider; + + $this->backupProvider = $backupProvider ?: $this->getDefaultProvider(); + $this->locationCache = $locationCache ?: self::getDefaultLocationCache(); + $this->dao = $dao ?: new RawLogDao(); + $this->logger = $logger ?: StaticContainer::get(LoggerInterface::class); + } + + public function getLocation($userInfo, $useClassCache = true) + { + $userInfoKey = md5(implode(',', $userInfo)); + if ( + $useClassCache + && $this->locationCache->contains($userInfoKey) + ) { + return $this->locationCache->fetch($userInfoKey); + } + + $location = $this->getLocationObject($this->provider, $userInfo); + + if (empty($location)) { + $providerId = $this->provider->getId(); + Common::printDebug("GEO: couldn't find a location with Geo Module '$providerId'"); + + // Only use the default provider as fallback if the configured one isn't "disabled" + if ($providerId != DisabledProvider::ID && $providerId != $this->backupProvider->getId()) { + Common::printDebug("Using default provider as fallback..."); + + $location = $this->getLocationObject($this->backupProvider, $userInfo); + } + } + + $location = $location ?: array(); + if (empty($location['country_code'])) { + $location['country_code'] = Visit::UNKNOWN_CODE; + } + + $this->locationCache->save($userInfoKey, $location); + + return $location; + } + + /** + * @param array $userInfo + * @return array|false + */ + private function getLocationObject(LocationProvider $provider, $userInfo) + { + $location = $provider->getLocation($userInfo); + $providerId = $provider->getId(); + $ipAddress = $userInfo['ip']; + + if ($location === false) { + return false; + } + + Common::printDebug("GEO: Found IP $ipAddress location (provider '" . $providerId . "'): " . var_export($location, true)); + + return $location; + } + + /** + * Geolocates an existing visit and then updates it if its current attributes are different than + * what was geolocated. Also updates all conversions of a visit. + * + * **This method should NOT be used from within the tracker.** + * + * @param array $visit The visit information. Must contain an `"idvisit"` element and `"location_ip"` element. + * @param bool $useClassCache + * @return array|null The visit properties that were updated in the DB mapped to the updated values. If null, + * required information was missing from `$visit`. + */ + public function attributeExistingVisit($visit, $useClassCache = true) + { + if (empty($visit['idvisit'])) { + $this->logger->debug('Empty idvisit field. Skipping re-attribution..'); + return null; + } + + $idVisit = $visit['idvisit']; + + if (empty($visit['location_ip'])) { + $this->logger->debug('Empty location_ip field for idvisit = %s. Skipping re-attribution.', array('idvisit' => $idVisit)); + return null; + } + + $ip = IPUtils::binaryToStringIP($visit['location_ip']); + $location = $this->getLocation(array('ip' => $ip), $useClassCache); + + $valuesToUpdate = $this->getVisitFieldsToUpdate($visit, $location); + + if (!empty($valuesToUpdate)) { + $this->logger->debug('Updating visit with idvisit = {idVisit} (IP = {ip}). Changes: {changes}', array( + 'idVisit' => $idVisit, + 'ip' => $ip, + 'changes' => json_encode($valuesToUpdate), + )); + + $this->dao->updateVisits($valuesToUpdate, $idVisit); + $this->dao->updateConversions($valuesToUpdate, $idVisit); + } else { + $this->logger->debug('Nothing to update for idvisit = %s (IP = {ip}). Existing location info is same as geolocated.', array( + 'idVisit' => $idVisit, + 'ip' => $ip, + )); + } + + return $valuesToUpdate; + } + + /** + * Returns location log values that are different than the values currently in a log row. + * + * @param array $row The visit row. + * @param array $location The location information. + * @return array The location properties to update. + */ + private function getVisitFieldsToUpdate(array $row, $location) + { + if (isset($location[LocationProvider::COUNTRY_CODE_KEY])) { + $location[LocationProvider::COUNTRY_CODE_KEY] = strtolower($location[LocationProvider::COUNTRY_CODE_KEY]); + } + + $valuesToUpdate = array(); + foreach (self::$logVisitFieldsToUpdate as $column => $locationKey) { + if (empty($location[$locationKey])) { + continue; + } + + $locationPropertyValue = $location[$locationKey]; + $existingPropertyValue = $row[$column]; + + if (!$this->areLocationPropertiesEqual($locationKey, $locationPropertyValue, $existingPropertyValue)) { + $valuesToUpdate[$column] = $locationPropertyValue; + } + } + return $valuesToUpdate; + } + + /** + * Re-geolocate visits within a date range for a specified site (if any). + * + * @param string $from A datetime string to treat as the lower bound. Visits newer than this date are processed. + * @param string $to A datetime string to treat as the upper bound. Visits older than this date are processed. + * @param int|null $idSite If supplied, only visits for this site are re-attributed. + * @param int $iterationStep The number of visits to re-attribute at the same time. + * @param callable|null $onLogProcessed If supplied, this callback is called after every row is processed. + * The processed visit and the updated values are passed to the callback. + */ + public function reattributeVisitLogs($from, $to, $idSite = null, $iterationStep = 1000, $onLogProcessed = null) + { + $visitFieldsToSelect = array_merge(array('idvisit', 'location_ip'), array_keys(VisitorGeolocator::$logVisitFieldsToUpdate)); + + $conditions = array( + array('visit_last_action_time', '>=', $from), + array('visit_last_action_time', '<', $to), + ); + + if (!empty($idSite)) { + $conditions[] = array('idsite', '=', $idSite); + } + + $self = $this; + $this->dao->forAllLogs('log_visit', $visitFieldsToSelect, $conditions, $iterationStep, function ($logs) use ($self, $onLogProcessed) { + foreach ($logs as $row) { + $updatedValues = $self->attributeExistingVisit($row); + + if (!empty($onLogProcessed)) { + $onLogProcessed($row, $updatedValues); + } + } + }, $willDelete = false); + } + + /** + * @return LocationProvider + */ + public function getProvider() + { + return $this->provider; + } + + /** + * @return LocationProvider + */ + public function getBackupProvider() + { + return $this->backupProvider; + } + + private function areLocationPropertiesEqual($locationKey, $locationPropertyValue, $existingPropertyValue) + { + if ( + ($locationKey == LocationProvider::LATITUDE_KEY + || $locationKey == LocationProvider::LONGITUDE_KEY) + && $existingPropertyValue != 0 + ) { + // floating point comparison + return abs(($locationPropertyValue - $existingPropertyValue) / $existingPropertyValue) < self::LAT_LONG_COMPARE_EPSILON; + } else { + return $locationPropertyValue == $existingPropertyValue; + } + } + + private function getDefaultProvider() + { + return LocationProvider::getProviderById(LocationProvider::getDefaultProviderId()); + } + + public static function getDefaultLocationCache() + { + if (self::$defaultLocationCache === null) { + if (class_exists('\Piwik\Cache\Transient')) { + // during the oneclickupdate from 3.x => greater, this class will be loaded, so we have to use it instead of the Matomo namespaced one + self::$defaultLocationCache = new \Piwik\Cache\Transient(); + } else { + self::$defaultLocationCache = new Transient(); + } + } + return self::$defaultLocationCache; + } +} diff --git a/plugins/UserCountry/config/config.php b/plugins/UserCountry/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..303c5aefcd447be12dcffb8376e7ff2f5406f680 --- /dev/null +++ b/plugins/UserCountry/config/config.php @@ -0,0 +1,7 @@ + Piwik\DI::add(array( + Piwik\DI::get('Piwik\Plugins\UserCountry\Diagnostic\GeolocationDiagnostic'), + )), +); diff --git a/plugins/UserCountry/config/tracker.php b/plugins/UserCountry/config/tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/UserCountry/config/tracker.php @@ -0,0 +1,3 @@ +Podeu seleccionar visitants dins d'un interval de latitud/longitud utilitzant %1$s.", + "Latitudes": "Latituds", + "Location": "Ubicació", + "LocationProvider": "Proveïdor de localitzacions", + "LocationsSubcategoryHelp": "La secció \"Ubicacions\" és la millor manera de saber de quins països, continents, regions i ciutats provenen els visitants del vostre lloc web, en forma de taula i mapa. També diu en quin idioma està configurat el seu navegador, que ajuda a identificar visitants internacionals en ubicacions alternatives.", + "Longitude": "Longitud", + "Longitudes": "Longituds", + "MaxMindLinkExplanation": "%1$sFeu clic aquí per saber com generar l'URL de descàrrega%2$s si feu servir la geolocalització MaxMind.", + "NoDataForGeoIPReport1": "No hi ha dades per a aquest informe, ja que no hi ha dades d'ubicació disponibles o les adreces IP dels visitants no es poden geolocalitzar.", + "NoDataForGeoIPReport2": "%1$sCanvieu aquesta configuració%2$s i utilitzeu una base de dades a nivell de %3$sciutat%4$s per obtenir una geolocalització precisa.", + "NoProviders": "No hi ha proveïdors de geolocalització addicionals disponibles. Es recomana %1$sDB-IP%2$s, però requereix el connector GeoIp2. (També podeu instal·lar un complement de geolocalització de tercers des del mercat.)", + "PluginDescription": "Informa de la ubicació dels vostres visitants: país, regió, ciutat i coordenades geogràfiques (latitud i longitud).", + "Region": "Regió", + "SubmenuLocations": "Localització", + "TestIPLocatorFailed": "No s'ha pogut trobar la ubicació de l'adreça IP \"%1$s\". Potser aquest proveïdor no està configurat correctament, la base de dades de geolocalització té un nom incorrecte o és a la carpeta equivocada?", + "ToGeolocateOldVisits": "Utilitzeu %1$saquest script%2$s per obtenir dades d'ubicació de les vostres visites antigues.", + "VisitLocation": "Ubicació del visitant", + "WidgetLocation": "Ubicació del visitant", + "country_a1": "Proxy anònim", + "country_a2": "Proveïdor de satèl·lit", + "country_cat": "Comunitats catalano-parlants", + "country_o1": "Un altre pais", + "getCityDocumentation": "Mostra les ciutats des de les quals es connecten els visitants quan accedeixen al vostre lloc web.", + "getContinentDocumentation": "Mostra des de quin continent es van connectar els visitants en accedir al vostre lloc web.", + "getCountryDocumentation": "Mostra des de quin país es van connectar els visitants quan accedien al vostre lloc web.", + "getRegionDocumentation": "Mostra des de quina regió es van connectar els visitants quan accedien al lloc web." + } +} diff --git a/plugins/UserCountry/lang/cs.json b/plugins/UserCountry/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..ad2b43470d75c1eb8e95c968f13db71f51866c46 --- /dev/null +++ b/plugins/UserCountry/lang/cs.json @@ -0,0 +1,41 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP adresa %s je místní a nemůže být geolokována.", + "City": "Město", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Continents": "Kontinenty", + "Country": "Státy", + "CountryCode": "Kód země", + "CurrentLocationIntro": "Podle tohoto poskytovatele je vaše aktuální poloha", + "DefaultLocationProviderDesc1": "Výchozí poskytovatel umístění odhaduje zemi návštěvníka na základě použitého jazyka.", + "DefaultLocationProviderExplanation": "Používáte výchozího poskytovatele umístění, což znamená, že se Matomo pokusí odhadnout zemi návštěvníka na základě použitého jazyka. %1$sPřečtěte si%2$s, jak nastavit přesnější geolokaci.", + "DistinctCountries": "%s jedinečných států", + "FromDifferentCities": "různá města", + "GeoIPDocumentationSuffix": "Pokud chcete vidět toto hlášení, musíte nastavit GEOIP v administraci na záložce geolokace. Komerční %1$sMaxmind%2$s GeoIP databáze mají větší přesnost než volně šiřitelné. Pokud chcete vidět, jak přesné jsou, klikněte %3$szde%4$s.", + "Geolocation": "Geolokace", + "GeolocationPageDesc": "Na této stránce můžete změnit, jak Matomo určuje umístění návštěvníků.", + "Latitude": "Zeměpisná šířka", + "Latitudes": "Zeměpisné šířky", + "Location": "Umístění", + "LocationProvider": "Poskytovatel umístění", + "Longitude": "Zeměpisná délka", + "Longitudes": "Zeměpisné délky", + "NoDataForGeoIPReport1": "Pro toto hlášení nejsou k dispozici žádná data, protože buď nejsou k dispozici GeoIP data, nebo nemohly být geolokovány IP adresy návštěvníků.", + "NoDataForGeoIPReport2": "Pokud chcete umožnit přesnou geolokaci, změňte nastavení %1$szde%2$s a použijte %3$sdatabázi na úrovni měst%4$s.", + "PluginDescription": "Hlásí umístění vašich návštěvníků: zemi, oblast, město a geografické koordináty (šířku/délku).", + "Region": "Region", + "SubmenuLocations": "Umístění", + "ToGeolocateOldVisits": "Pokud chcete získat data o umístění pro staré návštěvy, použijte skript popisovaný %1$szde%2$s.", + "VisitLocation": "Prohlédnout umístění", + "WidgetLocation": "Umístění návštěvníka", + "country_a1": "Anonymní proxy", + "country_a2": "Satelitní poskytovatel", + "country_cat": "Katalánsky mluvící země", + "country_o1": "ostatní země", + "getCityDocumentation": "Toto hlášení zobrazuje města návštěvníků, když přistoupili k vašim stránkám.", + "getContinentDocumentation": "Toto hlášení zobrazuje, na kterém kontinentu byli návštěvníci při přístupu k vašim stránkám.", + "getCountryDocumentation": "Toto hlášení zobrazuje zemi, ve které byli návštěvníci, když přistoupili k vašim stránkám.", + "getRegionDocumentation": "Toto hlášení zobrazuje region, ve kterém byli návštěvníci, když přistupovali k vašim stránkám." + } +} diff --git a/plugins/UserCountry/lang/cy.json b/plugins/UserCountry/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/UserCountry/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/UserCountry/lang/da.json b/plugins/UserCountry/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..37b2457c4d445573c13e46cc4b58e3200f2838ed --- /dev/null +++ b/plugins/UserCountry/lang/da.json @@ -0,0 +1,41 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP-adressen %s er en lokal adresse uden geografisk tilknytning.", + "City": "By", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Continents": "Kontinenter", + "Country": "Land", + "CountryCode": "Landekode", + "CurrentLocationIntro": "I følge denne tjeneste er din aktuelle lokation", + "DefaultLocationProviderDesc1": "Standardlokationstjenesten gætter en besøgendes land baseret på det sprog de bruger.", + "DefaultLocationProviderExplanation": "Du bruger standardlokaliseringstjenesten, hvilket betyder, at Matomo vil gætte på den besøgendes placering ud fra det sprog, de anvender. %1$sLæs dette%2$s for at se, hvordan du opsætter mere nøjagtig geolokalisering.", + "DistinctCountries": "%s distinkte lande", + "FromDifferentCities": "forskellige byer", + "GeoIPDocumentationSuffix": "For at se data for rapporten, skal du opsætte GeoIP i Geolokaliseringsfanen. Den kommercielle %1$sMaxmind%2$s GeoIP-database er mere præcise end den gratis er. For at se hvor nøjagtig den er, skal du klikke %3$sher%4$s.", + "Geolocation": "Geolokation", + "GeolocationPageDesc": "På denne side kan du ændre hvordan Matomo bestemmer de besøgendes lokation.", + "Latitude": "Breddegrad", + "Latitudes": "Breddegrader", + "Location": "Sted", + "LocationProvider": "Lokationstjeneste", + "Longitude": "Længdegrad", + "Longitudes": "Længdegrader", + "NoDataForGeoIPReport1": "Der er ingen data for rapporten, fordi der enten ikke er lokaliseringsdata til rådighed, eller besøgendes IP-adresser ikke kan geografisk placeres.", + "NoDataForGeoIPReport2": "For at aktivere nøjagtig geolokalisering skal indstillingerne ændres %1$sher%2$s og bruge en %3$sbyniveaudatabase%4$s.", + "PluginDescription": "Rapporter placeringen af besøgende: land, region, by og geografiske koordinater (længde/bredde).", + "Region": "Region", + "SubmenuLocations": "Steder", + "ToGeolocateOldVisits": "For at få lokaliseringsdata for gamle besøg, brug scriptet beskrevet %1$sher%2$s.", + "VisitLocation": "Brugerlokation", + "WidgetLocation": "Besøgendes lokation", + "country_a1": "Anonym proxy", + "country_a2": "Satellit udbyder", + "country_cat": "Catalansk-talende samfund", + "country_o1": "Andet land", + "getCityDocumentation": "Denne rapport viser hvilke byer dine besøgende var i da de besøgte dhjemmesiden.", + "getContinentDocumentation": "Denne rapport viser hvilke kontinenter dine besøgende var i da de besøgte hjemmesiden.", + "getCountryDocumentation": "Denne rapport viser hvilke lande dine besøgende var i da de besøgte hjemmesiden.", + "getRegionDocumentation": "Denne rapport viser hvilke regioner dine besøgende var i da de besøgte hjemmesiden." + } +} diff --git a/plugins/UserCountry/lang/de.json b/plugins/UserCountry/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..84593adc23e8666955525f8c6330140a37f0dae7 --- /dev/null +++ b/plugins/UserCountry/lang/de.json @@ -0,0 +1,52 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Die IP-Adresse „%s“ ist lokal und kann daher nicht geolokalisiert werden.", + "City": "Stadt", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Continents": "Kontinente", + "Country": "Land", + "CountryCode": "Länderkürzel", + "CountrySegmentHelp": "ISO 3166-1 Alpha-2 Ländercodes (de, us, fr, in, es usw.)", + "CurrentLocationIntro": "Gemäß diesem Anbieter ist Ihr derzeitiger Standort", + "DefaultLocationProviderDesc1": "Der Standard-Standortanbieter bestimmt das Land, aus dem die Besucher eine Verbindung herstellen, anhand der von ihnen gewählten Sprache.", + "DefaultLocationProviderDesc2": "Dies ist nicht sehr genau, daher %1$sinstallieren und verwenden Sie eine %2$sGeolocation-Datenbank%3$s%4$s.", + "DefaultLocationProviderExplanation": "Sie benutzen den standardmäßigen Standortanbieter. Das bedeutet, der Standort wird über die eingestellte Sprache des Besuchers festgelegt. %1$sLernen Sie, wie eine genauere Standortbestimmung installiert wird%2$s.", + "DisabledLocationProvider": "Deaktiviert die Geolokalisierung.", + "DistinctCountries": "%s unterschiedliche Länder", + "FromDifferentCities": "Unterschiedliche Städte", + "GeoIPDocumentationSuffix": "Installieren Sie GeoIP im Standortbestimmung Administrations-Tab, um Daten für diesen Bericht bereitzustellen. Die kommerziellen %1$sMaxMind%2$s GeoIP Datenbanken sind genauer als die kostenlosen. Um zu sehen, wie genau sie sind, klicken Sie %3$shier%4$s.", + "GeoIpDbIpAccuracyNote": "Die DB-IP Datenbanken sind frei und können automatisch heruntergeladen werden. MaxMind ist für Städte genauer, benötigt aber %1$sein Benutzerkonto%2$s.", + "Geolocation": "Standorterkennung", + "GeolocationPageDesc": "Hier können Sie einstellen wie Matomo die Herkunft eines Besuchers ermittelt.", + "GeolocationProviderBroken": "Der konfigurierte Standorterkennungsdienst (%1$s) funktioniert nicht. Bitte korrigieren Sie die Einstellung, oder setzen Sie einen anderen Anbieter.", + "GeolocationProviderUnavailable": "Der konfigurierte Anbieter (%1$s) zur Standortbestimmung ist nicht mehr verfügbar. Bitte setzen Sie einen anderen ein.", + "HowToInstallGeoIPDatabases": "Woher bekommt man die Geolocation-Datenbanken?", + "Latitude": "Breitengrad", + "Latitudes": "Breitengrade", + "Location": "Ort", + "LocationProvider": "Standorterkennungsdienst", + "LocationsSubcategoryHelp": "Der \"Orte\" Abschnitt ist der beste Weg herauszufinden aus welchen Ländern, Kontinenten, Regionen und Städten Ihre Webseitenbesucher kommen — in Tabellen- oder Kartenform. Er gibt auch Auskunft darüber, welche Sprache im Browser eingestellt ist, um zu helfen, internationale Besucher aus alternativen Orten zu identifizieren.", + "Longitude": "Längengrad", + "Longitudes": "Längengrade", + "MaxMindLinkExplanation": "%1$sKlicken Sie hier um zu lernen wie Ihre URL zum Herunterladen generiert werden kann%2$s, wenn Sie die MaxMind Standortbestimmung verwenden.", + "NoDataForGeoIPReport1": "Keine Daten für diesen Bericht vorhanden, da entweder keine Lokalisierungsdaten vorhanden sind oder die Besucher IP-Adresse nicht geortet werden kann.", + "NoDataForGeoIPReport2": "%1$sÄndern Sie diese Einstellungen%2$s, und verwenden Sie eine %3$sstädtebasierte Datenbank%4$s, um genaue Standortbestimmung zu ermöglichen.", + "NoProviders": "Keine zusätzlichen Geolocation Anbieter verfügbar. %1$sDB-IP%2$s wird empfohlen, benötigt aber das Plugin GeoIp2. (Alternativ können Sie ein Third Party Plugin aus dem Marktplatz installieren.)", + "PluginDescription": "Berichtet über den Standort Ihrer Besucher: Land, Region, Stadt und geographische Koordinaten (geographische Breite und geographische Länge).", + "Region": "Region", + "SubmenuLocations": "Orte", + "TestIPLocatorFailed": "Konnte den Standort der IP-Adresse \"%1$s\" nicht finden. Möglicherweise ist der Dienst nicht korrekt installiert, die Datenbank zur Standortbestimmung ist falsch benannt oder im falschen Verzeichnis?", + "ToGeolocateOldVisits": "Verwenden Sie bitte %1$sdieses Skript%2$s, um Standortdaten Ihrer alten Besuche zu erhalten.", + "VisitLocation": "Besucherstandort", + "WidgetLocation": "Besucherstandort", + "country_a1": "Anonymer Proxy", + "country_a2": "Satelliten-Anbieter", + "country_cat": "Katalanisch sprechende Gemeinden", + "country_o1": "Anderes Land", + "getCityDocumentation": "Zeigt die Städte, aus denen sich Ihre Besucher verbunden haben, als sie Ihre Webseite besuchten.", + "getContinentDocumentation": "Zeigt auf welchem Kontinent sich Ihre Besucher beim Verbinden mit Ihrer Webseite befunden haben.", + "getCountryDocumentation": "Zeigt aus welchem Land sich Ihre Besucher mit Ihrer Webseite verbunden haben.", + "getRegionDocumentation": "Zeigt aus welcher Region Ihre Besucher kamen, die sich mit Ihrer Webseite verbunden haben." + } +} diff --git a/plugins/UserCountry/lang/dv.json b/plugins/UserCountry/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/UserCountry/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/UserCountry/lang/el.json b/plugins/UserCountry/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..dc318f046c86b5b014a646c33ab097031b09ac81 --- /dev/null +++ b/plugins/UserCountry/lang/el.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Η IP διεύθυνση \"%s\" είναι τοπική διεύθυνση και δεν μπορεί να προσδιοριστεί γεωγραφικά.", + "City": "Πόλη", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Ήπειρος", + "Continents": "Ήπειροι", + "Country": "Χώρα", + "CountryCode": "Κωδικός χώρας", + "CountrySegmentHelp": "Κωδικοί χωρών 2 χαρακτήρων κατά ISO 3166-1 alpha-2 (de, us, fr, in, es, κτλ.)", + "CurrentLocationIntro": "Ο πάροχος δείχνει ότι η τρέχουσα τοποθεσία σας είναι", + "DefaultLocationProviderDesc1": "Ο προεπιλεγμένος πάροχος τοποθεσίας εικάζει τη χώρα των επισκεπτών με βάση τη γλώσσα που χρησιμοποιούν.", + "DefaultLocationProviderDesc2": "Αυτό δεν είναι πολύ ακριβές, έτσι %1$sπροτείνουμε την εγκατάσταση και τη χρήση %2$sμιας βάσης γεωτοποθεσίας%3$s.%4$s.", + "DefaultLocationProviderExplanation": "Χρησιμοποιείτε τον προκαθορισμένο πάροχο τοποθεσίας, ώστε η χώρα από την οποία συνδέονται οι επισκέπτες καθορίζεται από την επιλεγμένη γλώσσα. %1$sΔιαβάστε πως να καθορίζετε πιο ακριβή μέθοδο γεωτοποθεσίας%2$s.", + "DisabledLocationProvider": "Απενεργοποιεί την γεωτοποθεσία.", + "DistinctCountries": "%s διακεκριμένες χώρες", + "FromDifferentCities": "διαφορετικές πόλεις", + "GeoIPDocumentationSuffix": "Ρυθμίστε το GeoIP στην διαχειριστική καρτέλα Γεωτοποθεσίας για να σας παρέχει δεδομένα για αυτή την αναφορά. Οι εμπορικές βάσεις GeoIP της %1$sMaxMind%2$s είναι περισσότερο ακριβείς σε σχέση με τις δωρεάν. %3$sΚάντε κλικ εδώ%4$s για να δείτε πόσο ακριβείς είναι.", + "GeoIpDbIpAccuracyNote": "Οι βάσεις γεωτοποθεσίας DB-IP είναι δωρεάν και μπορούν να κατεβαίνουν αυτόματα. Της MaxMind είναι περισσότερο ακριβείς, αλλά απαιτούν %1$sτη δημιουργία λογαριασμού%2$s.", + "Geolocation": "Γεωτοποθεσία", + "GeolocationPageDesc": "Εδώ μπορείτε να αλλάξετε τον τρόπο που το Matomo καθορίζει τις τοποθεσίες των επισκεπτών.", + "GeolocationProviderBroken": "Ο πάροχος της γεωτοποθεσίας σας (%1$s) είναι προβληματικός. Διορθώστε τον πάροχο ή παραμετροποιήστε παρακάτω ένα διαφορετικό.", + "GeolocationProviderUnavailable": "Ο πάροχός σας της γεωτοποθεσίας (%1$s) δεν είναι πλέον διαθέσιμος. Ρυθμίστε παρακάτω ένα διαφορετικό.", + "HowToInstallGeoIPDatabases": "Πως παίρνω τις βάσεις δεδομένων γεωτοποθεσίας;", + "Latitude": "Γεωγραφικό Πλάτος", + "LatitudeSegmentHelp": "-33.578, 40.830, κτλ.
Μπορείτε να επιλέξετε επισκέπτες μέσα σε ένα εύρος συντεταγμένων πλάτους/μήκους με χρήση του %1$s.", + "Latitudes": "Γεωγραφικά πλάτη", + "Location": "Τοποθεσία", + "LocationProvider": "Πάροχος τοποθεσίας", + "LocationsSubcategoryHelp": "Το τμήμα \"Τοποθεσίες\" αποτελεί το καλύτερο σημείο για να μάθετε από ποιες χώρες, ηπείρους, περιοχές και πόλεις προέρχονται οι επισκέπτες σας — σε μορφή πίνακα και χάρτη. Εμφανίζει επίσης σε ποια γλώσσα είναι καθορισμένο το πρόγραμμα πλοήγησής τους, βοηθώντας τον εντοπισμό διεθνών επισκεπτών σε διαφορετικές τοποθεσίες.", + "Longitude": "Γεωγραφικό μήκος", + "Longitudes": "Γεωγραφικά μήκη", + "MaxMindLinkExplanation": "%1$sΚάντε κλικ εδώ για να μάθετε πως να δημιουργείτε τις διευθύνσεις URL μεταφόρτωσης%2$s αν χρησιμοποιείτε τη γεοτοποθεσία της MaxMind.", + "NoDataForGeoIPReport1": "Δεν υπάρχουν δεδομένα για αυτή την αναφορά, καθώς δεν υπάρχουν δεδομένα τοποθεσίας διαθέσιμα ή δεν ήταν δυνατός ο εντοπισμός της τοποθεσίας τους για τις διευθύνσεις IP των επισκεπτών.", + "NoDataForGeoIPReport2": "%1$sΑλλάξτε αυτές τις ρυθμίσεις%2$s και χρησιμοποιήστε %3$sμια βάση δεδομένων επιπέδου πόλεων%4$s για να έχετε ακριβή γεωτοποθεσία.", + "NoProviders": "Δεν υπάρχουν πρόσθετοι διαθέσιμοι πάροχοι γεωτοποθεσίας. Προτείνεται το %1$sDB-IP%2$s, αλλά απαιτεί το πρόσθετο GeoIp2. (Μπορείτε επίσης να εγκαταστήσετε από την αγορά ένα πρόσθετο γεωτοποθεσίας.)", + "PluginDescription": "Παρουσιάζει την τοποθεσία των επισκεπτών σας: χώρα, περιοχή, πόλη και γεωγραφικές συντεταγμένες (πλάτος και μήκος).", + "Region": "Περιοχή", + "SubmenuLocations": "Τοποθεσίες", + "TestIPLocatorFailed": "Δεν ήταν δυνατή η εύρεση της τοποθεσίας της διεύθυνσης IP \"%1$s\". Ενδέχεται ο πάροχος να μην είναι σωστά παραμετροποιημένος, η βάση δεδομένων γεωτοποθεσίας να έχει λάθος όνομα ή να είναι σε λάθος φάκελο;", + "ToGeolocateOldVisits": "Χρησιμοποιήστε αυτό το %1$sσενάριο%2$s για να έχετε δεδομένα τοποθεσίας για τις παλιές σας επισκέψεις.", + "VisitLocation": "Τοποθεσία επισκέπτη", + "WidgetLocation": "Τοποθεσία επισκέπτη", + "country_a1": "Ανώνυμος διακομιστής μεσολάβησης", + "country_a2": "Πάροχος δορυφόρου", + "country_cat": "Καταλανικόφωνες κοινότητες", + "country_o1": "Άλλη χώρα", + "getCityDocumentation": "Παρουσιάζει τις πόλεις που βρίσκονταν οι επισκέπτες σας όταν επισκέφτηκαν την ιστοσελίδα σας.", + "getContinentDocumentation": "Παρουσιάζει από ποια ήπειρο συνδέθηκαν οι επισκέπτες σας κατά την πρόσβασή τους στον ιστοτόπο σας.", + "getCountryDocumentation": "Παρουσιάζει από ποια χώρα ήταν συνδεδεμένοι οι επισκέπτες σας όταν επισκέφτηκαν την ιστοσελίδα σας.", + "getRegionDocumentation": "Παρουσιάζει από ποια περιοχή συνδέθηκαν οι επισκέπτες σας κατά την πρόσβασή τους στον ιστοτόπο." + } +} diff --git a/plugins/UserCountry/lang/en.json b/plugins/UserCountry/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..544365f0f20318a4a97ca782babad9d0ace8081c --- /dev/null +++ b/plugins/UserCountry/lang/en.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "The IP address \"%s\" is local, so it cannot be geolocated.", + "City": "City", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continent", + "Continents": "Continents", + "Country": "Country", + "CountryCode": "Country code", + "CountrySegmentHelp": "ISO 3166-1 alpha-2 country codes (de, us, fr, in, es, etc.)", + "country_a1": "Anonymous proxy", + "country_a2": "Satellite provider", + "country_cat": "Catalan-speaking communities", + "country_o1": "Other country", + "VisitLocation": "Visitor location", + "CurrentLocationIntro": "This provider determines your current location to be", + "DefaultLocationProviderDesc1": "The default location provider determines the country visitors connect from based on their selected language.", + "DefaultLocationProviderDesc2": "This is not very accurate, so %1$sinstall and use a %2$sgeolocation database%3$s%4$s.", + "DefaultLocationProviderExplanation": "You are using the default location provider, so the country visitors connect from is determined from their selected language. %1$sLearn how to set up more accurate geolocation%2$s.", + "DisabledLocationProvider": "Turns off geolocation.", + "DistinctCountries": "%s distinct countries", + "FromDifferentCities": "different cities", + "GeoIPDocumentationSuffix": "Set up GeoIP in the Geolocation admin tab to provide data for this report. The commercial %1$sMaxMind%2$s GeoIP databases are more accurate than the gratis ones. %3$sClick here%4$s to see how accurate they are.", + "Geolocation": "Geolocation", + "GeolocationPageDesc": "Here you can change how Matomo determines visitor locations.", + "GeolocationProviderBroken": "Your geolocation provider (%1$s) is broken. Please fix the provider, or set up another one.", + "GeolocationProviderUnavailable": "Your geolocation provider (%1$s) is no longer available. Please set up another one.", + "getCityDocumentation": "Shows the cities your visitors connected from when accessing your website.", + "getContinentDocumentation": "Shows which continent your visitors connected from when accessing your website.", + "getCountryDocumentation": "Shows which country your visitors connected from when accessing your website.", + "getRegionDocumentation": "Shows which region your visitors connected from when accessing website.", + "HowToInstallGeoIPDatabases": "How do I get geolocation databases?", + "Latitude": "Latitude", + "LatitudeSegmentHelp": "-33.578, 40.830, etc.
You can select visitors within a lat/long range using %1$s.", + "Latitudes": "Latitudes", + "Location": "Location", + "LocationProvider": "Location provider", + "Longitude": "Longitude", + "Longitudes": "Longitudes", + "NoDataForGeoIPReport1": "No data for this report, as there is either no location data available, or visitor IP addresses cannot be geolocated.", + "NoDataForGeoIPReport2": "%1$sChange these settings%2$s, and use a %3$scity-level database%4$s to get accurate geolocation.", + "NoProviders": "No additional geolocation providers available. %1$sDB-IP%2$s is recommended, but requires the GeoIp2 plugin. (You can also install a third-party geolocation plugin from the marketplace.)", + "PluginDescription": "Reports the location of your visitors: country, region, city and geographic coordinates (latitude and longitude).", + "Region": "Region", + "SubmenuLocations": "Locations", + "TestIPLocatorFailed": "Couldn't find the location of the IP address \"%1$s\". Maybe this provider is not set up correctly, the geolocation database is incorrectly named, or in the wrong folder?", + "ToGeolocateOldVisits": "Use %1$sthis script%2$s to get location data for your old visits.", + "WidgetLocation": "Visitor location", + "GeoIpDbIpAccuracyNote": "The DB-IP geolocation databases are gratis and can be auto-downloaded. MaxMind is more accurate for cities, but requires %1$smaking an account%2$s.", + "MaxMindLinkExplanation": "%1$sClick here to learn how to generate your download URL%2$s if you are using MaxMind's geolocation.", + "LocationsSubcategoryHelp": "The \"Locations\" section is the best way to find out what countries, continents, regions, and cities your website visitors come from — in table and map form. It also says what language their browser is set to, helping identify international visitors in alternative locations." + } +} diff --git a/plugins/UserCountry/lang/eo.json b/plugins/UserCountry/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/UserCountry/lang/eo.json @@ -0,0 +1 @@ +{} diff --git a/plugins/UserCountry/lang/es-ar.json b/plugins/UserCountry/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..041e0407f62fd79b37c06fdc4d1b772951cfe2cb --- /dev/null +++ b/plugins/UserCountry/lang/es-ar.json @@ -0,0 +1,44 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "La dirección IP %s es una dirección local y no puede ser geolocalizada.", + "City": "Ciudad", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continente", + "Continents": "Continentes", + "Country": "País", + "CountryCode": "Código de país", + "CurrentLocationIntro": "De acuerdo a este proveedor, tu ubicación actual es", + "DefaultLocationProviderDesc1": "El proveedor de ubicación predeterminado asume que el país del visitante es por el lenguaje que él utiliza.", + "DefaultLocationProviderDesc2": "Esto no es muy preciso, por lo tanto %1$srecomendamos instalar y utilizar %2$suna base de datos de geolocalización%3$s.%4$s", + "DefaultLocationProviderExplanation": "Estás usando el proveedor de ubicación predeterminado, lo que significa que Matomo intentará adivinar la ubicación del visitante basándose en el idioma que usa. %1$sLeé esto%2$s para aprender cómo configurar una geolocalización más precisa.", + "DistinctCountries": "%s países distintos", + "FromDifferentCities": "ciudades diferentes", + "GeoIPDocumentationSuffix": "Para ver la información de este informe, tenés que configurar GeoIP en la sección \"Geolocalización\" en la pestaña del administrador. Las bases de datos GeoIP comerciales %1$sMaxmind%2$s son más fiables que las gratuitas. Para ver cuán precisas son, hacé clic %3$sacá%4$s.", + "Geolocation": "Geolocalización", + "GeolocationPageDesc": "En esta página podés cambiar cómo Matomo determina las ubicaciones de los visitantes.", + "HowToInstallGeoIPDatabases": "¿Cómo obtengo las bases de datos de geolocalización?", + "Latitude": "Latitud", + "Latitudes": "Latitudes", + "Location": "Ubicación", + "LocationProvider": "Proveedor de ubicación", + "Longitude": "Longitud", + "Longitudes": "Longitudes", + "NoDataForGeoIPReport1": "No hay datos para este informe debido a que no existe información o las direcciones IP de los visitantes no pueden ser geolocalizadas.", + "NoDataForGeoIPReport2": "Para habilitar una geolocalización precisa, cambiá la configuración %1$sacá%2$s y usá una %3$sbase de datos a nivel ciudad%4$s.", + "PluginDescription": "Informa la ubicación de tus visitantes: país, región, ciudad y coordenadas geográficas (latitud y longitud).", + "Region": "Región", + "SubmenuLocations": "Ubicaciones", + "TestIPLocatorFailed": "Matomo intentó revisar la ubicación de una dirección IP conocida (%1$s), pero el servidor no devolvió ninguna información. Este proveedor puede no estar configurado correctamente (por ejemplo, la base de datos de geolocalización puede tener un nombre incorrecto o estar ubicada en un directorio incorrecto).", + "ToGeolocateOldVisits": "Para obtener información de ubicación de tus antiguas visitas, usá el script descripto %1$sacá%2$s.", + "VisitLocation": "Ubicación de la visita", + "WidgetLocation": "Ubicación del visitante", + "country_a1": "Proxy anónimo", + "country_a2": "Proveedor satelital", + "country_cat": "Comunidades de habla catalana", + "country_o1": "Otro país", + "getCityDocumentation": "Este informe muestra las ciudades desde las cuales tus visitantes accedieron a tu sitio web.", + "getContinentDocumentation": "Este informe muestra en qué continente estaban tus visitantes cuando visitaron tu sitio web.", + "getCountryDocumentation": "Este informe muestra en qué país estaban tus visitantes cuando visitaron tu sitio web.", + "getRegionDocumentation": "Este informe muestra en qué región estaban tus visitantes cuando visitaron tu sitio web." + } +} diff --git a/plugins/UserCountry/lang/es.json b/plugins/UserCountry/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..aabfd90b84120fbc86c2b2de80322c300fcdea1c --- /dev/null +++ b/plugins/UserCountry/lang/es.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "La dirección IP \"%s\" es local, por lo que no se puede geolocalizar.", + "City": "Ciudad", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continente", + "Continents": "Continentes", + "Country": "País", + "CountryCode": "Código de país", + "CurrentLocationIntro": "Este proveedor determina que tu ubicación actual es", + "DefaultLocationProviderDesc1": "El proveedor de localización por defecto determina el país desde el que se conectan los visitantes en función del idioma seleccionado.", + "DefaultLocationProviderDesc2": "Esto no es muy preciso, así que %1$sinstale y use una %2$sbase de datos de ubicación geográfica%3$s%4$s.", + "DefaultLocationProviderExplanation": "Está utilizando el proveedor de localización predeterminado, por lo que el país desde el que se conectan los visitantes se determina a partir del idioma seleccionado. %1$sAprenda a configurar una geolocalización más precisa%2$s.", + "DisabledLocationProvider": "Desactiva la geolocalización.", + "DistinctCountries": "%s países distintos", + "FromDifferentCities": "ciudades distintas", + "GeoIPDocumentationSuffix": "Configure GeoIP en la pestaña de administración Geolocalización para proporcionar datos para este informe. Las bases de datos comerciales %1$sMaxMind%2$s GeoIP son más precisas que las gratuitas. %3$sHaga clic aquí%4$s para comprobar su precisión.", + "GeoIpDbIpAccuracyNote": "Las bases de datos de geolocalización DB-IP son gratuitas y se pueden descargar automáticamente. MaxMind es más preciso para las ciudades, pero requiere %1$shacer una cuenta%2$s.", + "Geolocation": "Geolocalización", + "GeolocationPageDesc": "Aquí puede cambiar la forma en que Matomo determina la ubicación de los visitantes.", + "GeolocationProviderBroken": "Su proveedor de geolocalización (%1$s) no funciona. Por favor, arregla el proveedor o configura otro.", + "GeolocationProviderUnavailable": "Su proveedor de geolocalización (%1$s) ya no está disponible. Por favor, configura otro.", + "HowToInstallGeoIPDatabases": "¿Cómo se obtienen las bases de datos de geolocalización?", + "Latitude": "Latitud", + "Latitudes": "Latitudes geográficas", + "Location": "Ubicación", + "LocationProvider": "Proveedor de la ubicación", + "LocationsSubcategoryHelp": "La sección \"Ubicaciones\" es la mejor manera de averiguar de qué países, continentes, regiones y ciudades provienen los visitantes de su sitio web, en forma de tabla y mapa. También dice en qué idioma está configurado su navegador, lo que ayuda a identificar a los visitantes internacionales en ubicaciones alternativas.", + "Longitude": "Longitud", + "Longitudes": "Longitud", + "MaxMindLinkExplanation": "%1$sHaga clic aquí para obtener información sobre cómo generar su URL de descarga%2$s si está utilizando la geolocalización de MaxMind.", + "NoDataForGeoIPReport1": "No hay datos para este informe, ya que o bien no se dispone de datos de localización, o bien no se pueden geolocalizar las direcciones IP de los visitantes.", + "NoDataForGeoIPReport2": "%1$sCambie esta configuración%2$s, y use una %3$sbase de datos por ciudades%4$s para obtener una geolocalización precisa.", + "NoProviders": "No hay proveedores de geolocalización adicionales en este momento. Se recomienda %1$sDB-IP%2$s, pero esto requiere el complemento GeoIp2. (Alternativamente, puede instalar un complemento de geolocalización de terceros del mercado)", + "PluginDescription": "Informa de la ubicación de sus visitantes: país, región, ciudad y coordenadas geográficas (latitud y longitud).", + "Region": "Región", + "SubmenuLocations": "Ubicaciones", + "TestIPLocatorFailed": "No se pudo encontrar la ubicación de la dirección IP \"%1$s\". ¿Quizás este proveedor no está configurado correctamente, la base de datos de geolocalización tiene un nombre incorrecto o está en la carpeta incorrecta?", + "ToGeolocateOldVisits": "Utilice %1$seste script%2$s para obtener datos de ubicación de sus visitas anteriores.", + "VisitLocation": "Ubicación de los visitantes", + "WidgetLocation": "Ubicación de los visitantes", + "country_a1": "Proxy anónimo", + "country_a2": "Proveedor de satélite", + "country_cat": "Comunidades de habla catalana", + "country_o1": "Otro país", + "getCityDocumentation": "Muestra las ciudades desde las que se conectaron sus visitantes al acceder a su sitio web.", + "getContinentDocumentation": "Muestra desde qué continente se conectaron sus visitantes cuando accedieron a su sitio web.", + "getCountryDocumentation": "Muestra desde qué país se conectaron sus visitantes cuando accedieron a su sitio web.", + "getRegionDocumentation": "Muestra desde qué región se conectaron sus visitantes cuando accedieron al sitio web." + } +} diff --git a/plugins/UserCountry/lang/et.json b/plugins/UserCountry/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..ac5ec569ccc2f79a3d4eea40395d1336fb820cdf --- /dev/null +++ b/plugins/UserCountry/lang/et.json @@ -0,0 +1,25 @@ +{ + "UserCountry": { + "City": "Linn", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Country": "Riik", + "CurrentLocationIntro": "Antud pakkuja andmete alusel on sinu praegune asukoht", + "DistinctCountries": "%s eri riigist", + "FromDifferentCities": "erinevad linnad", + "Geolocation": "Geolokatsioon", + "GeolocationPageDesc": "Sellel lehel saad muuta, kuidas Matomo tuvastab külastajate asukoha infot.", + "Latitude": "Laiuskraad", + "Location": "Asukoht", + "LocationProvider": "Asukohainfo pakkuja", + "Longitude": "Pikkuskraad", + "NoDataForGeoIPReport1": "Antud raporti jaoks puuduvad andmed, kuna Matomo asukoha andmebaas on tühi või külastajate IP aadresse ei ole olnud võimalik kaardil tuvastada.", + "Region": "Regioon", + "SubmenuLocations": "Asukohad", + "WidgetLocation": "Külastajate asukohad", + "country_a1": "Anonüümne vahendusserver", + "country_a2": "Satelliidi pakkuja", + "country_cat": "Katalaani kommuunid", + "country_o1": "Muu riik" + } +} diff --git a/plugins/UserCountry/lang/eu.json b/plugins/UserCountry/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..f13078dae1af000bd1bd1b2e8d2172e9b7f6c662 --- /dev/null +++ b/plugins/UserCountry/lang/eu.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "\"%s\" IP helbidea lokala da, ondorioz ezin da geolokalizatu.", + "City": "Hiria", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinentea", + "Continents": "Kontinenteak", + "Country": "Herrialdea", + "CountryCode": "Herrialde-kodea", + "CurrentLocationIntro": "Hornitzaile honen arabera zure kokapena hau da", + "DefaultLocationProviderDesc1": "Kokapen hornitzaile lehenetsiak hautatutako hizkuntzaren arabera erabakitzen du bisitarien herrialdea.", + "DefaultLocationProviderDesc2": "Hori ez da oso zehatza, beraz %1$sinstalatu eta erabili %2$sgeolokalizazio datu-base bat%3$s%4$s.", + "DefaultLocationProviderExplanation": "Kokapen hornitzaile lehenetsia erabiltzen ari zara, beraz bisitarien herrialdea hautatu duten hizkuntzaren arabera erabakitzen da. %1$sIkasi nola konfiguratu dezakezun geolokalizazio zehatzagoa%2$s.", + "DisabledLocationProvider": "Geolokalizazioa desaktibatzen du.", + "DistinctCountries": "%s herrialde desberdin", + "FromDifferentCities": "hiri desberdinak", + "GeoIPDocumentationSuffix": "Konfiguratu GeoIP administrazioko Geolokalizazioa fitxan txosten honentzako datuak hornitzeko. %1$sMaxMind%2$s GeoIP datu-base komertziala zehatzagoa da doakoak baino. %3$sEgin klik hemen%4$s zenbateraino zehatzak diren ikusteko.", + "GeoIpDbIpAccuracyNote": "DB-IP geolokalizazio datu-baseak doan dira eta automatikoki deskargatu daitezke. MaxMind zehatzagoa da hirientzat, baina %1$skontu bat sortzea%2$s eskatzen du.", + "Geolocation": "Geolokalizazioa", + "GeolocationPageDesc": "Hemen zure Matomoak bisitarien kokapenak zehazteko duen modua aldatu dezakezu.", + "GeolocationProviderBroken": "Zure geolokalizazio hornitzailea (%1$s) hondatuta dago. Konpondu hornitzailea edo konfiguratu beste bat.", + "GeolocationProviderUnavailable": "Zure geolokalizazio hornitzailea (%1$s) ez dago erabilgarri jada. Konfiguratu beste bat.", + "HowToInstallGeoIPDatabases": "Nola eskuratu ditzaket geolokalizazio datu-baseak?", + "Latitude": "Latitudea", + "Latitudes": "Latitudeak", + "Location": "Kokapena", + "LocationProvider": "Kokapen-hornitzailea", + "LocationsSubcategoryHelp": "\"Kokapenak\" atala da zure webguneko bisitariak zein herrialde, kontinente, eskualde eta hiritatik datozen aurkitzeko modurik onena — taula eta mapa moduan. Haien nabigatzailean ezarritako hizkuntzaren berri ere ematen du, beste kokapenetan dauden nazioarteko bisitariak identifikatzeko aukera emanez.", + "Longitude": "Longitudea", + "Longitudes": "Longitudeak", + "MaxMindLinkExplanation": "MaxMind geolokalizazioa erabiltzen ari bazara, %1$segin klik hemen zure deskarga URLa nola sortu ikasteko%2$s.", + "NoDataForGeoIPReport1": "Ez dago daturik txosten honentzat, ez dagoelako kokapen daturik erabilgarri edo bisitarien IP helbideak ezin direlako geolokalizatu.", + "NoDataForGeoIPReport2": "%1$sAldatu ezarpen hauek%2$s eta erabili %3$shiri mailako datu-base bat%4$s geolokalizazio zehatzagoa lortzeko.", + "NoProviders": "Ez dago geolokalizazio hornitzaile gehigarririk erabilgarri. %1$sDB-IP%2$s gomendatzen da, baina GeoIp2 plugina behar du. (Hirugarrenen geolokalizazio plugin bat ere instalatu dezakezu Merkatutik.)", + "PluginDescription": "Zure bisitarien kokapenaren berri ematen du: herrialdea, eskualdea, hiria eta koordenatu geografikoak (latitudea eta longitudea).", + "Region": "Eskualdea", + "SubmenuLocations": "Kokalekuak", + "TestIPLocatorFailed": "Ezin izan da \"%1$s\" IP helbidearen kokapena aurkitu. Behar bada hornitzaile hau ez dago behar bezala konfiguratuta, geolokalizazio datu-basearen izena okerra da edo okerreko karpetan dago?", + "ToGeolocateOldVisits": "Erabili %1$sscript hau%2$s zure bisita zaharren kokapen datuak eskuratzeko.", + "VisitLocation": "Bisitariaren kokapena", + "WidgetLocation": "Bisitariaren kokapena", + "country_a1": "Proxy anonimoa", + "country_a2": "Satelite-hornitzailea", + "country_cat": "Katalanez hitz egiten duten komunitateak", + "country_o1": "Beste herrialde bat", + "getCityDocumentation": "Zure bisitariak zure webgunera zein hiritatik konektatzen diren erakusten du.", + "getContinentDocumentation": "Zure bisitariak zure webgunera zein kontinentetatik konektatzen diren erakusten du.", + "getCountryDocumentation": "Zure bisitariak zure webgunera zein herrialdetatik konektatzen diren erakusten du.", + "getRegionDocumentation": "Zure bisitariak zure webgunera zein eskualdetatik konektatzen diren erakusten du." + } +} diff --git a/plugins/UserCountry/lang/fa.json b/plugins/UserCountry/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..67ff5d067886165202a6c15199de3a9efb3bd2e8 --- /dev/null +++ b/plugins/UserCountry/lang/fa.json @@ -0,0 +1,31 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "آدرس آیپی (IP) %s یک آدرس محلی است و قابلیت مکان یابی ندارد.", + "City": "شهر", + "Continent": "قاره", + "Country": "کشور", + "CurrentLocationIntro": "بر اساس این ارائه دهنده خدمات , محل شما اینجاست", + "DefaultLocationProviderDesc1": "ارائه دهنده خدمات پیشفرض , کشور یک بازدیدکننده را بر اساس زبانی که استفاده میکند , مشخص می کند.", + "DistinctCountries": "%s کشورهای قابل تشخیص", + "FromDifferentCities": "شهرهای مختلف", + "Geolocation": "منطقه جغرافیایی", + "GeolocationPageDesc": "در این صفحه شما می توانید نحوه تشخیص منطقه بازدیدکنندگان توسط پیویک را تغییر دهید.", + "Latitude": "عرض جغرافیایی", + "Location": "موقعیت", + "LocationProvider": "ارائه دهنده خدمات محلی", + "Longitude": "طول جغرافیایی", + "NoDataForGeoIPReport1": "هیچ اطلاعاتی ای برای این گزارش وجود ندارد زیرا اطلاعاتی درباره مکان بازدید کننده ها یا آی پی آن ها نمی توان بدست آورد.", + "Region": "منطقه", + "SubmenuLocations": "موقعیت ها", + "ToGeolocateOldVisits": "برای اینکه اطلاعات مکان بازدیدهای پیشین تان را بدست آورید ، اسکریپتی که %1$s اینجا %2$s توضیح داده شده را به کار ببرید.", + "WidgetLocation": "مکان بازدیدکننده", + "country_a1": "پروکسی ناشناس", + "country_a2": "ارائه دهنده خدمات ماهواره ای", + "country_cat": "جوامع کاتالان زبان", + "country_o1": "سایر کشورها", + "getCityDocumentation": "ن گزارش نشان میدهد که بازدیدکنندگان هنگام ورود به وبسایت شما در کدام شهر بوده اند.", + "getContinentDocumentation": "ن گزارش نشان میدهد که بازدیدکنندگان هنگام ورود به وبسایت شما در کدام قاره بوده اند.", + "getCountryDocumentation": "این گزارش نشان میدهد که بازدیدکنندگان هنگام ورود به وبسایت شما در کدام کشور بوده اند.", + "getRegionDocumentation": "این گزارش نشان میدهد که بازدیدکنندگان هنگام ورود به وبسایت شما در کدام منطقه بوده اند." + } +} diff --git a/plugins/UserCountry/lang/fi.json b/plugins/UserCountry/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..efc472b30ccfd465ea0949b9bc975d4df9ce6f49 --- /dev/null +++ b/plugins/UserCountry/lang/fi.json @@ -0,0 +1,42 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP-osoite \"%s\" on paikallinen osoite, eikä sitä voida geopaikantaa.", + "City": "Kaupunki", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Maanosa", + "Continents": "Mantereet", + "Country": "Maa", + "CountryCode": "Maakoodi", + "CurrentLocationIntro": "Tämän toteutuksen mukaan nykyinen sijaintisi on", + "DefaultLocationProviderDesc1": "Oletustoteutus arvaa käyttäjien maan kielen perusteella.", + "DefaultLocationProviderExplanation": "Käytät sijainnin oletuspalvelua, mikä tarkoittaa sitä, että Matomo päättelee kävijän sijainnin hänen käyttämän kielen perusteella. %1$sLue tämä%2$s saadaksesi tietoa tarkemman geopaikannuksen käyttöönottamisesta.", + "DistinctCountries": "%s uniikkia maata", + "FromDifferentCities": "eri kaupunkeja", + "GeoIPDocumentationSuffix": "Nähdäksesi tietoja tässä raportissa, sinun täytää asettaa GeoIP geopaikannuksen admin-välilehdessä. Kaupalliset %1$sMaxmind%2$s GeoIP tietokannat ovat tarkempia kuin ilmaiset. Klikkaa %3$stästä%4$s nähdäksesi miten tarkkoja ne ovat.", + "Geolocation": "Geopaikannus", + "GeolocationPageDesc": "Tällä sivulla voit säätää, miten Matomo päättelee kävijöiden sijainnin, ja tarkastella eri vaihtoehtojen asetuksia.", + "HowToInstallGeoIPDatabases": "Kuinka saan paikkatietoa sisältäviä tietokantoja?", + "Latitude": "Leveysaste", + "Latitudes": "Leveyspiirit", + "Location": "Sijainti", + "LocationProvider": "Sijaintipalvelu", + "Longitude": "Pituusaste", + "Longitudes": "Pituuspiirit", + "NoDataForGeoIPReport1": "Tämä raportti ei sisällä tietoja, koska sijantitietoja ei ole saatavilla tai kävijöiden IP-osoitteita ei voida geopaikantaa.", + "NoDataForGeoIPReport2": "Aktivoidaksesi tarkan geopaikannuksen, muuta asetukset %1$stäällä%2$s ja käytä %3$skaupaunkitason tietokantaa%4$s.", + "PluginDescription": "Kertoo kävijöiden sijainnin: Maa, alue, kaupunki ja maantieteelliset koordinaatit (pohjoinen leveys/itäinen pituus).", + "Region": "Alue", + "SubmenuLocations": "Sijainnit", + "ToGeolocateOldVisits": "Saadaksesi vanhojen käyntien sijaintitietoja, käytä %1$stäällä%2$s kuvailtua skriptiä.", + "VisitLocation": "Kävijän sijainti", + "WidgetLocation": "Kävijän sijainti", + "country_a1": "Anonyymi välityspalvelin", + "country_a2": "Satelliittiyhteys", + "country_cat": "Katalaania puhuvat alueet", + "country_o1": "Muu maa", + "getCityDocumentation": "Tämä raportti näyttää missä kaupungeissa käyttäjät olivat käydessään verkkosivullasi.", + "getContinentDocumentation": "Tämä raportti näyttää missä maanosassa käyttäjät olivat käydessään verkkosivullasi.", + "getCountryDocumentation": "Tämä raportti näyttää missä maassa käyttäjät olivat käydessään verkkosivullasi.", + "getRegionDocumentation": "Tämä raportti näyttää millä alueella käyttäjät olivat käydessään verkkosivullasi." + } +} diff --git a/plugins/UserCountry/lang/fr.json b/plugins/UserCountry/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..90086485503d1e46b43a4ce339b7c6102d386397 --- /dev/null +++ b/plugins/UserCountry/lang/fr.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "L'adresse IP %s est locale et ne peut pas être géolocalisée.", + "City": "Ville", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continent", + "Continents": "Continents", + "Country": "Pays", + "CountryCode": "Code pays", + "CountrySegmentHelp": "Codes pays ISO 3166-1 alpha-2 (de, us, fr, in, es, etc.)", + "CurrentLocationIntro": "Ce fournisseur détermine votre emplacement actuel à", + "DefaultLocationProviderDesc1": "Le fournisseur de localisation par défaut détermine le pays depuis lequel les visiteurs se connectent en fonction de la langue qu'ils ont sélectionnée.", + "DefaultLocationProviderDesc2": "Ce n'est pas très précis, alors %1$sinstallez et utilisez une %2$sbase de données de géolocalisation%3$s%4$s.", + "DefaultLocationProviderExplanation": "Vous utilisez le fournisseur de localisation par défaut ; le pays de connexion des visiteurs est donc déterminé en fonction de la langue sélectionnée. %1$sApprenez à configurer une géolocalisation plus précise%2$s.", + "DisabledLocationProvider": "Désactiver la géolocalisation.", + "DistinctCountries": "%s pays différents", + "FromDifferentCities": "villes différentes", + "GeoIPDocumentationSuffix": "Configurez la géolocalisation IP dans l'onglet d'administration pour fournir les données de ce rapport. Les bases de données commerciales de %1$sMaxMind%2$s GeoIP sont plus précises que les bases de données gratuites. %3$sCliquez ici%4$s pour voir leur niveau de précision.", + "GeoIpDbIpAccuracyNote": "Les bases de données de géolocalisation DB-IP sont gratuites et peuvent être téléchargées automatiquement. MaxMind est plus précis pour les villes, mais nécessite de %1$scréer un compte%2$s.", + "Geolocation": "Géolocalisation", + "GeolocationPageDesc": "Ici, vous pouvez modifier la façon dont Matomo détermine la localisation des visiteurs.", + "GeolocationProviderBroken": "Votre fournisseur de géolocalisation (%1$s) est défaillant. Veuillez le réparer ou en configurer un autre.", + "GeolocationProviderUnavailable": "Le fournisseur de géolocalisation configuré (%1$s) n'est plus disponible. Veuillez en configurer un autre.", + "HowToInstallGeoIPDatabases": "Comment puis-je récupérer des bases de données de géolocalisation ?", + "Latitude": "Latitude", + "LatitudeSegmentHelp": "-33.578, 40.830, etc.
Vous pouvez sélectionner les visiteurs situés dans une plage de latitude/longitude en utilisant %1$s.", + "Latitudes": "Lattitudes", + "Location": "Localisation", + "LocationProvider": "Fournisseur de localisation", + "LocationsSubcategoryHelp": "La section « Provenance géographique » est le meilleur moyen de découvrir la provenance (pays, continents, régions et villes) des visiteurs de votre site web, sous forme de tableau et de carte. Elle indique également la langue de leur navigateur, ce qui permet d'identifier les visiteurs internationaux situés dans d'autres régions.", + "Longitude": "Longitude", + "Longitudes": "Longitudes", + "MaxMindLinkExplanation": "%1$sCliquez ici pour apprendre comment générer votre URL de téléchargement%2$s si vous utilisez la géolocalisation de MaxMind.", + "NoDataForGeoIPReport1": "Aucune donnée n'est disponible pour ce rapport, soit parce qu'il n'y a pas de données de localisation disponibles, soit parce que les adresses IP des visiteurs ne peuvent pas être géolocalisées.", + "NoDataForGeoIPReport2": "%1$sModifiez ces paramètres%2$s et utilisez une %3$sbase de données au niveau de la ville%4$s pour obtenir une géolocalisation précise.", + "NoProviders": "Aucun autre fournisseur de géolocalisation n'est disponible. %1$sDB-IP%2$s est recommandé, mais nécessite l'extension GeoIp2. (Vous pouvez également installer une extension de géolocalisation tierce depuis la marketplace.)", + "PluginDescription": "Indique la localisation de vos visiteurs : pays, région, ville et coordonnées géographiques (latitude et longitude).", + "Region": "Région", + "SubmenuLocations": "Provenances géographiques", + "TestIPLocatorFailed": "Impossible de localiser l'adresse IP « %1$s ». Ce fournisseur est peut-être mal configuré, la base de données de géolocalisation est mal nommée ou se trouve dans le mauvais dossier.", + "ToGeolocateOldVisits": "Utilisez %1$sce script%2$s pour obtenir les données de localisation de vos anciennes visites.", + "VisitLocation": "Emplacement de la visite", + "WidgetLocation": "Emplacement du visiteur", + "country_a1": "Proxy anonyme", + "country_a2": "Fournisseur de services satellites", + "country_cat": "Communautés de langue Catalane", + "country_o1": "Autre pays", + "getCityDocumentation": "Affiche les villes ou se sont connectés vos visiteurs lorsqu'ils ont accédés à votre site web.", + "getContinentDocumentation": "Indique de quel continent vos visiteurs se sont connectés lorsqu'ils ont accédé à votre site Web.", + "getCountryDocumentation": "Indique de quel pays vos visiteurs se sont connectés lorsqu'ils ont accédé à votre site Web.", + "getRegionDocumentation": "Indique de quelle région vos visiteurs se sont connectés lorsqu'ils ont accédé au site Web." + } +} diff --git a/plugins/UserCountry/lang/ga.json b/plugins/UserCountry/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..d3b4bd4ca9faf71d4cbd0354c367992d132139da --- /dev/null +++ b/plugins/UserCountry/lang/ga.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Tá an seoladh IP \"%s\" logánta, mar sin ní féidir é a geolonnú.", + "City": "Cathair", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Mór-roinn", + "Continents": "Mór-roinne", + "Country": "Tír", + "CountryCode": "Cód Tíre", + "CountrySegmentHelp": "Cóid tíre ISO 3166-1 alpha-2 (de, us, fr, in, es, etc.)", + "CurrentLocationIntro": "Cinneann an soláthraí seo do shuíomh reatha", + "DefaultLocationProviderDesc1": "Socraíonn soláthraí an tsuímh réamhshocraithe an tír a nascann cuairteoirí uaidh bunaithe ar a dteanga roghnaithe.", + "DefaultLocationProviderDesc2": "Níl sé seo an-chruinn, mar sin %1$suiteáil agus úsáid %2$sbunachar sonraí geolocation %3$s%4$s.", + "DefaultLocationProviderExplanation": "Tá tú ag úsáid an tsoláthraí suímh réamhshocraithe, mar sin de réir na teanga roghnaithe a shocraítear an tír a ndéanann cuairteoirí ceangal léi. %1$sFoghlaim conas geolocation níos cruinne a shocrú%2$s.", + "DisabledLocationProvider": "Múchann sé geolocation.", + "DistinctCountries": "%s tíortha ar leith", + "FromDifferentCities": "cathracha éagsúla", + "GeoIPDocumentationSuffix": "Socraigh GeoIP sa chluaisín riaracháin Geolocation chun sonraí a sholáthar don tuarascáil seo. Tá bunachair shonraí GeoIP tráchtála %1$sMaxMind%2$s níos cruinne ná na cinn in aisce. %3$s Cliceáil anseo %4$s chun a fháil amach cé chomh cruinn agus atá siad.", + "GeoIpDbIpAccuracyNote": "Tá bunachair shonraí geolocation DB-IP saor in aisce agus is féidir iad a íoslódáil go huathoibríoch. Tá MaxMind níos cruinne do chathracha, ach caithfidh %1$s cuntas%2$s a dhéanamh.", + "Geolocation": "Geoshuíomh", + "GeolocationPageDesc": "Anseo is féidir leat an chaoi a gcinneann Matomo láithreacha cuairteoirí a athrú.", + "GeolocationProviderBroken": "Tá do sholáthraí geoshuímh (%1$s) briste. Ceartaigh an soláthraí, nó socraigh ceann eile.", + "GeolocationProviderUnavailable": "Níl do sholáthraí geolocation (%1$s) ar fáil a thuilleadh. Socraigh ceann eile le do thoil.", + "HowToInstallGeoIPDatabases": "Conas a gheobhaidh mé bunachair shonraí geolocation?", + "Latitude": "Domhanleithead", + "LatitudeSegmentHelp": "-33.578, 40.830, srl.
Is féidir leat cuairteoirí a roghnú laistigh de raon domhanleithid/fadlíne ag baint úsáide as %1$s.", + "Latitudes": "Comhthreomhar", + "Location": "Suíomh", + "LocationProvider": "Soláthraí suímh", + "LocationsSubcategoryHelp": "Is é an rannán \"Suíomhanna\" an bealach is fearr chun a fháil amach cé na tíortha, ilchríocha, réigiúin agus cathracha as a dtagann do chuairteoirí ar an láithreán gréasáin - i bhfoirm tábla agus léarscáil. Deir sé freisin cén teanga a bhfuil a mbrabhsálaí socraithe aici, ag cuidiú le cuairteoirí idirnáisiúnta a aithint in áiteanna eile.", + "Longitude": "Domhanfhad", + "Longitudes": "Domhanfhads", + "MaxMindLinkExplanation": "%1$s Cliceáil anseo chun a fháil amach conas do URL íoslódála%2$s a ghiniúint má tá tú ag úsáid geolocation MaxMind.", + "NoDataForGeoIPReport1": "Níl aon sonraí don tuarascáil seo, mar níl aon sonraí suímh ar fáil, nó ní féidir seoltaí IP cuairteoirí a geolonnú.", + "NoDataForGeoIPReport2": "%1$sAthraigh na socruithe seo%2$s, agus úsáid bunachar sonraí %3$s ar leibhéal na scoile%4$s chun geoshuíomh cruinn a fháil.", + "NoProviders": "Níl aon soláthraithe geolocation breise ar fáil. Moltar %1$sDB-IP%2$s, ach tá an breiseán GeoIp2 ag teastáil. (Is féidir leat breiseán geolocation tríú páirtí a shuiteáil ón margadh freisin.)", + "PluginDescription": "Tuairiscíonn suíomh do chuairteoirí: tír, réigiún, cathair agus comhordanáidí geografacha (domhanleithead agus domhanfhad).", + "Region": "Réigiún", + "SubmenuLocations": "Suímh", + "TestIPLocatorFailed": "Níorbh fhéidir suíomh an tseolta IP \"%1$s\" a aimsiú. B'fhéidir nach bhfuil an soláthraí seo socraithe i gceart, go bhfuil an bunachar sonraí geolocation ainmnithe go mícheart, nó san fhillteán mícheart?", + "ToGeolocateOldVisits": "Úsáid %1$sthis script%2$s chun sonraí suímh a fháil do do sheanchuairteanna.", + "VisitLocation": "Suíomh cuairteora", + "WidgetLocation": "Suíomh cuairteora", + "country_a1": "Seachfhreastalaí gan ainm", + "country_a2": "Soláthraí satailíte", + "country_cat": "pobail ina labhraítear Catalóinis", + "country_o1": "Tír eile", + "getCityDocumentation": "Taispeánann sé na cathracha a raibh baint ag do chuairteoirí leo agus iad ag teacht ar do shuíomh Gréasáin.", + "getContinentDocumentation": "Léiríonn sé na mór-roinne a raibh baint ag do chuairteoirí léi agus iad ag teacht ar do shuíomh Gréasáin.", + "getCountryDocumentation": "Léiríonn sé cén tír a raibh baint ag do chuairteoirí léi agus iad ag rochtain ar do shuíomh Gréasáin.", + "getRegionDocumentation": "Taispeánann sé cén réigiún a raibh baint ag do chuairteoirí leis agus iad ag teacht ar an suíomh Gréasáin." + } +} diff --git a/plugins/UserCountry/lang/gl.json b/plugins/UserCountry/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..eb7ab1941bd91123810843d0f953ade25a1bf9c2 --- /dev/null +++ b/plugins/UserCountry/lang/gl.json @@ -0,0 +1,10 @@ +{ + "UserCountry": { + "Continent": "Continente", + "Country": "País", + "DistinctCountries": "%s continentes distintos", + "Region": "Rexión", + "SubmenuLocations": "Lugares", + "country_o1": "Outro país" + } +} diff --git a/plugins/UserCountry/lang/gu.json b/plugins/UserCountry/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..6877ee5d08af6207daed7676b2eb5ab31ed0ea4f --- /dev/null +++ b/plugins/UserCountry/lang/gu.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP સરનામું \"%s\" સ્થાનિક છે, તેથી તેને ભૌગોલિક સ્થાન આપી શકાતું નથી.", + "City": "શહેર", + "CityAndCountry": "%1$s, %2$s", + "Continent": "ખંડ", + "Continents": "ખંડો", + "Country": "દેશ", + "CountryCode": "દેશનો કોડ", + "CurrentLocationIntro": "આ પ્રદાતા તમારું વર્તમાન સ્થાન નક્કી કરે છે", + "DefaultLocationProviderDesc1": "ડિફૉલ્ટ સ્થાન પ્રદાતા નક્કી કરે છે કે મુલાકાતીઓ તેમની પસંદ કરેલી ભાષાના આધારે કયા દેશમાંથી કનેક્ટ થાય છે.", + "DefaultLocationProviderDesc2": "આ બહુ સચોટ નથી, તેથી %1$sઇન્સ્ટોલ કરો અને %2$s ભૌગોલિક સ્થાન ડેટાબેઝ%3$s%4$s નો ઉપયોગ કરો.", + "DefaultLocationProviderExplanation": "તમે ડિફૉલ્ટ સ્થાન પ્રદાતાનો ઉપયોગ કરી રહ્યાં છો, તેથી મુલાકાતીઓ જે દેશથી કનેક્ટ થાય છે તે તેમની પસંદ કરેલી ભાષા પરથી નક્કી થાય છે. %1$s વધુ સચોટ ભૌગોલિક સ્થાન%2$s કેવી રીતે સેટ કરવું તે જાણો.", + "DisabledLocationProvider": "ભૌગોલિક સ્થાન બંધ કરે છે.", + "DistinctCountries": "%s અલગ દેશો", + "FromDifferentCities": "વિવિધ શહેરો", + "GeoIPDocumentationSuffix": "આ રિપોર્ટ માટે ડેટા પ્રદાન કરવા માટે ભૌગોલિક સ્થાન એડમિન ટેબમાં GeoIP સેટ કરો. વાણિજ્યિક %1$sMaxMind%2$s GeoIP ડેટાબેસેસ મફત કરતા વધુ સચોટ છે. તેઓ કેટલા સચોટ છે તે જોવા માટે %3$s અહીં ક્લિક કરો%4$s.", + "GeoIpDbIpAccuracyNote": "DB-IP ભૌગોલિક સ્થાન ડેટાબેસેસ મફત છે અને તે ઓટો-ડાઉનલોડ થઈ શકે છે. MaxMind શહેરો માટે વધુ સચોટ છે, પરંતુ તેને %1$s એકાઉન્ટ બનાવવાની જરૂર છે%2$s.", + "Geolocation": "ભૌગોલિક સ્થાન", + "GeolocationPageDesc": "અહીં તમે Matomo મુલાકાતીઓના સ્થાનો કેવી રીતે નક્કી કરે છે તે બદલી શકો છો.", + "GeolocationProviderBroken": "તમારા ભૌગોલિક સ્થાન પ્રદાતા (%1$s) તૂટેલા છે. કૃપા કરીને પ્રદાતાને ઠીક કરો અથવા બીજું સેટ કરો.", + "GeolocationProviderUnavailable": "તમારું ભૌગોલિક સ્થાન પ્રદાતા (%1$s) હવે ઉપલબ્ધ નથી. કૃપા કરીને બીજું સેટ કરો.", + "HowToInstallGeoIPDatabases": "હું ભૌગોલિક સ્થાન ડેટાબેસેસ કેવી રીતે મેળવી શકું?", + "Latitude": "અક્ષાંશ", + "Latitudes": "અક્ષાંશો", + "Location": "સ્થાન", + "LocationProvider": "સ્થાન પ્રદાતા", + "LocationsSubcategoryHelp": "તમારી વેબસાઇટના મુલાકાતીઓ કયા દેશો, ખંડો, પ્રદેશો અને શહેરોમાંથી આવે છે તે શોધવા માટે \"સ્થાનો\" વિભાગ શ્રેષ્ઠ માર્ગ છે — કોષ્ટક અને નકશા સ્વરૂપમાં. તે એમ પણ કહે છે કે તેમનું બ્રાઉઝર કઈ ભાષામાં સેટ છે, જે વૈકલ્પિક સ્થળોએ આંતરરાષ્ટ્રીય મુલાકાતીઓને ઓળખવામાં મદદ કરે છે.", + "Longitude": "રેખાંશ", + "Longitudes": "રેખાંશો", + "MaxMindLinkExplanation": "જો તમે MaxMind ભૌગોલિક સ્થાનનો ઉપયોગ કરી રહ્યાં હોવ તો %1$sતમારું ડાઉનલોડ URL%2$s કેવી રીતે જનરેટ કરવું તે જાણવા માટે અહીં ક્લિક કરો.", + "NoDataForGeoIPReport1": "આ રિપોર્ટ માટે કોઈ ડેટા નથી, કારણ કે ત્યાં કોઈ સ્થાન ડેટા ઉપલબ્ધ નથી, અથવા મુલાકાતીઓના IP સરનામાઓ ભૌગોલિક સ્થાન નક્કી કરી શકાતા નથી.", + "NoDataForGeoIPReport2": "%1$sઆ સેટિંગ્સ બદલો%2$s, અને ચોક્કસ ભૌગોલિક સ્થાન મેળવવા માટે %3$s શહેર-સ્તરના ડેટાબેઝ%4$s નો ઉપયોગ કરો.", + "NoProviders": "કોઈ વધારાના ભૌગોલિક સ્થાન પ્રદાતાઓ ઉપલબ્ધ નથી. %1$sDB-IP%2$s ની ભલામણ કરવામાં આવે છે, પરંતુ તેને GeoIp2 પ્લગઇનની જરૂર છે. (તમે માર્કેટપ્લેસમાંથી તૃતીય-પક્ષ ભૌગોલિક સ્થાન પ્લગઇન પણ ઇન્સ્ટોલ કરી શકો છો.)", + "PluginDescription": "તમારા મુલાકાતીઓના સ્થાનની જાણ કરે છે: દેશ, પ્રદેશ, શહેર અને ભૌગોલિક કોઓર્ડિનેટ્સ (અક્ષાંશ અને રેખાંશ).", + "Region": "પ્રદેશ", + "SubmenuLocations": "સ્થાનો", + "TestIPLocatorFailed": "IP સરનામા \"%1$s\" નું સ્થાન શોધી શકાયું નથી. કદાચ આ પ્રદાતા યોગ્ય રીતે સેટ કરેલ નથી, ભૌગોલિક સ્થાન ડેટાબેઝનું નામ ખોટું છે, અથવા ખોટા ફોલ્ડરમાં છે?", + "ToGeolocateOldVisits": "તમારી જૂની વિડિઓઝ માટે સ્થાન ડેટા મેળવવા માટે %1$sઆ સ્ક્રિપ્ટ%2$s નો ઉપયોગ કરો.", + "VisitLocation": "મુલાકાતી સ્થાન", + "WidgetLocation": "મુલાકાતી સ્થાન", + "country_a1": "અનામી પ્રોક્સી", + "country_a2": "સેટેલાઇટ પ્રદાતા", + "country_cat": "કતલાન-ભાષી સમુદાયો", + "country_o1": "અન્ય દેશ", + "getCityDocumentation": "તમારી વેબસાઇટ ઍક્સેસ કરતી વખતે તમારા મુલાકાતીઓ જે શહેરોથી જોડાયેલા છે તે દર્શાવે છે.", + "getContinentDocumentation": "તમારી વેબસાઇટને ઍક્સેસ કરતી વખતે તમારા મુલાકાતીઓ કયા ખંડથી જોડાયેલા છે તે બતાવે છે.", + "getCountryDocumentation": "તમારી વેબસાઇટ ઍક્સેસ કરતી વખતે તમારા મુલાકાતીઓ કયા દેશમાંથી જોડાયેલા છે તે બતાવે છે.", + "getRegionDocumentation": "વેબસાઇટને ઍક્સેસ કરતી વખતે તમારા મુલાકાતીઓ કયા પ્રદેશથી જોડાયેલા છે તે બતાવે છે." + } +} diff --git a/plugins/UserCountry/lang/he.json b/plugins/UserCountry/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..0c443782d25834ac46247b24a20aff2c180ac722 --- /dev/null +++ b/plugins/UserCountry/lang/he.json @@ -0,0 +1,15 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "כתובת איי.פי. %s היא כתובת מקומית ולא ניתן לאתרה גיאוגרפית.", + "City": "עיר", + "CityAndCountry": "%1$s, %2$s", + "Continent": "יבשת", + "Country": "מדינה", + "Geolocation": "מיקום גאוגרפי", + "Location": "מיקום", + "country_a1": "מתווך אנונימי", + "country_a2": "ספק לוויני", + "country_cat": "קהילות דוברות קטלאנית", + "country_o1": "מדינה אחרת" + } +} diff --git a/plugins/UserCountry/lang/hi.json b/plugins/UserCountry/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..c5af34a067c06d5590ad71f7fbbf7982496b2f76 --- /dev/null +++ b/plugins/UserCountry/lang/hi.json @@ -0,0 +1,32 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "आईपी पता \"%s\" स्थानीय है, इसलिए इसे जियोलोकेट नहीं किया जा सकता।", + "City": "शहर", + "Continent": "महाद्वीप", + "Country": "देश", + "CurrentLocationIntro": "इस प्रदाता के अनुसार, आपका वर्तमान स्थान है", + "DefaultLocationProviderDesc1": "एक आगंतुक का देश डिफ़ॉल्ट स्थान प्रदाता के अनुमान का इस्तेमाल वे भाषा के आधार पर करते है.", + "DistinctCountries": "%s विशिष्ट देशों", + "GeoIPDocumentationSuffix": "इस रिपोर्ट के लिए डेटा को देखने के लिए, आप जियोलोकेशन व्यवस्थापक टैब में GeoIP सेटअप चाहिए. वाणिज्यिक %1$sMaxmind%2$s GeoIP डेटाबेस मुक्त वालों की तुलना में अधिक सटीक हैं. वे कितने सटीक है देखने के लिए यहां %3$sक्लिक%4$s करें.", + "Geolocation": "भौगोलिक स्थान", + "GeolocationPageDesc": "इस पेज पर आप को बदल सकते हैं कि कैसे Matomo आगंतुक स्थानों को निर्धारित करता है.", + "Latitude": "अक्षांश", + "Location": "स्थान", + "LocationProvider": "स्थान प्रदाता", + "Longitude": "देशान्तर", + "NoDataForGeoIPReport1": "इस रिपोर्ट के लिए कोई डेटा नहीं है क्योंकि कोई स्थान डेटा उपलब्ध तो नहीं है या आगंतुक आईपी पते geolocated नहीं किया जा सकता.", + "NoDataForGeoIPReport2": "सटीक जियोलोकेशन सक्षम करने के लिए %1$sयहाँ%2$s सेटिंग में बदलाव और एक %3$sशहर के स्तर पर डेटाबेस%4$s का उपयोग करें.", + "Region": "क्षेत्र", + "SubmenuLocations": "स्थान", + "ToGeolocateOldVisits": "अपने पुराने यात्राओं के लिए स्थान डेटा प्राप्त करने के लिए, %1$sयहाँ%2$s वर्णित स्क्रिप्ट का उपयोग करें.", + "WidgetLocation": "आगंतुक स्थान", + "country_a1": "बेनाम प्रॉक्सी", + "country_a2": "सैटेलाइट प्रदाता", + "country_cat": "कैटलन भाषी समुदायों", + "country_o1": "अन्य देश", + "getCityDocumentation": "इस रिपोर्ट से पता चलता है अपने दर्शकों के शहरों, वे कब आपकी वेबसाइट पे पहुचे", + "getContinentDocumentation": "इस रिपोर्ट से पता चलता है आपके आगंतुकों के महाद्वीप का ,वे कब आपकी वेबसाइट पे पहुचे.", + "getCountryDocumentation": "इस रिपोर्ट से पता चलता है आपके आगंतुकों के देश का,वे कब आपकी वेबसाइट पे पहुचे", + "getRegionDocumentation": "इस रिपोर्ट से पता चलता है आपके आगंतुकों के क्षेत्र का वे कब आपकी वेबसाइट पे पहुचे" + } +} diff --git a/plugins/UserCountry/lang/hr.json b/plugins/UserCountry/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..239e942a92895a18628c9b63a6bc9712e4d24fcd --- /dev/null +++ b/plugins/UserCountry/lang/hr.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP adresa „%s” je lokalna, stoga se ne može geolocirati.", + "City": "Grad", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Continents": "Kontinenti", + "Country": "Zemlja", + "CountryCode": "Kod zemlje", + "CountrySegmentHelp": "ISO 3166-1 alpha-2 kodovi zemalja (de, us, fr, in, es, itd.)", + "CurrentLocationIntro": "Ovaj pružatelj usluge je odredio da je tvoja trenutačna lokacija", + "DefaultLocationProviderDesc1": "Zadani pružatelj usluge lokacije određuje zemlju iz koje se posjetitelji povezuju na temelju njihovog odabranog jezika.", + "DefaultLocationProviderDesc2": "Ovo nije vrlo precizno, stoga %1$sinstaliraj i koristit jednu %2$sbazu podataka za geolokaciju%3$s%4$s.", + "DefaultLocationProviderExplanation": "Koristiš zadanog pružatelja usluge lokacije. Stoga se zemlja iz koje se posjetitelji povezuju određuje na temelju njihovog odabranog jezika. %1$sSaznaj kako postaviti točniju geolokaciju%2$s.", + "DisabledLocationProvider": "Isključuje geolokaciju.", + "DistinctCountries": "%s različite zemlje", + "FromDifferentCities": "različiti gradovi", + "GeoIPDocumentationSuffix": "Postavi GeoIP u administracijskom kartici za geolokaciju za osiguravanje podataka za ovaj izvještaj. Komercijalne %1$sMaxMind%2$s GeoIP baze podataka su točnije od besplatnih. %3$sKlikni ovdje%4$s za prikaz točnosti tih baza podataka.", + "GeoIpDbIpAccuracyNote": "DB-IP baze podataka geolokacije su besplatne i mogu se automatski preuzeti. MaxMind je točniji za gradove, ali zahtijeva %1$sizradu računa%2$s.", + "Geolocation": "Geolokacija", + "GeolocationPageDesc": "Ovdje možeš promijeniti način na koji Matomo određuje lokacije posjetitelja.", + "GeolocationProviderBroken": "Tvoj pružatelj usluge geolokacije (%1$s) ne radi. Popravi pružatelja usluge ili postavi jedan drugi.", + "GeolocationProviderUnavailable": "Tvoj pružatelj usluge geolokacije (%1$s) više nije dostupan. Postavi jedan drugi.", + "HowToInstallGeoIPDatabases": "Kako mogu dobiti baze podataka geolokacije?", + "Latitude": "Geografska širina", + "LatitudeSegmentHelp": "-33.578, 40.830, itd.
Posjetitelje unutar raspona geografskih širina i dužina možeš odabrati pomoću %1$s.", + "Latitudes": "Geografske širine", + "Location": "Lokacija", + "LocationProvider": "Pružatelj usluge lociranja", + "LocationsSubcategoryHelp": "Odjeljak „Lokacije” je najbolji način za uvid iz kojih zemalja, kontinenata, regija i gradova dolaze posjetitelji tvoje web-stranice – u obliku tablice i karte. Također prikazuje koji je jezik postavljen u njihovom pregledniku, što pomaže identificirati međunarodne posjetitelje u alternativnim lokacijama.", + "Longitude": "Geografska dužina", + "Longitudes": "Geografske dužine", + "MaxMindLinkExplanation": "%1$sKlikni ovdje i saznaj kako generirati tvoj URL za preuzimanje%2$s ako koristiš MaxMindovu geolokaciju.", + "NoDataForGeoIPReport1": "Nema podataka za ovaj izvještaj, jer nema dostupnih podataka o lokaciji ili se IP adrese posjetitelja ne mogu geolocirati.", + "NoDataForGeoIPReport2": "%1$sPromijeni ove postavke%2$s i koristi %3$sbazu podataka na razini gradova%4$s za dobivanje točne geolokacije.", + "NoProviders": "Nema dostupnih dodatnih pružatelja usluge geolokacije. Preporučuje se %1$sDB-IP%2$s, ali zahtijeva GeoIp2 dodatak. (Također možeš instalirati dodatak treće strane za geolokaciju s tržišta.)", + "PluginDescription": "Prikazuje lokaciju tvojih posjetitelja: zemlju, regiju, grad i geografske koordinate (geografsku širinu i dužinu).", + "Region": "Regija", + "SubmenuLocations": "Lokacije", + "TestIPLocatorFailed": "Nije bilo moguće pronaći lokaciju IP adrese „%1$s”. Možda ovaj pružatelj usluge nije ispravno postavljen, baza podataka geolakacije nije ispravno imenovana ili se nalazi u pogrešnoj mapi?", + "ToGeolocateOldVisits": "Koristi %1$sovu skriptu%2$s za dobivanje podataka o lokaciji za tvoje stare posjete.", + "VisitLocation": "Lokacija posjetitelja", + "WidgetLocation": "Lokacija posjetitelja", + "country_a1": "Anonimni proxy", + "country_a2": "Pružatelj satelitskih usluga", + "country_cat": "Zajednice koje govore katalonskim jezikom", + "country_o1": "Druga zemlja", + "getCityDocumentation": "Prikazuje gradove iz kojih su se tvoji posjetitelji povezali prilikom pristupa tvojoj web-stranici.", + "getContinentDocumentation": "Prikazuje kontinente iz kojih su se tvoji posjetitelji povezali prilikom pristupa tvojoj web-stranici.", + "getCountryDocumentation": "Prikazuje iz koje su se zemlje tvoji posjetitelji povezali prilikom pristupa tvojoj web-stranici.", + "getRegionDocumentation": "Prikazuje iz koje su se regije tvoji posjetitelji povezali prilikom pristupa tvojoj web-stranici." + } +} diff --git a/plugins/UserCountry/lang/hu.json b/plugins/UserCountry/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..5bf0c89341d5a0b4b7b98e9513080946ffdf878b --- /dev/null +++ b/plugins/UserCountry/lang/hu.json @@ -0,0 +1,13 @@ +{ + "UserCountry": { + "Continent": "Kontinens", + "Country": "Ország", + "DistinctCountries": "%s különböző ország", + "Location": "Hely", + "SubmenuLocations": "Helyek", + "country_a1": "Anonim proxy", + "country_a2": "Szatellit szolgáltató", + "country_cat": "Katalán közösségek", + "country_o1": "Egyéb ország" + } +} diff --git a/plugins/UserCountry/lang/hy.json b/plugins/UserCountry/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/UserCountry/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/UserCountry/lang/id.json b/plugins/UserCountry/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..b9f98cc32fd4509b7e6ccd6d99c012069c52eee3 --- /dev/null +++ b/plugins/UserCountry/lang/id.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Alamat IP \"%s\" adalah lokal, jadi tidak dapat di-geolokasi.", + "City": "Kota", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Benua", + "Continents": "Benua-benua", + "Country": "Negara", + "CountryCode": "Kode negara", + "CountrySegmentHelp": "Kode negara ISO 3166-1 alpha-2 (de, us, fr, in, es, dll.)", + "CurrentLocationIntro": "Penyedia ini menentukan lokasi Anda saat ini adalah", + "DefaultLocationProviderDesc1": "Penyedia lokasi bawaan menentukan negara tempat pengunjung terhubung berdasarkan bahasa yang mereka pilih.", + "DefaultLocationProviderDesc2": "Ini tidak terlalu akurat, jadi %1$sinstall dan gunakan %2$sdatabase geolokasi%3$s%4$s.", + "DefaultLocationProviderExplanation": "Anda menggunakan penyedia lokasi bawaan, jadi negara tempat pengunjung terhubung ditentukan dari bahasa yang mereka pilih. %1$sPelajari cara menyiapkan geolokasi yang lebih akurat%2$s.", + "DisabledLocationProvider": "Mematikan geolokasi.", + "DistinctCountries": "%s negara berbeda", + "FromDifferentCities": "kota berbeda", + "GeoIPDocumentationSuffix": "Siapkan GeoIP di tab admin Geolokasi untuk menyediakan data untuk laporan ini. Database GeoIP %1$sMaxMind%2$s yang komersial lebih akurat daripada yang gratis. %3$sKlik di sini%4$s untuk melihat akurasinya.", + "GeoIpDbIpAccuracyNote": "Database geolokasi DB-IP gratis dan dapat diunduh secara otomatis. MaxMind lebih akurat untuk kota-kota, tetapi memerlukan %1$smembuat akun%2$s.", + "Geolocation": "Geolokasi", + "GeolocationPageDesc": "Di sini Anda dapat mengubah bagaimana Matomo menentukan lokasi pengunjung.", + "GeolocationProviderBroken": "Penyedia geolokasi Anda (%1$s) rusak. Harap perbaiki penyedia, atau siapkan yang lain.", + "GeolocationProviderUnavailable": "Penyedia geolokasi Anda (%1$s) tidak lagi tersedia. Harap siapkan yang lain.", + "HowToInstallGeoIPDatabases": "Bagaimana cara mendapatkan database geolokasi?", + "Latitude": "Lintang", + "LatitudeSegmentHelp": "-33.578, 40.830, dll.
Anda dapat memilih pengunjung dalam rentang lat/long menggunakan %1$s.", + "Latitudes": "Lintang-lintang", + "Location": "Lokasi", + "LocationProvider": "Penyedia lokasi", + "LocationsSubcategoryHelp": "Bagian \"Lokasi\" adalah cara terbaik untuk mengetahui negara, benua, wilayah, dan kota apa yang berasal dari pengunjung website Anda — dalam bentuk tabel dan peta. Bagian ini juga mengatakan bahasa apa yang disetel di browser mereka, membantu mengidentifikasi pengunjung internasional di lokasi alternatif.", + "Longitude": "Bujur", + "Longitudes": "Bujur-bujur", + "MaxMindLinkExplanation": "%1$sKlik di sini untuk mempelajari cara membuat URL unduhan Anda%2$s jika Anda menggunakan geolokasi MaxMind.", + "NoDataForGeoIPReport1": "Tidak ada data untuk laporan ini, karena tidak ada data lokasi yang tersedia, atau alamat IP pengunjung tidak dapat di-geolokasi.", + "NoDataForGeoIPReport2": "%1$sUbah pengaturan ini%2$s, dan gunakan %3$sdatabase tingkat kota%4$s untuk mendapatkan geolokasi yang akurat.", + "NoProviders": "Tidak ada penyedia geolokasi tambahan yang tersedia. %1$sDB-IP%2$s direkomendasikan, tetapi memerlukan plugin GeoIp2. (Anda juga dapat menginstal plugin geolokasi pihak ketiga dari marketplace.)", + "PluginDescription": "Melaporkan lokasi pengunjung Anda: negara, wilayah, kota, dan koordinat geografis (lintang dan bujur).", + "Region": "Wilayah", + "SubmenuLocations": "Lokasi", + "TestIPLocatorFailed": "Tidak dapat menemukan lokasi alamat IP \"%1$s\". Mungkin penyedia ini tidak dikonfigurasi dengan benar, database geolokasi salah nama, atau berada di folder yang salah?", + "ToGeolocateOldVisits": "Gunakan %1$sskrip ini%2$s untuk mendapatkan data lokasi untuk kunjungan lama Anda.", + "VisitLocation": "Lokasi pengunjung", + "WidgetLocation": "Lokasi pengunjung", + "country_a1": "Proxy anonim", + "country_a2": "Penyedia satelit", + "country_cat": "Komunitas penutur bahasa Katalan", + "country_o1": "Negara lain", + "getCityDocumentation": "Menampilkan kota tempat pengunjung Anda terhubung saat mengakses website Anda.", + "getContinentDocumentation": "Menampilkan benua tempat pengunjung Anda terhubung saat mengakses website Anda.", + "getCountryDocumentation": "Menampilkan negara tempat pengunjung Anda terhubung saat mengakses website Anda.", + "getRegionDocumentation": "Menampilkan wilayah tempat pengunjung Anda terhubung saat mengakses website." + } +} diff --git a/plugins/UserCountry/lang/is.json b/plugins/UserCountry/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..b7e9436239f35041bcb5003e89dd4b2e966698a5 --- /dev/null +++ b/plugins/UserCountry/lang/is.json @@ -0,0 +1,12 @@ +{ + "UserCountry": { + "Continent": "Heimsálfa", + "Country": "Land", + "DistinctCountries": "%s mismunandi lönd", + "Location": "Staðsetning", + "SubmenuLocations": "Staðsetningar", + "country_a1": "Nafnlaus milliþjónn", + "country_a2": "Gerfihnattasamband", + "country_o1": "Annað land" + } +} diff --git a/plugins/UserCountry/lang/it.json b/plugins/UserCountry/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..27e75427a72ce13499db965f767ed477336a6a0a --- /dev/null +++ b/plugins/UserCountry/lang/it.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "L'indirizzo IP \"%s\" è locale, quindi non può essere geolocalizzato.", + "City": "Città", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continente", + "Continents": "Continenti", + "Country": "Paese", + "CountryCode": "Codice nazione", + "CountrySegmentHelp": "Codici paese ISO 3166-1 alpha-2 (de, us, fr, in, es, ecc.)", + "CurrentLocationIntro": "Questo provider determina la tua posizione attuale", + "DefaultLocationProviderDesc1": "Il provider di posizione predefinito determina il paese da cui i visitatori si connettono in base alla lingua selezionata.", + "DefaultLocationProviderDesc2": "Questo non è molto accurato, dunque %1$sinstalla e utilizza %2$sun database di geolocalizzazione%3$s%4$s.", + "DefaultLocationProviderExplanation": "Stai utilizzando il provier di posizione, quindi il paese da cui i tuoi visitatori si connettono sarà determinato dalla lingua da loro selezionata. %1$sLeggi come impostare una geolocalizzazione più accurata%2$s.", + "DisabledLocationProvider": "Disattiva geolocalizzazione.", + "DistinctCountries": "%s continenti differenti", + "FromDifferentCities": "città diverse", + "GeoIPDocumentationSuffix": "Imposta GeoIP nella scheda Amministrazione Geolocalizzazione per fornire i dati per questo report. I database GeoIP commerciali%1$sMaxMind%2$s sono più accurati di quelli gratuiti. Per vedere come sono precisi, clicca %3$squi%4$s.", + "GeoIpDbIpAccuracyNote": "I database di geolocalizzazione DB-IP sono gratuiti e possono essere scaricati automaticamente. MaxMind è più preciso per le città, ma richiede la %1$screazione di un account%2$s.", + "Geolocation": "Geolocalizzazione", + "GeolocationPageDesc": "Qui puoi cambiare le impostazioni di come Matomo determina la località dei visitatori.", + "GeolocationProviderBroken": "Il provider di geolocalizzazione (%1$s) non funziona. Correggi il provider o impostane un altro.", + "GeolocationProviderUnavailable": "Il tuo provider di geolocalizzazione (%1$s) non sarà più disponibile, impostane un altro.", + "HowToInstallGeoIPDatabases": "Come mi procuro un database di geolocalizzazione?", + "Latitude": "Latitudine", + "LatitudeSegmentHelp": "-33.578, 40.830, ecc.
È possibile selezionare i visitatori entro un intervallo di latitudine/longitudine utilizzando %1$s.", + "Latitudes": "Latitudini", + "Location": "Posizione", + "LocationProvider": "Fornitore di localizzazione", + "LocationsSubcategoryHelp": "La sezione \"Posizioni\" è il modo migliore per scoprire da quali paesi, continenti, regioni e città provengono i visitatori del tuo sito web, sotto forma di tabella e mappa. Indica anche su quale lingua è impostato il browser, aiutando a identificare i visitatori esteri che si trovano in località alternative.", + "Longitude": "Longitudine", + "Longitudes": "Longitudini", + "MaxMindLinkExplanation": "%1$sClicca qui per sapere come generare l'URL di download%2$s se utilizzi la geolocalizzazione MaxMind.", + "NoDataForGeoIPReport1": "Non ci sono dati per questo report perché non ci sono dati di localizzazione disponibili o gli indirizzi IP dei visitatori non possono essere geolocalizzati.", + "NoDataForGeoIPReport2": "%1$sModifica queste impostazioni%2$s e utilizza un%3$s database a livello di città%4$s per ottenere una geolocalizzazione accurata.", + "NoProviders": "Non è disponibile alcun fornitore di geolocalizzazione aggiuntivo. È consigliato %1$sDB-IP%2$s, ma richiede il plugin GeoIp2. (Puoi anche installare un plug-in di geolocalizzazione di terze parti preso dal Marketplace.)", + "PluginDescription": "Segnala la provenienza dei tuoi visitatori: nazione, regione, città e coordinate geografiche (latitudine/longitudine).", + "Region": "Regione", + "SubmenuLocations": "Località", + "TestIPLocatorFailed": "Impossibile trovare la posizione dell'indirizzo IP \"%1$s\". Forse questo provider non è impostato correttamente, il database di geolocalizzazione ha un nome errato o si trova nella cartella sbagliata?", + "ToGeolocateOldVisits": "Per avere i dati di localizzazione delle tue vecchie visite, usa %1$squesto%2$s script.", + "VisitLocation": "Posizione del visitatore", + "WidgetLocation": "Posizione visitatore", + "country_a1": "Proxy anonimo", + "country_a2": "Fornitore satellitare", + "country_cat": "Comunità che parlano catalano", + "country_o1": "Altri paesi", + "getCityDocumentation": "Mostra le città da cui i tuoi visitatori si sono connessi quando accedono al tuo sito.", + "getContinentDocumentation": "Questo report mostra da quale continente si sono connessi i tuoi visitatori quando sono entrati nel tuo sito.", + "getCountryDocumentation": "Questo report mostra da quale nazione si sono collegati i tuoi visitatori quando sono entrati nel tuo sito.", + "getRegionDocumentation": "Questo report mostra da quale regione si sono collegati i tuoi visitatori quando sono entrati nel tuo sito." + } +} diff --git a/plugins/UserCountry/lang/ja.json b/plugins/UserCountry/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..0612cebf41e73b2fc43c79aafb4969843cfcc6e6 --- /dev/null +++ b/plugins/UserCountry/lang/ja.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP アドレス \"%s\" はローカルであるため、位置情報を特定できません。", + "City": "都市", + "CityAndCountry": "%1$s、 %2$s", + "Continent": "大陸", + "Continents": "大陸", + "Country": "国", + "CountryCode": "国コード", + "CurrentLocationIntro": "このプロバイダーによると、あなたの現在地は", + "DefaultLocationProviderDesc1": "デフォルトのロケーション プロバイダーは、選択した言語に基づいて、ビジターが接続する国を決定します。", + "DefaultLocationProviderDesc2": "これはあまり正確ではないため、位置情報データベースを%1$sインストールして使用%2$sしてください%3$s%4$s。", + "DefaultLocationProviderExplanation": "デフォルトのロケーション プロバイダーを使用しているため、ビジターが接続する国は、選択した言語によって決定されます。 %1$sより正確な位置情報を設定する方法を学びます%2$s。", + "DisabledLocationProvider": "ジオロケーションをオフにします。", + "DistinctCountries": "%s 個別の国々", + "FromDifferentCities": "異なる都市", + "GeoIPDocumentationSuffix": "このレポートにデータを提供するには、[Geolocation admin] タブで GeoIP を設定します。 商用の %1$sMaxMind %2$sGeoIP データベースは、無料のものよりも正確です。 %3$sここをクリック%4$sして、正確性を確認してください。", + "GeoIpDbIpAccuracyNote": "DB-IP ジオロケーション データベースは無料で、自動ダウンロードできます。 MaxMind は都市に対してより正確ですが、%1$sアカウントを作成%2$sする必要があります。", + "Geolocation": "ジオロケーション", + "GeolocationPageDesc": "ここで Matomoがビジターの位置を決定する方法を変更できます。", + "GeolocationProviderBroken": "位置情報プロバイダー (%1$s) が壊れています。 プロバイダーを修正するか、別のプロバイダーをセットアップしてください。", + "GeolocationProviderUnavailable": "位置情報プロバイダー (%1$s) は利用できなくなりました。 別のものを設定してください。", + "HowToInstallGeoIPDatabases": "ジオロケーションデータベースを取得するにはどうすればよいですか?", + "Latitude": "緯度", + "Latitudes": "緯度", + "Location": "位置情報", + "LocationProvider": "位置情報プロバイダー", + "LocationsSubcategoryHelp": "”場所“ セクションは、ウェブサイトのビジターがどの国、大陸、地域、都市から来ているかを表と地図の形で知るための最良の方法です. また、ブラウザがどの言語に設定されているかも表示され、別の場所にいる海外からのビジターを識別するのに役立ちます。", + "Longitude": "経度", + "Longitudes": "経度", + "MaxMindLinkExplanation": "MaxMind の位置情報を使用している場合に%1$sダウンロード URL を生成する方法については、ここをクリックしてください%2$s。", + "NoDataForGeoIPReport1": "利用可能な位置データがないか、ビジターの IP アドレスを地理的に特定できないため、このレポートのデータはありません。", + "NoDataForGeoIPReport2": "%1$sこれらの設定を変更し%2$s、%3$s都市レベルのデータベース%4$sを使用して正確な位置情報を取得します。", + "NoProviders": "利用可能な追加の地理位置情報プロバイダーはありません。 %1$sDB-IP%2$s が推奨されますが、GeoIp2 プラグインが必要です。 (マーケットプレイスからサードパーティの位置情報プラグインをインストールすることもできます。)", + "PluginDescription": "ビジターの場所を報告します: 国、地域、都市、地理座標 (緯度と経度)。", + "Region": "地域", + "SubmenuLocations": "場所", + "TestIPLocatorFailed": "IP アドレス \"%1$s\" の場所が見つかりませんでした。 このプロバイダーが正しく設定されていないか、地理位置情報データベースの名前が間違っているか、間違ったフォルダーにある可能性がありますか?", + "ToGeolocateOldVisits": "%1$sこのスクリプト%2$sを使用して、以前のビジットの場所データを取得します。", + "VisitLocation": "ビジターの場所", + "WidgetLocation": "ビジターの位置", + "country_a1": "匿名プロキシ", + "country_a2": "衛星プロバイダー", + "country_cat": "カタロニア語圏のコミュニティ", + "country_o1": "その他の国", + "getCityDocumentation": "ビジターが Web サイトにアクセスしたときに接続した都市を示します。", + "getContinentDocumentation": "Web サイトにアクセスしたときにビジターがどの大陸から接続したかを示します。", + "getCountryDocumentation": "ビジターが Web サイトにアクセスしたときにどの国から接続したかを示します。", + "getRegionDocumentation": "ビジターがウェブサイトにアクセスしたときに接続した地域を示します。" + } +} diff --git a/plugins/UserCountry/lang/ka.json b/plugins/UserCountry/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..fb3341538dcdae349163fe197ebb7126a1bc03b5 --- /dev/null +++ b/plugins/UserCountry/lang/ka.json @@ -0,0 +1,12 @@ +{ + "UserCountry": { + "Continent": "კონტინენტი", + "Country": "ქვეყანა", + "DistinctCountries": "%s განსხვავებული ქვეყანა", + "Location": "მდებარეობა", + "SubmenuLocations": "მდებარეობები", + "country_a1": "ანონიმური პროქსი", + "country_a2": "სატელიტური პროვაიდერი", + "country_o1": "სხვა ქვეყანა" + } +} diff --git a/plugins/UserCountry/lang/ko.json b/plugins/UserCountry/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..c0ec53e6d247543764fce549af098883e9fdc67b --- /dev/null +++ b/plugins/UserCountry/lang/ko.json @@ -0,0 +1,36 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP 주소 %s는 로컬 주소이기 때문에 위치를 추적할 수 없습니다.", + "City": "도시", + "CityAndCountry": "%1$s, %2$s", + "Continent": "대륙", + "Country": "국가", + "CurrentLocationIntro": "이 공급자에 따르면, 현재 위치는", + "DefaultLocationProviderDesc1": "방문자의 국가에 기반하는 언어에서 추측하는 기본 위치 공급자입니다.", + "DefaultLocationProviderExplanation": "당신은 현재 기본 위치 공급자를 사용하고 있어, Matomo는 방문자의 위치를 그들이 사용하는 언어에 기반하여 추측하고 있습니다. %1$s이 문서%2$s에서 어떻게 더 정확한 위치를 얻어낼 수 있는지 얘기하고 있습니다.", + "DistinctCountries": "%s개 국가", + "FromDifferentCities": "다른 도시", + "GeoIPDocumentationSuffix": "이 보고서에 대한 데이터를 볼 수 있도록 당신은 위치 정보관리 탭에서 GeoIP를 설정해야합니다. 상업용 %1$sMaxmind%2$s GeoIP 데이터베이스는 무료로 버전보다 더 정확합니다. 정확도를 높이려면 %3$s공유%4$s를 클릭하세요.", + "Geolocation": "지리적 위치", + "GeolocationPageDesc": "이 페이지에서 Matomo 방문자 위치를 결정하는 방법을 변경할 수 있습니다.", + "Latitude": "위도", + "Location": "위치", + "LocationProvider": "위치 공급자", + "Longitude": "경도", + "NoDataForGeoIPReport1": "이 보고서에 대한 데이터가 없습니다. 어떠한 위치 데이터도 사용할 수 없거나, 방문자의 IP 주소에서 지역정보를 찾을 수 없기 때문입니다.", + "NoDataForGeoIPReport2": "더 정확한 위치 정보를 활성화시키려면 %1$s이 곳%2$s의 설정을 %3$s도시 수준의 데이터베이스%4$s로 사용해야 합니다.", + "PluginDescription": "방문자 위치 보고서: 국가, 지역, 도시, 지리적 좌표(위도/경도)", + "Region": "지역", + "SubmenuLocations": "위치", + "ToGeolocateOldVisits": "기존의 방문의 위치 데이터를 얻으려면 %1$s이 곳%2$s에 있는 스크립트 설명을 사용하세요.", + "WidgetLocation": "방문자 위치", + "country_a1": "익명 프록시", + "country_a2": "위성 공급자", + "country_cat": "카탈로니아 지역의 커뮤니티", + "country_o1": "기타 국가", + "getCityDocumentation": "이 보고서는 방문자가 웹사이트를 어느 도시에서 접근했는지 보여줍니다.", + "getContinentDocumentation": "이 보고서는 방문자가 웹사이트로 접속한 대륙을 보여줍니다.", + "getCountryDocumentation": "이 보고서는 웹사이트로 접근한 방문자가 어느 국가에 있었는지 보여줍니다.", + "getRegionDocumentation": "이 보고서는 방문자가 웹사이트에 접속할 때의 지역이 표시됩니다." + } +} diff --git a/plugins/UserCountry/lang/ku.json b/plugins/UserCountry/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/UserCountry/lang/ku.json @@ -0,0 +1 @@ +{} diff --git a/plugins/UserCountry/lang/lb.json b/plugins/UserCountry/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/UserCountry/lang/lb.json @@ -0,0 +1 @@ +{} diff --git a/plugins/UserCountry/lang/lt.json b/plugins/UserCountry/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..dc0a10ab4ad7a16a6e83e8c82b20b1cd8a5fc6b5 --- /dev/null +++ b/plugins/UserCountry/lang/lt.json @@ -0,0 +1,24 @@ +{ + "UserCountry": { + "City": "Miestas", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinentas", + "Continents": "Kontinentai", + "Country": "Šalis", + "CountryCode": "Šalies kodas", + "DistinctCountries": "%s atskirti šalis", + "Geolocation": "Geografinė vietovė", + "GeolocationPageDesc": "Šiame puslapyje galite keisti kaip Matomo nustato lankytojų geografines vietas.", + "Latitude": "Platuma", + "Latitudes": "Platumos", + "Location": "Vietovė", + "Longitude": "Ilguma", + "Longitudes": "Ilgumos", + "Region": "Regionas", + "SubmenuLocations": "Vietovės", + "country_a1": "Anoniminis įgaliotasis serveris", + "country_a2": "Palydovinis tiekėjas", + "country_cat": "Kataloniškai kalbančios bendruomenės", + "country_o1": "Kita šalis" + } +} diff --git a/plugins/UserCountry/lang/lv.json b/plugins/UserCountry/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..a3eda9a9504e6f2f3a23fe9e0354d54fe11c8545 --- /dev/null +++ b/plugins/UserCountry/lang/lv.json @@ -0,0 +1,12 @@ +{ + "UserCountry": { + "Continent": "Kontinents", + "Country": "Valsts", + "Location": "Lokācijas", + "SubmenuLocations": "Lokācijas", + "country_a1": "Anonīms starpniekserveris", + "country_a2": "Satelītpieslēgums", + "country_cat": "Katalāņu valodā runājošās kopienas", + "country_o1": "Cita valsts" + } +} diff --git a/plugins/UserCountry/lang/ms.json b/plugins/UserCountry/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/UserCountry/lang/ms.json @@ -0,0 +1 @@ +{} diff --git a/plugins/UserCountry/lang/nb.json b/plugins/UserCountry/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..767e2aae248af6fe8a557ed370bf399deab1a297 --- /dev/null +++ b/plugins/UserCountry/lang/nb.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP-adressen %s er en lokal adresse og kan ikke lokaliseres.", + "City": "By", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Continents": "Kontinenter", + "Country": "Land", + "CountryCode": "Landskode", + "CurrentLocationIntro": "Ifølge denne leverandøren, er din nåværende posisjon", + "DefaultLocationProviderDesc1": "Forvalg posisjonstilbyder gjetter en besøkers land basert på språket de bruker.", + "DefaultLocationProviderDesc2": "Dette er ikke veldig nøyaktig, så %1$s det anbefales å installere og bruke %2$sen geoposisjonsdatabase%3$s. %4$s", + "DefaultLocationProviderExplanation": "Du bruker forvalgt posisjonstilbyder, noe som betyr at Matomo gjetter den besøkendes posisjon basert på språket de bruker. %1$sLes dette%2$s for å lære mer om hvordan du setter opp mer nøyaktig geoposisjonering.", + "DisabledLocationProvider": "Skrur av geolokaliseringen.", + "DistinctCountries": "%s bestemte land", + "FromDifferentCities": "forskjellige byer", + "GeoIPDocumentationSuffix": "For å kunne se data for denne rapporten, må du sette opp GeoIP i administratorfanen for geoposisjonering. De kommersielle %1$sMaxMind%2$sGeoIP-databasene er mer nøyaktige enn dem som er gratis. For å se hvor nøyaktige de er kan du klikke %3$sher%4$s.", + "GeoIpDbIpAccuracyNote": "Merk: DBIP-databasene er gratis og kan lastes ned automatisk, men geoposisjoneringsresultater (spesifikt byresultater) er ikke like nøyaktige som MaxMind sine. Maxmind krever dog at du oppretter konto selv for gratis-databasen. Hvis du vil bruke MaxMind sin geoposisjoneringsdatabasem, kan du starte prosessen %1$sher%2$s.", + "Geolocation": "Geoposisjonering", + "GeolocationPageDesc": "På denne siden kan du endre hvordan Matomo bestemmer posisjon for besøk.", + "GeolocationProviderBroken": "Oppsatt %1$s-geoposisjonstilbyder er ødelagt. Fiks tilbyderen eller sett opp en annen for å få geoposisjonering til å virke igjen.", + "GeolocationProviderUnavailable": "Den oppsatte %1$s-geoposisjoneringstilbyderen er ikke tilgjengelig lenger. Sett opp en annen.", + "HowToInstallGeoIPDatabases": "Hvordan skaffer jeg geoposisjoneringsdatabaser?", + "Latitude": "Breddegrad", + "Latitudes": "Breddegrader", + "Location": "Sted", + "LocationProvider": "Posisjonstilbyder", + "LocationsSubcategoryHelp": "Posisjonsdelen er den beste måten å finne ut hvor folk var da de besøkte nettsiden din. Den avslører land, kontinenter, regioner, og byere dine besøkende var i, i tabeller og kartform. Videre kan du se hvilket språk nettleserne deres var satt til, noe som lar deg finne internasjonale besøkere på andre steder.", + "Longitude": "Lengdegrad", + "Longitudes": "Lengdegrader", + "MaxMindLinkExplanation": "%1$sKlikk her%2$s hvis du bruker MaxMind sine geoposisjoneringsdatabaser og ikke allerede vet hvordan du lager en nedlastningsnettadresse.", + "NoDataForGeoIPReport1": "Det er ingen data for denne rapporten fordi det enten ikke finner posisjonsdata tilgjengelig, eller IP-adressene tilhørende besøkende ikke kan geoposisjoneres.", + "NoDataForGeoIPReport2": "For å skru på nøyaktig geoposisjonering kan du endre innstillingene %1$sher%2$s og bruke en %3$sbynivåsdatabase%4$s.", + "NoProviders": "Det er ingen ytterligere geoposisjoneringstilbydere tilgjengelig. Matomo anbefaler å bruke %1$sDBIP%2$s-databaser, men dette krever aktivering av GeoIP2-programtillegget. (Alternativt kan du installere et tredjepartsprogramtillegg som tilbyr sin egen geoposisjoneringsfunksjonalitet fra markedsplassen.)", + "PluginDescription": "Rapporterer posisjonen for dine besøkende: Land, region, by, og geografiske koordinater (breddegrad/lengdegrad).", + "Region": "Region", + "SubmenuLocations": "Lokasjoner", + "TestIPLocatorFailed": "Matomo prøvde å sjekke posisjonen for en kjent IP-adresse (%1$s), men din tjener returnerte ingen info. Denne tilbyder kan være satt opp feil (geoposisjoneringsdatabasen kan ha feil navn, eller befinne seg i feil mappe, for eksempel).", + "ToGeolocateOldVisits": "For å hente posisjonsdata for dine gamle besøk, kan du bruke skriptene beskrevet %1$sher%2$s.", + "VisitLocation": "Besøkssted", + "WidgetLocation": "Besøkendes lokasjon", + "country_a1": "Anonym Proxy", + "country_a2": "Satellitt-tilbyder", + "country_cat": "Områder som snakker katalansk", + "country_o1": "Annet land", + "getCityDocumentation": "Denne rapporten viser hvilke byer dine besøkende var i da de besøkte nettsiden din.", + "getContinentDocumentation": "Denne rapporten viser hvilket kontinent dine besøkende var i da de besøkte nettsiden din.", + "getCountryDocumentation": "Denne rapporten viser hvilket land dine besøkende var i da de besøkte nettsiden din.", + "getRegionDocumentation": "Denne rapporten viser hvor dine besøkende var da de besøkte nettsiden din." + } +} diff --git a/plugins/UserCountry/lang/nl.json b/plugins/UserCountry/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..4a43b8faf4d20437f5b4b70a22e4ab48b0147c1c --- /dev/null +++ b/plugins/UserCountry/lang/nl.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Het IP-adres \"%s\" is lokaal, waardoor geolocatie niet mogelijk is.", + "City": "Stad", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continent", + "Continents": "Continenten", + "Country": "Land", + "CountryCode": "Land code", + "CurrentLocationIntro": "Volgens deze provider is uw huidige locatie", + "DefaultLocationProviderDesc1": "De standaard locatie provider bepaalt vanuit welk land bezoekers verbinding maken op basis van de door hen geselecteerde taal.", + "DefaultLocationProviderDesc2": "Dit is niet erg nauwkeurig, dus %1$sinstalleer en gebruik een %2$sgeolocatie database%3$s%4$s.", + "DefaultLocationProviderExplanation": "U gebruikt de standaard locatie provider, dus het land van waaruit bezoekers verbinding maken, wordt bepaald aan de hand van hun geselecteerde taal. %1$sLees meer over hoe u een nauwkeuriger geolocatie kunt instellen%2$s.", + "DisabledLocationProvider": "Schakelt geolocatie uit.", + "DistinctCountries": "%s opvallende landen", + "FromDifferentCities": "verschillende steden", + "GeoIPDocumentationSuffix": "Stel GeoIP in op het tabblad Geolocation admin om gegevens voor dit rapport te leveren. De commerciële %1$sMaxMind%2$s GeoIP databases zijn nauwkeuriger dan de gratis databases. %3$sKlik hier%4$s om te zien hoe nauwkeurig ze zijn.", + "GeoIpDbIpAccuracyNote": "De DB-IP geolocatie databases zijn gratis en kunnen automatisch worden gedownload. MaxMind is nauwkeuriger voor steden, maar vereist %1$shet maken van een account%2$s.", + "Geolocation": "Geolocatie", + "GeolocationPageDesc": "Op deze pagina kunt u instellen hoe Matomo de locaties van bezoekers bepaalt.", + "GeolocationProviderBroken": "Uw geolocatie provider (%1$s) is kapot. Repareer de provider, of stel een andere in.", + "GeolocationProviderUnavailable": "Uw geolocatie provider (%1$s) is niet langer beschikbaar. Stel een andere in.", + "HowToInstallGeoIPDatabases": "Hoe kom ik aan geolocatie databases?", + "Latitude": "Latitude", + "Latitudes": "Latitudes", + "Location": "Locatie", + "LocationProvider": "Locatie provider", + "LocationsSubcategoryHelp": "De sectie \"Locaties\" is de beste manier om te weten te komen uit welke landen, continenten, regio's en steden de bezoekers van uw website komen — in tabel- en kaartvorm. Ook wordt aangegeven met welke taal hun browser is ingesteld, zodat internationale bezoekers op andere locaties kunnen worden geïdentificeerd.", + "Longitude": "Longitude", + "Longitudes": "Longitudes", + "MaxMindLinkExplanation": "%1$sKlik hier om te leren hoe u uw download-URL kunt genereren%2$s indien u de geolocatie van MaxMind gebruikt.", + "NoDataForGeoIPReport1": "Geen gegevens voor dit rapport, aangezien er geen locatie informatie beschikbaar is, of IP-adressen van bezoekers niet geografisch bepaald kunnen worden.", + "NoDataForGeoIPReport2": "%1$sWijzig deze instellingen%2$s, en gebruik een %3$sdatabase op stedenniveau%4$s voor een nauwkeurige geolocatie.", + "NoProviders": "Geen extra geolocatie providers beschikbaar. %1$sDB-IP%2$s wordt aanbevolen, maar vereist de GeoIp2 plugin. (U kunt ook een geolocatie-plugin van derden uit de marktplaats installeren.)", + "PluginDescription": "Rapporteert de locatie van je bezoekers: land, regio, stad en geografische coördinaten (lengte- en breedtegraad).", + "Region": "Regio", + "SubmenuLocations": "Locaties", + "TestIPLocatorFailed": "Kon de locatie van het IP-adres \"%1$s\" niet vinden. Misschien is deze provider niet correct ingesteld, heeft de geolocatie database een verkeerde naam, of staat die in de verkeerde map?", + "ToGeolocateOldVisits": "Gebruik %1$sdit script%2$s om locatiegegevens te krijgen voor uw oude bezoeken.", + "VisitLocation": "Bezoeker locatie", + "WidgetLocation": "Bezoeker locatie", + "country_a1": "Anonieme proxy", + "country_a2": "Satelliet provider", + "country_cat": "Catalaans-sprekende gemeenschappen", + "country_o1": "Ander land", + "getCityDocumentation": "Toont de steden van waaruit uw bezoekers verbinding maakten bij het bekijken van uw website.", + "getContinentDocumentation": "Toont vanuit welk continent uw bezoekers verbinding maakten bij het bekijken van uw website.", + "getCountryDocumentation": "Toont vanuit welk land uw bezoekers verbinding maakten bij het bekijken van uw website.", + "getRegionDocumentation": "Toont vanuit welke regio uw bezoekers verbinding maakten bij het bekijken van de website." + } +} diff --git a/plugins/UserCountry/lang/nn.json b/plugins/UserCountry/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..55539acc3b8e034f47ba5e75142bc10d21f7724c --- /dev/null +++ b/plugins/UserCountry/lang/nn.json @@ -0,0 +1,13 @@ +{ + "UserCountry": { + "Continent": "Kontinent", + "Country": "Land", + "DistinctCountries": "%s distinkte land", + "Location": "Plass", + "SubmenuLocations": "Plassar", + "country_a1": "Anonym mellomtenar", + "country_a2": "Satelitt-tilbydar", + "country_cat": "Katalanske område", + "country_o1": "Andre land" + } +} diff --git a/plugins/UserCountry/lang/pl.json b/plugins/UserCountry/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..5939eccad4b254bdab036c8d3263fed229b61060 --- /dev/null +++ b/plugins/UserCountry/lang/pl.json @@ -0,0 +1,41 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Adres IP %s jest adresem lokalnym i nie można go poddać geolokacji.", + "City": "Miasto", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontynent", + "Continents": "Kontynenty", + "Country": "Kraj", + "CurrentLocationIntro": "Według tego dostawcy, twoją aktualną lokalizacją jest", + "DefaultLocationProviderDesc1": "Domyślny dostawca usług geolokacyjnych zgaduje państwo odwiedzającego na podstawie języka używanego w przeglądarce.", + "DefaultLocationProviderExplanation": "Korzystasz z domyślnego dostawcy usług geolokalizacyjnych, co oznacza, że Matomo zgaduje lokalizację odwiedzającego na podstawie języka jego przeglądarki. %1$sPrzeczytaj%2$s aby dowiedzieć się jak skonfigurować dokładniejszą geolokalizację.", + "DistinctCountries": "%s różnych krajów", + "FromDifferentCities": "różne miasta", + "GeoIPDocumentationSuffix": "W celu wyświetlenia danych tego raportu konieczna jest konfiguracja GeoIP w zakładce Geolokalizacja panelu administracyjnego. Płatne bazy GeoIP dostarczane przez %1$sMaxmind%2$s cechuje wyższa dokładność od baz bezpłatnych. Aby porównać ich dokładność kliknij %3$stutaj%4$s.", + "Geolocation": "Geolokalizacja", + "GeolocationPageDesc": "Na tej stronie można zmienić sposób wykrywania lokalizacji odwiedzających stosowany przez Matomo'a.", + "HowToInstallGeoIPDatabases": "Jak pozyskać bazy danych geolokalizacyjnych?", + "Latitude": "Szerokość", + "Latitudes": "Szerokości", + "Location": "Lokalizacja", + "LocationProvider": "Dostawca Lokalizacji", + "Longitude": "Długość", + "Longitudes": "Długości", + "NoDataForGeoIPReport1": "Brak danych dla tego raportu ponieważ albo dane lokalizacyjne nie są dostępne albo adres IP odwiedzającego nie może zostać zlokalizowany", + "NoDataForGeoIPReport2": "Aby włączyć dokładną geolokalizację, zmień %1$ste ustawienia%2$s i użyj %3$sbazy lokalizacji o dokładności miasta%4$s.", + "PluginDescription": "Informuje o położeniu Twoich odwiedzających: kraju, regionie, mieście i koordynatach geograficznych (szerokość / długość geograficzna).", + "Region": "Region", + "SubmenuLocations": "Lokalizacje", + "ToGeolocateOldVisits": "Aby ustalić lokalizację archiwalnych wizyt użyj skryptu opisanego %1$stutaj%2$s.", + "VisitLocation": "Lokalizacja odwiedzającego", + "WidgetLocation": "Lokalizacja odwiedzającego", + "country_a1": "Anonimowe Proxy", + "country_a2": "Dostawca satelitarny", + "country_cat": "społeczność mówiąca po Katalońsku", + "country_o1": "Nieokreślony kraj pochodzenia", + "getCityDocumentation": "Raport ten pokazuje miasta, w których przebywali odwiedzający gdy whcodzili na stronę.", + "getContinentDocumentation": "Raport then pokazuje kraje, w których byli odwiedzający gdzy oglądali strony.", + "getCountryDocumentation": "Ten raport pokazuje, w którym kraju był odwiedzający kiedy wchodził na twoją stronę.", + "getRegionDocumentation": "Ten raport pokazuje,w którym regionie był odwiedzający kiedy wchodził na twoją stronę." + } +} diff --git a/plugins/UserCountry/lang/pt-br.json b/plugins/UserCountry/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..5be0eb8d1f6572d803a0ca222e5619748b404048 --- /dev/null +++ b/plugins/UserCountry/lang/pt-br.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "O endereço IP \"%s\" é local, portanto não pode ser geolocalizado.", + "City": "Cidade", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continente", + "Continents": "Continentes", + "Country": "País", + "CountryCode": "Código do país", + "CountrySegmentHelp": "Códigos de país ISO 3166-1 alpha-2 (de, us, fr, in, es, etc.)", + "CurrentLocationIntro": "Este provedor determina que sua localização atual seja", + "DefaultLocationProviderDesc1": "O provedor de localização padrão determina o país de onde os visitantes se conectam com base no idioma selecionado.", + "DefaultLocationProviderDesc2": "Isso não é muito preciso, então %1$sinstale e use um banco de dados de %2$sgeolocalização%3$s%4$s.", + "DefaultLocationProviderExplanation": "Você está usando o provedor de localização padrão, portanto, o país de onde os visitantes se conectam é determinado a partir do idioma selecionado. %1$sSaiba como configurar uma geolocalização mais precisa%2$s.", + "DisabledLocationProvider": "Desativa a geolocalização.", + "DistinctCountries": "%s países distintos", + "FromDifferentCities": "cidades diferentes", + "GeoIPDocumentationSuffix": "Configure o GeoIP na guia de administração de geolocalização para fornecer dados para este relatório. Os bancos de dados GeoIP comerciais %1$sMaxMind%2$s são mais precisos do que os gratuitos. %3$sClique aqui%4$s para ver a precisão deles.", + "GeoIpDbIpAccuracyNote": "Os bancos de dados de geolocalização DB-IP são gratuitos e podem ser baixados automaticamente. MaxMind é mais preciso para cidades, mas requer %1$scriar uma conta%2$s.", + "Geolocation": "Geolocalização", + "GeolocationPageDesc": "Aqui você pode alterar como o Matomo determina os locais dos visitantes.", + "GeolocationProviderBroken": "Seu provedor de geolocalização (%1$s) está quebrado. Corrija o provedor ou configure outro.", + "GeolocationProviderUnavailable": "Seu provedor de geolocalização (%1$s) não está mais disponível. Por favor, configure outro.", + "HowToInstallGeoIPDatabases": "Como eu consigo bases de dados de geolocalização?", + "Latitude": "Latitude", + "LatitudeSegmentHelp": "-33,578; 40,830; etc.
Você pode selecionar visitantes dentro de um intervalo de latitude/longitude usando %1$s.", + "Latitudes": "Latitudes", + "Location": "Localização", + "LocationProvider": "Provedor de localização", + "LocationsSubcategoryHelp": "A seção \"Locais\" é a melhor maneira de descobrir de quais países, continentes, regiões e cidades vêm os visitantes do seu site - em forma de tabela e mapa. Ele também diz em qual idioma o navegador está configurado, ajudando a identificar visitantes internacionais em locais alternativos.", + "Longitude": "Longitude", + "Longitudes": "Longitudes", + "MaxMindLinkExplanation": "%1$sClique aqui para saber como gerar sua URL de download%2$s se estiver usando a geolocalização do MaxMind.", + "NoDataForGeoIPReport1": "Não há dados para este relatório, pois não há dados de localização disponíveis ou os endereços IP dos visitantes não podem ser geolocalizados.", + "NoDataForGeoIPReport2": "%1$sAltere essas configurações%2$s e use um %3$sbanco de dados no nível da cidade%4$s para obter geolocalização precisa.", + "NoProviders": "No momento não há outros provedores de geolocalização disponíveis. O Matomo recomenda o uso das bases de dados %1$sdbip%2$s, mas isso requer ativar o plugin GeoIp2. (Como alternativa, você pode instalar da loja de aplicativos um plugin de terceiros que tenha sua própria funcionalidade de geolocalização.)", + "PluginDescription": "Informa a localização de seus visitantes: país, região, cidade e coordenadas geográficas (latitude e longitude).", + "Region": "Região", + "SubmenuLocations": "Locais", + "TestIPLocatorFailed": "Não foi possível encontrar a localização do endereço IP \"%1$s\". Talvez este provedor não esteja configurado corretamente, o banco de dados de geolocalização esteja nomeado incorretamente ou na pasta errada?", + "ToGeolocateOldVisits": "Use %1$seste script%2$s para obter dados de localização para suas visitas anteriores.", + "VisitLocation": "Localização do visitante", + "WidgetLocation": "Localização do visitante", + "country_a1": "Proxy anônimo", + "country_a2": "Provedor de satélite", + "country_cat": "Comunidades Catalães", + "country_o1": "Outro país", + "getCityDocumentation": "Mostra as cidades de onde seus visitantes se conectaram ao acessar seu site.", + "getContinentDocumentation": "Mostra de qual continente seus visitantes se conectaram ao acessar seu site.", + "getCountryDocumentation": "Mostra de qual país seus visitantes se conectaram ao acessar seu site.", + "getRegionDocumentation": "Mostra de qual região seus visitantes se conectaram ao acessar o site." + } +} diff --git a/plugins/UserCountry/lang/pt.json b/plugins/UserCountry/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..1b4b3fee78bf7fbfe911609f7b07758ebb921a42 --- /dev/null +++ b/plugins/UserCountry/lang/pt.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "O endereço de IP %s é um endereço local e não pode ser geolocalizado.", + "City": "Cidade", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continente", + "Continents": "Continentes", + "Country": "País", + "CountryCode": "Código do país", + "CurrentLocationIntro": "Este provedor determina que sua localização atual seja", + "DefaultLocationProviderDesc1": "O provedor de localização padrão determina o país de onde os visitantes se conectam com base no idioma selecionado.", + "DefaultLocationProviderDesc2": "Isso não é muito preciso, então %1$sinstale e use um %2$sbanco de dados de geolocalização%3$s%4$s.", + "DefaultLocationProviderExplanation": "Você está usando o provedor de localização padrão, portanto, o país de onde os visitantes se conectam é determinado a partir do idioma selecionado. %1$sSaiba como configurar uma geolocalização mais precisa%2$s.", + "DisabledLocationProvider": "Desativa a geolocalização.", + "DistinctCountries": "%s países distintos", + "FromDifferentCities": "cidades diferentes", + "GeoIPDocumentationSuffix": "Configure o GeoIP na guia de administração de geolocalização para fornecer dados para este relatório. Os bancos de dados GeoIP comerciais %1$sMaxMind%2$s são mais precisos do que os gratuitos. %3$sClique aqui%4$s para ver a precisão deles.", + "GeoIpDbIpAccuracyNote": "Os bancos de dados de geolocalização DB-IP são gratuitos e podem ser descarregados automaticamente. MaxMind é mais preciso para cidades, mas requer %1$scriar uma conta%2$s.", + "Geolocation": "Geolocalização", + "GeolocationPageDesc": "Aqui você pode alterar como o Matomo determina os locais dos visitantes.", + "GeolocationProviderBroken": "Seu provedor de geolocalização (%1$s) está quebrado. Corrija o provedor ou configure outro.", + "GeolocationProviderUnavailable": "Seu provedor de geolocalização (%1$s) não está mais disponível. Por favor, configure outro.", + "HowToInstallGeoIPDatabases": "Como obtenho bases de dados de geolocalização?", + "Latitude": "Latitude", + "Latitudes": "Latitudes", + "Location": "Localização", + "LocationProvider": "Provedor de localização", + "LocationsSubcategoryHelp": "A seção \"Locais\" é a melhor maneira de descobrir de quais países, continentes, regiões e cidades vêm os visitantes do seu site - em forma de tabela e mapa. Ele também diz em qual idioma o navegador está configurado, ajudando a identificar visitantes internacionais em locais alternativos.", + "Longitude": "Longitude", + "Longitudes": "Longitudes", + "MaxMindLinkExplanation": "%1$sClique aqui para saber como gerar a sua URL de descarga%2$s se usa a geolocalização do MaxMind.", + "NoDataForGeoIPReport1": "Não há dados para este relatório, pois não há dados de localização disponíveis ou os endereços IP dos visitantes não podem ser geolocalizados.", + "NoDataForGeoIPReport2": "%1$sAltere essas configurações%2$s e use um %3$sbanco de dados no nível da cidade%4$s para obter uma geolocalização precisa.", + "NoProviders": "Nenhum provedor de geolocalização adicional disponível. %1$sDB-IP%2$s é recomendado, mas requer o plug-in GeoIp2. (Você também pode instalar um plug-in de geolocalização de terceiros no marketplace.)", + "PluginDescription": "Informa a localização de seus visitantes: país, região, cidade e coordenadas geográficas (latitude e longitude).", + "Region": "Região", + "SubmenuLocations": "Localizações", + "TestIPLocatorFailed": "Não foi possível encontrar a localização do endereço IP \"%1$s\". Talvez este provedor não esteja configurado corretamente, ou o banco de dados de geolocalização esteja nomeado incorretamente ou está na pasta errada?", + "ToGeolocateOldVisits": "Para obter os dados de localização para as suas visitas antigas, utilize o %1$sscript descrito aqui%2$s.", + "VisitLocation": "Localização do visitante", + "WidgetLocation": "Localização do visitante", + "country_a1": "Proxy anônimo", + "country_a2": "Provedor de satélite", + "country_cat": "Comunidades língua Catalã", + "country_o1": "Outro país", + "getCityDocumentation": "Mostra as cidades de onde seus visitantes se conectaram ao acessar seu site.", + "getContinentDocumentation": "Mostra de qual continente seus visitantes se conectaram ao acessar seu site.", + "getCountryDocumentation": "Mostra de qual país seus visitantes se conectaram ao acessar seu site.", + "getRegionDocumentation": "Mostra de qual região seus visitantes se conectaram ao acessar o site." + } +} diff --git a/plugins/UserCountry/lang/ro.json b/plugins/UserCountry/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..71ea36be64cad1666f1ce837bc7886a93bca0a7d --- /dev/null +++ b/plugins/UserCountry/lang/ro.json @@ -0,0 +1,35 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Adresa IP %s este o adresă locală și nu pot fi localizate geografic.", + "City": "Oraş", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Continent", + "Country": "Ţară", + "CurrentLocationIntro": "Conform acestui furnizor, locația curentă este", + "DefaultLocationProviderDesc1": "Furnizorul de locație implicită ghiceste tara unui vizitator bazat pe limba care o folosesc.", + "DefaultLocationProviderExplanation": "Acum folositi locatie implicita de furnizorul l, ceea ce înseamnă că Matomo va ghici locația vizitatorilor bazat pe limba pe care o folosesc. %1$scititi acest%2$s pentru a învăța cum să setați geolocalizarea mai precis.", + "DistinctCountries": "%s ţări distincte", + "FromDifferentCities": "diferite orașe", + "GeoIPDocumentationSuffix": "Pentru a vedea datele pentru acest raport, trebuie să configurati GeoIP în fila Geolocation admin tine.Comercial %1$sMaxmind%2$s baze de date GeoIP sunt mai precise decât cele libere. Pentru a vedea cât de exacte sunt, faceți clic pe %3$saici%4$s.", + "Geolocation": "Geolocation", + "GeolocationPageDesc": "Pe această pagină poți schimba cum Matomo determină locațiile vizitatorilor.", + "Latitude": "Latitudine", + "Location": "Localitate", + "LocationProvider": "Locație Provider", + "Longitude": "Longitudine", + "NoDataForGeoIPReport1": "Nu există date pentru acest raport, deoarece nu există, fie nu există date disponibile de localizare sau adresele IP ale vizitatorilor nu pot fi localizate geografic.", + "NoDataForGeoIPReport2": "Pentru a activa geolocalizare exacta, modifica setările %1$saici%2$s și de a folosi o %3$s nivel oras baza de date%4$s.", + "Region": "Regiune", + "SubmenuLocations": "Locații", + "ToGeolocateOldVisits": "Pentru a obține datele de localizare pentru d vizite vechi, script-ul descris %1$s aici %2$s.", + "WidgetLocation": "Locaţia vizitatorului", + "country_a1": "Proxy anonim", + "country_a2": "Provider Satelit", + "country_cat": "Comunitățile vorbitoare de limbă catalană", + "country_o1": "Altă ţară", + "getCityDocumentation": "Acest raport arată din ce oras sunt vizitatorii când au accesat site-ul dumneavoastră.", + "getContinentDocumentation": "Acest raport arată pe ce continent au fost vizitatori când au accesat site-ul dumneavoastră.", + "getCountryDocumentation": "Acest raport arată din ce țară sunt vizitatorii dvs. cand au accesat site-ul dumneavoastră.", + "getRegionDocumentation": "Acest raport arată din ce regiune sunt vizitatorii dvs. când au accesat site-ul dumneavoastră." + } +} diff --git a/plugins/UserCountry/lang/ru.json b/plugins/UserCountry/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..81365f28a00d49ef661e8ddc7e56d5aa08d37eb5 --- /dev/null +++ b/plugins/UserCountry/lang/ru.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP адрес %s - это локальный адрес и его конкретное местонахождение не может быть определено.", + "City": "Город", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Континент", + "Continents": "Континент", + "Country": "Страна", + "CountryCode": "Код страны", + "CurrentLocationIntro": "Согласно используемому способу отслеживания вы находитесь здесь", + "DefaultLocationProviderDesc1": "Этот способ отслеживания определяет местоположение на основе языка, которым пользователи пользуются в браузере и системе.", + "DefaultLocationProviderDesc2": "Это не очень точно, поэтому %1$sмы рекомендуем установить и использовать %2$sбазу данных геолокации%3$s.%4$s", + "DefaultLocationProviderExplanation": "Вы используете провайдер определения местоположения по умолчанию. Это означает, что Matomo будет судить о местоположении посетителя по используемому им языку. %1$sПрочтите здесь%2$s о том, как устанавливать местоположение более точно.", + "DisabledLocationProvider": "Отключает геолокацию.", + "DistinctCountries": "%s уникальных стран", + "FromDifferentCities": "разные города", + "GeoIPDocumentationSuffix": "Чтобы увидеть данные по этому отчету вы должны установить GeoIP в секции Geolocation, которая находится в панели администрирования. Платные базы данных GeoIP %1$sMaxmind%2$s более точны, чем бесплатные. Насколько именно они точные, вы можете посмотреть %3$sздесь%4$s.", + "GeoIpDbIpAccuracyNote": "Примечание: базы данных DBIP бесплатны и могут быть загружены автоматически, но результаты геолокации (в частности, результаты по городам) не так точны, как у MaxMind. Однако, MaxMind, требует, чтобы вы создали учетную запись даже для бесплатной базы данных. Если вы хотите использовать базу данных геолокации MaxMind, вы можете начать процесс %1$sтут%2$s", + "Geolocation": "Геолокация", + "GeolocationPageDesc": "На этой странице вы можете изменить способ определения местоположения посетителей.", + "GeolocationProviderBroken": "Настроенный провайдер геолокации %1$s неисправен. Пожалуйста, исправьте провайдера или настройте другого, чтобы геолокация снова работала.", + "GeolocationProviderUnavailable": "Настроенный поставщик геолокации %1$s больше не доступен, пожалуйста, настройте другого.", + "HowToInstallGeoIPDatabases": "Как получить базы данных геолокации?", + "Latitude": "Широта", + "Latitudes": "Широта", + "Location": "Локация", + "LocationProvider": "Способ определения локации пользователя", + "LocationsSubcategoryHelp": "Раздел Locations — это лучший способ узнать, где находятся люди, посещающие ваш сайт. Он показывает страны, континенты, регионы, города, из которых приходят ваши посетители, в виде таблиц и карт. Кроме того, вы можете увидеть, на какой язык настроен их браузер, что поможет определить иностранных посетителей в других местах.", + "Longitude": "Долгота", + "Longitudes": "Долгота", + "MaxMindLinkExplanation": "Если вы используете геолокационные базы данных MaxMind и еще не знаете, как генерировать URL-адрес загрузки, %1$sнажмите здесь, чтобы узнать как%2$s.", + "NoDataForGeoIPReport1": "Не существует данных для этого отчета, потому что либо нет данных о местоположении или IP адрес посетителя не может быть определён географически.", + "NoDataForGeoIPReport2": "Для обеспечения точной геолокации, изменить параметры %1$sтут%2$s и используйте %3$sбазу данных городов%4$s.", + "NoProviders": "В настоящее время не существует дополнительных провайдеров геолокации. Matomo рекомендует использовать базы данных %1$sdbip%2$s, но для этого необходимо активировать плагин GeoIp2. (В качестве альтернативы вы можете установить плагин стороннего производителя, который предоставляет свои собственные функции геолокации из маректплейса).", + "PluginDescription": "Сообщает местонахождение посетителей: страна, регион, город и географические координаты (широта/долгота).", + "Region": "Регион", + "SubmenuLocations": "Локации", + "TestIPLocatorFailed": "Matomo попытался проверить местоположение известного IP-адреса (%1$s), но ваш сервер не вернул никакой информации. Возможно, этот провайдер настроен неправильно (например, база данных геолокации может быть названа неверно или расположена в неправильном каталоге).", + "ToGeolocateOldVisits": "Чтобы получить данные о местоположении для старых посещений, используйте скрипт, описанный %1$sтут%2$s.", + "VisitLocation": "Местоположение посещения", + "WidgetLocation": "Местонахождение посетителя", + "country_a1": "Анонимный прокси", + "country_a2": "Спутниковый провайдер", + "country_cat": "Каталаноязычные общества", + "country_o1": "Другая страна", + "getCityDocumentation": "Этот отчет показывает города посетителей вашего сайта.", + "getContinentDocumentation": "Этот отчет показывает континенты посетителей вашего сайта.", + "getCountryDocumentation": "Этот отчет показывает страны посетителей вашего сайта.", + "getRegionDocumentation": "Этот отчет показывает регионы посетителей вашего сайта." + } +} diff --git a/plugins/UserCountry/lang/si.json b/plugins/UserCountry/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/UserCountry/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/UserCountry/lang/sk.json b/plugins/UserCountry/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..ac8cf3f9057da7378fbcc0a7e48733f05c36d975 --- /dev/null +++ b/plugins/UserCountry/lang/sk.json @@ -0,0 +1,10 @@ +{ + "UserCountry": { + "City": "Mesto", + "Continent": "Kontinent", + "Country": "Krajina", + "DistinctCountries": "Počet rôznych krajín: %s", + "Location": "Miesto", + "SubmenuLocations": "Umiestnenie" + } +} diff --git a/plugins/UserCountry/lang/sl.json b/plugins/UserCountry/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..446535113f2d7dc3029bf0f6f0f36c91f82dfa9b --- /dev/null +++ b/plugins/UserCountry/lang/sl.json @@ -0,0 +1,15 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP naslov %s je lokalen in ne more biti geolociran.", + "City": "Mesto", + "Continent": "Kontitent", + "Country": "Država", + "DistinctCountries": "%s različnih držav", + "Location": "Lokacija", + "Region": "Regija", + "SubmenuLocations": "Lokacije", + "WidgetLocation": "Lokacija obiskovalca", + "country_a1": "Anonimen Proxy", + "country_o1": "Druge države" + } +} diff --git a/plugins/UserCountry/lang/sq.json b/plugins/UserCountry/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..5386c8e062a38925dd3a7888a320992b3508bd4a --- /dev/null +++ b/plugins/UserCountry/lang/sq.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Adresa IP “%s” është një adresë vendore, ndaj s’mund të gjeolokalizohet.", + "City": "Qytet", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Continents": "Kontinente", + "Country": "Vend", + "CountryCode": "Kod vendi", + "CountrySegmentHelp": "Kode ISO 3166-1 alpha-2 vendesh (de, us, fr, in, es, etj.)", + "CurrentLocationIntro": "Ky furnizues përcakton vendndodhjen tuaj aktuale si", + "DefaultLocationProviderDesc1": "Furnizuesi parazgjedhje i vendeve e hamendëson vendin e vizitorëve bazuar në gjuhën e përzgjedhur prej tyre.", + "DefaultLocationProviderDesc2": "Kjo s’është shumë e saktë, ndaj %1$sinstaloni dhe përdorni një %2$sbazë të dhënash gjeolokalizimi%3$s%4$s.", + "DefaultLocationProviderExplanation": "Po përdorni furnizuesin parazgjedhje të vendeve, që do të thotë se vendi prej nga lidhen vizitorët përcaktohet bazuar në gjuhën e përzgjedhur prej tyre. %1$sMësoni se si të rregulloni gjeolokalizim më të saktë%2$s.", + "DisabledLocationProvider": "Çaktivizon gjeolokalizimin.", + "DistinctCountries": "%s vende ndaras", + "FromDifferentCities": "qytete të ndryshme", + "GeoIPDocumentationSuffix": "Që të sillen të dhëna për këtë raport, duhet të rregulloni GeoIP-në te skeda e përgjegjësit për Gjeolokalizimin. Bazat komerciale të të dhënave GeoIP %1$sMaxMind%2$s janë më të sakta se sa ato falas. Që të shihni se sa të sakta janë, klikoni %3$skëtu%4$s.", + "GeoIpDbIpAccuracyNote": "Bazat e të dhënave DBIP të gjeolokalizimit janë falas dhe mund të shkarkohen automatikisht. MaxMind është më i përpiktë për qytete, por lyp %1$skrijimin e një llogarie%2$s.", + "Geolocation": "Gjeolokalizim", + "GeolocationPageDesc": "Këtu mund të ndryshoni mënyrën se si përcakton Matomo vendndodhjet e vizitorëve.", + "GeolocationProviderBroken": "Furnizuesi juaj për gjeolokalizim (%1$s) është i dëmtuar. Ju lutemi, ndreqni furnizuesin ose ujdisni një tjetër.", + "GeolocationProviderUnavailable": "Furnizuesi i formësuar për gjeolokalizim (%1$s) s’është më i passshëm. Ju lutemi, ujdisni një tjetër.", + "HowToInstallGeoIPDatabases": "Si të marr baza të dhënash gjeolokalizimi?", + "Latitude": "Gjerësi gjeografike", + "LatitudeSegmentHelp": "-33.578, 40.830, etj.
Mund të përzgjidhni vizitorë brenda një intervali gjerësish/gjatësish gjeografike duke përdorur %1$s.", + "Latitudes": "Gjerësi gjeografike", + "Location": "Vend", + "LocationProvider": "Furnizues vendesh", + "LocationsSubcategoryHelp": "Ndarja “Vendndodhje” është rruga më e mirë për të mësuar se prej cilave vende, kontinente, rajone dhe qytete vijnë vizitorët e sajtit tuaj — në formë tabelash dhe harte. Tregon gjithashtu se në ç’gjuhë është vënë shfletuesi i tyre, për t’ju ndihmuar të identifikoni vizitorë ndërkombëtarë në vendndodhje të ndryshme.", + "Longitude": "Gjatësi gjeografike", + "Longitudes": "Gjatësi gjeografike", + "MaxMindLinkExplanation": "Nëse përdorni MaxMind për gjeolokalizim, %1$sklikoni këtu që të mësoni se si të prodhoni URL-në e shkarkimit tuaj%2$s.", + "NoDataForGeoIPReport1": "S’ka të dhëna për këtë raport, ngaqë s’ka të dhëna vendesh, ose adresat IP të vizitorëve s’gjeolokalizohen dot.", + "NoDataForGeoIPReport2": "Që të aktivizoni gjeolokalizim të saktë, %1$sndryshoni këto rregullime%2$s dhe përdorni një %3$sbazë të dhënash në nivel qytetesh%4$s.", + "NoProviders": "Hëpërhë s’ka furnizues shtesë gjeolokalizimi. Rekomandohet %1$sDB-IP%2$s, por kjo lyp shtojcën GeoIp2. (Ndryshe, mund të instaloni që nga tregu një shtojcë pale të tretë gjeolokalizimi.)", + "PluginDescription": "Raporton vendndodhjen e vizitorëve tuaj: vend, rajon, qytet dhe koordinata gjeografike (gjerësi gjeografike dhe gjatësi gjeografike).", + "Region": "Rajon", + "SubmenuLocations": "Vende", + "TestIPLocatorFailed": "S’u gjet dot vendndodhja e adresës IP “%1$s”. Ndoshta ky furnizues mund të mos jetë formësuar si duhet, baza e të dhënave për gjeolokalizim mund të jetë emërtuar jo saktë, ose të gjendet në dosje të gabuar?", + "ToGeolocateOldVisits": "Që të kini të dhëna vendesh për vizitat tuaja të vjetra, përdorni %1$skëtë programth%2$s.", + "VisitLocation": "Vend vizite", + "WidgetLocation": "Vend vizitori", + "country_a1": "Ndërmjetës anonim", + "country_a2": "Furnizues satelitor", + "country_cat": "Bashkësi që flasin katalançe", + "country_o1": "Vend tjetër", + "getCityDocumentation": "Shfaq qytetet prej nga u lidhën vizitorët tuaj, kur përdorën sajtin tuaj.", + "getContinentDocumentation": "Shfaq nga cili kontinent u lidhën vizitorët tuaj, kur përdorën sajtin tuaj.", + "getCountryDocumentation": "Shfaq vendin nga u lidhën vizitorët tuaj, kur përdorën sajtin tuaj.", + "getRegionDocumentation": "Shfaq rajonin nga u lidhën vizitorët tuaj, kur përdorën sajtin tuaj." + } +} diff --git a/plugins/UserCountry/lang/sr.json b/plugins/UserCountry/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..a28f9b1689a5b72eb5e531d3162698749ce2c54c --- /dev/null +++ b/plugins/UserCountry/lang/sr.json @@ -0,0 +1,36 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP adresa %s je lokalna i ne može biti locirana.", + "City": "Grad", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Country": "Zemlja", + "CurrentLocationIntro": "Sudeći po ovom provajderu, vaša trenutna lokacija je", + "DefaultLocationProviderDesc1": "Podrazumevani provajder lokacija nagađa zemlju posetioca na osnovu jezika koji oni koriste.", + "DefaultLocationProviderExplanation": "Koristite podrazumevanog provajdera lokacije što znači da Matomo pokušava da pogodi lokaciju posetilaca na osnovu jezika koji oni koriste. %1$sPogledajte ovde%2$s kako da podesite znatno sigurniju geolokaciju.", + "DistinctCountries": "%s različitih zemalja", + "FromDifferentCities": "različiti gradovi", + "GeoIPDocumentationSuffix": "Da biste videli podatke za ovaj izveštaj potrebno je da podesite GeoIP preko taba Podešavannje geolokacije. Komercijalna %1$sMaxmind%2$s GeoIP baza je preciznija od besplatnih. Da biste videli koliko su one precizne, kliknite %3$sovde%4$s.", + "Geolocation": "Geolokacija", + "GeolocationPageDesc": "Na ovoj stranici možete promeniti način na koji Matomo prepoznaje lokacije posetilaca.", + "Latitude": "Geografska širina", + "Location": "Lokacija", + "LocationProvider": "Provajder lokacije", + "Longitude": "Geografska dužina", + "NoDataForGeoIPReport1": "Nema podataka za ovaj izveštaj zato što ili podaci o lokaciji nisu dostupni ili IP adresu posetioca nije moguće geolocirati.", + "NoDataForGeoIPReport2": "Da biste omogućili preciznu geolokaciju, promenite podešavanja %1$sovde%2$s i koristite %3$sbazu gradova%4$s.", + "PluginDescription": "Prikazuje lokaciju vaših posetilaca: zemlja, region, grad i geografske koordinate (geografska dužina i širina).", + "Region": "Regija", + "SubmenuLocations": "Lokacije", + "ToGeolocateOldVisits": "Kako biste dobili podatke o lokacijama starih poseta, iskoristite skript %1$ssa ove adrese%2$s.", + "WidgetLocation": "Lokacija posetioca", + "country_a1": "Anonimni proksi", + "country_a2": "Satelitski provajder", + "country_cat": "Katalonske zajednice", + "country_o1": "Ostale zemlje", + "getCityDocumentation": "Ovaj izveštaj prikazuje gradove vaših posetilaca dok su pristupali vašem sajtu.", + "getContinentDocumentation": "Ovaj izveštaj prikazuje sa kojih kontinenata posetioci pristupaju vašem sajtu.", + "getCountryDocumentation": "Ovaj izveštaj prikazuje u kojim gradovima se nalaze vaši posetioci dok pristupaju vašem sajtu.", + "getRegionDocumentation": "Ovaj izveštaj prikazuje iz kojih regiona posetioci pristupaju vašem sajtu." + } +} diff --git a/plugins/UserCountry/lang/sv.json b/plugins/UserCountry/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..22a47ae0c0d09d621be12bcc0eb8e33522ea13c4 --- /dev/null +++ b/plugins/UserCountry/lang/sv.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP-adressen \"%s\" är lokal, så den kan inte geolokaliseras.", + "City": "Stad", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kontinent", + "Continents": "Kontinenter", + "Country": "Land", + "CountryCode": "Landskod", + "CountrySegmentHelp": "ISO 3166-1 alpha-2 landskoder (de, us, fr, in, es, etc.)", + "CurrentLocationIntro": "Denna leverantör slår fast att din aktuella plats är", + "DefaultLocationProviderDesc1": "Platstjänsten avgör besökarens plats baserat på vilket språk denna har.", + "DefaultLocationProviderDesc2": "Detta är inte särskilt exakt, så %1$sinstallera och använd%2$s en geolokaliseringsdatabas%3$s%4$s.", + "DefaultLocationProviderExplanation": "Du använder den förvalda platsleverantören, vilket gör att landet besökaren ansluter från baseras på språket de använder. %1$sLär dig sätta upp en mer exakt geolokalisering%2$s.", + "DisabledLocationProvider": "Stäng av geoposition.", + "DistinctCountries": "%s distinkta länder", + "FromDifferentCities": "Olika städer", + "GeoIPDocumentationSuffix": "Ställ in GeoIP i inställningarna, under fliken Geolocation för att ge data till denna rapport. Dom kommersiella versionerna av GeoIP-databaserna från %1$sMaxMind%2$s är mer exakta än dom som är gratis. Klicka %3$shär%4$s för att se hur exakta dom är.", + "GeoIpDbIpAccuracyNote": "DB-IP-databaserna är gratis och kan laddas ner automatiskt. MaxMind är mer exakt för städer, men kräver att du skapar %1$sett konto%2$s.", + "Geolocation": "Geografisk plats", + "GeolocationPageDesc": "Här kan du ändra hur Matomo avgör besökarnas platser.", + "GeolocationProviderBroken": "Din geolokaliseringsleverantör (%1$s) är trasig. Vänligen åtgärda leverantören eller sätt upp en annan.", + "GeolocationProviderUnavailable": "Din leverantör av geolokalisering (%1$s) är inte tillgänglig längre. Vänligen konfigurera en annan.", + "HowToInstallGeoIPDatabases": "Hur får jag geolokaliseringsdatabaser?", + "Latitude": "Latitud", + "LatitudeSegmentHelp": "-33.578, 40.830, osv.
Du kan välja besökare inom ett lat/long-område med hjälp av %1$s.", + "Latitudes": "Latituder", + "Location": "Plats", + "LocationProvider": "Platstjänst", + "LocationsSubcategoryHelp": "Avsnittet \"Platser\" är det bästa sättet att ta reda på vilka länder, kontinenter, regioner och städer som dina besökare kommer från - i tabell- och kartform. Det säger också vilket språk webbläsaren är inställd på, vilket hjälper till att identifiera internationella besökare på alternativa platser.", + "Longitude": "Longitud", + "Longitudes": "Longituder", + "MaxMindLinkExplanation": "%1$sKlicka här för att lära dig generera din egen nedladdnings-URL%2$s ifall du använder MaxMinds geolokalisering.", + "NoDataForGeoIPReport1": "Ingen data för denna rapport då det antingen inte fanns platsdata tillgänglig eller så kan inte besökarnas IP-adresser lokaliseras.", + "NoDataForGeoIPReport2": "%1$sÄndra inställningarna%2$s och använd en %3$sdatabas på stadsnivå%4$s för mer exakt geopositionering.", + "NoProviders": "Inga ytterligare geolokaliseringsleverantörer tillgängliga. %1$sDB-IP%2$s rekommenderas, men kräver ett GeoIp2-plugin. (Du kan också installera ett tredjeparts geolokaliseringstillägg från Matomos marknadsplats.)", + "PluginDescription": "Rapporterar platsen för dina besökare: land, region, stad och geografiska koordinater (latitud och longitud).", + "Region": "Region", + "SubmenuLocations": "Platser", + "TestIPLocatorFailed": "Kunde inte hitta platsen för IP-adressen \"%1$s\". Möjligen är inte dess leverantör uppsatt korrekt, databasen för geografisk plats inkonsekvent namngiven eller i fel katalog?", + "ToGeolocateOldVisits": "Använd %1$sdetta skript%2$s för att få platsdata för gamla besök.", + "VisitLocation": "Besökarens plats", + "WidgetLocation": "Besökares plats", + "country_a1": "Anonym proxy", + "country_a2": "Satellitleverantör", + "country_cat": "Katalanska språkgemenskaperna", + "country_o1": "Annat land", + "getCityDocumentation": "Visar vilka städer dina besökare var i när dom besökte webbplatsen.", + "getContinentDocumentation": "Visar vilka kontinenter dina besökare var på när dom besökte webbplatsen.", + "getCountryDocumentation": "Visar vilka länder dina besökare var i när dom besökte webbplatsen.", + "getRegionDocumentation": "Visar vilka regioner dina besökare var i när dom besökte webbplatsen." + } +} diff --git a/plugins/UserCountry/lang/ta.json b/plugins/UserCountry/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..e2d946d0c2dc0af74a40cf64fb3ddbe3ac8afd46 --- /dev/null +++ b/plugins/UserCountry/lang/ta.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "\"%s\" ஐபி முகவரி உள்ளக, எனவே அதை புவிஇயுதிக்க முடியாது.", + "City": "நகரம்", + "CityAndCountry": "%1$s, %2$s", + "Continent": "கண்டம்", + "Continents": "கண்டங்கள்", + "Country": "நாடு", + "CountryCode": "நாட்டின் குறியீடு", + "CountrySegmentHelp": "ISO 3166-1 alpha-2 நாட்டுக் குறியீடுகள் (de, us, fr, in, es, முதலியன)", + "CurrentLocationIntro": "இந்த வழங்குநர் உங்கள் தற்போதைய இருப்பிடத்தை தீர்மானிக்கிறார்", + "DefaultLocationProviderDesc1": "இயல்புநிலை இருப்பிட வழங்குநர் நாட்டின் பார்வையாளர்கள் தேர்ந்தெடுக்கப்பட்ட மொழியின் அடிப்படையில் இணைப்பதை தீர்மானிக்கிறார்.", + "DefaultLocationProviderDesc2": "இது மிகவும் துல்லியமானதல்ல, எனவே %1$s ஒரு %2$s புவிஇருப்பிட தரவுத்தளம் %3$s %4$s ஐ நிறுவிப் பயன்படுத்து.", + "DefaultLocationProviderExplanation": "நீங்கள் இயல்புநிலை இருப்பிட வழங்குநரைப் பயன்படுத்துகிறீர்கள், எனவே நாட்டின் பார்வையாளர்கள் இணைக்கிறார்கள் அவர்களின் தேர்ந்தெடுக்கப்பட்ட மொழியிலிருந்து தீர்மானிக்கப்படுகிறது. %1$selarn welaarn மிகவும் துல்லியமான புவிஇருப்பிடத்தை எவ்வாறு அமைப்பது%2$s.", + "DisabledLocationProvider": "புவிஇருப்பிடத்தை முடக்குகிறது.", + "DistinctCountries": "%s தனிப்பட்ட நாடுகள்", + "FromDifferentCities": "வேறுபட்ட நகரங்கள்", + "GeoIPDocumentationSuffix": "இந்த அறிக்கைக்கான தரவை வழங்க புவிஇஇலோகேசன் நிர்வாகத் தாவலில் சியோஐபி அமைக்கவும். வணிக %1$sMAXMIND %2$s GEOIP தரவுத்தளங்கள் இலவசங்களை விடத் துல்லியமானவை. %3$s ச்க்லிக் இங்கே%4$s அவை எவ்வளவு துல்லியமானவை என்பதைக் காண.", + "GeoIpDbIpAccuracyNote": "DB-IP புவிஇருப்பிட தரவுத்தளங்கள் இலவசம் மற்றும் தானாகவே ஏற்றப்படலாம். நகரங்களுக்கு அதிகபட்சம் மிகவும் துல்லியமானது, ஆனால் %1$sஒரு கணக்கு %2$s ஐத் தேவை.", + "Geolocation": "உலகம் நீக்கம்", + "GeolocationPageDesc": "பார்வையாளர் இருப்பிடங்களை மாடோமோ எவ்வாறு தீர்மானிக்கிறது என்பதை இங்கே மாற்றலாம்.", + "GeolocationProviderBroken": "உங்கள் புவிஇருப்பிட வழங்குநர் (%1$s) உடைந்துவிட்டது. தயவுசெய்து வழங்குநரை சரிசெய்யவும் அல்லது இன்னொன்றை அமைக்கவும்.", + "GeolocationProviderUnavailable": "உங்கள் புவிஇருப்பிட வழங்குநர் (%1$s) இனி கிடைக்காது. தயவுசெய்து இன்னொன்றை அமைக்கவும்.", + "HowToInstallGeoIPDatabases": "புவிஇருப்பிட தரவுத்தளங்களை எவ்வாறு பெறுவது?", + "Latitude": "அகலாங்கு", + "LatitudeSegmentHelp": "-33.578, 40.830, முதலியன.
%1$sஐப் பயன்படுத்தி லேட்/நீண்ட வரம்பில் பார்வையாளர்களைத் தேர்ந்தெடுக்கலாம்.", + "Latitudes": "அட்சரேகைகள்", + "Location": "இடம்", + "LocationProvider": "இருப்பிட வழங்குநர்", + "LocationsSubcategoryHelp": "உங்கள் வலைத்தள பார்வையாளர்கள் எந்த நாடுகள், கண்டங்கள், பிராந்தியங்கள் மற்றும் நகரங்களிலிருந்து வருகிறார்கள் - அட்டவணை மற்றும் வரைபட வடிவத்தில் என்ன என்பதைக் கண்டறிய \"இருப்பிடங்கள்\" பிரிவு சிறந்த வழியாகும். மாற்று இடங்களில் பன்னாட்டு பார்வையாளர்களை அடையாளம் காண உதவுகிறது, இது அவர்களின் உலாவி எந்த மொழியில் அமைக்கப்பட்டுள்ளது என்றும் அது கூறுகிறது.", + "Longitude": "நெட்டாங்கு", + "Longitudes": "நீளமான", + "MaxMindLinkExplanation": "%1$sநீங்கள் மேக்ச் மைண்ட் புவிஇருப்பிடத்தைப் பயன்படுத்துகிறீர்கள் என்றால் உங்கள் பதிவிறக்க URL%2$s ஐ எவ்வாறு உருவாக்குவது என்பதை அறிய இங்கே சொடுக்கு செய்க.", + "NoDataForGeoIPReport1": "இந்த அறிக்கைக்கான தரவு எதுவும் இல்லை, ஏனெனில் இருப்பிடத் தரவு எதுவும் கிடைக்கவில்லை, அல்லது பார்வையாளர் ஐபி முகவரிகளை உலகம் ஒலிக்க முடியாது.", + "NoDataForGeoIPReport2": "%1$sஇந்த அமைப்புகளை%2$s ஐ மாற்றவும், துல்லியமான புவிஇருப்பிடத்தைப் பெற %3$sநகர அளவிலான தரவுத்தள%4$s ஐப் பயன்படுத்தவும்.", + "NoProviders": "கூடுதல் புவிஇருப்பிட வழங்குநர்கள் எதுவும் கிடைக்கவில்லை. %1$sDB-IP%2$s பரிந்துரைக்கப்படுகிறது, ஆனால் GEOIP2 சொருகி தேவைப்படுகிறது. (சந்தையில் இருந்து மூன்றாம் தரப்பு புவிஇருப்பிட சொருகி நிறுவலாம்.)", + "PluginDescription": "உங்கள் பார்வையாளர்களின் இருப்பிடத்தைப் புகாரளிக்கிறது: நாடு, பகுதி, நகரம் மற்றும் புவியியல் ஆயத்தொலைவுகள் (அட்சரேகை மற்றும் தீர்க்கரேகை).", + "Region": "பகுதி", + "SubmenuLocations": "இருப்பிடங்கள்", + "TestIPLocatorFailed": "\"%1$s\" ஐபி முகவரியின் இருப்பிடத்தைக் கண்டுபிடிக்க முடியவில்லை. இந்த வழங்குநர் சரியாக அமைக்கப்படவில்லை, புவிஇருப்பிட தரவுத்தளம் தவறாக பெயரிடப்பட்டுள்ளது, அல்லது தவறான கோப்புறையில்?", + "ToGeolocateOldVisits": "உங்கள் பழைய வீடியோக்களுக்கான இருப்பிட தரவைப் பெற %1$sஇந்த ச்கிரிப்ட் %2$s ஐப் பயன்படுத்தவும்.", + "VisitLocation": "பார்வையாளர் இடம்", + "WidgetLocation": "பார்வையாளர் இடம்", + "country_a1": "அநாமதேய பதிலாள்", + "country_a2": "செயற்கைக்கோள் வழங்குநர்", + "country_cat": "கற்றலான் பேசும் சமூகங்கள்", + "country_o1": "மற்ற நாடு", + "getCityDocumentation": "உங்கள் வலைத்தளத்தை அணுகும்போது உங்கள் பார்வையாளர்கள் இணைக்கப்பட்ட நகரங்களைக் காட்டுகிறது.", + "getContinentDocumentation": "உங்கள் வலைத்தளத்தை அணுகும்போது உங்கள் பார்வையாளர்கள் எந்த கண்டத்தை இணைத்துள்ளனர் என்பதைக் காட்டுகிறது.", + "getCountryDocumentation": "உங்கள் வலைத்தளத்தை அணுகும்போது உங்கள் பார்வையாளர்கள் எந்த நாட்டிலிருந்து இணைகிறார்கள் என்பதைக் காட்டுகிறது.", + "getRegionDocumentation": "வலைத்தளத்தை அணுகும்போது உங்கள் பார்வையாளர்கள் எந்த பிராந்தியத்தை இணைத்துள்ளனர் என்பதைக் காட்டுகிறது." + } +} diff --git a/plugins/UserCountry/lang/te.json b/plugins/UserCountry/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..938a9b50c7514d30ad671777f861f7d531da2055 --- /dev/null +++ b/plugins/UserCountry/lang/te.json @@ -0,0 +1,12 @@ +{ + "UserCountry": { + "City": "నగరం", + "Continent": "ఖండం", + "Country": "దేశం", + "DistinctCountries": "%s ఇతర దేశాలు", + "Latitude": "అక్షాంశం", + "Location": "ప్రాంతం", + "Longitude": "రేఖాంశం", + "SubmenuLocations": "ప్రాంతాలు" + } +} diff --git a/plugins/UserCountry/lang/th.json b/plugins/UserCountry/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..acbc51b34ec569d956202508b90c5eb6af1de296 --- /dev/null +++ b/plugins/UserCountry/lang/th.json @@ -0,0 +1,15 @@ +{ + "UserCountry": { + "City": "เมือง", + "Continent": "ทวีป", + "Country": "ประเทศ", + "DistinctCountries": "%s การเข้าชมตามประเทศ", + "Latitude": "ละติจูด", + "Location": "ตำแหน่งที่ตั้ง", + "Longitude": "ลองจิจูด", + "Region": "ภูมิภาค", + "SubmenuLocations": "ตำแหน่งที่อยู่", + "country_a2": "ผู้ให้บริการดาวเทียม", + "country_o1": "ประเทศอื่นๆ" + } +} diff --git a/plugins/UserCountry/lang/tl.json b/plugins/UserCountry/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..476f4123dc994af393cbea5f68d44d8e6a7615fe --- /dev/null +++ b/plugins/UserCountry/lang/tl.json @@ -0,0 +1,35 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Ang IP address %s ay isang lokal na address at hindi maaaring i-geolocated.", + "City": "Lungsod", + "CityAndCountry": "%1$s %2$s", + "Continent": "Kontinente", + "Country": "Bansa", + "CurrentLocationIntro": "Ayon sa provider ang iyong kasalukuyang lokasyon ay", + "DefaultLocationProviderDesc1": "Ang default na lokasyon ng provider na humu-hula sa mga bansa ng bumibisita batay sa wika na ginagamit nila.", + "DefaultLocationProviderExplanation": "Iyong ginagamit ang default location provider na nangangahulugan na ang Matomo ay huhulaan ang lokasyon ng iyong bisita base sa wika na kanilang ginagamit. %1$sRead this%2$s upang malaman panu e-setup na may tamang geolocation.", + "DistinctCountries": "natatanging mga bansa %s", + "FromDifferentCities": "ibang mga lungsod", + "GeoIPDocumentationSuffix": "Upang makita ang dataus para sa ulat na ito kailangan mong mag setup ng GeoIP sa Geolocation admin tab. Ang komersyal %1$sMaxmind%2$s GeoIP databases ay higit na tiyak kaysa sa mga libre. Upang makita kung gaano ito ka-tiyak i-click ang %3$shere%4$s.", + "Geolocation": "Geolocation", + "GeolocationPageDesc": "Sa pahinang ito ay maaari mo nang baguhin kung paano malalaman ng Matomo ang lokasyon ng bisita.", + "Latitude": "Latitud", + "Location": "Lokasyon", + "LocationProvider": "Lokasyon ng provider", + "Longitude": "Longhitud", + "NoDataForGeoIPReport1": "Walang mga datus para sa ulat na ito dahil walang data na magagamit sa lokasyon o ang IP address ay hindi maaaring e-geolocated.", + "NoDataForGeoIPReport2": "Upang paganahin ang tamang geolocation baguhin ang mga setting %1$sdito%2$s at gumamit ng%3$scity database%4$s.", + "Region": "Rehiyon", + "SubmenuLocations": "Mga Lokasyon", + "ToGeolocateOldVisits": "Upang makakuha ng data ng lokasyon para sa iyong lumang mga pagbisita gamitin ang script na inilarawan %1$shere%2$s.", + "WidgetLocation": "Lokasyon ng bisita", + "country_a1": "Anonymous Proxy", + "country_a2": "Satellite Provider", + "country_cat": "Komunidad ng wikang Catalan", + "country_o1": "Iba pang mga bansa", + "getCityDocumentation": "Ipinapakita ng ulat na ito ang lungsod ng iyong mga bisita ng in-access nila ang iyong website.", + "getContinentDocumentation": "Ang ulat na ito ay nagpapakita kung aling mga kontinente galing ang iyong mga bisita na umaccess sa iyong website.", + "getCountryDocumentation": "Ang ulat na ito ay nagpapakita kung saang bansa galing ang iyong bisita ng ina-access nila ang iyong website.", + "getRegionDocumentation": "Ang ulat na ito ay nagpapakita kung saang rehiyon galing ang iyong mga bisita ng in-access nila ang iyong website." + } +} diff --git a/plugins/UserCountry/lang/tr.json b/plugins/UserCountry/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..54b84a409c083e350162558f79e25af3ad1ecf9d --- /dev/null +++ b/plugins/UserCountry/lang/tr.json @@ -0,0 +1,53 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "\"%s\" IP adresi yerel bir adres olduğundan coğrafi konumu belirlemekte kullanılamaz.", + "City": "İl", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Kıta", + "Continents": "Kıta", + "Country": "Ülke", + "CountryCode": "Ülke kodu", + "CountrySegmentHelp": "ISO 3166-1 alpha-2 ülke kodları (de, us, fr, in, es, etc.)", + "CurrentLocationIntro": "Bu hizmet sağlayıcı geçerli konumunuzu şu şekilde belirler", + "DefaultLocationProviderDesc1": "Varsayılan konum hizmeti sağlayıcısı bir ziyaretçinin bağlandığı ülkeyi kullandığı dile göre belirler.", + "DefaultLocationProviderDesc2": "Bu yöntem çok güvenilir değildir. %1$sBu nedenle %2$scoğrafi konum veri tabanı%3$s özelliğini kurup kullanın%4$s.", + "DefaultLocationProviderExplanation": "Varsayılan konum hizmeti sağlayıcısını kullanıyorsunuz. Bu yöntemde bir ziyaretçinin ülkesi kullandığı dile göre belirlenir. %1$sDaha doğru coğrafi konumlama kurulumu hakkında ayrıntılı bilgi alın%2$s.", + "DisabledLocationProvider": "Coğrafi konumu kapatır.", + "DistinctCountries": "%s ayrı ülke", + "FromDifferentCities": "ayrı iller", + "GeoIPDocumentationSuffix": "Bu rapora veri sağlamak için Coğrafi konum yönetici sekmesinden GeoIP kurulumunu yapın. Ticari %1$sMaxmind%2$s GeoIP veri tabanları ücretsiz olanlardan daha doğrudur. Bunların ne kadar doğru olduğunu görmek için %3$sburaya tıklayın%4$s.", + "GeoIpDbIpAccuracyNote": "DB-IP coğrafi konum veri tabanları ücretsizdir ve otomatik olarak indirilebilir. MaxMind şehirlerde daha iyi çalışır ancak %1$sbir hesap açılmasını%2$s ister.", + "Geolocation": "Coğrafi konum", + "GeolocationPageDesc": "Ziyaretçi konumun Matomo tarafından nasıl bulunacağı bu bölümden ayarlanabilir.", + "GeolocationProviderBroken": "Coğrafi konum hizmeti sağlayıcınız (%1$s) çalışmıyor. Lütfen hizmet sağlayıcıyı çalışır duruma getirin ya da başka bir hizmet sağlayıcı yapılandırın.", + "GeolocationProviderUnavailable": "Coğrafi konum hizmeti sağlayıcınız (%1$s) artık kullanılamıyor. Lütfen başka bir hizmet sağlayıcı yapılandırın.", + "HowToInstallGeoIPDatabases": "Coğrafi konum veri tabanları nasıl alınır?", + "Latitude": "Enlem", + "LatitudeSegmentHelp": "-33.578, 40.830 gibi.
%1$s kullanarak belirli bir enlem/boylam içindeki ziyaretçileri seçebilirsiniz.", + "Latitudes": "Enlem", + "Location": "Konum", + "LocationProvider": "Konum hizmeti sağlayıcı", + "LocationsSubcategoryHelp": "\"Konumlar\" bölümü, ziyaretçilerin geldiği ülkeleri, kıtaları, bölgeleri, illeri tablo ve harita olarak görüntülemenin en iyi yoludur. Ayrıca ziyaretçilerin kullandığını tarayıcını dilini görüntüleyerek farklı konumlarda bulunan uluslararası ziyaretçilerin belirlenmesine yardımcı olabilir.", + "Longitude": "Boylam", + "Longitudes": "Boylam", + "MaxMindLinkExplanation": "MaxMind coğrafi konum veri tabanlarını kullanıyorsanız %1$sindirme adresinizi nasıl üreteceğinizi öğrenmek için buraya tıklayın%2$s.", + "NoDataForGeoIPReport1": "Kullanılabilecek bir konum verisi olmadığından ya da ziyaretçi IP adreslerinin coğrafi konumu algılanamadığından bu raporda herhangi bir veri yok.", + "NoDataForGeoIPReport2": "Coğrafi konumun doğru olarak bulunması için %1$sbu ayarları değiştirin%2$s ve %3$sil düzeyindeki bir veri tabanı%4$s kullanın.", + "NoProviders": "Kullanılabilecek bir ek coğrafi konum hizmeti sağlayıcısı yok. %1$sDB-IP%2$s kullanılması önerilir. Ancak bunun için GeoIp2 eklentisi gereklidir. (Alternatif olarak mağazadan bir üçüncü taraf coğrafi konum eklentisi kurabilirsiniz).", + "PluginDescription": "Ziyaretçilerin konumlarını görüntüler: Ülke, bölge, il ve coğrafi koordinatlar (enlem ve boylam).", + "Region": "Bölge", + "SubmenuLocations": "Konumlar", + "TestIPLocatorFailed": "\"%1$s\" IP adresinin konumu bulunamadı. Bu hizmet sağlayıcı doğru yapılandırılmamış olabilir. Coğrafi konum veri tabanının adı ya da bulunduğu klasör yanlış olabilir mi?", + "ToGeolocateOldVisits": "Eski ziyaretlerinizle ilgili konum bilgilerini almak için %1$sbu betiği%2$s kullanın.", + "VisitLocation": "Ziyaretçinin konumu", + "WidgetLocation": "Ziyaretçi konumu", + "country_a1": "Anonim vekil sunucu", + "country_a2": "Uydu hizmeti sağlayıcı", + "country_cat": "Katalanca konuşulan topluluklar", + "country_o1": "Başka bir ülke", + "getCityDocumentation": "Ziyaretçilerin siteye erişirken geldikleri illeri görüntüler.", + "getContinentDocumentation": "Ziyaretçilerin siteye erişirken geldikleri kıtaları görüntüler.", + "getCountryDocumentation": "Ziyaretçilerin siteye erişirken geldikleri ülkeleri görüntüler.", + "getRegionDocumentation": "Ziyaretçilerin siteye erişirken geldikleri bölgeleri görüntüler." + } +} diff --git a/plugins/UserCountry/lang/tzm.json b/plugins/UserCountry/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..ec0fbb10a96278ab6a188846f205bfad007b69ff --- /dev/null +++ b/plugins/UserCountry/lang/tzm.json @@ -0,0 +1,6 @@ +{ + "UserCountry": { + "City": "ⵜⵉⵖⵔⵎⵜ", + "Region": "ⵜⴰⵎⵏⴰⴹⵜ" + } +} diff --git a/plugins/UserCountry/lang/uk.json b/plugins/UserCountry/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..db84f414d58579273de728ed16f5fdb624529d27 --- /dev/null +++ b/plugins/UserCountry/lang/uk.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP-адреса «%s» локальна, тому її неможливо визначити її геолокацію.", + "City": "Місто", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Континент", + "Continents": "Материки", + "Country": "Країна", + "CountryCode": "Код країни", + "CurrentLocationIntro": "Цей провайдер визначає ваше поточне місце перебування як", + "DefaultLocationProviderDesc1": "Постачальник послуг типово визначає країну, з якої під'єднуються відвідувачі, на основі обраної ними мови.", + "DefaultLocationProviderDesc2": "Це не дуже точно, тому %1$sвстановіть і використовуйте %2$sбазу даних геолокації%3$s%4$s.", + "DefaultLocationProviderExplanation": "Ви використовуєте типового постачальника визначення розташування, тому країна, з якої відвідувачі під'єднуються, визначається на основі вибраної ними мови. %1$sДізнайтеся, як налаштувати точніше визначення розташування%2$s.", + "DisabledLocationProvider": "Вимикає геолокацію.", + "DistinctCountries": "%s унікальних країн", + "FromDifferentCities": "різні міста", + "GeoIPDocumentationSuffix": "Налаштуйте GeoIP у вкладці «Геолокація», щоб надати дані для цього звіту. Комерційні бази даних GeoIP %1$sMaxMind%2$s точніші, ніж безплатні. %3$sКлацніть тут%4$s, щоб побачити, наскільки вони точні.", + "GeoIpDbIpAccuracyNote": "Бази даних геолокації DB-IP безплатні та можуть автоматично завантажуватися. MaxMind точніший для міст, але вимагає %1$sстворення облікового запису%2$s.", + "Geolocation": "Геолокація", + "GeolocationPageDesc": "Тут ви можете змінити спосіб визначення місця перебування відвідувачів.", + "GeolocationProviderBroken": "Ваш постачальник визначення розташування (%1$s) зламаний. Виправте постачальника або налаштуйте іншого.", + "GeolocationProviderUnavailable": "Ваш постачальник визначення розташування (%1$s) більше недоступний. Налаштуйте іншого.", + "HowToInstallGeoIPDatabases": "Як отримати бази даних геолокації?", + "Latitude": "Широта", + "Latitudes": "Широта", + "Location": "Локація", + "LocationProvider": "Провайдер локації", + "LocationsSubcategoryHelp": "Розділ «Локації» — це найкращий спосіб дізнатися, з яких країн, континентів, регіонів і міст походять відвідувачі вашого вебсайту — у формі таблиці та карти. Він також повідомляє, яку мову налаштовано у браузері, що допомагає ідентифікувати іноземних відвідувачів у альтернативних місцях.", + "Longitude": "Довгота", + "Longitudes": "Довгота", + "MaxMindLinkExplanation": "%1$sКлацніть тут, щоб дізнатися, як створити URL-адресу завантаження%2$s, якщо ви використовуєте геолокацію від MaxMind.", + "NoDataForGeoIPReport1": "Немає даних для цього звіту, оскільки або немає доступних даних про місце перебування, або неможливо визначити геолокацію IP-адрес відвідувачів.", + "NoDataForGeoIPReport2": "%1$sЗмініть ці налаштування%2$s, а для отримання точної геолокації використовуйте %3$sбазу даних на рівні міст%4$s.", + "NoProviders": "Немає додаткових постачальників визначення розташування. %1$sDB-IP%2$s рекомендовано, але потрібен плагін GeoIp2. (Ви також можете встановити сторонній плагін геолокації з маркету.)", + "PluginDescription": "Повідомляє місце перебування ваших відвідувачів: країну, регіон, місто та географічні координати (широта та довгота).", + "Region": "Регіон", + "SubmenuLocations": "Локації", + "TestIPLocatorFailed": "Не вдалося знайти місце перебування IP-адреси «%1$s». Можливо, цей постачальник налаштований некоректно, база даних геолокації некоректно названа, або розміщена не в тій теці?", + "ToGeolocateOldVisits": "Використовуйте %1$sцей скрипт%2$s, щоб отримати дані про місце перебування для ваших старих відвідувань.", + "VisitLocation": "Розташування відвідувача", + "WidgetLocation": "Розташування відвідувача", + "country_a1": "Анонімний проксі", + "country_a2": "Супутниковий провайдер", + "country_cat": "Каталаномовні суспільства", + "country_o1": "Інша країна", + "getCityDocumentation": "Показує міста, з яких відвідувачі під'єднуються до вашого сайту.", + "getContinentDocumentation": "Показує, з якого континенту відвідувачі під'єднуються до вашого сайту.", + "getCountryDocumentation": "Показує, з якої країни відвідувачі під'єднуються до вашого сайту.", + "getRegionDocumentation": "Показує, з якого регіону під'єднуються відвідувачі, коли заходять на вебсайт." + } +} diff --git a/plugins/UserCountry/lang/ur.json b/plugins/UserCountry/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..a8ee666bf6d297a2a519f4ac076ab7c5f275b990 --- /dev/null +++ b/plugins/UserCountry/lang/ur.json @@ -0,0 +1,10 @@ +{ + "UserCountry": { + "City": "شہر", + "Continent": "براعظم", + "Country": "ملک", + "country_a1": "گمنام پراکسی", + "country_a2": "سیٹلائٹ فراہم کرنے والے", + "country_cat": "کیٹالان زبان بولنے والی برادریاں" + } +} diff --git a/plugins/UserCountry/lang/vi.json b/plugins/UserCountry/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..816236640750d5641dcbb63cf30f9e9309b304ba --- /dev/null +++ b/plugins/UserCountry/lang/vi.json @@ -0,0 +1,35 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "Địa chỉ IP %s là một địa chỉ địa phương và không có thể được geolocated.", + "City": "Thành phố", + "CityAndCountry": "%1$s, %2$s", + "Continent": "Châu lục", + "Country": "Quốc gia", + "CurrentLocationIntro": "Theo nhà cung cấp này, vị trí hiện tại của bạn là", + "DefaultLocationProviderDesc1": "Nhà cung cấp vị trí mặc định phỏng đoán quốc gia của người truy cập dựa trên ngôn ngữ mà họ sử dụng.", + "DefaultLocationProviderExplanation": "Bạn đang sử dụng các nhà cung cấp vị trí mặc định, có nghĩa là Matomo sẽ đoán vị trí của khách truy cập dựa trên ngôn ngữ mà họ sử dụng. %1$s đọc %2$s này để tìm hiểu cách thiết lập định vị chính xác hơn.", + "DistinctCountries": "%s Các nước riêng biệt", + "FromDifferentCities": "Các thành phố khác nhau", + "GeoIPDocumentationSuffix": "Theo thứ tự xem dữ liệu báo cáo này, bạn phải thiết lập GeoIP trong tab quản trị định vị. Cơ sở dữ liệu GeoIP %1$sMaxmind%2$s thương mại chính xác hơn những bản miễn phí. Để xem cách chính xác họ đang có, click %3$s tại đây %4$s.", + "Geolocation": "Định vị", + "GeolocationPageDesc": "Trên trang này bạn có thể thay đổi cách Matomo xác định địa điểm khách truy cập.", + "Latitude": "Miền", + "Location": "Vị trí", + "LocationProvider": "Nhà cung cấp vị trí", + "Longitude": "Kinh độ", + "NoDataForGeoIPReport1": "Không có dữ liệu cho báo cáo này bởi vì ở đó không có dữ liệu vị trí có sẵn hoặc địa chỉ IP khách truy cập không thể định vị.", + "NoDataForGeoIPReport2": "Để cho phép định vị chính xác, thay đổi thiết lập %1$s tại đây %2$s và sử dụng một cơ sở dữ liệu %4$s mức thành phố %3$s.", + "Region": "Vùng", + "SubmenuLocations": "Các vị trí", + "ToGeolocateOldVisits": "Để có được dữ liệu vị trí cho các truy cập cũ của bạn, sử dụng đoạn mã được mô tả %1$s tại đây %2$s", + "WidgetLocation": "Vị trí khách truy cập", + "country_a1": "Proxy ẩn danh", + "country_a2": "Nhà cung cấp truyền hình vệ tinh", + "country_cat": "Cộng đồng nói tiếng Catalan", + "country_o1": "Quốc gia khác", + "getCityDocumentation": "Báo cáo này cho thấy các thành phố khách truy cập của bạn đã ở khi họ truy cập website của bạn.", + "getContinentDocumentation": "Báo cáo này cho thấy lục địa khách truy cập của bạn đã ở khi họ truy cập website của bạn.", + "getCountryDocumentation": "Báo cáo này cho thấy quốc gia khách truy cập của bạn đã ở khi họ truy cập website của bạn.", + "getRegionDocumentation": "Báo cáo này cho thấy vùng miền khách truy cập của bạn đã ở khi họ truy cập website của bạn." + } +} diff --git a/plugins/UserCountry/lang/zh-cn.json b/plugins/UserCountry/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..aca95fd126a703e8c33e8f8ce7da71797a389f87 --- /dev/null +++ b/plugins/UserCountry/lang/zh-cn.json @@ -0,0 +1,50 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP地址“%s”是局域网地址,因此无法对其进行地理定位。", + "City": "城市", + "CityAndCountry": "%1$s, %2$s", + "Continent": "大洲", + "Continents": "大洲", + "Country": "国家", + "CountryCode": "国家代码", + "CurrentLocationIntro": "该提供商确定您当前的位置是", + "DefaultLocationProviderDesc1": "默认位置提供商根据访问者选择的语言确定访问者连接的国家/地区。", + "DefaultLocationProviderDesc2": "这不是很准确,因此%1$s安装并使用%2$s地理位置数据库%3$s%4$s。", + "DefaultLocationProviderExplanation": "您使用的是默认位置提供商,因此访问者连接的国家/地区是根据他们选择的语言确定的。 %1$s了解如何设置更准确的地理位置%2$s。", + "DisabledLocationProvider": "关闭地理定位。", + "DistinctCountries": "%s 个不同的国家/地区", + "FromDifferentCities": "不同城市", + "GeoIPDocumentationSuffix": "在“地理位置管理”选项卡中设置 GeoIP 以为此报告提供数据。商业 %1$sMaxMind%2$s GeoIP 数据库比免费数据库更准确。 %3$s单击此处%4$s查看它们的准确性。", + "GeoIpDbIpAccuracyNote": "注意:DBIP数据库是免费的,可以自动下载,但是地理位置结果(特别是城市结果)不如MaxMind的准确。 但是,MaxMind甚至要求您为免费数据库创建一个帐户。 如果要使用MaxMind的地理位置数据库,则可以在%2$s处启动%1$s进程", + "Geolocation": "地理位置", + "GeolocationPageDesc": "您可以在此处更改 Matomo 确定访客位置的方式。", + "GeolocationProviderBroken": "您的地理定位提供商 (%1$s) 已损坏。请修复提供商,或设置另一个提供商。", + "GeolocationProviderUnavailable": "您的地理定位提供商 (%1$s) 不再可用。请再设置一个。", + "HowToInstallGeoIPDatabases": "如何获得地理位置数据库?", + "Latitude": "纬度", + "Latitudes": "纬度", + "Location": "位置", + "LocationProvider": "位置提供者", + "Longitude": "经度", + "Longitudes": "经度", + "MaxMindLinkExplanation": "如果您使用的是MaxMind的地理位置数据库,并且您尚不知道如何生成下载URL,%1$s请单击此处以了解如何%2$s。", + "NoDataForGeoIPReport1": "此报告没有数据,因为没有可用的位置数据,或者无法对访问者 IP 地址进行地理定位。", + "NoDataForGeoIPReport2": "%1$s更改这些设置%2$s,并使用%3$s城市级数据库%4$s来获取准确的地理位置。", + "NoProviders": "没有其他可用的地理定位提供商。建议使用 %1$sDB-IP%2$s,但需要 GeoIp2 插件。 (您还可以从市场安装第三方地理定位插件。)", + "PluginDescription": "报告访客的位置:国家、地区、城市和地理坐标(纬度和经度)。", + "Region": "地区", + "SubmenuLocations": "所在地", + "TestIPLocatorFailed": "找不到 IP 地址“%1$s”的位置。也许该提供程序设置不正确,地理位置数据库命名不正确,或者位于错误的文件夹中?", + "ToGeolocateOldVisits": "要获得以前访问的地理数据,请使用 %1$s这里%2$s的脚步。", + "VisitLocation": "访客位置", + "WidgetLocation": "访客位置", + "country_a1": "匿名代理", + "country_a2": "卫星提供商", + "country_cat": "加泰罗尼亚语社区", + "country_o1": "其它国家", + "getCityDocumentation": "显示访问者访问您的网站时所连接的城市。", + "getContinentDocumentation": "显示访问者在访问您的网站时从哪个大陆进行连接。", + "getCountryDocumentation": "显示访问者在访问您的网站时从哪个国家/地区连接。", + "getRegionDocumentation": "显示访问者在访问网站时从哪个区域连接。" + } +} diff --git a/plugins/UserCountry/lang/zh-tw.json b/plugins/UserCountry/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..cc90e8e1cc5d9085634ff276bae5a0e621683844 --- /dev/null +++ b/plugins/UserCountry/lang/zh-tw.json @@ -0,0 +1,51 @@ +{ + "UserCountry": { + "CannotLocalizeLocalIP": "IP 位址“%s”是本地位址,因此無法顯示地理位置。", + "City": "城市", + "CityAndCountry": "%1$s,%2$s", + "Continent": "洲", + "Continents": "洲", + "Country": "國家與地區", + "CountryCode": "國家代碼", + "CurrentLocationIntro": "此地理位置供應商推測你當前位置在", + "DefaultLocationProviderDesc1": "預設的地理位置供應商是透過訪客設定的語言來推測他來自哪個國家與地區。", + "DefaultLocationProviderDesc2": "這不太準確,不如%1$s安裝並使用一個%2$s地理位置資料庫%3$s%4$s。", + "DefaultLocationProviderExplanation": "你正在使用預設地理位置供應商,將以訪客設定的語言來推測他們來自哪個國家。%1$s點此查看如何設定更準確的地理定位%2$s。", + "DisabledLocationProvider": "停用地理定位。", + "DistinctCountries": "%s 個不同的國家與地區", + "FromDifferentCities": "不同城市", + "GeoIPDocumentationSuffix": "在地理定位管理中設定 GeoIP 以提供報表所需資料。商業版 %1$sMaxmind%2$s GeoIP 資料庫比免費版更精確。%3$s點擊這裡%4$s看看它們多精確。", + "GeoIpDbIpAccuracyNote": "DB-IP 的地理定位資料庫是免費的,可以自動下載。MaxMind 對城市的精確性更高,但需要%1$s註冊帳戶%2$s。", + "Geolocation": "地理位置", + "GeolocationPageDesc": "你可以在這裡改變 Matomo 推斷訪客位置的方式。", + "GeolocationProviderBroken": "您的地理定位供應程式(%1$s)已損壞。請修復它,或設定另一個供應程式。", + "GeolocationProviderUnavailable": "您的地理定位供應器(%1$s)不再可用。請設定另一個。", + "HowToInstallGeoIPDatabases": "我該如何取得地理位置資料庫?", + "Latitude": "緯度", + "Latitudes": "緯度", + "Location": "位置", + "LocationProvider": "地理定位器", + "LocationsSubcategoryHelp": "「位置」部分是了解您的網站訪客來自哪些國家、洲、地區和城市的最佳方式——以表格和地圖形式呈現。它還會顯示他們的瀏覽器設定的語言,有助於識別來自其他地方的國際訪客。", + "Longitude": "經度", + "Longitudes": "經度", + "MaxMindLinkExplanation": "如果您正在使用 MaxMind 的地理定位服務,%1$s點擊這裡了解如何產生下載 URL%2$s。", + "NoDataForGeoIPReport1": "這份報表沒有資料,因為沒有可用的位置資料以及 IP 位址無法被定位。", + "NoDataForGeoIPReport2": "%1$s更改這些設定%2$s,並使用%3$s城市級的資料庫%4$s以獲得精確的地理位置。", + "NoProviders": "沒有額外可用的地理定位器。建議使用 %1$sDB-IP%2$s,但需要安裝 GeoIp2 外掛。(您也可以從市集安裝第三方地理定位外掛。)", + "PluginDescription": "報告你訪客的位置:國家、地區、城市和地理坐標(經度及緯度)。", + "Region": "地區", + "SubmenuLocations": "地理位置", + "TestIPLocatorFailed": "找不到 IP 位址「%1$s」的位置。也許這個定位器未被正確設定,地理定位資料庫的名稱不正確,或者位於錯誤的文件夾中?", + "ToGeolocateOldVisits": "使用%1$s這個程式腳本%2$s以取得舊訪客紀錄中的地理定位資料。", + "VisitLocation": "訪客位置", + "WidgetLocation": "訪客位置", + "country_a1": "匿名代理", + "country_a2": "衛星網路業者", + "country_cat": "加泰羅尼亞語社區", + "country_o1": "其他國家與地區", + "getCityDocumentation": "顯示您的訪客由哪個城市連線瀏覽您的網站。", + "getContinentDocumentation": "顯示您的訪客由哪個大洲連線瀏覽您的網站。", + "getCountryDocumentation": "顯示您的訪客由哪個國家連線瀏覽您的網站。", + "getRegionDocumentation": "顯示您的訪客由哪個地區連線瀏覽您的網站。" + } +} diff --git a/plugins/UserCountry/stylesheets/userCountry.less b/plugins/UserCountry/stylesheets/userCountry.less new file mode 100644 index 0000000000000000000000000000000000000000..666fd9c9383be18b7d2632d132dc1723d2288f7a --- /dev/null +++ b/plugins/UserCountry/stylesheets/userCountry.less @@ -0,0 +1,55 @@ +#widgetUserCountrygetRegion, +#widgetUserCountrygetCountry, +#widgetUserCountrygetCity { + .dataTable .label > img { + border: 1px solid @theme-color-border; + box-sizing: content-box; + background: white; + } +} + +input.location-provider { + cursor: pointer; +} + +span.is-installed { + color: @color-green-matomo; +} + +span.is-broken { + color: @color-red-matomo; +} + +.loc-provider-status { + margin-left: .5em; +} + +#geoipdb-update-info tr input[type="text"], #geoipdb-screen2-update tr input[type="text"] { + width: 90%; +} + +#geoipdb-screen1>div>p { + line-height: 25px; + font-size: 18px; + max-width: 400px; +} + +.error { + font-weight: bold; + color: red; + padding: 4px 8px 4px 8px; +} + +#done-updating-updater { + margin-top: 16px; +} + +[name="geoip-isp-db"] .alert { + margin-bottom: 0; +} + +.locationProviderSelection { + .loadingPiwik { + padding: 1px; + } +} diff --git a/plugins/UserCountry/templates/_profileSummary.twig b/plugins/UserCountry/templates/_profileSummary.twig new file mode 100644 index 0000000000000000000000000000000000000000..02ae4d4506e048f3b98b5810bbd6b1375c494aa3 --- /dev/null +++ b/plugins/UserCountry/templates/_profileSummary.twig @@ -0,0 +1,43 @@ +
+

{{ 'UserCountry_Location'|translate }}

+

+ {%- for entry in visitorData.countries -%} + + {% set entryCity -%} + {% if entry.cities is defined and 1 == entry.cities|length and entry.cities|join -%} + {{ entry.cities|join }} + {%- elseif entry.cities is defined and 1 < entry.cities|length -%} + {{ 'UserCountry_FromDifferentCities'|translate }} + {%- endif %} + {%- endset %} + + {% set entryVisits -%} + + {% if entry.nb_visits == 1 -%} + {{ 'General_OneVisit'|translate }} + {%- else -%} + {{ 'General_NVisits'|translate(entry.nb_visits) }} + {%- endif -%} + + {%- endset %} + + {% set entryCountry -%} + {%- if entryCity -%} + {{ 'UserCountry_CityAndCountry'|translate(entryCity, entry.prettyName)|raw }} + {%- else -%} + {{ entry.prettyName }} + {%- endif -%} + +   + {%- endset %} + + {{- 'General_XFromY'|translate(entryVisits, entryCountry)|raw -}}{% if not loop.last %}, {% endif %} + {%- endfor %} + ({{ 'Live_ShowMap'|translate|replace({' ': ' '})|raw }}) + +

+ +
diff --git a/plugins/UserCountry/templates/adminIndex.twig b/plugins/UserCountry/templates/adminIndex.twig new file mode 100644 index 0000000000000000000000000000000000000000..062ac6432438310e080530509c601091ddd4db96 --- /dev/null +++ b/plugins/UserCountry/templates/adminIndex.twig @@ -0,0 +1,22 @@ +{% extends 'admin.twig' %} + +{% set title %}{{ 'UserCountry_Geolocation'|translate }}{% endset %} + +{% block content %} +{% import 'macros.twig' as piwik %} + +
+ +{{ configurations|raw }} + +{% endblock %} + diff --git a/plugins/UserCountry/templates/getDistinctCountries.twig b/plugins/UserCountry/templates/getDistinctCountries.twig new file mode 100644 index 0000000000000000000000000000000000000000..2051568b556725c27df3eef946d5333fd24e0ab2 --- /dev/null +++ b/plugins/UserCountry/templates/getDistinctCountries.twig @@ -0,0 +1,5 @@ +
diff --git a/plugins/UserCountry/vue/dist/UserCountry.umd.js b/plugins/UserCountry/vue/dist/UserCountry.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..432f0a54e0c0d813ef001e84c380dcd61d75cbf6 --- /dev/null +++ b/plugins/UserCountry/vue/dist/UserCountry.umd.js @@ -0,0 +1,550 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("vue"), require("CorePluginsAdmin")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", , "CorePluginsAdmin"], factory); + else if(typeof exports === 'object') + exports["UserCountry"] = factory(require("CoreHome"), require("vue"), require("CorePluginsAdmin")); + else + root["UserCountry"] = factory(root["CoreHome"], root["Vue"], root["CorePluginsAdmin"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__, __WEBPACK_EXTERNAL_MODULE_a5a2__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/UserCountry/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "a5a2": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE_a5a2__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "LocationProviderSelection", function() { return /* reexport */ LocationProviderSelection; }); +__webpack_require__.d(__webpack_exports__, "AdminPage", function() { return /* reexport */ AdminPage; }); +__webpack_require__.d(__webpack_exports__, "GetDistinctCountries", function() { return /* reexport */ GetDistinctCountries; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/UserCountry/vue/src/LocationProviderSelection/LocationProviderSelection.vue?vue&type=template&id=ad135132 + +const _hoisted_1 = { + class: "locationProviderSelection" +}; +const _hoisted_2 = ["innerHTML"]; +const _hoisted_3 = { + class: "row" +}; +const _hoisted_4 = { + class: "col s12 push-m9 m3" +}; +const _hoisted_5 = { + class: "col s12 m4 l2" +}; +const _hoisted_6 = ["id", "disabled", "checked", "onChange"]; +const _hoisted_7 = { + class: "loc-provider-status" +}; +const _hoisted_8 = { + key: 0, + class: "is-not-installed" +}; +const _hoisted_9 = { + key: 1, + class: "is-installed" +}; +const _hoisted_10 = { + key: 2, + class: "is-broken" +}; +const _hoisted_11 = { + class: "col s12 m4 l6" +}; +const _hoisted_12 = ["innerHTML"]; +const _hoisted_13 = ["innerHTML"]; +const _hoisted_14 = { + class: "col s12 m4 l4" +}; +const _hoisted_15 = { + key: 0, + class: "form-help" +}; +const _hoisted_16 = { + key: 0 +}; +const _hoisted_17 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", null, null, -1); +const _hoisted_18 = { + style: { + "position": "absolute" + } +}; +const _hoisted_19 = ["innerHTML"]; +const _hoisted_20 = { + class: "text-right" +}; +const _hoisted_21 = ["onClick"]; +const _hoisted_22 = { + key: 1 +}; +const _hoisted_23 = { + key: 1, + class: "form-help" +}; +const _hoisted_24 = { + key: 0 +}; +const _hoisted_25 = ["innerHTML"]; +const _hoisted_26 = ["innerHTML"]; +const _hoisted_27 = { + key: 1 +}; +const _hoisted_28 = ["innerHTML"]; +function render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_ActivityIndicator = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ActivityIndicator"); + const _component_Notification = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Notification"); + const _component_SaveButton = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("SaveButton"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_1, [!_ctx.isThereWorkingProvider ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", { + key: 0, + innerHTML: _ctx.$sanitize(_ctx.setUpGuides || '') + }, null, 8, _hoisted_2)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_3, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_4, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_InfoFor', _ctx.thisIp)), 1)]), (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["renderList"])(_ctx.visibleLocationProviders, (provider, id) => { + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", { + key: id, + class: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeClass"])(`row form-group provider${id}`) + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_5, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("label", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("input", { + class: "location-provider", + name: "location-provider", + type: "radio", + id: `provider_input_${id}`, + disabled: provider.status !== 1, + checked: _ctx.selectedProvider === id, + onChange: $event => _ctx.selectedProvider = id + }, null, 40, _hoisted_6), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translateOrDefault(provider.title)), 1)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", _hoisted_7, [provider.status === 0 ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_8, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_NotInstalled')), 1)) : provider.status === 1 ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_9, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Installed')), 1)) : provider.status === 2 ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("span", _hoisted_10, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Broken')), 1)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_11, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", { + innerHTML: _ctx.$sanitize(_ctx.translateOrDefault(provider.description)) + }, null, 8, _hoisted_12), provider.status !== 1 && provider.install_docs ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("p", { + key: 0, + innerHTML: _ctx.$sanitize(provider.install_docs) + }, null, 8, _hoisted_13)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_14, [provider.status === 1 ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_15, [_ctx.thisIp !== '127.0.0.1' && _ctx.thisIp !== '::1' ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_16, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('UserCountry_CurrentLocationIntro')) + ": ", 1), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", null, [_hoisted_17, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_18, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ActivityIndicator, { + loading: _ctx.updateLoading[id] + }, null, 8, ["loading"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + class: "location", + style: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["normalizeStyle"])({ + visibility: _ctx.providerLocations[id] ? 'visible' : 'hidden' + }) + }, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("strong", { + innerHTML: _ctx.$sanitize(_ctx.providerLocations[id] || ' ') + }, null, 8, _hoisted_19)], 4)]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", _hoisted_20, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("a", { + onClick: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withModifiers"])($event => _ctx.refreshProviderInfo(id), ["prevent"]) + }, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Refresh')), 9, _hoisted_21)])])) : (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_22, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('UserCountry_CannotLocalizeLocalIP', _ctx.thisIp)), 1))])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), provider.statusMessage ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_23, [provider.status === 2 ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("strong", _hoisted_24, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('General_Error')) + ":", 1)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(provider.statusMessage) + }, null, 8, _hoisted_25)])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), provider.extra_message ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", { + key: 2, + class: "form-help", + innerHTML: _ctx.$sanitize(provider.extra_message) + }, null, 8, _hoisted_26)) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true)])], 2); + }), 128)), !Object.keys(_ctx.locationProvidersNotDefaultOrDisabled).length ? (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", _hoisted_27, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Notification, { + noclear: true, + context: "warning" + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("span", { + innerHTML: _ctx.$sanitize(_ctx.noProvidersText) + }, null, 8, _hoisted_28)]), + _: 1 + })])) : Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createCommentVNode"])("", true), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_SaveButton, { + onConfirm: _cache[0] || (_cache[0] = $event => _ctx.save()), + saving: _ctx.isLoading + }, null, 8, ["saving"])]); +} +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/LocationProviderSelection/LocationProviderSelection.vue?vue&type=template&id=ad135132 + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// EXTERNAL MODULE: external "CorePluginsAdmin" +var external_CorePluginsAdmin_ = __webpack_require__("a5a2"); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/UserCountry/vue/src/LocationProviderSelection/LocationProviderSelection.vue?vue&type=script&lang=ts + + + +/* harmony default export */ var LocationProviderSelectionvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + currentProviderId: { + type: String, + required: true + }, + isThereWorkingProvider: Boolean, + setUpGuides: String, + thisIp: { + type: String, + required: true + }, + locationProviders: { + type: Object, + required: true + }, + defaultProviderId: { + type: String, + required: true + }, + disabledProviderId: { + type: String, + required: true + } + }, + components: { + ActivityIndicator: external_CoreHome_["ActivityIndicator"], + Notification: external_CoreHome_["Notification"], + SaveButton: external_CorePluginsAdmin_["SaveButton"] + }, + data() { + return { + isLoading: false, + updateLoading: {}, + selectedProvider: this.currentProviderId, + providerLocations: Object.fromEntries(Object.entries(this.locationProviders).map(([k, p]) => [k, p.location])) + }; + }, + methods: { + refreshProviderInfo(providerId) { + // this should not be in a controller... ideally we fetch this data always from client side + // and do not prefill it server side + this.updateLoading[providerId] = true; + delete this.providerLocations[providerId]; + external_CoreHome_["AjaxHelper"].fetch({ + module: 'UserCountry', + action: 'getLocationUsingProvider', + id: providerId, + format: 'html' + }, { + format: 'html' + }).then(response => { + this.providerLocations[providerId] = response; + }).finally(() => { + this.updateLoading[providerId] = false; + }); + }, + save() { + if (!this.selectedProvider) { + return; + } + this.isLoading = true; + external_CoreHome_["AjaxHelper"].fetch({ + method: 'UserCountry.setLocationProvider', + providerId: this.selectedProvider + }, { + withTokenInUrl: true + }).then(() => { + const notificationInstanceId = external_CoreHome_["NotificationsStore"].show({ + message: Object(external_CoreHome_["translate"])('General_Done'), + context: 'success', + noclear: true, + type: 'toast', + id: 'userCountryLocationProvider' + }); + external_CoreHome_["NotificationsStore"].scrollToNotification(notificationInstanceId); + }).finally(() => { + this.isLoading = false; + }); + } + }, + computed: { + visibleLocationProviders() { + return Object.fromEntries(Object.entries(this.locationProviders).filter(([, p]) => p.isVisible)); + }, + locationProvidersNotDefaultOrDisabled() { + return Object.fromEntries(Object.entries(this.locationProviders).filter(([, p]) => p.id !== this.defaultProviderId && p.id !== this.disabledProviderId)); + }, + noProvidersText() { + return Object(external_CoreHome_["translate"])('UserCountry_NoProviders', '', ''); + } + } +})); +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/LocationProviderSelection/LocationProviderSelection.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/LocationProviderSelection/LocationProviderSelection.vue + + + +LocationProviderSelectionvue_type_script_lang_ts.render = render + +/* harmony default export */ var LocationProviderSelection = (LocationProviderSelectionvue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/UserCountry/vue/src/AdminPage/AdminPage.vue?vue&type=template&id=387617c4 + +function AdminPagevue_type_template_id_387617c4_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_EnrichedHeadline = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("EnrichedHeadline"); + const _component_LocationProviderSelection = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("LocationProviderSelection"); + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + const _directive_content_intro = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveDirective"])("content-intro"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])(external_commonjs_vue_commonjs2_vue_root_Vue_["Fragment"], null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withDirectives"])((Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementBlock"])("div", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("h2", null, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_EnrichedHeadline, { + "help-url": _ctx.externalRawLink('https://matomo.org/docs/geo-locate/'), + id: "location-providers" + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createTextVNode"])(Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('UserCountry_Geolocation')), 1)]), + _: 1 + }, 8, ["help-url"])]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("p", null, Object(external_commonjs_vue_commonjs2_vue_root_Vue_["toDisplayString"])(_ctx.translate('UserCountry_GeolocationPageDesc')), 1)])), [[_directive_content_intro]]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_ContentBlock, { + "content-title": _ctx.translate('UserCountry_LocationProvider') + }, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_LocationProviderSelection, { + "current-provider-id": _ctx.currentProviderId, + "is-there-working-provider": _ctx.isThereWorkingProvider, + "set-up-guides": _ctx.setUpGuides, + "this-ip": _ctx.thisIp, + "location-providers": _ctx.locationProviders, + "default-provider-id": _ctx.defaultProviderId, + "disabled-provider-id": _ctx.disabledProviderId + }, null, 8, ["current-provider-id", "is-there-working-provider", "set-up-guides", "this-ip", "location-providers", "default-provider-id", "disabled-provider-id"])]), + _: 1 + }, 8, ["content-title"])], 64); +} +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/AdminPage/AdminPage.vue?vue&type=template&id=387617c4 + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/UserCountry/vue/src/AdminPage/AdminPage.vue?vue&type=script&lang=ts + + + +/* harmony default export */ var AdminPagevue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + currentProviderId: { + type: String, + required: true + }, + isThereWorkingProvider: Boolean, + setUpGuides: String, + thisIp: { + type: String, + required: true + }, + locationProviders: { + type: Object, + required: true + }, + defaultProviderId: { + type: String, + required: true + }, + disabledProviderId: { + type: String, + required: true + } + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"], + LocationProviderSelection: LocationProviderSelection, + EnrichedHeadline: external_CoreHome_["EnrichedHeadline"] + }, + directives: { + ContentIntro: external_CoreHome_["ContentIntro"], + ContentBlock: external_CoreHome_["ContentBlock"] + } +})); +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/AdminPage/AdminPage.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/AdminPage/AdminPage.vue + + + +AdminPagevue_type_script_lang_ts.render = AdminPagevue_type_template_id_387617c4_render + +/* harmony default export */ var AdminPage = (AdminPagevue_type_script_lang_ts); +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js??ref--13-0!./node_modules/@vue/cli-plugin-babel/node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist/templateLoader.js??ref--6!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/UserCountry/vue/src/GetDistinctCountries/GetDistinctCountries.vue?vue&type=template&id=5d044f71 + +const GetDistinctCountriesvue_type_template_id_5d044f71_hoisted_1 = { + class: "sparkline" +}; +const GetDistinctCountriesvue_type_template_id_5d044f71_hoisted_2 = ["innerHTML"]; +const GetDistinctCountriesvue_type_template_id_5d044f71_hoisted_3 = /*#__PURE__*/Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("br", { + style: { + "clear": "left" + } +}, null, -1); +function GetDistinctCountriesvue_type_template_id_5d044f71_render(_ctx, _cache, $props, $setup, $data, $options) { + const _component_Sparkline = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("Sparkline"); + const _component_ContentBlock = Object(external_commonjs_vue_commonjs2_vue_root_Vue_["resolveComponent"])("ContentBlock"); + return Object(external_commonjs_vue_commonjs2_vue_root_Vue_["openBlock"])(), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createBlock"])(_component_ContentBlock, null, { + default: Object(external_commonjs_vue_commonjs2_vue_root_Vue_["withCtx"])(() => [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", GetDistinctCountriesvue_type_template_id_5d044f71_hoisted_1, [Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createVNode"])(_component_Sparkline, { + params: _ctx.urlSparklineCountries, + width: 100, + height: 25 + }, null, 8, ["params"]), Object(external_commonjs_vue_commonjs2_vue_root_Vue_["createElementVNode"])("div", { + innerHTML: _ctx.$sanitize(_ctx.distinctCountriesText) + }, null, 8, GetDistinctCountriesvue_type_template_id_5d044f71_hoisted_2)]), GetDistinctCountriesvue_type_template_id_5d044f71_hoisted_3]), + _: 1 + }); +} +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/GetDistinctCountries/GetDistinctCountries.vue?vue&type=template&id=5d044f71 + +// CONCATENATED MODULE: ./node_modules/@vue/cli-plugin-typescript/node_modules/cache-loader/dist/cjs.js??ref--15-0!./node_modules/babel-loader/lib!./node_modules/@vue/cli-plugin-typescript/node_modules/ts-loader??ref--15-2!./node_modules/@vue/cli-service/node_modules/cache-loader/dist/cjs.js??ref--1-0!./node_modules/@vue/cli-service/node_modules/vue-loader-v16/dist??ref--1-1!./plugins/UserCountry/vue/src/GetDistinctCountries/GetDistinctCountries.vue?vue&type=script&lang=ts + + +/* harmony default export */ var GetDistinctCountriesvue_type_script_lang_ts = (Object(external_commonjs_vue_commonjs2_vue_root_Vue_["defineComponent"])({ + props: { + numberDistinctCountries: { + type: Number, + required: true + }, + urlSparklineCountries: { + type: [Object, String], + required: true + } + }, + components: { + ContentBlock: external_CoreHome_["ContentBlock"], + Sparkline: external_CoreHome_["Sparkline"] + }, + computed: { + distinctCountriesText() { + return Object(external_CoreHome_["translate"])('UserCountry_DistinctCountries', `${this.numberDistinctCountries}`); + } + } +})); +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/GetDistinctCountries/GetDistinctCountries.vue?vue&type=script&lang=ts + +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/GetDistinctCountries/GetDistinctCountries.vue + + + +GetDistinctCountriesvue_type_script_lang_ts.render = GetDistinctCountriesvue_type_template_id_5d044f71_render + +/* harmony default export */ var GetDistinctCountries = (GetDistinctCountriesvue_type_script_lang_ts); +// CONCATENATED MODULE: ./plugins/UserCountry/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=UserCountry.umd.js.map \ No newline at end of file diff --git a/plugins/UserCountry/vue/dist/UserCountry.umd.min.js b/plugins/UserCountry/vue/dist/UserCountry.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..cddbfe10b1b1d7d473e397d866d8b91700b8d980 --- /dev/null +++ b/plugins/UserCountry/vue/dist/UserCountry.umd.min.js @@ -0,0 +1,8 @@ +(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t(require("CoreHome"),require("vue"),require("CorePluginsAdmin")):"function"===typeof define&&define.amd?define(["CoreHome",,"CorePluginsAdmin"],t):"object"===typeof exports?exports["UserCountry"]=t(require("CoreHome"),require("vue"),require("CorePluginsAdmin")):e["UserCountry"]=t(e["CoreHome"],e["Vue"],e["CorePluginsAdmin"])})("undefined"!==typeof self?self:this,(function(e,t,r){return function(e){var t={};function r(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=e,r.c=t,r.d=function(e,t,o){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},r.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)r.d(o,n,function(t){return e[t]}.bind(null,n));return o},r.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="plugins/UserCountry/vue/dist/",r(r.s="fae3")}({"19dc":function(t,r){t.exports=e},"8bbf":function(e,r){e.exports=t},a5a2:function(e,t){e.exports=r},fae3:function(e,t,r){"use strict";if(r.r(t),r.d(t,"LocationProviderSelection",(function(){return w})),r.d(t,"AdminPage",(function(){return G})),r.d(t,"GetDistinctCountries",(function(){return R})),"undefined"!==typeof window){var o=window.document.currentScript,n=o&&o.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);n&&(r.p=n[1])}var i=r("8bbf");const c={class:"locationProviderSelection"},l=["innerHTML"],a={class:"row"},s={class:"col s12 push-m9 m3"},d={class:"col s12 m4 l2"},u=["id","disabled","checked","onChange"],p={class:"loc-provider-status"},b={key:0,class:"is-not-installed"},j={key:1,class:"is-installed"},O={key:2,class:"is-broken"},m={class:"col s12 m4 l6"},v=["innerHTML"],f=["innerHTML"],y={class:"col s12 m4 l4"},k={key:0,class:"form-help"},g={key:0},h=Object(i["createElementVNode"])("br",null,null,-1),C={style:{position:"absolute"}},P=["innerHTML"],N={class:"text-right"},B=["onClick"],E={key:1},V={key:1,class:"form-help"},L={key:0},S=["innerHTML"],I=["innerHTML"],_={key:1},x=["innerHTML"];function T(e,t,r,o,n,T){const D=Object(i["resolveComponent"])("ActivityIndicator"),H=Object(i["resolveComponent"])("Notification"),M=Object(i["resolveComponent"])("SaveButton");return Object(i["openBlock"])(),Object(i["createElementBlock"])("div",c,[e.isThereWorkingProvider?Object(i["createCommentVNode"])("",!0):(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",{key:0,innerHTML:e.$sanitize(e.setUpGuides||"")},null,8,l)),Object(i["createElementVNode"])("div",a,[Object(i["createElementVNode"])("div",s,Object(i["toDisplayString"])(e.translate("General_InfoFor",e.thisIp)),1)]),(Object(i["openBlock"])(!0),Object(i["createElementBlock"])(i["Fragment"],null,Object(i["renderList"])(e.visibleLocationProviders,(t,r)=>(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",{key:r,class:Object(i["normalizeClass"])("row form-group provider"+r)},[Object(i["createElementVNode"])("div",d,[Object(i["createElementVNode"])("p",null,[Object(i["createElementVNode"])("label",null,[Object(i["createElementVNode"])("input",{class:"location-provider",name:"location-provider",type:"radio",id:"provider_input_"+r,disabled:1!==t.status,checked:e.selectedProvider===r,onChange:t=>e.selectedProvider=r},null,40,u),Object(i["createElementVNode"])("span",null,Object(i["toDisplayString"])(e.translateOrDefault(t.title)),1)])]),Object(i["createElementVNode"])("p",p,[0===t.status?(Object(i["openBlock"])(),Object(i["createElementBlock"])("span",b,Object(i["toDisplayString"])(e.translate("General_NotInstalled")),1)):1===t.status?(Object(i["openBlock"])(),Object(i["createElementBlock"])("span",j,Object(i["toDisplayString"])(e.translate("General_Installed")),1)):2===t.status?(Object(i["openBlock"])(),Object(i["createElementBlock"])("span",O,Object(i["toDisplayString"])(e.translate("General_Broken")),1)):Object(i["createCommentVNode"])("",!0)])]),Object(i["createElementVNode"])("div",m,[Object(i["createElementVNode"])("p",{innerHTML:e.$sanitize(e.translateOrDefault(t.description))},null,8,v),1!==t.status&&t.install_docs?(Object(i["openBlock"])(),Object(i["createElementBlock"])("p",{key:0,innerHTML:e.$sanitize(t.install_docs)},null,8,f)):Object(i["createCommentVNode"])("",!0)]),Object(i["createElementVNode"])("div",y,[1===t.status?(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",k,["127.0.0.1"!==e.thisIp&&"::1"!==e.thisIp?(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",g,[Object(i["createTextVNode"])(Object(i["toDisplayString"])(e.translate("UserCountry_CurrentLocationIntro"))+": ",1),Object(i["createElementVNode"])("div",null,[h,Object(i["createElementVNode"])("div",C,[Object(i["createVNode"])(D,{loading:e.updateLoading[r]},null,8,["loading"])]),Object(i["createElementVNode"])("span",{class:"location",style:Object(i["normalizeStyle"])({visibility:e.providerLocations[r]?"visible":"hidden"})},[Object(i["createElementVNode"])("strong",{innerHTML:e.$sanitize(e.providerLocations[r]||" ")},null,8,P)],4)]),Object(i["createElementVNode"])("div",N,[Object(i["createElementVNode"])("a",{onClick:Object(i["withModifiers"])(t=>e.refreshProviderInfo(r),["prevent"])},Object(i["toDisplayString"])(e.translate("General_Refresh")),9,B)])])):(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",E,Object(i["toDisplayString"])(e.translate("UserCountry_CannotLocalizeLocalIP",e.thisIp)),1))])):Object(i["createCommentVNode"])("",!0),t.statusMessage?(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",V,[2===t.status?(Object(i["openBlock"])(),Object(i["createElementBlock"])("strong",L,Object(i["toDisplayString"])(e.translate("General_Error"))+":",1)):Object(i["createCommentVNode"])("",!0),Object(i["createElementVNode"])("span",{innerHTML:e.$sanitize(t.statusMessage)},null,8,S)])):Object(i["createCommentVNode"])("",!0),t.extra_message?(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",{key:2,class:"form-help",innerHTML:e.$sanitize(t.extra_message)},null,8,I)):Object(i["createCommentVNode"])("",!0)])],2))),128)),Object.keys(e.locationProvidersNotDefaultOrDisabled).length?Object(i["createCommentVNode"])("",!0):(Object(i["openBlock"])(),Object(i["createElementBlock"])("div",_,[Object(i["createVNode"])(H,{noclear:!0,context:"warning"},{default:Object(i["withCtx"])(()=>[Object(i["createElementVNode"])("span",{innerHTML:e.$sanitize(e.noProvidersText)},null,8,x)]),_:1})])),Object(i["createVNode"])(M,{onConfirm:t[0]||(t[0]=t=>e.save()),saving:e.isLoading},null,8,["saving"])])}var D=r("19dc"),H=r("a5a2"),M=Object(i["defineComponent"])({props:{currentProviderId:{type:String,required:!0},isThereWorkingProvider:Boolean,setUpGuides:String,thisIp:{type:String,required:!0},locationProviders:{type:Object,required:!0},defaultProviderId:{type:String,required:!0},disabledProviderId:{type:String,required:!0}},components:{ActivityIndicator:D["ActivityIndicator"],Notification:D["Notification"],SaveButton:H["SaveButton"]},data(){return{isLoading:!1,updateLoading:{},selectedProvider:this.currentProviderId,providerLocations:Object.fromEntries(Object.entries(this.locationProviders).map(([e,t])=>[e,t.location]))}},methods:{refreshProviderInfo(e){this.updateLoading[e]=!0,delete this.providerLocations[e],D["AjaxHelper"].fetch({module:"UserCountry",action:"getLocationUsingProvider",id:e,format:"html"},{format:"html"}).then(t=>{this.providerLocations[e]=t}).finally(()=>{this.updateLoading[e]=!1})},save(){this.selectedProvider&&(this.isLoading=!0,D["AjaxHelper"].fetch({method:"UserCountry.setLocationProvider",providerId:this.selectedProvider},{withTokenInUrl:!0}).then(()=>{const e=D["NotificationsStore"].show({message:Object(D["translate"])("General_Done"),context:"success",noclear:!0,type:"toast",id:"userCountryLocationProvider"});D["NotificationsStore"].scrollToNotification(e)}).finally(()=>{this.isLoading=!1}))}},computed:{visibleLocationProviders(){return Object.fromEntries(Object.entries(this.locationProviders).filter(([,e])=>e.isVisible))},locationProvidersNotDefaultOrDisabled(){return Object.fromEntries(Object.entries(this.locationProviders).filter(([,e])=>e.id!==this.defaultProviderId&&e.id!==this.disabledProviderId))},noProvidersText(){return Object(D["translate"])("UserCountry_NoProviders",'',"")}}});M.render=T;var w=M;function q(e,t,r,o,n,c){const l=Object(i["resolveComponent"])("EnrichedHeadline"),a=Object(i["resolveComponent"])("LocationProviderSelection"),s=Object(i["resolveComponent"])("ContentBlock"),d=Object(i["resolveDirective"])("content-intro");return Object(i["openBlock"])(),Object(i["createElementBlock"])(i["Fragment"],null,[Object(i["withDirectives"])((Object(i["openBlock"])(),Object(i["createElementBlock"])("div",null,[Object(i["createElementVNode"])("h2",null,[Object(i["createVNode"])(l,{"help-url":e.externalRawLink("https://matomo.org/docs/geo-locate/"),id:"location-providers"},{default:Object(i["withCtx"])(()=>[Object(i["createTextVNode"])(Object(i["toDisplayString"])(e.translate("UserCountry_Geolocation")),1)]),_:1},8,["help-url"])]),Object(i["createElementVNode"])("p",null,Object(i["toDisplayString"])(e.translate("UserCountry_GeolocationPageDesc")),1)])),[[d]]),Object(i["createVNode"])(s,{"content-title":e.translate("UserCountry_LocationProvider")},{default:Object(i["withCtx"])(()=>[Object(i["createVNode"])(a,{"current-provider-id":e.currentProviderId,"is-there-working-provider":e.isThereWorkingProvider,"set-up-guides":e.setUpGuides,"this-ip":e.thisIp,"location-providers":e.locationProviders,"default-provider-id":e.defaultProviderId,"disabled-provider-id":e.disabledProviderId},null,8,["current-provider-id","is-there-working-provider","set-up-guides","this-ip","location-providers","default-provider-id","disabled-provider-id"])]),_:1},8,["content-title"])],64)}var U=Object(i["defineComponent"])({props:{currentProviderId:{type:String,required:!0},isThereWorkingProvider:Boolean,setUpGuides:String,thisIp:{type:String,required:!0},locationProviders:{type:Object,required:!0},defaultProviderId:{type:String,required:!0},disabledProviderId:{type:String,required:!0}},components:{ContentBlock:D["ContentBlock"],LocationProviderSelection:w,EnrichedHeadline:D["EnrichedHeadline"]},directives:{ContentIntro:D["ContentIntro"],ContentBlock:D["ContentBlock"]}});U.render=q;var G=U;const z={class:"sparkline"},A=["innerHTML"],$=Object(i["createElementVNode"])("br",{style:{clear:"left"}},null,-1);function W(e,t,r,o,n,c){const l=Object(i["resolveComponent"])("Sparkline"),a=Object(i["resolveComponent"])("ContentBlock");return Object(i["openBlock"])(),Object(i["createBlock"])(a,null,{default:Object(i["withCtx"])(()=>[Object(i["createElementVNode"])("div",z,[Object(i["createVNode"])(l,{params:e.urlSparklineCountries,width:100,height:25},null,8,["params"]),Object(i["createElementVNode"])("div",{innerHTML:e.$sanitize(e.distinctCountriesText)},null,8,A)]),$]),_:1})}var F=Object(i["defineComponent"])({props:{numberDistinctCountries:{type:Number,required:!0},urlSparklineCountries:{type:[Object,String],required:!0}},components:{ContentBlock:D["ContentBlock"],Sparkline:D["Sparkline"]},computed:{distinctCountriesText(){return Object(D["translate"])("UserCountry_DistinctCountries",`${this.numberDistinctCountries}`)}}});F.render=W;var R=F; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */}})})); +//# sourceMappingURL=UserCountry.umd.min.js.map \ No newline at end of file diff --git a/plugins/UserCountry/vue/dist/umd.metadata.json b/plugins/UserCountry/vue/dist/umd.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..dce4477a3c58c74fe9a906c021f164b7fa49111e --- /dev/null +++ b/plugins/UserCountry/vue/dist/umd.metadata.json @@ -0,0 +1,6 @@ +{ + "dependsOn": [ + "CoreHome", + "CorePluginsAdmin" + ] +} \ No newline at end of file diff --git a/plugins/UserCountry/vue/src/AdminPage/AdminPage.vue b/plugins/UserCountry/vue/src/AdminPage/AdminPage.vue new file mode 100644 index 0000000000000000000000000000000000000000..2c80fdf4e52f6e3f08e51fe8f6a66a8acc1735d5 --- /dev/null +++ b/plugins/UserCountry/vue/src/AdminPage/AdminPage.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/plugins/UserCountry/vue/src/GetDistinctCountries/GetDistinctCountries.vue b/plugins/UserCountry/vue/src/GetDistinctCountries/GetDistinctCountries.vue new file mode 100644 index 0000000000000000000000000000000000000000..06e2aabb2863b409d727c874375693693bea2a10 --- /dev/null +++ b/plugins/UserCountry/vue/src/GetDistinctCountries/GetDistinctCountries.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/plugins/UserCountry/vue/src/LocationProviderSelection/LocationProviderSelection.vue b/plugins/UserCountry/vue/src/LocationProviderSelection/LocationProviderSelection.vue new file mode 100644 index 0000000000000000000000000000000000000000..99eb1245a96c64e984afd52f750aa73534519c1b --- /dev/null +++ b/plugins/UserCountry/vue/src/LocationProviderSelection/LocationProviderSelection.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/plugins/UserCountry/vue/src/index.ts b/plugins/UserCountry/vue/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fe402700246b7e44e568f0e57ca20854e017eee --- /dev/null +++ b/plugins/UserCountry/vue/src/index.ts @@ -0,0 +1,12 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +export { + default as LocationProviderSelection, +} from './LocationProviderSelection/LocationProviderSelection.vue'; +export { default as AdminPage } from './AdminPage/AdminPage.vue'; +export { default as GetDistinctCountries } from './GetDistinctCountries/GetDistinctCountries.vue';