Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cid.log4
-rw-r--r--cid.log~21
-rw-r--r--cloud-archive.sh20
-rw-r--r--cloud-config.sh2
-rw-r--r--cloud-generate.sh17
-rw-r--r--cloud-setup.sh43
-rw-r--r--config/common.config.ini.php.not62
-rw-r--r--info.php1
-rw-r--r--misc/user/example.com/logo-header.pngbin0 -> 45698 bytes
-rw-r--r--misc/user/example.com/logo.pngbin0 -> 311123 bytes
m---------plugins/AdvancedCampaignReporting0
m---------plugins/ApiGetWithSitesInfo0
-rw-r--r--plugins/AutoLogImporter/.gitignore1
-rw-r--r--plugins/AutoLogImporter/.travis.yml132
-rw-r--r--plugins/AutoLogImporter/API.php93
-rw-r--r--plugins/AutoLogImporter/AutoLogImporter.php38
-rw-r--r--plugins/AutoLogImporter/Controller.php56
-rw-r--r--plugins/AutoLogImporter/Dao.php106
-rw-r--r--plugins/AutoLogImporter/Formatter.php44
-rw-r--r--plugins/AutoLogImporter/LogImporter.php90
-rw-r--r--plugins/AutoLogImporter/LogImporter/File.php75
-rw-r--r--plugins/AutoLogImporter/LogImporter/Import.php61
-rw-r--r--plugins/AutoLogImporter/LogImporter/LogFileList.php80
-rw-r--r--plugins/AutoLogImporter/LogImporter/Result.php51
-rw-r--r--plugins/AutoLogImporter/Menu.php31
-rw-r--r--plugins/AutoLogImporter/README.md100
-rw-r--r--plugins/AutoLogImporter/Settings.php108
-rw-r--r--plugins/AutoLogImporter/Tasks.php44
-rw-r--r--plugins/AutoLogImporter/config/config.php10
-rw-r--r--plugins/AutoLogImporter/config/test.php26
-rw-r--r--plugins/AutoLogImporter/lang/en.json26
-rw-r--r--plugins/AutoLogImporter/plugin.json16
-rw-r--r--plugins/AutoLogImporter/screenshots/.gitkeep0
-rw-r--r--plugins/AutoLogImporter/screenshots/Error_If_File_Was_Not_Copied_Successfully.pngbin0 -> 52896 bytes
-rw-r--r--plugins/AutoLogImporter/screenshots/Error_Import_Details.pngbin0 -> 17082 bytes
-rw-r--r--plugins/AutoLogImporter/screenshots/Plugin_Settings.pngbin0 -> 25745 bytes
-rw-r--r--plugins/AutoLogImporter/screenshots/Status_Initially.pngbin0 -> 62897 bytes
-rw-r--r--plugins/AutoLogImporter/screenshots/Status_Report.pngbin0 -> 78344 bytes
-rw-r--r--plugins/AutoLogImporter/screenshots/Successful_Import_Details.pngbin0 -> 63380 bytes
-rw-r--r--plugins/AutoLogImporter/stylesheets/status.less29
-rw-r--r--plugins/AutoLogImporter/templates/status.twig117
m---------plugins/Bandwidth0
m---------plugins/ComparisonDashboard0
m---------plugins/ConcurrentVisits0
m---------plugins/CustomTrackerJs0
m---------plugins/DFOCanada0
-rw-r--r--plugins/FileSynchronizer/.gitignore1
-rw-r--r--plugins/FileSynchronizer/.travis.yml132
-rw-r--r--plugins/FileSynchronizer/API.php73
-rw-r--r--plugins/FileSynchronizer/Controller.php50
-rw-r--r--plugins/FileSynchronizer/Dao.php112
-rw-r--r--plugins/FileSynchronizer/FileSynchronizer.php39
-rw-r--r--plugins/FileSynchronizer/Formatter.php48
-rw-r--r--plugins/FileSynchronizer/Menu.php31
-rw-r--r--plugins/FileSynchronizer/README.md66
-rw-r--r--plugins/FileSynchronizer/Settings.php207
-rw-r--r--plugins/FileSynchronizer/SyncFiles.php121
-rw-r--r--plugins/FileSynchronizer/SyncFiles/Copy.php89
-rw-r--r--plugins/FileSynchronizer/SyncFiles/File.php45
-rw-r--r--plugins/FileSynchronizer/SyncFiles/FileList.php52
-rw-r--r--plugins/FileSynchronizer/SyncFiles/Result.php51
-rw-r--r--plugins/FileSynchronizer/Tasks.php46
-rw-r--r--plugins/FileSynchronizer/config/config.php8
-rw-r--r--plugins/FileSynchronizer/config/test.php22
-rw-r--r--plugins/FileSynchronizer/lang/en.json25
-rw-r--r--plugins/FileSynchronizer/plugin.json16
-rw-r--r--plugins/FileSynchronizer/screenshots/.gitkeep0
-rw-r--r--plugins/FileSynchronizer/screenshots/Error_Sync_Details.pngbin0 -> 10828 bytes
-rw-r--r--plugins/FileSynchronizer/screenshots/Plugin_Settings.pngbin0 -> 86828 bytes
-rw-r--r--plugins/FileSynchronizer/screenshots/Status_Report.pngbin0 -> 57922 bytes
-rw-r--r--plugins/FileSynchronizer/stylesheets/status.less23
-rw-r--r--plugins/FileSynchronizer/templates/status.twig100
m---------plugins/Funnel0
m---------plugins/InterSites0
m---------plugins/LogViewer0
m---------plugins/LoginHttpAuth0
m---------plugins/MediaPlayer0
m---------plugins/MetaSites0
m---------plugins/RawDataExporter0
m---------plugins/SimplePageBuilder0
m---------plugins/SiteMigration0
m---------plugins/TrafficMonitor0
m---------plugins/WebsiteGroups0
m---------plugins/WhiteLabel0
m---------plugins/WhitelistAdmin0
85 files changed, 2861 insertions, 0 deletions
diff --git a/cid.log b/cid.log
new file mode 100644
index 0000000000..5577db17e3
--- /dev/null
+++ b/cid.log
@@ -0,0 +1,4 @@
+
+5348954:180.153.201.215 - - [02/Aug/2015:21:15:38 +0000] "GET /piwik.php?action_name=Kavos aparatai nuo 36 ??\xAC&idsite=1&rec=1&r=199956&h=0&m=18&s=24&url=http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456&urlref=http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http://www.googleadservices.com/pagead/aclk?sa=L&ai=Cii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB&ohost=www.google.com&cid=5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw&sig=AOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g&clui=0&rct=j&q=&ved=0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw&adurl=http://www.kavosdraugas.lt/kavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=7972 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456" "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-Manager Mobile Safari/537.36"
+
+
diff --git a/cid.log~ b/cid.log~
new file mode 100644
index 0000000000..d266b0dce5
--- /dev/null
+++ b/cid.log~
@@ -0,0 +1,21 @@
+5333697:87.247.69.4 - - [02/Aug/2015:21:12:54 +0000] "GET /piwik.php?action_name=Kavos%20aparatai%20nuo%2036%20%E2%82%AC&idsite=1&rec=1&r=006963&h=0&m=15&s=41&url=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai%3Fgclid%3DCL6m2O6oi8cCFY_JtAod_tYIPQ&urlref=http%3A%2F%2Fwww.googleadservices.com%2Fpagead%2Faclk%3Fsa%3DL%26ai%3DCii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB%26ohost%3Dwww.google.com%26cid%3D5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw%26sig%3DAOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g%26clui%3D0%26rct%3Dj%26q%3D%26ved%3D0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw%26adurl%3Dhttp%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=1&_refts=1438550142&_viewts=1438550142&_ref=http%3A%2F%2Fwww.googleadservices.com%2Fpagead%2Faclk%3Fsa%3DL%26ai%3DCii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB%26ohost%3Dwww.google.com%26cid%3D5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw%26sig%3DAOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g%26clui%3D0%26rct%3Dj%26q%3D%26ved%3D0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw%26adurl%3Dhttp%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=3068 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36 OPR/30.0.1835.125"
+
+5334309:87.247.69.4 - - [02/Aug/2015:21:13:01 +0000] "GET /piwik.php?e_c=997&e_a=8218%2Fshown&idsite=1&rec=1&r=680202&h=0&m=15&s=48&url=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai%3Fgclid%3DCL6m2O6oi8cCFY_JtAod_tYIPQ&urlref=http%3A%2F%2Fwww.googleadservices.com%2Fpagead%2Faclk%3Fsa%3DL%26ai%3DCii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB%26ohost%3Dwww.google.com%26cid%3D5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw%26sig%3DAOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g%26clui%3D0%26rct%3Dj%26q%3D%26ved%3D0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw%26adurl%3Dhttp%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http%3A%2F%2Fwww.googleadservices.com%2Fpagead%2Faclk%3Fsa%3DL%26ai%3DCii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB%26ohost%3Dwww.google.com%26cid%3D5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw%26sig%3DAOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g%26clui%3D0%26rct%3Dj%26q%3D%26ved%3D0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw%26adurl%3Dhttp%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=3068 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36 OPR/30.0.1835.125"
+
+5348878:87.247.69.4 - - [02/Aug/2015:21:15:37 +0000] "GET /piwik.php?action_name=Kavos%20aparatai%20nuo%2036%20%E2%82%AC&idsite=1&rec=1&r=199956&h=0&m=18&s=24&url=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai%3Fgclid%3DCL6m2O6oi8cCFY_JtAod_tYIPQ%26price%3D216%2C456&urlref=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai%3Fgclid%3DCL6m2O6oi8cCFY_JtAod_tYIPQ&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http%3A%2F%2Fwww.googleadservices.com%2Fpagead%2Faclk%3Fsa%3DL%26ai%3DCii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB%26ohost%3Dwww.google.com%26cid%3D5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw%26sig%3DAOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g%26clui%3D0%26rct%3Dj%26q%3D%26ved%3D0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw%26adurl%3Dhttp%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=7972 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36 OPR/30.0.1835.125"
+
+5348954:180.153.201.215 - - [02/Aug/2015:21:15:38 +0000] "GET /piwik.php?action_name=Kavos aparatai nuo 36 ??\xAC&idsite=1&rec=1&r=199956&h=0&m=18&s=24&url=http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456&urlref=http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http://www.googleadservices.com/pagead/aclk?sa=L&ai=Cii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB&ohost=www.google.com&cid=5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw&sig=AOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g&clui=0&rct=j&q=&ved=0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw&adurl=http://www.kavosdraugas.lt/kavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=7972 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456" "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-Manager Mobile Safari/537.36"
+
+5349667:87.247.69.4 - - [02/Aug/2015:21:15:46 +0000] "GET /piwik.php?action_name=Kavos%20aparatai%20nuo%2036%20%E2%82%AC&idsite=1&rec=1&r=298865&h=0&m=18&s=33&url=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai%3Fgclid%3DCL6m2O6oi8cCFY_JtAod_tYIPQ%26price%3D216%2C456%2615%3D97&urlref=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai%3Fgclid%3DCL6m2O6oi8cCFY_JtAod_tYIPQ%26price%3D216%2C456&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http%3A%2F%2Fwww.googleadservices.com%2Fpagead%2Faclk%3Fsa%3DL%26ai%3DCii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB%26ohost%3Dwww.google.com%26cid%3D5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw%26sig%3DAOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g%26clui%3D0%26rct%3Dj%26q%3D%26ved%3D0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw%26adurl%3Dhttp%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=2467 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456&15=97" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36 OPR/30.0.1835.125"
+
+5350042:112.64.235.91 - - [02/Aug/2015:21:15:50 +0000] "GET /piwik.php?action_name=Kavos aparatai nuo 36 ??\xAC&idsite=1&rec=1&r=298865&h=0&m=18&s=33&url=http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456&15=97&urlref=http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http://www.googleadservices.com/pagead/aclk?sa=L&ai=Cii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB&ohost=www.google.com&cid=5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw&sig=AOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g&clui=0&rct=j&q=&ved=0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw&adurl=http://www.kavosdraugas.lt/kavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=2467 HTTP/1.1" 200 54 "-" "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-Manager Mobile Safari/537.36"
+
+5354857:87.247.69.4 - - [02/Aug/2015:21:16:40 +0000] "GET /piwik.php?action_name=Kavos%20aparatas%20De%27Longhi%20%E2%80%9EECAM%2022.110%E2%80%9C%20-%20Kavos%20draugas&idsite=1&rec=1&r=863518&h=0&m=19&s=27&url=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatas-delonghi-ecam-22-110&urlref=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai%3Fgclid%3DCL6m2O6oi8cCFY_JtAod_tYIPQ%26price%3D216%2C456%2615%3D97&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http%3A%2F%2Fwww.googleadservices.com%2Fpagead%2Faclk%3Fsa%3DL%26ai%3DCii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB%26ohost%3Dwww.google.com%26cid%3D5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw%26sig%3DAOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g%26clui%3D0%26rct%3Dj%26q%3D%26ved%3D0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw%26adurl%3Dhttp%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=1226 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatas-delonghi-ecam-22-110" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36 OPR/30.0.1835.125"
+
+5354859:87.247.69.4 - - [02/Aug/2015:21:16:40 +0000] "GET /piwik.php?action_name=Kavos%20aparatas%20De%27Longhi%20%E2%80%9EEC%20850%20M%E2%80%9C%20-%20Kavos%20draugas&idsite=1&rec=1&r=751500&h=0&m=19&s=27&url=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatas-delonghi-ec-850m&urlref=http%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai%3Fgclid%3DCL6m2O6oi8cCFY_JtAod_tYIPQ%26price%3D216%2C456%2615%3D97&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http%3A%2F%2Fwww.googleadservices.com%2Fpagead%2Faclk%3Fsa%3DL%26ai%3DCii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB%26ohost%3Dwww.google.com%26cid%3D5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw%26sig%3DAOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g%26clui%3D0%26rct%3Dj%26q%3D%26ved%3D0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw%26adurl%3Dhttp%3A%2F%2Fwww.kavosdraugas.lt%2Fkavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=1140 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatas-delonghi-ec-850m" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36 OPR/30.0.1835.125"
+
+5354946:112.65.193.14 - - [02/Aug/2015:21:16:41 +0000] "GET /piwik.php?action_name=Kavos aparatas De'Longhi ???ECAM 22.110??\x9C - Kavos draugas&idsite=1&rec=1&r=863518&h=0&m=19&s=27&url=http://www.kavosdraugas.lt/kavos-aparatas-delonghi-ecam-22-110&urlref=http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456&15=97&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http://www.googleadservices.com/pagead/aclk?sa=L&ai=Cii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB&ohost=www.google.com&cid=5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw&sig=AOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g&clui=0&rct=j&q=&ved=0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw&adurl=http://www.kavosdraugas.lt/kavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=1226 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatas-delonghi-ecam-22-110" "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-Manager Mobile Safari/537.36"
+
+5354983:180.153.163.210 - - [02/Aug/2015:21:16:41 +0000] "GET /piwik.php?action_name=Kavos aparatas De'Longhi ???EC 850 M??\x9C - Kavos draugas&idsite=1&rec=1&r=751500&h=0&m=19&s=27&url=http://www.kavosdraugas.lt/kavos-aparatas-delonghi-ec-850m&urlref=http://www.kavosdraugas.lt/kavos-aparatai?gclid=CL6m2O6oi8cCFY_JtAod_tYIPQ&price=216,456&15=97&_id=54f474c0c50144df&_idts=1438550142&_idvc=1&_idn=0&_refts=1438550142&_viewts=1438550142&_ref=http://www.googleadservices.com/pagead/aclk?sa=L&ai=Cii7VcIi-VeDRGIaRjAaPqK-QBZr23PYFwq3-29QBtcrnsxUIABABIL_gsAMoA2C5A6ABrqKPywPIAQGpAtRzbKu7bLI-qgQiT9Dly94npwbCg5D7SkUsuqSK-SLDbgVFTQ55B4AxabBQloAFkE6IBgGAB7rd8DSQBwOoB6LCG6gHpr4b2AcB&ohost=www.google.com&cid=5GjGWDPimmkdeh1FrgcuUImc3pojqtIChK-EDtUjr4JZmw&sig=AOD64_3GESvwtzmjkBhlZRJcFCvb4QN70g&clui=0&rct=j&q=&ved=0CBoQ0QxqFQoTCIz3keuoi8cCFcKyFAodIQ0Ktw&adurl=http://www.kavosdraugas.lt/kavos-aparatai&pdf=1&qt=0&realp=0&wma=1&dir=0&fla=1&java=1&gears=0&ag=0&cookie=1&res=1366x768&gt_ms=1140 HTTP/1.1" 200 54 "http://www.kavosdraugas.lt/kavos-aparatas-delonghi-ec-850m" "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-Manager Mobile Safari/537.36"
+
+
diff --git a/cloud-archive.sh b/cloud-archive.sh
new file mode 100644
index 0000000000..4f7cc696e7
--- /dev/null
+++ b/cloud-archive.sh
@@ -0,0 +1,20 @@
+source cloud-config.sh
+
+for I in $(seq 1 $END);
+do
+ echo "Running enterprise:dashboard..."
+ echo ""
+
+ ./console enterprise:dashboard --piwik-domain=http://test$I.piwik.pro
+
+ echo "Running archiving for: test$I.piwik.pro... with --force-idsites"
+ echo ""
+ echo ""
+ ./console enterprise:archive --piwik-domain=http://test$I.piwik.pro --force-idsites=1
+ echo ""
+ echo ""
+
+done
+
+
+
diff --git a/cloud-config.sh b/cloud-config.sh
new file mode 100644
index 0000000000..56deaef82c
--- /dev/null
+++ b/cloud-config.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+END=5
diff --git a/cloud-generate.sh b/cloud-generate.sh
new file mode 100644
index 0000000000..df160bdc27
--- /dev/null
+++ b/cloud-generate.sh
@@ -0,0 +1,17 @@
+
+source cloud-config.sh
+
+for I in $(seq 1 $END); do
+ for ((n=1;n<$(( ( RANDOM % 5 ) + 1 ));n++));
+ do
+ echo "Generating traffic for Piwik instance $I and website id = $n..."
+ echo ""
+ CMD="./console visitorgenerator:generate-visits --idsite=$n --limit-fake-visits=$(( ( RANDOM % 50 ) + 1 )) --no-logs --piwik-domain=http://test$I.piwik.pro"
+ echo $CMD
+ $CMD
+ echo ""
+
+ done
+
+done
+
diff --git a/cloud-setup.sh b/cloud-setup.sh
new file mode 100644
index 0000000000..beebfd35b6
--- /dev/null
+++ b/cloud-setup.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+set -o xtrace
+source cloud-config.sh
+
+rm config/common.config.ini.php
+rm config/test*.piwik.pro.config.ini.php
+
+cat plugins/EnterpriseAdmin/ConfigTemplate/common.config.ini.php | sed "s/force_ssl = 1/force_ssl = 0/" > config/common.config.ini.php
+cat plugins/{LoginAdmin,WhitelistAdmin}/ConfigTemplate/common.config.ini.php >> config/common.config.ini.php
+
+
+for I in $(seq 1 $END); do
+ mysql -uroot -e "DROP DATABASE dbname$I;"
+
+ sudo chmod 777 -R tmp/test$I.piwik.pro/
+
+ # test no db prefix
+ ./console enterprise:install \
+ --db-host="localhost" --db-login="root" --db-password="" --db-name="dbname$I" --db-adapter="PDO\\MYSQL" \
+ --website-name="Main Shop $I" --website-url="http://shop$I.enterprise.com" --website-timezone="Africa/Accra" --website-ecommerce \
+ --superuser-login="admin" --superuser-password="secure" --superuser-email="admin@enterpriseanalytics.com" \
+ --piwik-domain=http://test$I.piwik.pro \
+ --skip-writable-check --skip-geoip-check --skip-https-check --skip-http-check #--db-dropexistingtables
+
+ echo "Generating websites, users and goals for cloud instance = $I..."
+ ./console enterprise:plugin activate VisitorGenerator --piwik-domain=http://test$I.piwik.pro
+
+ echo "Generating websites for cloud instance ID = $I..."
+ ./console visitorgenerator:generate-websites --piwik-domain=test$I.piwik.pro --limit=$(( ( RANDOM % 5 ) + 10 ))
+
+ echo "Generating users for cloud instance ID = $I..."
+ ./console visitorgenerator:generate-users --piwik-domain=test$I.piwik.pro --limit=5
+
+ echo "Generating goals for cloud instance ID = $I..."
+ for ((n=1;n<5;n++)); do (./console visitorgenerator:generate-goals --idsite=$n --piwik-domain=test$I.piwik.pro); done
+
+done
+
+chmod 755 config/*.config.ini.php
+
+echo "Setting master instance to test1.piwik.pro..."
+./console enterprise:setmasterinstance --piwik-domain=test1.piwik.pro -vvv
+
diff --git a/config/common.config.ini.php.not b/config/common.config.ini.php.not
new file mode 100644
index 0000000000..e8b2775483
--- /dev/null
+++ b/config/common.config.ini.php.not
@@ -0,0 +1,62 @@
+; <?php exit; ?> DO NOT REMOVE THIS LINE
+; Configuration settings which are applied to all Piwik instances.
+; This file here is not read by Piwik: the EnterpriseAdmin installer will ask to copy it to config/common.config.ini.php
+;
+; The config files are read in this order:
+; 1) global.ini.php
+; 2) common.config.ini.php
+; 3) $instanceHostname.config.ini.php
+;
+; Settings found here will apply to all instances, unless they are overriden in the customer config file
+
+[General]
+; Disable Super User features or features requiring internet access
+enable_marketplace = 0
+enable_plugins_admin = 0
+enable_geolocation_admin = 0
+enable_delete_old_data_settings_admin = 0
+enable_general_settings_admin = 0
+enable_auto_update = 0
+enable_update_communication = 0
+
+; Disable other features that are enabled in global.ini.php
+enable_load_data_infile = 0
+
+; Bypass the Custom Logo "is writable" check as we are writing the logo file to another backend
+enable_custom_logo_check = 0
+
+; Default timeout for archives (overrides the default 10 seconds)
+time_before_today_archive_considered_outdated = 60
+
+; Disable browser trigger archiving for all users by default
+enable_browser_archiving_triggering = 0
+
+; Uncomment to make sure that API requests with a &segment= parameter will not trigger archiving
+; browser_archiving_disabled_enforce = 1
+
+; Uncomment to make sure that all new Custom Segments created in the future
+; will be set to "Pre-processed (faster, requires cron core:archive command)"
+; enable_create_realtime_segments = 0
+
+; Email addresses
+noreply_email_address = "robot@piwik.pro"
+login_password_recovery_email_address = "robot@piwik.pro"
+login_password_recovery_email_name = "Password recovery (Piwik PRO)"
+login_password_recovery_replyto_email_address = "noreply@{DOMAIN}"
+login_password_recovery_replyto_email_name = "Password recovery (Piwik PRO)"
+
+feedback_email_address = contact@piwik.pro
+scheduled_reports_replyto_is_user_email_and_alias = 1
+
+; Enable Database storage of sessions
+session_save_handler = dbtable
+
+; Force SSL everywhere
+force_ssl = 1
+
+; Disable OPTIMIZE queries
+enable_sql_optimize_queries = 0
+
+process_new_segments_from = "segment_creation_time"
+
+
diff --git a/info.php b/info.php
new file mode 100644
index 0000000000..c4837a32b6
--- /dev/null
+++ b/info.php
@@ -0,0 +1 @@
+<?php phpinfo();
diff --git a/misc/user/example.com/logo-header.png b/misc/user/example.com/logo-header.png
new file mode 100644
index 0000000000..0c3bd89f8a
--- /dev/null
+++ b/misc/user/example.com/logo-header.png
Binary files differ
diff --git a/misc/user/example.com/logo.png b/misc/user/example.com/logo.png
new file mode 100644
index 0000000000..58a38ad82d
--- /dev/null
+++ b/misc/user/example.com/logo.png
Binary files differ
diff --git a/plugins/AdvancedCampaignReporting b/plugins/AdvancedCampaignReporting
new file mode 160000
+Subproject 13a6ddea85f7398bcfdf7ded16bd6187f72f651
diff --git a/plugins/ApiGetWithSitesInfo b/plugins/ApiGetWithSitesInfo
new file mode 160000
+Subproject c77f273ceedf8e93f5cd537c880faf1a6f3b37a
diff --git a/plugins/AutoLogImporter/.gitignore b/plugins/AutoLogImporter/.gitignore
new file mode 100644
index 0000000000..c8c9480010
--- /dev/null
+++ b/plugins/AutoLogImporter/.gitignore
@@ -0,0 +1 @@
+tests/System/processed/*xml \ No newline at end of file
diff --git a/plugins/AutoLogImporter/.travis.yml b/plugins/AutoLogImporter/.travis.yml
new file mode 100644
index 0000000000..1fc816a0d0
--- /dev/null
+++ b/plugins/AutoLogImporter/.travis.yml
@@ -0,0 +1,132 @@
+# do not edit this file manually, instead run the generate:travis-yml console command
+
+language: php
+
+php:
+ - 5.6
+ - 5.3.3
+# - hhvm
+
+services:
+ - redis-server
+
+# Separate different test suites
+env:
+ global:
+ - PLUGIN_NAME=AutoLogImporter
+ - PIWIK_ROOT_DIR=$TRAVIS_BUILD_DIR/piwik
+ - PIWIK_LATEST_STABLE_TEST_TARGET=2.15.0-b4
+ - secure: "uSQcAHxwIABymCg+L6ZzEasENSAIuRGXlpnhr7Rt+GrLLtSYnviBSsgGYuafLMs6AcnA7OQEv1e97N20B2JKPfG9TArpwFxGG6tqipZlV58bdd/HZ2MmJIzSf8bnkvvCOxNdzKR4myL07v9nCE9qvJef5lx9JHX+MH6pZzPX9QZmei4DoR+UweyW86AGpNVwNhRcCXzw6GQ/32n8kpj3fUEr7aZ6jRLtxxQMxTnSJq1Yr8hI7TgprzHq5Da36zVxzjxNPJdgUKreROqjuqA4zgB4QgYSNXNUbe4cto8HPFteRxyw5OqWHTAWV/wrK7yo1Vq4kU8jrUKjbzVPudR21NY4teicra2qANtyPVrqTXRkeUwcBEEeWYByEq84gz/bsk9luiwAKZHH4/26zT8RGoLVKRWI16cuBN8WQxvrvZBgoiJ9n2gxerfeKqZizzzY+67RrzYwjDTtYyAwXkp8H4TELKZO/oEimrEy2oEVu4R7UwijwugHgg8d65S1B/UaWtdUHFIhcMP/FjSwdNRikk24a/koTnqVKh0LWBHxPnmV9lLW6CVgSVN2xiVPQe4RWggnNcVlhkLTNkJzpN5ISYALN20mmaKq/PpdOdPbhsRWXObmCFhv89Ot54HaHixUB99B60I0zvwxX9j2C1nHUxWXzwxkePBLAZxNRBeJR2U="
+ - secure: "VYNfBVkaKl2AbmmW6jAp6UVFQA1z0acKyPrP7IQN2eFGXVn/mCgNBKwpVcNxVbrGre02MQhRI/8TCsPdZDCEa74mobozZ0ef87qF/eVgRQQjgxcVZ69QBL32K2VBg/RCwuC/WlnaFBTOn/ElkpJNYRJNUjWTLtw3lgIdi6zN4ZfBfvELuczi//n++KYH75MYnF//yX1+BD/4BQyC+yYLvc+NcdErTxQUS81C3nlp/VqKpqA0OQ+aNqtKb+MSNrHDH9K286oha8R2fJBR3YZvjCzrK8I2/+iF26lnnLaOfalMJjrdJXVV1w/OGps1hLYev5FMrNmBjgbKZP5E7SOJlfLbvbHtDI140ZXZbBLD4anMO0Uvc0cCfnk8QAu9kUvVq+nBszmBmArEvU4LTuvPqxJVU92kw8WYOA+AoSrT34mTq58rpo1sECQxtx8ivCKLRA6SMYlE7+m0quwhn/CnAeBfsOViEYhK6nkp/N/tyk39rgQ+F+JVm/AtVm/AVuKWlzxbYkI3m7eQ6oB5/BD4l1rI4ZWkYCyHAQkUAHCGsX8c2mQGOWJIOdz7Tm+Is+vFaGA6mS0JwxqXN8USs/3iUHh0VYAO1jp16QZ2/1IeO9hzx9Bj1oH/q7/JXpVeyuWMevqyQ9bWbYQvM1eOLXPIMyDgFOj9yL43ebe9leU3lQ0="
+ matrix:
+ - TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_LATEST_STABLE_TEST_TARGET
+ - TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=minimum_required_piwik
+ - TEST_SUITE=UITests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_LATEST_STABLE_TEST_TARGET
+
+matrix:
+ exclude:
+ # execute latest stable tests only w/ PHP 5.5
+ - php: 5.3.3
+ env: TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=minimum_required_piwik
+ # execute UI tests only w/ PHP 5.6
+ - php: 5.3.3
+ env: TEST_SUITE=UITests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_LATEST_STABLE_TEST_TARGET
+
+sudo: required
+
+script: $PIWIK_ROOT_DIR/tests/travis/travis.sh
+
+before_install:
+ # do not use the Zend allocator on PHP 5.3 since it will randomly segfault after program execution
+ - '[[ "$TRAVIS_PHP_VERSION" == 5.3* ]] && export USE_ZEND_ALLOC=0 || true'
+
+install:
+ # move all contents of current repo (which contains the plugin) to a new directory
+ - mkdir $PLUGIN_NAME
+ - cp -R !($PLUGIN_NAME) $PLUGIN_NAME
+ - cp -R .git/ $PLUGIN_NAME/
+ - cp .travis.yml $PLUGIN_NAME
+ # checkout piwik in the current directory
+ - git clone -q https://github.com/piwik/piwik.git piwik
+ - cd piwik
+ - git fetch -q --all
+ - git submodule update
+
+ # make sure travis-scripts repo is latest for initial travis setup
+ - '[ -d ./tests/travis/.git ] || sh -c "rm -rf ./tests/travis && git clone https://github.com/piwik/travis-scripts.git ./tests/travis"'
+ - cd ./tests/travis ; git checkout master ; cd ../..
+
+ - export GENERATE_TRAVIS_YML_COMMAND="php ./tests/travis/generator/main.php generate:travis-yml --plugin=\"AutoLogImporter\" --verbose"
+ - '[[ "$TRAVIS_JOB_NUMBER" != *.1 || "$TRAVIS_PULL_REQUEST" != "false" ]] || ./tests/travis/autoupdate_travis_yml.sh'
+
+ - ./tests/travis/checkout_test_against_branch.sh
+
+ - '[ ! -f ./tests/travis/install_mysql_5.6.sh ] || ./tests/travis/install_mysql_5.6.sh'
+
+ # Make sure we use Python 2.6
+ - '[ ! -f ./tests/travis/install_python_2.6.sh ] || ./tests/travis/install_python_2.6.sh'
+
+ - ./tests/travis/configure_git.sh
+
+ # travis now complains about this failing 9 times out of 10, so removing it
+ #- travis_retry composer self-update
+
+ - '[ "$SKIP_COMPOSER_INSTALL" == "1" ] || travis_retry composer install'
+
+ # move plugin contents to folder in the plugins subdirectory
+ - rm -rf plugins/$PLUGIN_NAME
+ - mv ../$PLUGIN_NAME plugins
+
+ # clone dependent repos
+ - ./tests/travis/checkout_dependent_plugins.sh
+
+before_script:
+ - phpenv config-rm xdebug.ini;
+
+ # add always_populate_raw_post_data=-1 to php.ini
+ - echo "always_populate_raw_post_data=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+
+ # disable opcache to avoid random failures on travis
+ - echo "opcache.enable=0" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+
+ # print out mysql information
+ - mysql --version
+ - mysql -e "SELECT VERSION();"
+
+ # configure mysql
+ - mysql -e "SET GLOBAL sql_mode = 'NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES'" # Travis default
+ # try to avoid mysql has gone away errors
+ - mysql -e "SET GLOBAL wait_timeout = 36000;"
+ - mysql -e "SET GLOBAL max_allowed_packet = 134209536;"
+ - mysql -e "SHOW VARIABLES LIKE 'max_allowed_packet';"
+ - mysql -e "SHOW VARIABLES LIKE 'wait_timeout';"
+
+ - mysql -e "SELECT @@sql_mode;"
+ # - mysql -e "SHOW GLOBAL VARIABLES;"
+
+ # print out more debugging info
+ - uname -a
+ - date
+ - php -r "var_dump(gd_info());"
+ - mysql -e 'create database piwik_tests;'
+
+ - ./tests/travis/prepare.sh
+ - ./tests/travis/setup_webserver.sh
+
+ - cd tests/PHPUnit
+
+after_script:
+ # change directory back to root travis dir
+ - cd $PIWIK_ROOT_DIR
+
+ # output contents of files w/ debugging info to screen
+ - cat /var/log/nginx/error.log
+ - cat $PIWIK_ROOT_DIR/tmp/php-fpm.log
+ - cat $PIWIK_ROOT_DIR/tmp/logs/piwik.log
+ - cat $PIWIK_ROOT_DIR/config/config.ini.php
+
+ # upload test artifacts (for debugging travis failures)
+ - ./tests/travis/upload_artifacts.sh
+
+after_success:
+ - cd $PIWIK_ROOT_DIR
diff --git a/plugins/AutoLogImporter/API.php b/plugins/AutoLogImporter/API.php
new file mode 100644
index 0000000000..fa3e09cc55
--- /dev/null
+++ b/plugins/AutoLogImporter/API.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+
+use Piwik\Container\StaticContainer;
+use Piwik\DataTable;
+use Piwik\DataTable\Row;
+use Piwik\Piwik;
+
+/**
+ * API for plugin AutoLogImporter
+ *
+ * @method static \Piwik\Plugins\AutoLogImporter\API getInstance()
+ */
+class API extends \Piwik\Plugin\API
+{
+ /**
+ * Get all imported files no matter if the import was successful or not.
+ *
+ * Latest imported files are listed first.
+ * @return array
+ */
+ public function getAllImportedFiles()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $dao = new Dao();
+ $files = $dao->getAllImportedFiles();
+
+ usort($files, function ($a, $b){
+ return $a['idlogimport'] > $b['idlogimport'] ? -1 : 1;
+ });
+
+ return $files;
+ }
+
+ /**
+ * Get all files that are currently in process importing logs.
+ *
+ * Latest importing files are listed first.
+ * @return array
+ */
+ public function getAllImportingFiles()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $dao = new Dao();
+ $files = $dao->getAllImportingFiles();
+
+ usort($files, function ($a, $b){
+ return $a['idlogimport'] > $b['idlogimport'] ? -1 : 1;
+ });
+
+ return $files;
+ }
+
+ /**
+ * Get a list of files that will be imported as soon as the task runs the next time.
+ *
+ * @return string[]
+ */
+ public function getFilesThatCanBeImported()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $importer = StaticContainer::get('Piwik\Plugins\AutoLogImporter\LogImporter');
+ return $importer->getFilesThatCanBeImported();
+ }
+
+
+ /**
+ * Get a list of files that have a hash file but the verify hash doesn't match. This usually indicates a not
+ * sucessfully copied log file.
+ *
+ * @return array
+ */
+ public function getFilesHavingInvalidHash()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $logFileList = StaticContainer::get('Piwik\Plugins\AutoLogImporter\LogImporter\LogFileList');
+ return $logFileList->findFilesHavingWrongHashFile();
+ }
+
+}
diff --git a/plugins/AutoLogImporter/AutoLogImporter.php b/plugins/AutoLogImporter/AutoLogImporter.php
new file mode 100644
index 0000000000..4c7261b95b
--- /dev/null
+++ b/plugins/AutoLogImporter/AutoLogImporter.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+
+class AutoLogImporter extends \Piwik\Plugin
+{
+ public function getListHooksRegistered()
+ {
+ return array(
+ 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
+ );
+ }
+
+ public function getStylesheetFiles(&$stylesheets)
+ {
+ $stylesheets[] = "plugins/AutoLogImporter/stylesheets/status.less";
+ }
+
+ public function install()
+ {
+ $dao = new Dao();
+ $dao->install();
+ }
+
+ public function uninstall()
+ {
+ $dao = new Dao();
+ $dao->uninstall();
+ }
+}
diff --git a/plugins/AutoLogImporter/Controller.php b/plugins/AutoLogImporter/Controller.php
new file mode 100644
index 0000000000..1d6c5d0a95
--- /dev/null
+++ b/plugins/AutoLogImporter/Controller.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+use Piwik\API\Request;
+use Piwik\Common;
+use Piwik\Piwik;
+
+/**
+ * A controller let's you for example create a page that can be added to a menu. For more information read our guide
+ * http://developer.piwik.org/guides/mvc-in-piwik or have a look at the our API references for controller and view:
+ * http://developer.piwik.org/api-reference/Piwik/Plugin/Controller and
+ * http://developer.piwik.org/api-reference/Piwik/View
+ */
+class Controller extends \Piwik\Plugin\ControllerAdmin
+{
+ public function getDefaultAction()
+ {
+ return 'status';
+ }
+
+ public function status()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $limit = Common::getRequestVar('limit', 200, 'int');
+
+ $importingFiles = Request::processRequest('AutoLogImporter.getAllImportingFiles');
+ $importingSoon = Request::processRequest('AutoLogImporter.getFilesThatCanBeImported');
+ $filesWithInvalidHashes = Request::processRequest('AutoLogImporter.getFilesHavingInvalidHash');
+ $importedFiles = Request::processRequest('AutoLogImporter.getAllImportedFiles', array(
+ 'filter_limit' => $limit
+ ));
+
+ if (count($importedFiles) < $limit) {
+ $limit = count($importedFiles);
+ }
+
+ $formatter = new Formatter();
+
+ return $this->renderTemplate('status', array(
+ 'importingFiles' => $formatter->addTimeAgoAsSentence($formatter->formatImportedFiles($importingFiles)),
+ 'importedFiles' => $formatter->formatImportedFiles($importedFiles),
+ 'scheduledFiles' => $importingSoon,
+ 'filesWithInvalidHashes' => $filesWithInvalidHashes,
+ 'limit' => $limit
+ ));
+ }
+}
diff --git a/plugins/AutoLogImporter/Dao.php b/plugins/AutoLogImporter/Dao.php
new file mode 100644
index 0000000000..ec4412fc41
--- /dev/null
+++ b/plugins/AutoLogImporter/Dao.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+
+use Piwik\Common;
+use Piwik\Date;
+use Piwik\Db;
+use Piwik\DbHelper;
+
+class Dao
+{
+ /**
+ * @var Db
+ */
+ private $db;
+
+ private $tableName = 'auto_log_importer';
+ private $tableNamePrefixed;
+
+ public function __construct()
+ {
+ $this->db = Db::get();
+ $this->tableNamePrefixed = Common::prefixTable($this->tableName);
+ }
+
+ public function getAllImportedFiles()
+ {
+ return Db::fetchAll('SELECT * FROM ' . $this->tableNamePrefixed . ' WHERE end_date is not null');
+ }
+
+ public function getAllImportingFiles()
+ {
+ return Db::fetchAll('SELECT * FROM ' . $this->tableNamePrefixed . ' WHERE end_date is null');
+ }
+
+ public function isAlreadyImported($fileHash)
+ {
+ return (bool) $this->db->fetchOne("SELECT 1 FROM " . $this->tableNamePrefixed . "
+ WHERE `file_hash` = ? LIMIT 1", array($fileHash));
+ }
+
+ public function startLogImport($logFile, $fileHash, $fileSize, $numLogLines, $startDateTime)
+ {
+ $this->db->insert($this->tableNamePrefixed, array(
+ 'file' => $logFile,
+ 'file_hash' => $fileHash,
+ 'file_size' => $fileSize,
+ 'num_log_lines' => $numLogLines,
+ 'start_date' => $startDateTime,
+ ));
+
+ return (int) $this->db->lastInsertId();
+ }
+
+ public function setImportFinished($id, $command, $output, $exitCode, $duration, $endDateTime)
+ {
+ // we cannot store unlimited characters in TEXT field
+ if ($posSummary = strpos($output, 'Logs import summary')) {
+ // if it contains a summary, only store the actual summary
+ $output = substr($output, strpos($output, 'Logs import summary'));
+ } elseif (strlen($output) > 10000) {
+ // only store last 10k characters
+ $output = substr($output, strlen($output) - 10000);
+ }
+
+ $this->db->update($this->tableNamePrefixed, array(
+ 'output' => $output,
+ 'command' => $command,
+ 'exit_code' => $exitCode,
+ 'end_date' => $endDateTime,
+ 'duration' => $duration,
+ ), 'idlogimport = ' . (int) $id);
+ }
+
+ public function install()
+ {
+ $table = "`idlogimport` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
+ `start_date` datetime NOT NULL ,
+ `end_date` datetime NULL ,
+ `duration` MEDIUMINT UNSIGNED NULL ,
+ `file` TEXT NOT NULL ,
+ `file_size` INT UNSIGNED NOT NULL DEFAULT 0,
+ `file_hash` VARCHAR(32) NOT NULL ,
+ `num_log_lines` INT UNSIGNED NOT NULL DEFAULT 0,
+ `command` TEXT NULL,
+ `output` TEXT NULL,
+ `exit_code` TINYINT UNSIGNED NULL,
+ UNIQUE KEY uniq_hash(file_hash)";
+
+ DbHelper::createTable($this->tableName, $table);
+ }
+
+ public function uninstall()
+ {
+ Db::dropTables(array($this->tableNamePrefixed));
+ }
+
+}
diff --git a/plugins/AutoLogImporter/Formatter.php b/plugins/AutoLogImporter/Formatter.php
new file mode 100644
index 0000000000..0872c7a275
--- /dev/null
+++ b/plugins/AutoLogImporter/Formatter.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Date;
+use Piwik\Metrics\Formatter as MetricsFormatter;
+
+class Formatter
+{
+ public function addTimeAgoAsSentence($files)
+ {
+ $formatter = new MetricsFormatter();
+ $now = StaticContainer::get('AutoLogImporter.currentTimestamp');
+
+ foreach ($files as &$file) {
+ $diff = $now - Date::factory($file['start_date'])->getTimestamp();
+ $file['time_ago'] = $formatter->getPrettyTimeFromSeconds($diff, true);
+ }
+
+ return $files;
+ }
+
+ public function formatImportedFiles($files)
+ {
+ $formatter = new MetricsFormatter();
+
+ foreach ($files as &$file) {
+ $file['filename'] = basename($file['file']);
+ $file['date'] = substr($file['start_date'], 0, 10);
+ $file['size_human'] = $formatter->getPrettySizeFromBytes((int) $file['file_size']);
+ }
+
+ return $files;
+ }
+}
diff --git a/plugins/AutoLogImporter/LogImporter.php b/plugins/AutoLogImporter/LogImporter.php
new file mode 100644
index 0000000000..594eecd746
--- /dev/null
+++ b/plugins/AutoLogImporter/LogImporter.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+
+use Piwik\Date;
+use Piwik\Plugins\AutoLogImporter\LogImporter\Import;
+use Piwik\Plugins\AutoLogImporter\LogImporter\LogFileList;
+use Piwik\Plugins\AutoLogImporter\LogImporter\File;
+
+class LogImporter
+{
+ /**
+ * @var Dao
+ */
+ private $dao;
+
+ /**
+ * @var LogFileList
+ */
+ private $logFileList;
+
+ /**
+ * @var File
+ */
+ private $file;
+
+ /**
+ * @var Import
+ */
+ private $import;
+
+ public function __construct(Dao $dao, LogFileList $logFileList, File $file, Import $import)
+ {
+ $this->dao = $dao;
+ $this->logFileList = $logFileList;
+ $this->file = $file;
+ $this->import = $import;
+ }
+
+ public function getFilesThatCanBeImported()
+ {
+ $files = array();
+
+ foreach ($this->logFileList->findLogFiles() as $logFile) {
+ $hash = $this->file->getHash($logFile);
+
+ if (!$this->dao->isAlreadyImported($hash)
+ && $this->file->wasFileSuccessfullyCopiedViaFileSynchronizerPlugin($logFile, $hash)) {
+ $files[$hash] = $logFile;
+ }
+ }
+
+ return $files;
+ }
+
+ public function importFiles()
+ {
+ $files = $this->getFilesThatCanBeImported();
+
+ foreach ($files as $hash => $logFile) {
+ $this->importFile($logFile, $hash);
+ }
+ }
+
+ private function importFile($logFile, $hash)
+ {
+ $size = $this->file->getSize($logFile);
+ $lines = $this->file->getNumberOfLines($logFile);
+ $id = $this->dao->startLogImport($logFile, $hash, $size, $lines, $this->getCurrentDateTime());
+
+ $start = time();
+ $result = $this->import->import($logFile);
+ $duration = time() - $start;
+
+ $this->dao->setImportFinished($id, $result->getCommand(), $result->getOutput(), $result->getExitCode(), $duration, $this->getCurrentDateTime());
+ }
+
+ private function getCurrentDateTime()
+ {
+ return Date::now()->getDatetime();
+ }
+}
diff --git a/plugins/AutoLogImporter/LogImporter/File.php b/plugins/AutoLogImporter/LogImporter/File.php
new file mode 100644
index 0000000000..e43c3564a7
--- /dev/null
+++ b/plugins/AutoLogImporter/LogImporter/File.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter\LogImporter;
+
+class File
+{
+ public function getNumberOfLines($file)
+ {
+ if (!file_exists($file) || !is_readable($file)) {
+ return 0;
+ }
+
+ $numLines = 0;
+ $fileHandle = fopen($file, 'rb');
+
+ while (!feof($fileHandle)) {
+ $numLines += substr_count(fread($fileHandle, 8192), "\n");
+ }
+
+ // +1 for fist line
+ $numLines++;
+
+ fclose($fileHandle);
+
+ return $numLines;
+ }
+
+ public function getSize($file)
+ {
+ return filesize($file);
+ }
+
+ public function getHash($file)
+ {
+ return md5_file($file);
+ }
+
+ public function getPathToVerifyHashFile($file)
+ {
+ return $file . '.hash';
+ }
+
+ public function getVerifyHash($file)
+ {
+ if (!$this->hasVerifyHash($file)) {
+ return;
+ }
+
+ $hash = file_get_contents($this->getPathToVerifyHashFile($file));
+
+ if (!empty($hash)) {
+ return trim($hash);
+ }
+ }
+
+ public function hasVerifyHash($file)
+ {
+ return file_exists($this->getPathToVerifyHashFile($file));
+ }
+
+ public function wasFileSuccessfullyCopiedViaFileSynchronizerPlugin($logFile, $hash)
+ {
+ $verifyHash = $this->getVerifyHash($logFile);
+
+ return !empty($verifyHash) && $verifyHash === trim($hash);
+ }
+}
diff --git a/plugins/AutoLogImporter/LogImporter/Import.php b/plugins/AutoLogImporter/LogImporter/Import.php
new file mode 100644
index 0000000000..c1a67acd0f
--- /dev/null
+++ b/plugins/AutoLogImporter/LogImporter/Import.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter\LogImporter;
+
+use Piwik\Container\StaticContainer;
+use Piwik\SettingsPiwik;
+use Psr\Log\LoggerInterface;
+
+class Import
+{
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ public function __construct(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ }
+
+ public function import($logFile)
+ {
+ $options = StaticContainer::get('AutoLogImporter.logImportOptions');
+ $options[] = '--url=' . escapeshellarg(SettingsPiwik::getPiwikUrl());
+ $options[] = '--replay-tracking';
+ $options[] = escapeshellarg($logFile);
+
+ if (preg_match('/_idsite_(\d+)/', $logFile, $matches)) {
+ $options[] = '--idsite=' . (int) $matches[1];
+ }
+
+ $command = PIWIK_INCLUDE_PATH . '/misc/log-analytics/import_logs.py';
+ foreach ($options as $option) {
+ $command .= ' ' . $option;
+ }
+
+ $command .= ' 2>&1';
+
+ $this->logger->debug("Executing command '$command' to import '$logFile'");
+
+ try {
+ exec($command, $output, $exitCode);
+ $output = implode("\n", $output);
+ } catch (\Exception $e) {
+ $output = $e->getMessage() . ' Trace: ' . $e->getTraceAsString();
+ $exitCode = 255;
+ }
+
+ $this->logger->debug("Finished command with exit code '$exitCode' and output '$output'");
+
+ return new Result($command, $output, $exitCode);
+ }
+}
diff --git a/plugins/AutoLogImporter/LogImporter/LogFileList.php b/plugins/AutoLogImporter/LogImporter/LogFileList.php
new file mode 100644
index 0000000000..defb51145b
--- /dev/null
+++ b/plugins/AutoLogImporter/LogImporter/LogFileList.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter\LogImporter;
+
+use Piwik\Db;
+use Piwik\Filesystem;
+use Piwik\Plugins\AutoLogImporter\Settings;
+
+class LogFileList
+{
+ /**
+ * @var Settings
+ */
+ private $settings;
+
+ /**
+ * @var File
+ */
+ private $file;
+
+ public function __construct(Settings $settings, File $file)
+ {
+ $this->settings = $settings;
+ $this->file = $file;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function findLogFiles()
+ {
+ $path = $this->settings->logFilesPath->getValue();
+ $pattern = $this->settings->filePattern->getValue();
+
+ if (!$this->settings->enabled->getValue()) {
+ return array();
+ }
+
+ if (empty($path) || !file_exists($path) || !is_readable($path) || !is_dir($path)) {
+ // case when plugin not configured yet
+ return array();
+ }
+
+ if (empty($pattern)) {
+ $pattern = '*';
+ }
+
+ return Filesystem::globr($path, $pattern);
+ }
+
+ public function findFilesHavingWrongHashFile()
+ {
+ $files = $this->findLogFiles();
+
+ $wrong = array();
+ foreach ($files as $file) {
+ $hash = $this->file->getHash($file);
+
+ if ($this->file->hasVerifyHash($file)
+ && !$this->file->wasFileSuccessfullyCopiedViaFileSynchronizerPlugin($file, $hash)) {
+ $wrong[] = array(
+ 'file' => $file,
+ 'hash' => $hash,
+ 'verify_file' => $this->file->getPathToVerifyHashFile($file),
+ 'verify_hash' => $this->file->getVerifyHash($file)
+ );
+ }
+ }
+
+ return $wrong;
+ }
+}
diff --git a/plugins/AutoLogImporter/LogImporter/Result.php b/plugins/AutoLogImporter/LogImporter/Result.php
new file mode 100644
index 0000000000..18333e92b9
--- /dev/null
+++ b/plugins/AutoLogImporter/LogImporter/Result.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter\LogImporter;
+
+use Piwik\Db;
+
+class Result
+{
+ private $command;
+ private $output;
+ private $exitCode;
+
+ public function __construct($command, $output, $exitCode)
+ {
+ $this->command = $command;
+ $this->output = $output;
+ $this->exitCode = $exitCode;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCommand()
+ {
+ return $this->command;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+
+ /**
+ * @return string
+ */
+ public function getExitCode()
+ {
+ return $this->exitCode;
+ }
+}
diff --git a/plugins/AutoLogImporter/Menu.php b/plugins/AutoLogImporter/Menu.php
new file mode 100644
index 0000000000..ae6b5fa3b1
--- /dev/null
+++ b/plugins/AutoLogImporter/Menu.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+
+use Piwik\Menu\MenuAdmin;
+use Piwik\Piwik;
+
+/**
+ * This class allows you to add, remove or rename menu items.
+ * To configure a menu (such as Admin Menu, Top Menu, User Menu...) simply call the corresponding methods as
+ * described in the API-Reference http://developer.piwik.org/api-reference/Piwik/Menu/MenuAbstract
+ */
+class Menu extends \Piwik\Plugin\Menu
+{
+
+ public function configureAdminMenu(MenuAdmin $menu)
+ {
+ if (Piwik::hasUserSuperUserAccess()) {
+ $menu->addDiagnosticItem('Auto Log Import', $this->urlForAction('status', array('limit' => 200)));
+ }
+ }
+
+}
diff --git a/plugins/AutoLogImporter/README.md b/plugins/AutoLogImporter/README.md
new file mode 100644
index 0000000000..45dcc04a06
--- /dev/null
+++ b/plugins/AutoLogImporter/README.md
@@ -0,0 +1,100 @@
+# Piwik AutoLogImporter Plugin
+
+[![Build Status](https://magnum.travis-ci.com/PiwikPRO/plugin-AutoLogImporter.svg?token=YG4RfcyzVryveJy9zhmw&branch=master)](https://magnum.travis-ci.com/PiwikPRO/plugin-AutoLogImporter)
+
+## Description
+
+Automatically imports all log files within a specified directory. It remembers which files of a directory have been
+imported and only imports new files. Important: If a file was changed after an import the file is considered as a
+new file and will be imported again.
+
+Features:
+* Automatically imports files from a specified directory
+* Makes sure to not import the same file twice
+* Gives you detailed output the log import result
+* Shows which files will be imported next
+* Shows which files are currently imported
+* Shows which files have been imported in the past
+* Custom log importer options can be configured to make it work with different log formats etc
+* Imported files won't be deleted after importing but marked as imported in the database
+
+## FAQ
+
+__What are the requirements?__
+
+* You need to have Python installed under `/usr/bin/python` see https://github.com/piwik/piwik-log-analytics.
+* It currently works only out of the box if the log format can be detected automatically (eg Apache). A different log format can be configured in `config.php` see further below.
+* We currently assume `--replay-tracking`, meaning the log file should contain requests to `piwik.php`.
+* The PHP method `exec()` must be enabled.
+* This plugin has not been tested with very large log files.
+* Beside each log file there has to be a `.log.hash` file containing the md5 sum of the log file, otherwise a log file will be skipped. This is to make sure the log file was copied correctly via the `FileSynchronizer` plugin.
+
+__How do I setup this plugin?__
+
+* Install this plugin
+* Configure it via "Administration => Plugin Settings".
+
+**Warning**:
+Make sure to not specify for example the path to the Apache log files directly as we would possibly import the same file
+multiple times. Imagine there's an `access.log` and Apache writes each request into it as they are coming. We would
+import this file every hour (as the task to import log files runs every hour). We cannot detect that we imported this
+file already has the MD5 hash changes every time the file is imported. Unfortunately, the
+[Log importer](https://github.com/piwik/piwik-log-analytics) cannot detect which lines of a file were already imported.
+
+The easiest way to setup this plugin is to install the plugin `FileSynchronizer` as well. Specify the Apache log files
+directory as source directory and `*-*-*.log` as file pattern. This makes sure to not copy `access.log` but log files
+of the previous day, eg `2015-02-10.log`. Specify any target directory in the `FileSynchronizer`, just make sure to also
+configure this directory for this plugin.
+
+This will make sure only (finished) log files of previous days will be copied and imported, and it makes sure to create
+a `.hash` file for each log file to make sure the log file will be actually imported.
+
+__Why is a ".hash" file needed for each log file?__
+
+Short version: To prevent "race conditions". Long version: Imagine the `FileSynchronizer` copies log files to the
+target directory, possibly from a remote server. At the same time this plugin is starting to import log files. We would
+import a log file that is currently still being copied and possibly even import twice as the MD5 hash would change once
+the file is fully copied. The `.hash` file is created after the file was copied so we can verify whether the file was
+copied successfully.
+
+__How can I access details about imported files?__
+
+Either by accessing the diagnostic status page under "Administration => Auto Log Import" or by having a look at the
+database table `auto_log_importer`.
+
+__Can I configure the log parameters passed to the log importer?__
+
+Yes! Create the file `/config/config.php` in case it does not exist and dfine additional parameters like this:
+
+```
+<?php
+
+return array(
+ 'AutoLogImporter.logImportOptions' => array('--token-auth=FOOBAR', '--enable-static')
+);
+```
+
+__How can I trigger a log import manually?__
+
+You can trigger a file import manually by executing the following command:
+
+`./console core:run-scheduled-tasks "Piwik\Plugins\AutoLogImporter\Tasks.importLogFiles"`
+
+__Can I force a certain log file to be written into a specific file?__
+
+If the filename contains the pattern `_idsite_$idSite` (eg "2015-01-01_idsite_1.log"), the log importer will
+automatically set the option `--idsite=$idSite`.
+
+__I want to see more than the latest 200 imported files, is it possible?__
+
+Yes, there is a `limit` URL parameter that you can change to any number.
+
+
+## Changelog
+
+* 0.1.0 Initial version
+
+## Contact
+To get a license for one of the Enterprise plugins, or to access the latest updates, contact us.
+If you have any suggestion, code review, or feedback please email contact@piwik.pro
+
diff --git a/plugins/AutoLogImporter/Settings.php b/plugins/AutoLogImporter/Settings.php
new file mode 100644
index 0000000000..e6a2d84013
--- /dev/null
+++ b/plugins/AutoLogImporter/Settings.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+
+use Piwik\Common;
+use Piwik\Settings\SystemSetting;
+
+/**
+ * Defines Settings for AutoLogImporter.
+ *
+ * Usage like this:
+ * $settings = new Settings('AutoLogImporter');
+ * $settings->autoRefresh->getValue();
+ * $settings->metric->getValue();
+ */
+class Settings extends \Piwik\Plugin\Settings
+{
+
+ /** @var SystemSetting */
+ public $enabled;
+
+ /** @var SystemSetting */
+ public $logFilesPath;
+
+ /** @var SystemSetting */
+ public $filePattern;
+
+ protected function init()
+ {
+ $this->createEnabledSetting();
+ $this->createLogFilesPathSetting();
+ $this->createFilePatternSetting();
+ }
+
+ private function createEnabledSetting()
+ {
+ $this->enabled = new SystemSetting('enabled', 'Enabled');
+ $this->enabled->type = static::TYPE_BOOL;
+ $this->enabled->uiControlType = static::CONTROL_CHECKBOX;
+ $this->enabled->defaultValue = false;
+
+ $this->addSetting($this->enabled);
+ }
+
+ private function createLogFilesPathSetting()
+ {
+ $this->logFilesPath = new SystemSetting('logFilesPath', 'Path to log files');
+ $this->logFilesPath->type = static::TYPE_STRING;
+ $this->logFilesPath->uiControlType = static::CONTROL_TEXT;
+
+ $self = $this;
+ $this->logFilesPath->validate = function ($value, $setting) use ($self) {
+ if (empty($value) && !$self->enabled->getValue()) {
+ // it is not enabled, an empty value is okay.
+ return;
+ }
+ if (empty($value)) {
+ throw new \Exception('A value must be specified');
+ }
+ if (!file_exists($value)) {
+ throw new \Exception('This path does not exist');
+ }
+ if (!is_dir($value)) {
+ throw new \Exception('Path is not a directory');
+ }
+ if (!is_readable($value)) {
+ throw new \Exception('This directory is not readable');
+ }
+ };
+ $this->logFilesPath->transform = function ($value) {
+ if (!empty($value) && Common::stringEndsWith($value, '/')) {
+ $value = substr($value, 0, -1);
+ }
+
+ return $value;
+ };
+ $this->logFilesPath->description = 'Defines the path to the log files.';
+
+ $this->addSetting($this->logFilesPath);
+ }
+
+ private function createFilePatternSetting()
+ {
+ $this->filePattern = new SystemSetting('filePattern', 'File Pattern');
+ $this->filePattern->type = static::TYPE_STRING;
+ $this->filePattern->uiControlType = static::CONTROL_TEXT;
+ $this->filePattern->defaultValue = '*.log';
+ $this->filePattern->description = 'If specified, only files matching this pattern will be imported.';
+ $this->filePattern->transform = function ($value) {
+ if (!empty($value)) {
+ return trim($value);
+ }
+
+ return '*';
+ };
+
+ $this->addSetting($this->filePattern);
+ }
+
+}
diff --git a/plugins/AutoLogImporter/Tasks.php b/plugins/AutoLogImporter/Tasks.php
new file mode 100644
index 0000000000..fd8c0cdd66
--- /dev/null
+++ b/plugins/AutoLogImporter/Tasks.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\AutoLogImporter;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Settings\Setting;
+
+class Tasks extends \Piwik\Plugin\Tasks
+{
+ public function schedule()
+ {
+ $this->hourly('importLogFiles');
+ }
+
+ public function importLogFiles()
+ {
+ $settings = StaticContainer::get('Piwik\Plugins\AutoLogImporter\Settings');
+
+ if (!$settings->enabled->getValue()) {
+ return;
+ }
+
+ // we validate to make sure directory is still readable etc, if not an exception will be thrown
+ $this->validateSetting($settings->logFilesPath);
+
+ $importer = StaticContainer::get('Piwik\Plugins\AutoLogImporter\LogImporter');
+ $importer->importFiles();
+ }
+
+ private function validateSetting(Setting $setting)
+ {
+ if (isset($setting->validate)) {
+ call_user_func($setting->validate, $setting->getValue(), $setting);
+ }
+ }
+}
diff --git a/plugins/AutoLogImporter/config/config.php b/plugins/AutoLogImporter/config/config.php
new file mode 100644
index 0000000000..08340534d2
--- /dev/null
+++ b/plugins/AutoLogImporter/config/config.php
@@ -0,0 +1,10 @@
+<?php
+
+return array(
+
+ 'AutoLogImporter.logImportOptions' => array(),
+
+ 'AutoLogImporter.currentTimestamp' => DI\factory(function () {
+ return \Piwik\Date::now()->getTimestamp();
+ }),
+);
diff --git a/plugins/AutoLogImporter/config/test.php b/plugins/AutoLogImporter/config/test.php
new file mode 100644
index 0000000000..5d479ddac8
--- /dev/null
+++ b/plugins/AutoLogImporter/config/test.php
@@ -0,0 +1,26 @@
+<?php
+
+return array(
+
+ 'Piwik\Plugins\AutoLogImporter\Settings' => DI\decorate(function (\Piwik\Plugins\AutoLogImporter\Settings $settings) {
+ if ($settings->enabled->isWritableByCurrentUser()) {
+ $path = PIWIK_INCLUDE_PATH;
+ if (\Piwik\Common::stringEndsWith($path, '/')) {
+ $path = substr($path, 0, -1);
+ }
+ $settings->enabled->setValue(true);
+ $settings->logFilesPath->setValue($path . '/plugins/AutoLogImporter/tests/resources');
+
+ if (!empty($_GET['no_autologimporter_match'])) {
+ $settings->filePattern->setValue('*.nothing');
+ } else {
+ $settings->filePattern->setValue('*.log');
+ }
+ }
+
+ return $settings;
+ }),
+
+ 'AutoLogImporter.logImportOptions' => array('--token-auth=9ad1de7f8b329ab919d854c556f860c1'),
+ 'AutoLogImporter.currentTimestamp' => '1440592473'
+);
diff --git a/plugins/AutoLogImporter/lang/en.json b/plugins/AutoLogImporter/lang/en.json
new file mode 100644
index 0000000000..2becd8e937
--- /dev/null
+++ b/plugins/AutoLogImporter/lang/en.json
@@ -0,0 +1,26 @@
+{
+ "AutoLogImporter": {
+ "ScheduledFiles": "Log files that will be imported next",
+ "ScheduledFilesDescription": "These files should be imported as soon as the import task runs the next time.",
+ "NoScheduledFiles": "No file is scheduled to be imported.",
+ "ImportingFiles": "Log files currently importing",
+ "NoImportingFiles": "No log file is currently being imported.",
+ "ImportedFiles": "Imported log files",
+ "ImportedFilesDescription": "The following table shows the latest %d imported files.",
+ "NoImportedFile": "No log file has been imported yet.",
+ "ToConfigurePluginGoToSettings": "To configure this plugin go to %s%s%s.",
+ "File": "File",
+ "StartDate": "Start date",
+ "InProcessSince": "In process since",
+ "NumLogLines": "Log lines count",
+ "FileSize": "File size",
+ "Imported": "Imported",
+ "TimeTaken": "Time taken",
+ "ImportDetails": "Exit code: '%s', output: '%s'",
+ "HashFile": "Hash file",
+ "ExitCode": "Exit Code",
+ "FileDetails": "'%s' via '%s'. Hash of file is '%s'",
+ "InvalidFilesDescription": "The following files do not match the expected hash. This usually indicates a not successfully copied log file. These files won't be imported. If a file was already imported either remove the hash file, update the expected hash file with the actual hash or copy the log file again if it was not copied correctly.",
+ "InvalidFileDetails": "'%s' should match hash '%s' in '%s' but matches '%s'."
+ }
+} \ No newline at end of file
diff --git a/plugins/AutoLogImporter/plugin.json b/plugins/AutoLogImporter/plugin.json
new file mode 100644
index 0000000000..e4fe65aa49
--- /dev/null
+++ b/plugins/AutoLogImporter/plugin.json
@@ -0,0 +1,16 @@
+{
+ "name": "AutoLogImporter",
+ "version": "0.1.0",
+ "description": "Imports log files automatically",
+ "theme": false,
+ "require": {
+ "piwik": ">=2.15.0-b4"
+ },
+ "authors": [
+ {
+ "name": "Piwik",
+ "email": "",
+ "homepage": ""
+ }
+ ]
+} \ No newline at end of file
diff --git a/plugins/AutoLogImporter/screenshots/.gitkeep b/plugins/AutoLogImporter/screenshots/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/plugins/AutoLogImporter/screenshots/.gitkeep
diff --git a/plugins/AutoLogImporter/screenshots/Error_If_File_Was_Not_Copied_Successfully.png b/plugins/AutoLogImporter/screenshots/Error_If_File_Was_Not_Copied_Successfully.png
new file mode 100644
index 0000000000..05a1703479
--- /dev/null
+++ b/plugins/AutoLogImporter/screenshots/Error_If_File_Was_Not_Copied_Successfully.png
Binary files differ
diff --git a/plugins/AutoLogImporter/screenshots/Error_Import_Details.png b/plugins/AutoLogImporter/screenshots/Error_Import_Details.png
new file mode 100644
index 0000000000..393814a107
--- /dev/null
+++ b/plugins/AutoLogImporter/screenshots/Error_Import_Details.png
Binary files differ
diff --git a/plugins/AutoLogImporter/screenshots/Plugin_Settings.png b/plugins/AutoLogImporter/screenshots/Plugin_Settings.png
new file mode 100644
index 0000000000..53b6853063
--- /dev/null
+++ b/plugins/AutoLogImporter/screenshots/Plugin_Settings.png
Binary files differ
diff --git a/plugins/AutoLogImporter/screenshots/Status_Initially.png b/plugins/AutoLogImporter/screenshots/Status_Initially.png
new file mode 100644
index 0000000000..6965d640b3
--- /dev/null
+++ b/plugins/AutoLogImporter/screenshots/Status_Initially.png
Binary files differ
diff --git a/plugins/AutoLogImporter/screenshots/Status_Report.png b/plugins/AutoLogImporter/screenshots/Status_Report.png
new file mode 100644
index 0000000000..50808568ab
--- /dev/null
+++ b/plugins/AutoLogImporter/screenshots/Status_Report.png
Binary files differ
diff --git a/plugins/AutoLogImporter/screenshots/Successful_Import_Details.png b/plugins/AutoLogImporter/screenshots/Successful_Import_Details.png
new file mode 100644
index 0000000000..6ee327f1ba
--- /dev/null
+++ b/plugins/AutoLogImporter/screenshots/Successful_Import_Details.png
Binary files differ
diff --git a/plugins/AutoLogImporter/stylesheets/status.less b/plugins/AutoLogImporter/stylesheets/status.less
new file mode 100644
index 0000000000..c8a04c3dfe
--- /dev/null
+++ b/plugins/AutoLogImporter/stylesheets/status.less
@@ -0,0 +1,29 @@
+ul.autoLogImportInvalidFiles {
+ list-style: circle;
+ margin-top: 20px;
+}
+
+#listImportingFiles,
+#listImportedFiles {
+ table {
+ max-width: 1000px;
+ min-width: 600px;
+ }
+
+ .date_field {
+ white-space: nowrap
+ }
+
+ .filename {
+ width: 300px;
+ word-break: break-all;
+ }
+
+ .icon-success {
+ color: green;
+ }
+
+ .icon-error {
+ color: #D4291F;
+ }
+} \ No newline at end of file
diff --git a/plugins/AutoLogImporter/templates/status.twig b/plugins/AutoLogImporter/templates/status.twig
new file mode 100644
index 0000000000..0f9698433b
--- /dev/null
+++ b/plugins/AutoLogImporter/templates/status.twig
@@ -0,0 +1,117 @@
+{% extends 'admin.twig' %}
+
+{% block content %}
+
+{% if filesWithInvalidHashes|length %}
+ {% set invalidFiles %}
+ {{ 'AutoLogImporter_InvalidFilesDescription'|translate }}
+
+ <ul class="autoLogImportInvalidFiles">
+ {% for fileWithInvalidHashes in filesWithInvalidHashes %}
+ <li>{{ 'AutoLogImporter_InvalidFileDetails'|translate(fileWithInvalidHashes.file, fileWithInvalidHashes.verify_hash, fileWithInvalidHashes.verify_file, fileWithInvalidHashes.hash) }}</li>
+ {% endfor %}
+ </ul>
+ {% endset %}
+
+ {{ invalidFiles|notification({'noclear': true, 'raw': true, 'context': 'error'}) }}
+{% endif %}
+
+
+<h2>{{ 'AutoLogImporter_ImportingFiles'|translate }}</h2>
+
+<p>{{ 'AutoLogImporter_ToConfigurePluginGoToSettings'|translate('<a href="' ~ linkTo({'module': 'CoreAdminHome', 'action': 'adminPluginSettings'}) ~ '#AutoLogImporter">', ('CoreAdminHome_PluginSettings'|translate), '</a>')|raw }}</p>
+
+<div id="listImportingFiles">
+ <table class="entityTable dataTable">
+ <thead>
+ <tr>
+ <th class="filename">{{ 'AutoLogImporter_File'|translate }}</th>
+ <th>{{ 'AutoLogImporter_StartDate'|translate }}</th>
+ <th>{{ 'AutoLogImporter_InProcessSince'|translate }}</th>
+ <th>{{ 'AutoLogImporter_FileSize'|translate }}</th>
+ <th>{{ 'AutoLogImporter_NumLogLines'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% if importingFiles|length %}
+ {% for importingFile in importingFiles %}
+ <tr>
+ <td class="filename" title="{{ 'AutoLogImporter_FileDetails'|translate(importingFile.file, importingFile.command, importingFile.file_hash) }}">{{ importingFile.filename }}</td>
+ <td class="date_field">{{ importingFile.start_date }}</td>
+ <td>{{ importingFile.time_ago }}</td>
+ <td>{{ importingFile.size_human }}</td>
+ <td>{{ importingFile.num_log_lines }}</td>
+ </tr>
+ {% endfor %}
+ {% else %}
+ <tr><td colspan="5">{{ 'AutoLogImporter_NoImportingFiles'|translate }}</td></tr>
+ {% endif %}
+ </tbody>
+ </table>
+</div>
+
+<h2>{{ 'AutoLogImporter_ScheduledFiles'|translate }}</h2>
+
+<p>{{ 'AutoLogImporter_ScheduledFilesDescription'|translate }}</p>
+
+<div id="listScheduledFiles">
+ <table class="entityTable dataTable">
+ <thead>
+ <tr>
+ <th class="filename">{{ 'AutoLogImporter_File'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% if scheduledFiles|length %}
+ {% for scheduledFile in scheduledFiles %}
+ <tr>
+ <td>{{ scheduledFile }}</td>
+ </tr>
+ {% endfor %}
+ {% else %}
+ <tr><td colspan="5">{{ 'AutoLogImporter_NoScheduledFiles'|translate }}</td></tr>
+ {% endif %}
+ </tbody>
+ </table>
+</div>
+
+
+<h2>{{ 'AutoLogImporter_ImportedFiles'|translate }}</h2>
+<p>{{ 'AutoLogImporter_ImportedFilesDescription'|translate(limit) }}</p>
+<div id="listImportedFiles">
+ <table class="entityTable dataTable">
+ <thead>
+ <tr>
+ <th class="filename">{{ 'AutoLogImporter_File'|translate }}</th>
+ <th>{{ 'General_Date'|translate }}</th>
+ <th>{{ 'AutoLogImporter_TimeTaken'|translate }}</th>
+ <th>{{ 'AutoLogImporter_FileSize'|translate }}</th>
+ <th>{{ 'AutoLogImporter_NumLogLines'|translate }}</th>
+ <th>{{ 'AutoLogImporter_Imported'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% if importedFiles|length %}
+ {% for importedFile in importedFiles %}
+ <tr>
+ <td class="filename" title="{{ 'AutoLogImporter_FileDetails'|translate(importedFile.file, importedFile.command, importedFile.file_hash) }}">{{ importedFile.filename }}</td>
+ <td class="date_field" title="{{ importedFile.start_date|e('html_attr') }} - {{ importedFile.end_date|e('html_attr') }}">{{ importedFile.date }}</td>
+ <td>{{ importedFile.duration }}s</td>
+ <td>{{ importedFile.size_human }}</td>
+ <td>{{ importedFile.num_log_lines }}</td>
+ <td title="{{ 'AutoLogImporter_ImportDetails'|translate(importedFile.exit_code, importedFile.output)|e('html_attr') }}">
+ {% if importedFile.exit_code == 0 %}
+ <span class="icon-success"></span>
+ {% else %}
+ <span class="icon-error"></span>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ {% else %}
+ <tr><td colspan="6">{{ 'AutoLogImporter_NoImportedFile'|translate }}</td></tr>
+ {% endif %}
+ </tbody>
+ </table>
+</div>
+{% endblock %} \ No newline at end of file
diff --git a/plugins/Bandwidth b/plugins/Bandwidth
new file mode 160000
+Subproject a046ad794f992f4b3c5cdd33cd56ee9d604ce1f
diff --git a/plugins/ComparisonDashboard b/plugins/ComparisonDashboard
new file mode 160000
+Subproject eadb0ea480b0037aea75d3d0f5e6b34dc92394c
diff --git a/plugins/ConcurrentVisits b/plugins/ConcurrentVisits
new file mode 160000
+Subproject 45280cb3ba4831d1a164696dd68eb07087b2b65
diff --git a/plugins/CustomTrackerJs b/plugins/CustomTrackerJs
new file mode 160000
+Subproject aa296ff7f76db00aa8e2c41b332b58f9b73d3ba
diff --git a/plugins/DFOCanada b/plugins/DFOCanada
new file mode 160000
+Subproject c6ab17d70785114904616584c3f62212c5e6983
diff --git a/plugins/FileSynchronizer/.gitignore b/plugins/FileSynchronizer/.gitignore
new file mode 100644
index 0000000000..c8c9480010
--- /dev/null
+++ b/plugins/FileSynchronizer/.gitignore
@@ -0,0 +1 @@
+tests/System/processed/*xml \ No newline at end of file
diff --git a/plugins/FileSynchronizer/.travis.yml b/plugins/FileSynchronizer/.travis.yml
new file mode 100644
index 0000000000..ecc6f3996a
--- /dev/null
+++ b/plugins/FileSynchronizer/.travis.yml
@@ -0,0 +1,132 @@
+# do not edit this file manually, instead run the generate:travis-yml console command
+
+language: php
+
+php:
+ - 5.6
+ - 5.3.3
+# - hhvm
+
+services:
+ - redis-server
+
+# Separate different test suites
+env:
+ global:
+ - PLUGIN_NAME=FileSynchronizer
+ - PIWIK_ROOT_DIR=$TRAVIS_BUILD_DIR/piwik
+ - PIWIK_LATEST_STABLE_TEST_TARGET=2.15.0-b3
+ - secure: "GwPMT+B1vLoyQLVcbvWPU3FZl1UvMFufQhlVKxnAy+FSmMamMCiNRd/XpYhB/zER1TuKQvSgWmZjFXrab6ih98MRJEvc+ae0nqCduSRvlPOWWOWkerUdeKJOis0tjaPdzBzdgIiP2lrw/TF+7Mo4CNPE0i8XDcoGjBQRT9dmgQpg130zasTdxkdp484NKFb8busQEBXZmAHYDLkF6u+hDVvcf1zR7ebOGPyEfTJCItvN/CCUvznj02zPUouknpdAuMGmlQmQD4KzhlV68NlBurB2dE9KG7LlIlX6fvnpxK/DiTNDrUvbAgBGUMmu1wKgC7mE9j1jYAwbWMldttBGQa7G81AP1l73w8SAyTXbyKLzf0g2hfaoQV/IEG2I6tQ6SicWkyapjJ8pGsHWhr7JS6z5LO/OQpHaytegEXga8CuUMPxj3U9BBQ7XEiGyMvVF2qBtL5KDvSZXPbDUlNzYCaiNJBZKuBLqaKowU7r91GkcSph8KSVg8lKq9DouUi7m65x21zAjLEEtTqtM5vRPcUFq3Nmvs1vYH+q5Su/35kXnH78sx8W2R/0oBgp6SaHOMKLw5l7KpZrL8Y5BL//mhnPs2llUnSYVKwyLiyCEG1QjiS9KfGYOD1WJ4UkXsbBWtSuwM0loV9VWa+DXSuE+zIA3Zu0jP9gbwqEWZEYBXZ0="
+ - secure: "ABmkRR2PtfFy2+ebTdaBzekuydJ7VyEE9z2DeO9e1FUVD2BS0oJ0blf3iJDXxrb50ypMRcfjnipM8UZFcHIC+H949gV/9OWyaV1r2iog0XvbouTZHDWG2DN4fAqnghMNgS5yavQGPTyiYZTgPKW85jE6PTvx0mj9sOg3fur1djb4f8V+CHl0bIO84udhpAXHDlu0/UK20vfhF02QwI9HPsFWqsej8FQFxzJHevYjGJGgHWOf/m8phd0r0Ag6K52S4bkmoLIL4bfU9KuujU76k6DFzAyoKEgxlpRXfomRutj4vRcjOX4vLy7PnOXkYc1fAI+Evstb+Bsczqa8C2BQe17VRaevx0wy6XXpoLkbvMJzZNapOFqnZmYxB2b1N1M4gKJF1seFsJ5WECSP2IVX/e0CY/rTC7RhXUO5c+MhqajXv/9rdPfy4dDCKQ/Ag1NhlYjRL2ybtKS/vFO3Kt5ImI0fLr6sibqfCnrddkhSZ2zlyvi4IA596EUSqfZZNa5j//j3gllAuDMyCzc8MEb10mMl6vrYiU8mGVOcG4xyHssXHaUSas9wTV2bmEVcjXc1rkn/f7pvMAwlMv9K5a8t3Zm+8k5sF2DwHguzO9g4UqJd9QBAnO5oKtPcdN6yQS30768YFI1ojXwn+YCRGDXjbn/DtRzRwHSyWHnPlNoDPh4="
+ matrix:
+ - TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_LATEST_STABLE_TEST_TARGET
+ - TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=minimum_required_piwik
+ - TEST_SUITE=UITests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_LATEST_STABLE_TEST_TARGET
+
+matrix:
+ exclude:
+ # execute latest stable tests only w/ PHP 5.5
+ - php: 5.3.3
+ env: TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=minimum_required_piwik
+ # execute UI tests only w/ PHP 5.6
+ - php: 5.3.3
+ env: TEST_SUITE=UITests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_PIWIK_BRANCH=$PIWIK_LATEST_STABLE_TEST_TARGET
+
+sudo: required
+
+script: $PIWIK_ROOT_DIR/tests/travis/travis.sh
+
+before_install:
+ # do not use the Zend allocator on PHP 5.3 since it will randomly segfault after program execution
+ - '[[ "$TRAVIS_PHP_VERSION" == 5.3* ]] && export USE_ZEND_ALLOC=0 || true'
+
+install:
+ # move all contents of current repo (which contains the plugin) to a new directory
+ - mkdir $PLUGIN_NAME
+ - cp -R !($PLUGIN_NAME) $PLUGIN_NAME
+ - cp -R .git/ $PLUGIN_NAME/
+ - cp .travis.yml $PLUGIN_NAME
+ # checkout piwik in the current directory
+ - git clone -q https://github.com/piwik/piwik.git piwik
+ - cd piwik
+ - git fetch -q --all
+ - git submodule update
+
+ # make sure travis-scripts repo is latest for initial travis setup
+ - '[ -d ./tests/travis/.git ] || sh -c "rm -rf ./tests/travis && git clone https://github.com/piwik/travis-scripts.git ./tests/travis"'
+ - cd ./tests/travis ; git checkout master ; cd ../..
+
+ - export GENERATE_TRAVIS_YML_COMMAND="php ./tests/travis/generator/main.php generate:travis-yml --plugin=\"FileSynchronizer\" --verbose"
+ - '[[ "$TRAVIS_JOB_NUMBER" != *.1 || "$TRAVIS_PULL_REQUEST" != "false" ]] || ./tests/travis/autoupdate_travis_yml.sh'
+
+ - ./tests/travis/checkout_test_against_branch.sh
+
+ - '[ ! -f ./tests/travis/install_mysql_5.6.sh ] || ./tests/travis/install_mysql_5.6.sh'
+
+ # Make sure we use Python 2.6
+ - '[ ! -f ./tests/travis/install_python_2.6.sh ] || ./tests/travis/install_python_2.6.sh'
+
+ - ./tests/travis/configure_git.sh
+
+ # travis now complains about this failing 9 times out of 10, so removing it
+ #- travis_retry composer self-update
+
+ - '[ "$SKIP_COMPOSER_INSTALL" == "1" ] || travis_retry composer install'
+
+ # move plugin contents to folder in the plugins subdirectory
+ - rm -rf plugins/$PLUGIN_NAME
+ - mv ../$PLUGIN_NAME plugins
+
+ # clone dependent repos
+ - ./tests/travis/checkout_dependent_plugins.sh
+
+before_script:
+ - phpenv config-rm xdebug.ini;
+
+ # add always_populate_raw_post_data=-1 to php.ini
+ - echo "always_populate_raw_post_data=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+
+ # disable opcache to avoid random failures on travis
+ - echo "opcache.enable=0" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+
+ # print out mysql information
+ - mysql --version
+ - mysql -e "SELECT VERSION();"
+
+ # configure mysql
+ - mysql -e "SET GLOBAL sql_mode = 'NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES'" # Travis default
+ # try to avoid mysql has gone away errors
+ - mysql -e "SET GLOBAL wait_timeout = 36000;"
+ - mysql -e "SET GLOBAL max_allowed_packet = 134209536;"
+ - mysql -e "SHOW VARIABLES LIKE 'max_allowed_packet';"
+ - mysql -e "SHOW VARIABLES LIKE 'wait_timeout';"
+
+ - mysql -e "SELECT @@sql_mode;"
+ # - mysql -e "SHOW GLOBAL VARIABLES;"
+
+ # print out more debugging info
+ - uname -a
+ - date
+ - php -r "var_dump(gd_info());"
+ - mysql -e 'create database piwik_tests;'
+
+ - ./tests/travis/prepare.sh
+ - ./tests/travis/setup_webserver.sh
+
+ - cd tests/PHPUnit
+
+after_script:
+ # change directory back to root travis dir
+ - cd $PIWIK_ROOT_DIR
+
+ # output contents of files w/ debugging info to screen
+ - cat /var/log/nginx/error.log
+ - cat $PIWIK_ROOT_DIR/tmp/php-fpm.log
+ - cat $PIWIK_ROOT_DIR/tmp/logs/piwik.log
+ - cat $PIWIK_ROOT_DIR/config/config.ini.php
+
+ # upload test artifacts (for debugging travis failures)
+ - ./tests/travis/upload_artifacts.sh
+
+after_success:
+ - cd $PIWIK_ROOT_DIR
diff --git a/plugins/FileSynchronizer/API.php b/plugins/FileSynchronizer/API.php
new file mode 100644
index 0000000000..5663a5d540
--- /dev/null
+++ b/plugins/FileSynchronizer/API.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+
+use Piwik\Container\StaticContainer;
+use Piwik\DataTable;
+use Piwik\DataTable\Row;
+use Piwik\Piwik;
+
+/**
+ * API for plugin FileSynchronizer
+ *
+ * @method static \Piwik\Plugins\FileSynchronizer\API getInstance()
+ */
+class API extends \Piwik\Plugin\API
+{
+ /**
+ * Get all synced files whether they were successful or not.
+ *
+ * Latest synced files are listed first.
+ * @return array
+ */
+ public function getAllSyncedFiles()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $dao = new Dao();
+ $files = $dao->getAllSyncedFiles();
+
+ usort($files, function ($a, $b){
+ return $a['idfilesync'] > $b['idfilesync'] ? -1 : 1;
+ });
+
+ return $files;
+ }
+
+ /**
+ * Get all files that are currently syncing.
+ *
+ * @return array
+ */
+ public function getAllSyncingFiles()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $dao = new Dao();
+ $files = $dao->getAllSyncingFiles();
+
+ return $files;
+ }
+
+ /**
+ * Get a list of all files that can be synced and will be synced as soon as the next tasks run.
+ *
+ * @return array
+ */
+ public function getFilesThatCanBeSynced()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $syncFiles = StaticContainer::get('Piwik\Plugins\FileSynchronizer\SyncFiles');
+ return $syncFiles->getFilesThatCanBeSynced();
+ }
+
+}
diff --git a/plugins/FileSynchronizer/Controller.php b/plugins/FileSynchronizer/Controller.php
new file mode 100644
index 0000000000..b0d8fee335
--- /dev/null
+++ b/plugins/FileSynchronizer/Controller.php
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+use Piwik\API\Request;
+use Piwik\Common;
+use Piwik\Piwik;
+
+/**
+ * A controller let's you for example create a page that can be added to a menu. For more information read our guide
+ * http://developer.piwik.org/guides/mvc-in-piwik or have a look at the our API references for controller and view:
+ * http://developer.piwik.org/api-reference/Piwik/Plugin/Controller and
+ * http://developer.piwik.org/api-reference/Piwik/View
+ */
+class Controller extends \Piwik\Plugin\ControllerAdmin
+{
+ public function status()
+ {
+ Piwik::checkUserHasSuperUserAccess();
+
+ $limit = Common::getRequestVar('limit', 200, 'int');
+
+ $syncingFiles = Request::processRequest('FileSynchronizer.getAllSyncingFiles');
+ $scheduledFiles = Request::processRequest('FileSynchronizer.getFilesThatCanBeSynced');
+ $syncedFiles = Request::processRequest('FileSynchronizer.getAllSyncedFiles', array(
+ 'filter_limit' => $limit
+ ));
+
+ if (count($syncedFiles) < $limit) {
+ $limit = count($syncedFiles);
+ }
+
+ $formatter = new Formatter();
+
+ return $this->renderTemplate('status', array(
+ 'syncedFiles' => $formatter->formatSyncedFiles($syncedFiles),
+ 'syncingFiles' => $formatter->addTimeAgoAsSentence($formatter->formatSyncedFiles($syncingFiles)),
+ 'scheduledFiles' => $scheduledFiles,
+ 'limit' => $limit
+ ));
+ }
+
+}
diff --git a/plugins/FileSynchronizer/Dao.php b/plugins/FileSynchronizer/Dao.php
new file mode 100644
index 0000000000..68f46b4823
--- /dev/null
+++ b/plugins/FileSynchronizer/Dao.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+
+use Piwik\Common;
+use Piwik\Date;
+use Piwik\Db;
+use Piwik\DbHelper;
+
+class Dao
+{
+ /**
+ * @var Db
+ */
+ private $db;
+
+ private $tableName = 'file_synchronizer';
+ private $tableNamePrefixed;
+
+ public function __construct()
+ {
+ $this->db = Db::get();
+ $this->tableNamePrefixed = Common::prefixTable($this->tableName);
+ }
+
+ public function getAllSyncedFiles()
+ {
+ return Db::fetchAll('SELECT * FROM ' . $this->tableNamePrefixed . ' WHERE end_date is not null');
+ }
+
+ public function getAllSyncingFiles()
+ {
+ return Db::fetchAll('SELECT * FROM ' . $this->tableNamePrefixed . ' WHERE end_date is null');
+ }
+
+ public function isSynced($fileHash)
+ {
+ // "end_date is null" => probably in process by another server
+ // "exit_code = 0" => file was already imported successfully
+ return (bool) $this->db->fetchOne("SELECT 1 FROM " . $this->tableNamePrefixed . "
+ WHERE `file_hash` = ? and (end_date is null or exit_code = 0) LIMIT 1", array($fileHash));
+ }
+
+ public function isHashFileCreated($fileHash)
+ {
+ return (bool) $this->db->fetchOne("SELECT 1 FROM " . $this->tableNamePrefixed . "
+ WHERE `file_hash` = ? and hash_file_created = 1 LIMIT 1", array($fileHash));
+ }
+
+ public function markHashFileCreated($fileHash)
+ {
+ $this->db->update($this->tableNamePrefixed, array(
+ 'hash_file_created' => 1,
+ ), '`file_hash` = "' . $fileHash . '"');
+ }
+
+ public function logFileSyncStart($source, $target, $fileHash, $fileSize, $startDate)
+ {
+ $this->db->insert($this->tableNamePrefixed, array(
+ 'source' => $source,
+ 'target' => $target,
+ 'file_hash' => $fileHash,
+ 'file_size' => $fileSize,
+ 'start_date' => $startDate,
+ ));
+
+ return (int) $this->db->lastInsertId();
+ }
+
+ public function logFileSyncFinished($id, $command, $output, $exitCode, $duration, $endDate)
+ {
+ $this->db->update($this->tableNamePrefixed, array(
+ 'output' => $output,
+ 'command' => $command,
+ 'exit_code' => $exitCode,
+ 'end_date' => $endDate,
+ 'duration_in_ms' => $duration,
+ ), 'idfilesync = ' . (int) $id);
+ }
+
+ public function install()
+ {
+ $table = "`idfilesync` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
+ `start_date` datetime NOT NULL ,
+ `end_date` datetime NULL ,
+ `duration_in_ms` INT UNSIGNED NULL ,
+ `source` TEXT NOT NULL ,
+ `target` TEXT NOT NULL ,
+ `file_size` INT UNSIGNED NOT NULL DEFAULT 0,
+ `file_hash` VARCHAR(32) NOT NULL ,
+ `hash_file_created` TINYINT UNSIGNED NOT NULL DEFAULT 0,
+ `command` TEXT NULL,
+ `output` TEXT NULL ,
+ `exit_code` TINYINT UNSIGNED NULL";
+
+ DbHelper::createTable($this->tableName, $table);
+ }
+
+ public function uninstall()
+ {
+ Db::dropTables(array($this->tableNamePrefixed));
+ }
+
+}
diff --git a/plugins/FileSynchronizer/FileSynchronizer.php b/plugins/FileSynchronizer/FileSynchronizer.php
new file mode 100644
index 0000000000..3e585132f2
--- /dev/null
+++ b/plugins/FileSynchronizer/FileSynchronizer.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+
+class FileSynchronizer extends \Piwik\Plugin
+{
+ public function getListHooksRegistered()
+ {
+ return array(
+ 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
+ );
+ }
+
+ public function getStylesheetFiles(&$stylesheets)
+ {
+ $stylesheets[] = "plugins/FileSynchronizer/stylesheets/status.less";
+ }
+
+ public function install()
+ {
+ $dao = new Dao();
+ $dao->install();
+ }
+
+ public function uninstall()
+ {
+ $dao = new Dao();
+ $dao->uninstall();
+ }
+
+}
diff --git a/plugins/FileSynchronizer/Formatter.php b/plugins/FileSynchronizer/Formatter.php
new file mode 100644
index 0000000000..1f10bbf8c1
--- /dev/null
+++ b/plugins/FileSynchronizer/Formatter.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Date;
+use Piwik\Db;
+use Piwik\Metrics\Formatter as MetricsFormatter;
+
+class Formatter
+{
+
+ public function addTimeAgoAsSentence($files)
+ {
+ $formatter = new MetricsFormatter();
+ $now = StaticContainer::get('FileSynchronizer.currentTimestamp');
+
+ foreach ($files as &$file) {
+ $diff = $now - Date::factory($file['start_date'])->getTimestamp();
+ $file['time_ago'] = $formatter->getPrettyTimeFromSeconds($diff, true);
+ }
+
+ return $files;
+ }
+
+ public function formatSyncedFiles($files)
+ {
+ $formatter = new MetricsFormatter();
+
+ foreach ($files as &$syncedFile) {
+ $syncedFile['source_filename'] = basename($syncedFile['source']);
+ $syncedFile['date'] = substr($syncedFile['start_date'], 0, 10);
+ $syncedFile['duration'] = $syncedFile['duration_in_ms'] . 'ms';
+ $syncedFile['size_human'] = $formatter->getPrettySizeFromBytes($syncedFile['file_size']);
+ }
+
+ return $files;
+ }
+
+}
diff --git a/plugins/FileSynchronizer/Menu.php b/plugins/FileSynchronizer/Menu.php
new file mode 100644
index 0000000000..608f52ebc7
--- /dev/null
+++ b/plugins/FileSynchronizer/Menu.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+
+use Piwik\Menu\MenuAdmin;
+use Piwik\Piwik;
+
+/**
+ * This class allows you to add, remove or rename menu items.
+ * To configure a menu (such as Admin Menu, Top Menu, User Menu...) simply call the corresponding methods as
+ * described in the API-Reference http://developer.piwik.org/api-reference/Piwik/Menu/MenuAbstract
+ */
+class Menu extends \Piwik\Plugin\Menu
+{
+
+ public function configureAdminMenu(MenuAdmin $menu)
+ {
+ if (Piwik::hasUserSuperUserAccess()) {
+ $menu->addDiagnosticItem('File Synchronizer', $this->urlForAction('status', array('limit' => 200)));
+ }
+ }
+
+}
diff --git a/plugins/FileSynchronizer/README.md b/plugins/FileSynchronizer/README.md
new file mode 100644
index 0000000000..42ab1e3281
--- /dev/null
+++ b/plugins/FileSynchronizer/README.md
@@ -0,0 +1,66 @@
+# Piwik FileSynchronizer Plugin
+
+[![Build Status](https://magnum.travis-ci.com/PiwikPRO/plugin-FileSynchronizer.svg?token=YG4RfcyzVryveJy9zhmw)](https://magnum.travis-ci.com/PiwikPRO/plugin-FileSynchronizer)
+
+## Description
+
+Synchronizes any files of a directory with another directory. Useful for example to copy log files to another directory.
+
+Features:
+
+* Syncs any files from one directory to another
+* Can copy files to a remote computer by changing the copy template (eg `scp $source user@host:$target`)
+* Specify a shell wildcard to sync only files matching this pattern
+* Modify the filename while syncing
+* Shows which files will be synced next
+* Shows which files are currently syncing
+* Shows which files were synced in the past
+* Gives detailed error output if a sync failed and will retry automatically
+* Syncs files once per hour
+
+## FAQ
+
+__Can I sync files to another server?__
+
+Yes, you can specify a copy template like `scp $source user@host:$target`. By default files are simply copied from
+`$source` to `$target` via the linux command `cp`.
+
+__How often are files synchronized?__
+
+Once per hour.
+
+__Is Windows supported?__
+
+No.
+
+__How can I verify the file was correctly copied?__
+
+We create a hash file for each copied file containing the MD5 hash of the content. If the file name is `access.log`,
+there will be a `access.log.hash` in the configured target directory once the file was copied successfully.
+
+__What happens when a file having the same name already exists in the target directory?__
+
+The file in the target directory will be overwritten.
+
+__How can I trigger a file sync manually?__
+
+You can trigger a file sync manually by executing the following command:
+
+`./console core:run-scheduled-tasks "Piwik\Plugins\FileSynchronizer\Tasks.syncFiles"`
+
+__How can I access details about previously synced files?__
+
+Either by accessing the diagnostic status page under "Administration => File Synchronizer" or by having a look at the
+database table `file_synchronizer`.
+
+__I want to see more than the latest 200 synced files, is it possible?__
+
+Yes, there is a `limit` URL parameter that you can change to any number.
+
+## Changelog
+
+* 0.1.0 Initial version
+
+## Contact
+To get a license for one of the Enterprise plugins, or to access the latest updates, contact us.
+If you have any suggestion, code review, or feedback please email contact@piwik.pro
diff --git a/plugins/FileSynchronizer/Settings.php b/plugins/FileSynchronizer/Settings.php
new file mode 100644
index 0000000000..37f2e5485e
--- /dev/null
+++ b/plugins/FileSynchronizer/Settings.php
@@ -0,0 +1,207 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+
+use Piwik\Common;
+use Piwik\Settings\Setting;
+use Piwik\Settings\SystemSetting;
+
+/**
+ * Defines Settings for FileSynchronizer.
+ *
+ * Usage like this:
+ * $settings = new Settings('FileSynchronizer');
+ * $settings->enabled->getValue();
+ * $settings->sourceDirectory->getValue();
+ */
+class Settings extends \Piwik\Plugin\Settings
+{
+ /** @var SystemSetting */
+ public $enabled;
+
+ /** @var SystemSetting */
+ public $sourceDirectory;
+
+ /** @var SystemSetting */
+ public $filePattern;
+
+ /** @var SystemSetting */
+ public $targetDirectory;
+
+ /** @var SystemSetting */
+ public $targetFilenameTemplate;
+
+ /** @var SystemSetting */
+ public $copyCommandTemplate;
+
+ protected function init()
+ {
+ $this->createEnabledSetting();
+ $this->createSourceDirectorySetting();
+ $this->createFilePatternSetting();
+ $this->createTargetDirectorySetting();
+ $this->createTargetFilenameSetting();
+ $this->createCopyTemplateSetting();
+ }
+
+ private function createEnabledSetting()
+ {
+ $this->enabled = new SystemSetting('enabled', 'Enabled');
+ $this->enabled->type = static::TYPE_BOOL;
+ $this->enabled->uiControlType = static::CONTROL_CHECKBOX;
+ $this->enabled->defaultValue = false;
+
+ $this->addSetting($this->enabled);
+ }
+
+ private function createSourceDirectorySetting()
+ {
+ $this->sourceDirectory = new SystemSetting('sourceDirectory', 'Source directory');
+ $this->configureDirectorySetting($this->sourceDirectory, $checkDir = true);
+ $this->sourceDirectory->description = 'Defines the source directory that contains the files that need to be synced.';
+
+ $this->addSetting($this->sourceDirectory);
+ }
+
+ private function createFilePatternSetting()
+ {
+ $this->filePattern = new SystemSetting('filePattern', 'File Pattern');
+ $this->filePattern->type = static::TYPE_STRING;
+ $this->filePattern->uiControlType = static::CONTROL_TEXT;
+ $this->filePattern->defaultValue = '*';
+ $this->filePattern->description = 'If specified, only files matching this pattern will be synced.';
+ $this->filePattern->inlineHelp = 'Any shell wildcards can be specified, for example "*.log".';
+ $this->filePattern->transform = function ($value) {
+ if (!empty($value)) {
+ return trim($value);
+ }
+
+ return '*';
+ };
+
+ $this->addSetting($this->filePattern);
+ }
+
+ private function createTargetDirectorySetting()
+ {
+ $this->targetDirectory = new SystemSetting('targetDirectory', 'Target directory');
+ $this->configureDirectorySetting($this->targetDirectory, $checkDir = false);
+ // we do not check dir as it might be on a remote computer
+ $this->targetDirectory->description = 'Defines the target directory the files should be copied to.';
+
+ $this->addSetting($this->targetDirectory);
+ }
+
+ private function createTargetFilenameSetting()
+ {
+ $this->targetFilenameTemplate = new SystemSetting('targetFilenameTemplate', 'Target filename template');
+ $this->targetFilenameTemplate->type = static::TYPE_STRING;
+ $this->targetFilenameTemplate->uiControlType = static::CONTROL_TEXT;
+ $this->targetFilenameTemplate->defaultValue = '$basename';
+ $this->targetFilenameTemplate->description = 'Allows to modify the filename. "$basename" or "$filename" must be specified, "$extension" can be specified optionally.';
+ $this->targetFilenameTemplate->inlineHelp = '"$basename" will be replaced with the filename including extension (eg "apache.log"), "$filename" will be replaced by the filename excluding extension (eg "apache") and "$extension" will be replaced by the file extension (eg "log"). This allows to modify the filename in the target directory for example like this: "$filename_idsite_1.$extension"';
+
+ $self = $this;
+ $this->targetFilenameTemplate->validate = function ($value, $setting) use ($self) {
+ if (empty($value) && !$self->enabled->getValue()) {
+ // it is not enabled, an empty value is okay.
+ return;
+ }
+
+ if (empty($value)) {
+ throw new \Exception('A value must be specified');
+ }
+
+ if (false === strpos($value, '$filename') && false === strpos($value, '$basename')) {
+ throw new \Exception('$filename or $basename must be specified');
+ }
+ };
+ $this->targetFilenameTemplate->transform = function ($value) {
+ if (!empty($value)) {
+ return trim($value);
+ }
+
+ return $value;
+ };
+
+ $this->addSetting($this->targetFilenameTemplate);
+ }
+
+ private function configureDirectorySetting(Setting $setting, $checkDir)
+ {
+ $setting->type = static::TYPE_STRING;
+ $setting->uiControlType = static::CONTROL_TEXT;
+
+ $self = $this;
+ $setting->validate = function ($value, $setting) use ($self, $checkDir) {
+ if (empty($value) && !$self->enabled->getValue()) {
+ // it is not enabled, an empty value is okay.
+ return;
+ }
+ if (empty($value)) {
+ throw new \Exception('A value must be specified');
+ }
+ if ($checkDir && !file_exists($value)) {
+ throw new \Exception('This path does not exist');
+ }
+ if ($checkDir && !is_dir($value)) {
+ throw new \Exception('Path is not a directory');
+ }
+ if ($checkDir && !is_readable($value)) {
+ throw new \Exception('This directory is not readable');
+ }
+ };
+ $setting->transform = function ($value) {
+ if (!empty($value) && Common::stringEndsWith($value, '/')) {
+ $value = substr($value, 0, -1);
+ }
+
+ return $value;
+ };
+ }
+
+ private function createCopyTemplateSetting()
+ {
+ $this->copyCommandTemplate = new SystemSetting('copyCommandTemplate', 'Copy Command Template');
+ $this->copyCommandTemplate->type = static::TYPE_STRING;
+ $this->copyCommandTemplate->uiControlType = static::CONTROL_TEXT;
+ $this->copyCommandTemplate->description = 'The shell command to sync files. "$source" and "$target" must be specified.';
+ $this->copyCommandTemplate->inlineHelp = '"$source" will be replaced by the path of the source file, "$target" by the path to the target file.';
+ $this->copyCommandTemplate->defaultValue = 'cp $source $target';
+
+ $self = $this;
+ $this->copyCommandTemplate->validate = function ($value, $setting) use ($self) {
+ if (empty($value) && !$self->enabled->getValue()) {
+ // it is not enabled, an empty value is okay.
+ return;
+ }
+
+ if (empty($value)) {
+ throw new \Exception('A value must be specified');
+ }
+ if (false === strpos($value, '$source')) {
+ throw new \Exception('$source is not specified');
+ }
+ if (false === strpos($value, '$target')) {
+ throw new \Exception('$target is not specified');
+ }
+ };
+ $this->copyCommandTemplate->transform = function ($value) {
+ if (!empty($value)) {
+ return trim($value);
+ }
+
+ return $value;
+ };
+
+ $this->addSetting($this->copyCommandTemplate);
+ }
+}
diff --git a/plugins/FileSynchronizer/SyncFiles.php b/plugins/FileSynchronizer/SyncFiles.php
new file mode 100644
index 0000000000..44f144ddd4
--- /dev/null
+++ b/plugins/FileSynchronizer/SyncFiles.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Date;
+use Piwik\Db;
+use Piwik\Plugins\FileSynchronizer\SyncFiles\Copy;
+use Piwik\Plugins\FileSynchronizer\SyncFiles\File;
+use Piwik\Plugins\FileSynchronizer\SyncFiles\FileList;
+
+class SyncFiles
+{
+ /**
+ * @var Dao
+ */
+ private $dao;
+
+ /**
+ * @var File
+ */
+ private $file;
+
+ /**
+ * @var string
+ */
+ private $copyCommandTemplate;
+
+ /**
+ * @var string[]
+ */
+ private $sourceFiles;
+
+ /**
+ * @var string
+ */
+ private $targetDirectory;
+
+ /**
+ * @var string
+ */
+ private $targetFilenameTemplate;
+
+ public function __construct(Dao $dao, File $file, Settings $settings, FileList $fileList)
+ {
+ $this->dao = $dao;
+ $this->file = $file;
+ $this->copyCommandTemplate = $settings->copyCommandTemplate->getValue();
+ $this->targetDirectory = $settings->targetDirectory->getValue();
+ $this->targetFilenameTemplate = $settings->targetFilenameTemplate->getValue();
+ $this->sourceFiles = $fileList->findFilesToSync();
+ }
+
+ public function getFilesThatCanBeSynced()
+ {
+ $files = array();
+
+ foreach ($this->sourceFiles as $sourceFile) {
+ $hash = $this->file->getHash($sourceFile);
+
+ if (!$this->dao->isSynced($hash)) {
+ $files[$hash] = $sourceFile;
+ }
+ }
+
+ return $files;
+ }
+
+ public function sync()
+ {
+ $copy = StaticContainer::get('Piwik\Plugins\FileSynchronizer\SyncFiles\Copy');
+
+ foreach ($this->sourceFiles as $sourceFile) {
+ $hash = $this->file->getHash($sourceFile);
+
+ if (!$this->dao->isSynced($hash)) {
+ $this->copyFile($copy, $sourceFile, $hash);
+ }
+
+ if ($this->dao->isSynced($hash) && !$this->dao->isHashFileCreated($hash)) {
+ $this->copyHashFile($copy, $sourceFile, $hash);
+ }
+ }
+ }
+
+ private function copyHashFile(Copy $copy, $sourceFile, $fileHash)
+ {
+ $targetFile = $this->file->buildTargetFileName($sourceFile, $this->targetFilenameTemplate);
+ $hashFile = $targetFile . '.hash';
+
+ $result = $copy->copyContent($hashFile, $fileHash, $this->targetDirectory, $this->copyCommandTemplate);
+
+ if ($result->getExitCode() == 0) {
+ $this->dao->markHashFileCreated($fileHash);
+ }
+ }
+
+ private function copyFile(Copy $copy, $sourceFile, $fileHash)
+ {
+ $fileSize = $this->file->getSize($sourceFile);
+ $targetFile = $this->file->buildTargetFilePath($sourceFile, $this->targetDirectory, $this->targetFilenameTemplate);
+
+ $id = $this->dao->logFileSyncStart($sourceFile, $targetFile, $fileHash, $fileSize, Date::now()->getDatetime());
+
+ $startTime = microtime(true);
+ $result = $copy->copy($sourceFile, $targetFile, $this->copyCommandTemplate);
+
+ $durationInMs = (microtime(true) - $startTime) * 1000;
+
+ $this->dao->logFileSyncFinished($id, $result->getCommand(), $result->getOutput(), $result->getExitCode(),
+ $durationInMs, Date::now()->getDatetime());
+ }
+}
diff --git a/plugins/FileSynchronizer/SyncFiles/Copy.php b/plugins/FileSynchronizer/SyncFiles/Copy.php
new file mode 100644
index 0000000000..8d3ffc1b75
--- /dev/null
+++ b/plugins/FileSynchronizer/SyncFiles/Copy.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer\SyncFiles;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Db;
+use Piwik\Filechecks;
+use Piwik\Filesystem;
+use Psr\Log\LoggerInterface;
+
+class Copy
+{
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ public function __construct(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ }
+
+ /**
+ * Copies the given source file to the target directory.
+ *
+ * @param string $sourceFile
+ * @param string $targetDir
+ * @param string $copyCommandTemplate eg 'cp $source $directory' or 'scp $source user@host:$target'
+ * @return Result
+ */
+ public function copy($sourceFile, $targetDir, $copyCommandTemplate)
+ {
+ $command = $this->buildCommandToCopyFile($sourceFile, $targetDir, $copyCommandTemplate);
+
+ $this->logger->debug("Executing command '$command' to copy '$sourceFile' to '$targetDir'");
+
+ try {
+ exec($command . ' 2>&1', $output, $exitCode);
+ $output = implode("\n", $output);
+ } catch (\Exception $e) {
+ $output = $e->getMessage() . ' Trace: ' . $e->getTraceAsString();
+ $exitCode = 255;
+ }
+
+ $this->logger->debug("Finished copying '$sourceFile'. Exit code: '$exitCode', output: '$output'");
+
+ return new Result($command, $output, $exitCode);
+ }
+
+ /**
+ * Similar to {@link copy()} but should be used if there is no file that can be copied but content instead.
+ *
+ * @param string $filename The name of the file that shall be created in the target directory. eg 'test.hash'
+ * @param string $content The content of the file, eg '123456'
+ * @param string $targetDir
+ * @param string $copyCommandTemplate
+ * @return Result
+ */
+ public function copyContent($filename, $content, $targetDir, $copyCommandTemplate)
+ {
+ $tmp = StaticContainer::get('path.tmp');
+ Filechecks::dieIfDirectoriesNotWritable(array($tmp));
+
+ $file = $tmp . DIRECTORY_SEPARATOR . $filename;
+ file_put_contents($file, $content);
+
+ $result = $this->copy($file, $targetDir, $copyCommandTemplate);
+
+ Filesystem::remove($file);
+
+ return $result;
+ }
+
+ private function buildCommandToCopyFile($sourceFile, $targetDir, $copyCommandTemplate)
+ {
+ $search = array('$source', '$target');
+ $replace = array(escapeshellarg($sourceFile), escapeshellarg($targetDir));
+
+ return str_replace($search, $replace, $copyCommandTemplate);
+ }
+}
diff --git a/plugins/FileSynchronizer/SyncFiles/File.php b/plugins/FileSynchronizer/SyncFiles/File.php
new file mode 100644
index 0000000000..6256707ee0
--- /dev/null
+++ b/plugins/FileSynchronizer/SyncFiles/File.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer\SyncFiles;
+
+class File
+{
+ public function getSize($file)
+ {
+ return filesize($file);
+ }
+
+ public function getHash($file)
+ {
+ return md5_file($file);
+ }
+
+ public function buildTargetFileName($sourceFile, $targetFilenameTemplate)
+ {
+ $basename = basename($sourceFile);
+ $extension = pathinfo($sourceFile, PATHINFO_EXTENSION);
+ $filename = pathinfo($sourceFile, PATHINFO_FILENAME);
+
+ $search = array('$basename', '$filename', '$extension');
+ $replace = array($basename, $filename, $extension);
+
+ $targetBasename = str_replace($search, $replace, $targetFilenameTemplate);
+
+ return $targetBasename;
+ }
+
+ public function buildTargetFilePath($sourceFile, $targetDirectory, $targetFilenameTemplate)
+ {
+ $targetBasename = $this->buildTargetFileName($sourceFile, $targetFilenameTemplate);
+
+ return $targetDirectory . DIRECTORY_SEPARATOR . $targetBasename;
+ }
+}
diff --git a/plugins/FileSynchronizer/SyncFiles/FileList.php b/plugins/FileSynchronizer/SyncFiles/FileList.php
new file mode 100644
index 0000000000..aa3a7b8dfb
--- /dev/null
+++ b/plugins/FileSynchronizer/SyncFiles/FileList.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer\SyncFiles;
+
+use Piwik\Db;
+use Piwik\Filesystem;
+use Piwik\Plugins\FileSynchronizer\Settings;
+
+class FileList
+{
+ /**
+ * @var Settings
+ */
+ private $settings;
+
+ public function __construct(Settings $settings)
+ {
+ $this->settings = $settings;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function findFilesToSync()
+ {
+ $path = $this->settings->sourceDirectory->getValue();
+ $pattern = $this->settings->filePattern->getValue();
+
+ if (!$this->settings->enabled->getValue()) {
+ return array();
+ }
+
+ if (empty($path) || !file_exists($path) || !is_readable($path) || !is_dir($path)) {
+ // case when plugin not configured yet
+ return array();
+ }
+
+ if (empty($pattern)) {
+ $pattern = '*';
+ }
+
+ return Filesystem::globr($path, $pattern);
+ }
+}
diff --git a/plugins/FileSynchronizer/SyncFiles/Result.php b/plugins/FileSynchronizer/SyncFiles/Result.php
new file mode 100644
index 0000000000..1b01621063
--- /dev/null
+++ b/plugins/FileSynchronizer/SyncFiles/Result.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer\SyncFiles;
+
+use Piwik\Db;
+
+class Result
+{
+ private $command;
+ private $output;
+ private $exitCode;
+
+ public function __construct($command, $output, $exitCode)
+ {
+ $this->command = $command;
+ $this->output = $output;
+ $this->exitCode = $exitCode;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCommand()
+ {
+ return $this->command;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+
+ /**
+ * @return string
+ */
+ public function getExitCode()
+ {
+ return $this->exitCode;
+ }
+}
diff --git a/plugins/FileSynchronizer/Tasks.php b/plugins/FileSynchronizer/Tasks.php
new file mode 100644
index 0000000000..b269ef691c
--- /dev/null
+++ b/plugins/FileSynchronizer/Tasks.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Copyright (C) Piwik PRO - All rights reserved.
+ *
+ * Using this code requires that you first get a license from Piwik PRO.
+ * Unauthorized copying of this file, via any medium is strictly prohibited.
+ *
+ * @link http://piwik.pro
+ */
+
+namespace Piwik\Plugins\FileSynchronizer;
+
+use Piwik\Container\StaticContainer;
+use Piwik\Settings\Setting;
+
+class Tasks extends \Piwik\Plugin\Tasks
+{
+ public function schedule()
+ {
+ $this->hourly('syncFiles');
+ }
+
+ public function syncFiles()
+ {
+ $settings = StaticContainer::get('Piwik\Plugins\FileSynchronizer\Settings');
+
+ if (!$settings->enabled->getValue()) {
+ return;
+ }
+
+ // we validate to make sure it is still readable etc.
+ $this->validateSetting($settings->copyCommandTemplate);
+ $this->validateSetting($settings->sourceDirectory);
+ $this->validateSetting($settings->targetDirectory);
+
+ $sync = StaticContainer::get('Piwik\Plugins\FileSynchronizer\SyncFiles');
+ $sync->sync();
+ }
+
+ private function validateSetting(Setting $setting)
+ {
+ if (isset($setting->validate)) {
+ call_user_func($setting->validate, $setting->getValue(), $setting);
+ }
+ }
+}
diff --git a/plugins/FileSynchronizer/config/config.php b/plugins/FileSynchronizer/config/config.php
new file mode 100644
index 0000000000..c2590057ec
--- /dev/null
+++ b/plugins/FileSynchronizer/config/config.php
@@ -0,0 +1,8 @@
+<?php
+
+return array(
+
+ 'FileSynchronizer.currentTimestamp' => DI\factory(function () {
+ return \Piwik\Date::now()->getTimestamp();
+ }),
+);
diff --git a/plugins/FileSynchronizer/config/test.php b/plugins/FileSynchronizer/config/test.php
new file mode 100644
index 0000000000..98de70b86e
--- /dev/null
+++ b/plugins/FileSynchronizer/config/test.php
@@ -0,0 +1,22 @@
+<?php
+
+return array(
+
+ 'Piwik\Plugins\FileSynchronizer\Settings' => DI\decorate(function (\Piwik\Plugins\FileSynchronizer\Settings $settings) {
+ if ($settings->enabled->isWritableByCurrentUser()) {
+ $path = PIWIK_INCLUDE_PATH;
+ if (\Piwik\Common::stringEndsWith($path, '/')) {
+ $path = substr($path, 0, -1);
+ }
+ $settings->enabled->setValue(true);
+ $settings->filePattern->setValue('*.log');
+ $settings->sourceDirectory->setValue($path . '/plugins/FileSynchronizer/tests/resources/source');
+ $settings->targetDirectory->setValue($path . '/plugins/FileSynchronizer/tests/resources/target');
+ $settings->targetFilenameTemplate->setValue('$basename_$filename_idsite_1_.$extension');
+ }
+
+ return $settings;
+ }),
+
+ 'FileSynchronizer.currentTimestamp' => '1440592473'
+);
diff --git a/plugins/FileSynchronizer/lang/en.json b/plugins/FileSynchronizer/lang/en.json
new file mode 100644
index 0000000000..f6801c25eb
--- /dev/null
+++ b/plugins/FileSynchronizer/lang/en.json
@@ -0,0 +1,25 @@
+{
+ "FileSynchronizer": {
+ "SynchronizingFiles": "Synchronizing files",
+ "ToConfigurePluginGoToSettings": "To configure this plugin go to %s%s%s.",
+ "ScheduledFiles": "Files that will be synchronized next",
+ "ScheduledFilesDescription": "These files should be synced as soon as the sync task runs the next time.",
+ "SynchronizedFiles": "Synchronized files",
+ "SynchronizedFilesDescription": "The following table shows the latest %d synchronized files.",
+ "NoSynchronizedFile": "No file has been synchronized yet.",
+ "NoSynchronizingFile": "No file is currently being synchronized.",
+ "NoScheduledFiles": "No file is scheduled to be synced.",
+ "File": "File",
+ "FileSize": "File size",
+ "Synced": "Synced",
+ "StartDate": "Start Date",
+ "TimeTaken": "Time taken",
+ "InProcessSince": "In process since",
+ "SyncDetails": "Exit code: '%s', output: '%s'",
+ "HashFileCreated": "Hash file was successfully created",
+ "HashFileNotCreated": "Hash file not created",
+ "HashFile": "Hash file",
+ "ExitCode": "Exit Code",
+ "FileDetails": "'%s' to '%s' via '%s'. Hash of file is '%s'"
+ }
+} \ No newline at end of file
diff --git a/plugins/FileSynchronizer/plugin.json b/plugins/FileSynchronizer/plugin.json
new file mode 100644
index 0000000000..d677352462
--- /dev/null
+++ b/plugins/FileSynchronizer/plugin.json
@@ -0,0 +1,16 @@
+{
+ "name": "FileSynchronizer",
+ "version": "0.1.0",
+ "description": "Synchronizes any files of a directory with another directory.",
+ "theme": false,
+ "require": {
+ "piwik": ">=2.15.0-b3"
+ },
+ "authors": [
+ {
+ "name": "Piwik",
+ "email": "",
+ "homepage": ""
+ }
+ ]
+} \ No newline at end of file
diff --git a/plugins/FileSynchronizer/screenshots/.gitkeep b/plugins/FileSynchronizer/screenshots/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/plugins/FileSynchronizer/screenshots/.gitkeep
diff --git a/plugins/FileSynchronizer/screenshots/Error_Sync_Details.png b/plugins/FileSynchronizer/screenshots/Error_Sync_Details.png
new file mode 100644
index 0000000000..faf7fcb196
--- /dev/null
+++ b/plugins/FileSynchronizer/screenshots/Error_Sync_Details.png
Binary files differ
diff --git a/plugins/FileSynchronizer/screenshots/Plugin_Settings.png b/plugins/FileSynchronizer/screenshots/Plugin_Settings.png
new file mode 100644
index 0000000000..b44d51486b
--- /dev/null
+++ b/plugins/FileSynchronizer/screenshots/Plugin_Settings.png
Binary files differ
diff --git a/plugins/FileSynchronizer/screenshots/Status_Report.png b/plugins/FileSynchronizer/screenshots/Status_Report.png
new file mode 100644
index 0000000000..50a0b7e88a
--- /dev/null
+++ b/plugins/FileSynchronizer/screenshots/Status_Report.png
Binary files differ
diff --git a/plugins/FileSynchronizer/stylesheets/status.less b/plugins/FileSynchronizer/stylesheets/status.less
new file mode 100644
index 0000000000..b197666db5
--- /dev/null
+++ b/plugins/FileSynchronizer/stylesheets/status.less
@@ -0,0 +1,23 @@
+#listSyncedFiles {
+ table {
+ max-width: 1000px;
+ min-width: 600px;
+ }
+
+ .date_field {
+ white-space: nowrap
+ }
+
+ .filename {
+ max-width: 300px;
+ word-break: break-all;
+ }
+
+ .icon-success {
+ color: green;
+ }
+
+ .icon-error {
+ color: #D4291F;
+ }
+} \ No newline at end of file
diff --git a/plugins/FileSynchronizer/templates/status.twig b/plugins/FileSynchronizer/templates/status.twig
new file mode 100644
index 0000000000..abbe8e5d78
--- /dev/null
+++ b/plugins/FileSynchronizer/templates/status.twig
@@ -0,0 +1,100 @@
+{% extends 'admin.twig' %}
+
+{% block content %}
+<h2>{{ 'FileSynchronizer_SynchronizingFiles'|translate }}</h2>
+<p>{{ 'FileSynchronizer_ToConfigurePluginGoToSettings'|translate('<a href="' ~ linkTo({'module': 'CoreAdminHome', 'action': 'adminPluginSettings'}) ~ '#FileSynchronizer">', ('CoreAdminHome_PluginSettings'|translate), '</a>')|raw }}</p>
+<div id="listSyncedFiles">
+ <table class="entityTable dataTable">
+ <thead>
+ <tr>
+ <th class="filename">{{ 'FileSynchronizer_File'|translate }}</th>
+ <th>{{ 'FileSynchronizer_StartDate'|translate }}</th>
+ <th>{{ 'FileSynchronizer_InProcessSince'|translate }}</th>
+ <th>{{ 'FileSynchronizer_FileSize'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% if syncingFiles|length %}
+ {% for syncingFile in syncingFiles %}
+ <tr>
+ <td class="filename" title="{{ 'FileSynchronizer_FileDetails'|translate(syncingFile.source, syncingFile.target, syncingFile.command, syncingFile.file_hash) }}">{{ syncingFile.source_filename }}</td>
+ <td class="date_field">{{ syncingFile.start_date }}</td>
+ <td>{{ syncingFile.time_ago }}</td>
+ <td>{{ syncingFile.size_human }}</td>
+ </tr>
+ {% endfor %}
+ {% else %}
+ <tr><td colspan="6">{{ 'FileSynchronizer_NoSynchronizingFile'|translate }}</td></tr>
+ {% endif %}
+ </tbody>
+ </table>
+</div>
+
+<h2>{{ 'FileSynchronizer_ScheduledFiles'|translate }}</h2>
+
+<p>{{ 'FileSynchronizer_ScheduledFilesDescription'|translate }}</p>
+
+<div id="listSyncingFiles">
+ <table class="entityTable dataTable">
+ <thead>
+ <tr>
+ <th class="filename">{{ 'FileSynchronizer_File'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% if scheduledFiles|length %}
+ {% for hash, scheduledFile in scheduledFiles %}
+ <tr>
+ <td>{{ scheduledFile }}</td>
+ </tr>
+ {% endfor %}
+ {% else %}
+ <tr><td colspan="5">{{ 'FileSynchronizer_NoScheduledFiles'|translate }}</td></tr>
+ {% endif %}
+ </tbody>
+ </table>
+</div>
+
+<h2>{{ 'FileSynchronizer_SynchronizedFiles'|translate }}</h2>
+<p>{{ 'FileSynchronizer_SynchronizedFilesDescription'|translate(limit) }}</p>
+<div id="listSyncedFiles">
+ <table class="entityTable dataTable">
+ <thead>
+ <tr>
+ <th class="filename">{{ 'FileSynchronizer_File'|translate }}</th>
+ <th>{{ 'General_Date'|translate }}</th>
+ <th>{{ 'FileSynchronizer_FileSize'|translate }}</th>
+ <th>{{ 'FileSynchronizer_TimeTaken'|translate }}</th>
+ <th>{{ 'FileSynchronizer_Synced'|translate }}</th>
+ <th>{{ 'FileSynchronizer_HashFile'|translate }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% if syncedFiles|length %}
+ {% for syncedFile in syncedFiles %}
+ <tr>
+ <td class="filename" title="{{ 'FileSynchronizer_FileDetails'|translate(syncedFile.source, syncedFile.target, syncedFile.command, syncedFile.file_hash) }}">{{ syncedFile.source_filename }}</td>
+ <td class="date_field" title="{{ syncedFile.start_date|e('html_attr') }} - {{ syncedFile.end_date|e('html_attr') }}">{{ syncedFile.date }}</td>
+ <td>{{ syncedFile.size_human }}</td>
+ <td>{{ syncedFile.duration }}</td>
+ <td title="{{ 'FileSynchronizer_SyncDetails'|translate(syncedFile.exit_code, syncedFile.output)|e('html_attr') }}">
+ {% if syncedFile.exit_code == 0 %}
+ <span class="icon-success"></span>
+ {% else %}
+ <span class="icon-error"></span>
+ {% endif %}
+ </td>
+ {% if syncedFile.hash_file_created == 0 %}
+ <td title="{{ 'FileSynchronizer_HashFileNotCreated'|translate }}"><span class="icon-error"></span></td>
+ {% else %}
+ <td title="{{ 'FileSynchronizer_HashFileCreated'|translate }}"><span class="icon-success"></span></td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ {% else %}
+ <tr><td colspan="6">{{ 'FileSynchronizer_NoSynchronizedFile'|translate }}</td></tr>
+ {% endif %}
+ </tbody>
+ </table>
+</div>
+{% endblock %} \ No newline at end of file
diff --git a/plugins/Funnel b/plugins/Funnel
new file mode 160000
+Subproject 363cc5b7a888e8be97d4b5b9fb62fe45df57c86
diff --git a/plugins/InterSites b/plugins/InterSites
new file mode 160000
+Subproject e2268b0beac1d0bacfb24794ee4be94d7ea7ebd
diff --git a/plugins/LogViewer b/plugins/LogViewer
-Subproject e51a8fd6b3d4d133b9588caf3971d44bcad85be
+Subproject 345dbecb786de105df7ee190be8282706de411d
diff --git a/plugins/LoginHttpAuth b/plugins/LoginHttpAuth
new file mode 160000
+Subproject 9044930813f45882eddd8e80878417d0a10b266
diff --git a/plugins/MediaPlayer b/plugins/MediaPlayer
new file mode 160000
+Subproject e837d26f1f9140b146470d53c5fc2b979d928c7
diff --git a/plugins/MetaSites b/plugins/MetaSites
new file mode 160000
+Subproject 20c5cc14ab28c4cef06496dd2a17fec503a1bb7
diff --git a/plugins/RawDataExporter b/plugins/RawDataExporter
new file mode 160000
+Subproject 459375ff10a7253963af7c8432156e45ef10cfc
diff --git a/plugins/SimplePageBuilder b/plugins/SimplePageBuilder
new file mode 160000
+Subproject ea18cae3b7fbc29605717afb3af72cef4c646f8
diff --git a/plugins/SiteMigration b/plugins/SiteMigration
new file mode 160000
+Subproject bc7f03f6ea2c2e173bba4c501ce29e3b13bb878
diff --git a/plugins/TrafficMonitor b/plugins/TrafficMonitor
new file mode 160000
+Subproject 4cf14c91afafc15c2ff578651d5714c6da67d11
diff --git a/plugins/WebsiteGroups b/plugins/WebsiteGroups
new file mode 160000
+Subproject 74cad4473ec63ccc5721d28d3a62dc4beb6b483
diff --git a/plugins/WhiteLabel b/plugins/WhiteLabel
new file mode 160000
+Subproject 0c0a6dc8701b56ef92b5448766af68839d58275
diff --git a/plugins/WhitelistAdmin b/plugins/WhitelistAdmin
new file mode 160000
+Subproject 53b5f26133ece499f427ccc70d9ea1c5483fb9b