Changeset 3704935
- Timestamp:
- 09/21/2026 06:19:26 AM (7 weeks ago)
- Location:
- koko-analytics
- Files:
-
- 2 deleted
- 36 edited
- 1 copied
-
tags/2.5.3 (copied) (copied from koko-analytics/trunk )
-
tags/2.5.3/CHANGELOG.md (modified) (2 diffs)
-
tags/2.5.3/assets/css/dashboard.css (modified) (2 diffs)
-
tags/2.5.3/assets/js/dashboard.js (modified) (1 diff)
-
tags/2.5.3/assets/js/src/imports/datepicker.js (modified) (4 diffs)
-
tags/2.5.3/data/referrer-blocklist (modified) (5 diffs)
-
tags/2.5.3/koko-analytics.php (modified) (2 diffs)
-
tags/2.5.3/phpstan.neon (deleted)
-
tags/2.5.3/readme.txt (modified) (5 diffs)
-
tags/2.5.3/src/Aggregator.php (modified) (4 diffs)
-
tags/2.5.3/src/Chart_View.php (modified) (1 diff)
-
tags/2.5.3/src/Dashboard.php (modified) (8 diffs)
-
tags/2.5.3/src/Dashboard_Public.php (modified) (3 diffs)
-
tags/2.5.3/src/Pageview_Aggregator.php (modified) (4 diffs)
-
tags/2.5.3/src/Resources/external-strings.php (modified) (7 diffs)
-
tags/2.5.3/src/Resources/functions/functions.php (modified) (1 diff)
-
tags/2.5.3/src/Resources/views/dashboard-page.php (modified) (5 diffs)
-
tags/2.5.3/src/Resources/views/settings/help.php (modified) (1 diff)
-
tags/2.5.3/src/Rest.php (modified) (1 diff)
-
tags/2.5.3/src/Upserter.php (modified) (3 diffs)
-
trunk/CHANGELOG.md (modified) (2 diffs)
-
trunk/assets/css/dashboard.css (modified) (2 diffs)
-
trunk/assets/js/dashboard.js (modified) (1 diff)
-
trunk/assets/js/src/imports/datepicker.js (modified) (4 diffs)
-
trunk/data/referrer-blocklist (modified) (5 diffs)
-
trunk/koko-analytics.php (modified) (2 diffs)
-
trunk/phpstan.neon (deleted)
-
trunk/readme.txt (modified) (5 diffs)
-
trunk/src/Aggregator.php (modified) (4 diffs)
-
trunk/src/Chart_View.php (modified) (1 diff)
-
trunk/src/Dashboard.php (modified) (8 diffs)
-
trunk/src/Dashboard_Public.php (modified) (3 diffs)
-
trunk/src/Pageview_Aggregator.php (modified) (4 diffs)
-
trunk/src/Resources/external-strings.php (modified) (7 diffs)
-
trunk/src/Resources/functions/functions.php (modified) (1 diff)
-
trunk/src/Resources/views/dashboard-page.php (modified) (5 diffs)
-
trunk/src/Resources/views/settings/help.php (modified) (1 diff)
-
trunk/src/Rest.php (modified) (1 diff)
-
trunk/src/Upserter.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
koko-analytics/tags/2.5.3/CHANGELOG.md
r3634028 r3704935 1 1 # Changelog 2 3 = 2.5.3 = 4 5 - security: enforce the same access checks for public dashboard pages and REST API requests, fixing bypasses of Pro password protection. 6 - dashboard: reduce crawler and cache load by only accepting read requests, canonicalizing query parameters, and hiding pagination and date navigation on public dashboards. 7 - dashboard: only enable component sorting in the admin dashboard, where changes can be saved. 8 - perf: process aggregation buffers in chunks to reduce peak memory usage on busy sites. 9 - docs: show the dedicated support address to Pro users on the help page. 10 2 11 3 12 = 2.5.2 = … … 7 16 - data: increase the aggregation interval to 65 seconds to reduce the frequency of scheduling conflicts. 8 17 - ux: show admin notice with location of analytics dashboard after initial plugin activation. 18 9 19 10 20 = 2.5.1 = -
koko-analytics/tags/2.5.3/assets/css/dashboard.css
r3605984 r3704935 244 244 } 245 245 .page-filter-active tr, 246 .page-filter-active .ka-pagination {246 .page-filter-active .ka-pagination2 { 247 247 opacity: 0.5; 248 248 } … … 587 587 588 588 /* pagination */ 589 590 /* old style pagination, used in Pro components */591 .ka-pagination {592 display: flex;593 padding: 0.5rem 1rem;594 border-top: 1px solid rgb(240, 240, 240);595 }596 .ka-pagination--next {597 margin-left: auto;598 }599 589 600 590 /* new style pagination (2026-07-03) */ -
koko-analytics/tags/2.5.3/assets/js/dashboard.js
r3548865 r3704935 1 (()=>{function l(t){this.root=t,this.barWidth=0,this.closeTooltipTimeout=null,this.root&&(this.tooltip=document.querySelector(".ka-chart--tooltip"),this.tooltip.remove(),document.body.appendChild(this.tooltip),[this.root,this.tooltip].forEach(i=>{i.addEventListener("mouseleave",this.closeTooltip.bind(this)),i.addEventListener("mouseenter",this.keepTooltip.bind(this))}),this.root.addEventListener("mouseover",i=>{i.target.tagName==="rect"&&this.showTooltip(i.target)}),this.redraw())}l.prototype.closeTooltip=function(){this.closeTooltipTimeout=window.setTimeout(()=>{this.tooltip.style.display="none"},20)};l.prototype.keepTooltip=function(){window.clearTimeout(this.closeTooltipTimeout)};l.prototype.showTooltip=function(t){let i=t.parentElement.dataset;this.tooltip.querySelector(".ka-chart--tooltip-heading").textContent=i.date,this.tooltip.querySelector(".ka--visitors").children[0].textContent=i.visitors,this.tooltip.querySelector(".ka--pageviews").children[0].textContent=i.pageviews;let o=document.body.clientWidth;this.tooltip.style.display="block";let e=t.parentElement.getBoundingClientRect(),r=Math.round(e.left-.5*this.tooltip.clientWidth+.5*this.barWidth),a=Math.round(e.top+window.scrollY-this.tooltip.clientHeight),s=0;r<12?s=-r+12:r+this.tooltip.clientWidth>o-12&&(s=o-(r+this.tooltip.clientWidth)-12),r+=s;let y=this.tooltip.querySelector(".ka-chart--tooltip-arrow");y.style.marginLeft=s===0?"auto":.5*this.tooltip.clientWidth-6-s+"px",this.tooltip.style.left=r+"px",this.tooltip.style.top=a+"px"};l.prototype.redraw=function(){let t=Array.from(this.root.querySelectorAll(".axes-y text")).reduce((e,r)=>Math.max(e,8+Math.max(5,r.textContent.length*8)),0),i=this.root.querySelectorAll(".bars g"),o=Math.max(1,(this.root.clientWidth-t)/i.length);this.barWidth=Math.max(1,o-2);for(let e=0;e<i.length;e++){let r=e*o+t+1,a=i[e].children;a[0].setAttribute("x",r),a[0].setAttribute("width",this.barWidth),a[1].setAttribute("x",r),a[1].setAttribute("width",this.barWidth);let s=e*o+t+.5*o;a[2].style.display=this.barWidth===1?"none":"",a[2].setAttribute("x1",s),a[2].setAttribute("x2",s)}};var n=document.getElementById("ka-components");if(n&&n.querySelector(".ka-col[data-sortable]")){let t=null,i=n.querySelectorAll(".ka-col[data-sortable]");n.addEventListener("mousedown",function(o){if(o.target.closest("th")){let e=o.target.closest(".ka-col");e&&(e.draggable=!0)}}),document.addEventListener("mouseup",function(){i.forEach(o=>{o.draggable=!1})}),n.addEventListener("dragstart",function(o){t=o.target.closest(".ka-col"),t&&(t.classList.add("ka-dragging"),o.dataTransfer.effectAllowed="move")}),n.addEventListener("dragover",function(o){o.preventDefault(),o.dataTransfer.dropEffect="move";let e=o.target.closest(".ka-col");!e||e===t||(t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING?n.insertBefore(t,e.nextSibling):n.insertBefore(t,e))}),n.addEventListener("dragend",function(){if(!t)return;t.classList.remove("ka-dragging"),t=null,i.forEach(function(r){r.draggable=!1});let o=Array.from(n.querySelectorAll(".ka-col[data-sortable]")).map(r=>r.id),e=new FormData;e.append("koko_analytics_action","save_component_order"),e.append("_nonce",n.dataset.nonce),o.forEach(r=>{e.append("component_order[]",r)}),window.fetch(window.location.href,{method:"POST",body:e})})}var d=window.setTimeout(c,61e3);function c(){document.hidden?d=window.setTimeout(c,61e3):window.location.reload()}document.addEventListener("mouseover",function(){window.clearTimeout(d),d=window.setTimeout(c,61e3)});var u=document.querySelector("#ka-date-presets"),h=document.querySelector("#ka-date-start"),p=document.querySelector("#ka-date-end");u&&u.addEventListener("change",function(){h.disabled=!0,p.disabled=!0,this.form.submit()});function f(){u.value="custom"}h&&h.addEventListener("change",f);p&&p.addEventListener("change",f);document.addEventListener("keydown",function(t){if(!t.defaultPrevented)switch(t.key){case"ArrowLeft":document.querySelector(".js-quicknav-prev").click();break;case"ArrowRight":document.querySelector(".js-quicknav-next").click();break}});document.querySelectorAll("a[data-href]").forEach(function(t){t.addEventListener("click",function(i){i.preventDefault(),window.location.href=t.getAttribute("data-href")})});document.addEventListener("DOMContentLoaded",()=>{new l(document.getElementById("ka-chart"))});function w(){sessionStorage.setItem("scrollX",window.pageXOffset),sessionStorage.setItem("scrollY",window.pageYOffset)}document.addEventListener("click",w);window.addEventListener("beforeunload",w);var m=parseInt(sessionStorage.getItem("scrollX")??0),g=parseInt(sessionStorage.getItem("scrollY")??0);(m!=0||g!=0)&&window.scroll(m,g);})();1 (()=>{function d(t){this.root=t,this.barWidth=0,this.closeTooltipTimeout=null,this.root&&(this.tooltip=document.querySelector(".ka-chart--tooltip"),this.tooltip.remove(),document.body.appendChild(this.tooltip),[this.root,this.tooltip].forEach(r=>{r.addEventListener("mouseleave",this.closeTooltip.bind(this)),r.addEventListener("mouseenter",this.keepTooltip.bind(this))}),this.root.addEventListener("mouseover",r=>{r.target.tagName==="rect"&&this.showTooltip(r.target)}),this.redraw())}d.prototype.closeTooltip=function(){this.closeTooltipTimeout=window.setTimeout(()=>{this.tooltip.style.display="none"},20)};d.prototype.keepTooltip=function(){window.clearTimeout(this.closeTooltipTimeout)};d.prototype.showTooltip=function(t){let r=t.parentElement.dataset;this.tooltip.querySelector(".ka-chart--tooltip-heading").textContent=r.date,this.tooltip.querySelector(".ka--visitors").children[0].textContent=r.visitors,this.tooltip.querySelector(".ka--pageviews").children[0].textContent=r.pageviews;let o=document.body.clientWidth;this.tooltip.style.display="block";let e=t.parentElement.getBoundingClientRect(),i=Math.round(e.left-.5*this.tooltip.clientWidth+.5*this.barWidth),a=Math.round(e.top+window.scrollY-this.tooltip.clientHeight),s=0;i<12?s=-i+12:i+this.tooltip.clientWidth>o-12&&(s=o-(i+this.tooltip.clientWidth)-12),i+=s;let b=this.tooltip.querySelector(".ka-chart--tooltip-arrow");b.style.marginLeft=s===0?"auto":.5*this.tooltip.clientWidth-6-s+"px",this.tooltip.style.left=i+"px",this.tooltip.style.top=a+"px"};d.prototype.redraw=function(){let t=Array.from(this.root.querySelectorAll(".axes-y text")).reduce((e,i)=>Math.max(e,8+Math.max(5,i.textContent.length*8)),0),r=this.root.querySelectorAll(".bars g"),o=Math.max(1,(this.root.clientWidth-t)/r.length);this.barWidth=Math.max(1,o-2);for(let e=0;e<r.length;e++){let i=e*o+t+1,a=r[e].children;a[0].setAttribute("x",i),a[0].setAttribute("width",this.barWidth),a[1].setAttribute("x",i),a[1].setAttribute("width",this.barWidth);let s=e*o+t+.5*o;a[2].style.display=this.barWidth===1?"none":"",a[2].setAttribute("x1",s),a[2].setAttribute("x2",s)}};var n=document.getElementById("ka-components");if(n&&n.querySelector(".ka-col[data-sortable]")){let t=null,r=n.querySelectorAll(".ka-col[data-sortable]");n.addEventListener("mousedown",function(o){if(o.target.closest("th")){let e=o.target.closest(".ka-col");e&&(e.draggable=!0)}}),document.addEventListener("mouseup",function(){r.forEach(o=>{o.draggable=!1})}),n.addEventListener("dragstart",function(o){t=o.target.closest(".ka-col"),t&&(t.classList.add("ka-dragging"),o.dataTransfer.effectAllowed="move")}),n.addEventListener("dragover",function(o){o.preventDefault(),o.dataTransfer.dropEffect="move";let e=o.target.closest(".ka-col");!e||e===t||(t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING?n.insertBefore(t,e.nextSibling):n.insertBefore(t,e))}),n.addEventListener("dragend",function(){if(!t)return;t.classList.remove("ka-dragging"),t=null,r.forEach(function(i){i.draggable=!1});let o=Array.from(n.querySelectorAll(".ka-col[data-sortable]")).map(i=>i.id),e=new FormData;e.append("koko_analytics_action","save_component_order"),e.append("_nonce",n.dataset.nonce),o.forEach(i=>{e.append("component_order[]",i)}),window.fetch(window.location.href,{method:"POST",body:e})})}var l=window.setTimeout(c,61e3);function c(){document.hidden?l=window.setTimeout(c,61e3):window.location.reload()}document.addEventListener("mouseover",function(){window.clearTimeout(l),l=window.setTimeout(c,61e3)});function m(t){var r=new URL(window.location.href),o=new URLSearchParams(new FormData(t));o.sort(),r.search=o.toString(),window.location.href=r.toString()}var f=document.querySelector("#ka-datepicker-dropdown form");f&&f.addEventListener("submit",function(t){t.preventDefault(),m(this)});var u=document.querySelector("#ka-date-presets"),h=document.querySelector("#ka-date-start"),p=document.querySelector("#ka-date-end");u&&u.addEventListener("change",function(){h.disabled=!0,p.disabled=!0,m(this.form)});function w(){u.value="custom"}h&&h.addEventListener("change",w);p&&p.addEventListener("change",w);document.addEventListener("keydown",function(t){if(!t.defaultPrevented){var r;switch(t.key){case"ArrowLeft":r=document.querySelector(".js-quicknav-prev");break;case"ArrowRight":r=document.querySelector(".js-quicknav-next");break}r&&r.click()}});document.querySelectorAll("a[data-start-date][data-end-date]").forEach(function(t){t.addEventListener("click",function(r){r.preventDefault();var o=new URL(window.location.href),e=new URLSearchParams;["page","koko-analytics-dashboard"].forEach(function(i){o.searchParams.has(i)&&e.set(i,o.searchParams.get(i))}),e.set("start_date",t.getAttribute("data-start-date")),e.set("end_date",t.getAttribute("data-end-date")),e.sort(),o.search=e.toString(),window.location.href=o.toString()})});document.addEventListener("DOMContentLoaded",()=>{new d(document.getElementById("ka-chart"))});function y(){sessionStorage.setItem("scrollX",window.pageXOffset),sessionStorage.setItem("scrollY",window.pageYOffset)}document.addEventListener("click",y);window.addEventListener("beforeunload",y);var g=parseInt(sessionStorage.getItem("scrollX")??0),v=parseInt(sessionStorage.getItem("scrollY")??0);(g!=0||v!=0)&&window.scroll(g,v);})(); -
koko-analytics/tags/2.5.3/assets/js/src/imports/datepicker.js
r3500426 r3704935 1 2 // navigate to the date range form its target URL ourselves, so that we can sort the query string 3 // keeping the query args in a fixed order means the same view always maps to the same URL, which is 4 // friendlier to any cache sitting in front of the (public) dashboard 5 function submitDateForm(form) { 6 var url = new URL(window.location.href); 7 var params = new URLSearchParams(new FormData(form)); 8 params.sort(); 9 url.search = params.toString(); 10 window.location.href = url.toString(); 11 } 12 13 var dateForm = document.querySelector('#ka-datepicker-dropdown form'); 14 dateForm && dateForm.addEventListener('submit', function(evt) { 15 evt.preventDefault(); 16 submitDateForm(this); 17 }); 1 18 2 19 // update date_start and date_end <input>'s whenever a preset is selected … … 7 24 dateStartInput.disabled = true; 8 25 dateEndInput.disabled = true; 9 this.form.submit();26 submitDateForm(this.form); 10 27 }); 11 28 … … 19 36 20 37 // click "prev date range" or "next date range" when using arrow keys 38 // note these links are not always there, eg on the public dashboard or at the edges of the available date range 21 39 document.addEventListener('keydown', function (evt) { 22 40 if (evt.defaultPrevented) { … … 24 42 } 25 43 44 var quicknav; 26 45 switch (evt.key) { 27 46 case 'ArrowLeft': 28 document.querySelector('.js-quicknav-prev').click();47 quicknav = document.querySelector('.js-quicknav-prev'); 29 48 break; 30 49 case 'ArrowRight': 31 document.querySelector('.js-quicknav-next').click();50 quicknav = document.querySelector('.js-quicknav-next'); 32 51 break; 33 52 } 53 54 quicknav && quicknav.click(); 34 55 }) 35 56 36 57 // fake <a> elements to stop bots from crawling infinitely 37 document.querySelectorAll('a[data-href]').forEach(function(el) { 58 // the HTML only carries the target dates, we assemble the URL here so the document holds no crawlable link 59 // note we keep just the query args identifying the dashboard itself, dropping any active filter or pagination 60 document.querySelectorAll('a[data-start-date][data-end-date]').forEach(function(el) { 38 61 el.addEventListener('click', function(evt) { 39 62 evt.preventDefault(); 40 window.location.href = el.getAttribute('data-href'); 63 64 var url = new URL(window.location.href); 65 var params = new URLSearchParams(); 66 ['page', 'koko-analytics-dashboard'].forEach(function(key) { 67 if (url.searchParams.has(key)) { 68 params.set(key, url.searchParams.get(key)); 69 } 70 }); 71 params.set('start_date', el.getAttribute('data-start-date')); 72 params.set('end_date', el.getAttribute('data-end-date')); 73 params.sort(); 74 url.search = params.toString(); 75 window.location.href = url.toString(); 41 76 }); 42 77 }); -
koko-analytics/tags/2.5.3/data/referrer-blocklist
r3597329 r3704935 489 489 concretepol.com 490 490 connectikastudio.com 491 considerin.net 491 492 constanceonline.top 492 493 cookie-law-enforcement-aa.xyz … … 1490 1491 oracle-patches.ru 1491 1492 orakul.spb.ru 1493 osdzc.com 1492 1494 osteochondrosis.ru 1493 1495 otdbiaxaem-vmeste.ru … … 1654 1656 rcb101.ru 1655 1657 rczhan.com 1658 readlife.net 1656 1659 realresultslist.com 1657 1660 recinziireale.com … … 1939 1942 suckoutlet.xyz 1940 1943 sudachitravel.com 1944 sunblog.asia 1941 1945 sundrugstore.com 1942 1946 super-seo-guru.com … … 2333 2337 zelena-mriya.com.ua 2334 2338 zexhgoods.xyz 2339 zeyao.net 2335 2340 zhcsapp.net 2336 2341 zhoobintravel.com -
koko-analytics/tags/2.5.3/koko-analytics.php
r3634028 r3704935 4 4 Plugin Name: Koko Analytics 5 5 Plugin URI: https://www.kokoanalytics.com/#utm_source=koko-analytics&utm_medium=link&utm_campaign=free-plugin-plugin-header 6 Version: 2.5. 26 Version: 2.5.3 7 7 Description: Privacy-friendly and efficient statistics for your WordPress site. 8 8 Author: ibericode … … 43 43 PHP_VERSION_ID >= 70400 || exit; 44 44 45 define('KOKO_ANALYTICS_VERSION', '2.5. 2');45 define('KOKO_ANALYTICS_VERSION', '2.5.3'); 46 46 define('KOKO_ANALYTICS_PLUGIN_FILE', __FILE__); 47 47 define('KOKO_ANALYTICS_PLUGIN_DIR', __DIR__); -
koko-analytics/tags/2.5.3/readme.txt
r3674437 r3704935 3 3 Tags: analytics, google analytics, privacy, statistics, website statistics 4 4 Requires at least: 6.2 5 Tested up to: 7. 16 Stable tag: 2.5. 25 Tested up to: 7.0 6 Stable tag: 2.5.3 7 7 License: GPL-3.0-or-later 8 8 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 14 14 15 15 Koko Analytics is a privacy-friendly and lightweight Google Analytics alternative for WordPress sites. It is a plug and play solution to get GDPR compliant insights into your website visitors without requiring any third-party services. 16 17 [youtube https://www.youtube.com/watch?v=4o0Rp3F4qh8]18 16 19 17 == Why choose Koko Analytics? == … … 126 124 == Changelog == 127 125 126 = 2.5.3 = 127 128 - security: enforce the same access checks for public dashboard pages and REST API requests, fixing bypasses of Pro password protection. 129 - dashboard: reduce crawler and cache load by only accepting read requests, canonicalizing query parameters, and hiding pagination and date navigation on public dashboards. 130 - dashboard: only enable component sorting in the admin dashboard, where changes can be saved. 131 - perf: process aggregation buffers in chunks to reduce peak memory usage on busy sites. 132 - docs: show the dedicated support address to Pro users on the help page. 133 134 128 135 = 2.5.2 = 129 136 … … 132 139 - data: increase the aggregation interval to 65 seconds to reduce the frequency of scheduling conflicts. 133 140 - ux: show admin notice with location of analytics dashboard after initial plugin activation. 141 134 142 135 143 = 2.5.1 = … … 156 164 - compatibility: raise the minimum supported WordPress version to 6.2. 157 165 158 159 = 2.4.0 =160 161 - tracking: hook into the visibilitychange event again to ignore prerender requests.162 - data: rewrite exporter and importer to use NDJSON instead of raw SQL.163 - import: fix column names for the referrer stats table.164 - rest: clamp date range for unauthenticated users to prevent large table scans.165 - review notice: simplify the review notice to one primary action.166 167 166 [View the full changelog on GitHub](https://github.com/ibericode/koko-analytics/blob/main/CHANGELOG.md) -
koko-analytics/tags/2.5.3/src/Aggregator.php
r3581336 r3704935 13 13 class Aggregator 14 14 { 15 private const CHUNK_SIZE = 10000; 16 15 17 /** 16 * Reads the buffer file in to memory and moves data into the MySQL database (in bulk)18 * Reads the buffer file in chunks and moves data into the MySQL database. 17 19 * 18 20 * @throws Exception … … 29 31 // init pageview aggregator 30 32 $pageview_aggregator = new Pageview_Aggregator(); 33 $records_in_chunk = 0; 31 34 32 35 // rename file to temporary location so nothing new is written to it while we process it … … 71 74 // add-on aggregators 72 75 do_action('koko_analytics_aggregate_line', $type, $params); 76 77 ++$records_in_chunk; 78 if ($records_in_chunk === self::CHUNK_SIZE) { 79 $this->flush($pageview_aggregator); 80 $records_in_chunk = 0; 81 } 73 82 } 83 84 if ($records_in_chunk > 0) { 85 $this->flush($pageview_aggregator); 86 } 87 88 // Finalize state that remains bounded across chunks, such as realtime counts. 89 $pageview_aggregator->finish(); 74 90 75 91 // close file & remove it from filesystem … … 77 93 \unlink($tmp_filename); 78 94 79 // tell aggregators to write their results to the database 80 $pageview_aggregator->finish(); 95 // signal that the entire buffer file was processed 81 96 do_action('koko_analytics_aggregate_finish'); 82 97 83 98 update_option('koko_analytics_last_aggregation_at', \time(), false); 84 99 } 100 101 private function flush(Pageview_Aggregator $pageview_aggregator): void 102 { 103 $pageview_aggregator->finish(false); 104 105 // Allow add-on aggregators to persist and reset their in-memory state. 106 do_action('koko_analytics_aggregate_chunk_finish'); 107 } 85 108 } -
koko-analytics/tags/2.5.3/src/Chart_View.php
r3597329 r3704935 44 44 <div class="ka-chart-group"> 45 45 <?php foreach ($group_options as $key => $option) : ?> 46 <a href="<?= esc_attr( add_query_arg(['group' => $key])); ?>" class="<?php echo $group === $key ? 'on' : ''; ?>" rel="nofollow"><?php echo esc_html($option['label']); ?></a>46 <a href="<?= esc_attr(dashboard_url(['group' => $key])); ?>" class="<?php echo $group === $key ? 'on' : ''; ?>" rel="nofollow"><?php echo esc_html($option['label']); ?></a> 47 47 <?php endforeach; ?> 48 48 </div> -
koko-analytics/tags/2.5.3/src/Dashboard.php
r3597329 r3704935 17 17 public const MAX_OFFSET = 10000; 18 18 19 private static bool $show_pagination_links = true; 20 19 21 public static function clamp_limit($value, int $default = 10, int $minimum = 1): int 20 22 { … … 39 41 $items_per_page = (int) apply_filters('koko_analytics_items_per_page', 20); 40 42 $date_format = get_option('date_format', 'Y-m-d'); 41 $dashboard_url = $this->get_base_url();42 43 43 44 // parse query params … … 272 273 } 273 274 274 public function pagination(string $key, int $offset, int $limit, int $count): void 275 /** 276 * Hides the previous/next links in the datepicker and in every pagination component rendered from here on. 277 */ 278 public static function hide_pagination_links(): void 279 { 280 self::$show_pagination_links = false; 281 } 282 283 /** 284 * Whether previous/next links should be rendered at all. 285 */ 286 public static function shows_pagination_links(): bool 287 { 288 return self::$show_pagination_links; 289 } 290 291 /** 292 * Renders the pagination component for a dashboard component. 293 * 294 * @param string $key The query string key holding the offset & limit for this component. 295 * @param array $extra_args Additional query args to preserve in the previous/next links. 296 */ 297 public static function pagination(string $key, int $offset, int $limit, int $count, array $extra_args = []): void 275 298 { 276 299 if ($offset >= $limit || $offset + $limit < $count) { … … 283 306 ?> 284 307 </span> 308 <?php if (self::$show_pagination_links) : ?> 285 309 <span> 286 310 <?php if ($offset >= $limit) : ?> 287 <a href="<?php echo esc_attr( add_query_arg(['p' => null, $key => $offset >= $limit * 2 ? ['offset' => $offset - $limit, 'limit' => $limit] : null])); ?>" rel="nofollow">← <?php esc_html_e('Previous', 'koko-analytics'); ?></a>311 <a href="<?php echo esc_attr(dashboard_url(['p' => null, $key => $offset >= $limit * 2 ? array_merge($extra_args, ['offset' => $offset - $limit, 'limit' => $limit]) : ($extra_args ?: null) ])); ?>" rel="nofollow">← <?php esc_html_e('Previous', 'koko-analytics'); ?></a> 288 312 <?php else : ?> 289 313 <span class="ka-pagination2-muted">← <?php esc_html_e('Previous', 'koko-analytics'); ?></span> … … 291 315 <span> · </span> 292 316 <?php if ($offset + $limit < $count) : ?> 293 <a href="<?php echo esc_attr( add_query_arg(['p' => null, $key => ['offset' => $offset + $limit, 'limit' => $limit]])); ?>" rel="nofollow"><?php esc_html_e('Next', 'koko-analytics'); ?> →</a>317 <a href="<?php echo esc_attr(dashboard_url(['p' => null, $key => array_merge($extra_args, ['offset' => $offset + $limit, 'limit' => $limit])])); ?>" rel="nofollow"><?php esc_html_e('Next', 'koko-analytics'); ?> →</a> 294 318 <?php else : ?> 295 319 <span class="ka-pagination2-muted"><?php esc_html_e('Next', 'koko-analytics'); ?> →</span> 296 320 <?php endif; ?> 297 321 </span> 322 <?php endif; ?> 298 323 </div> 299 324 <?php … … 340 365 <td class="text-muted"><?= esc_html((string) ($offset + $i + 1)); ?></td> 341 366 <td class="text-truncate"> 342 <a href="<?= esc_attr( add_query_arg(['p' => $page_filter])); ?>"><?= esc_html($p->label); ?></a>367 <a href="<?= esc_attr(dashboard_url(['p' => $page_filter])); ?>"><?= esc_html($p->label); ?></a> 343 368 <a class="ka-visit-link" href="<?= esc_url($p->post_permalink); ?>" target="_blank" rel="noopener" title="<?php esc_attr_e('View page', 'koko-analytics'); ?>"><i class="icon icon-sm icon-external-link" aria-hidden="true"></i></a> 344 369 </td> … … 354 379 <?php } ?> 355 380 356 <?php $this->pagination('posts', $offset, $limit, $count); ?>381 <?php self::pagination('posts', $offset, $limit, $count); ?> 357 382 358 383 <?php … … 406 431 <?php } ?> 407 432 408 <?php $this->pagination('referrers', $offset, $limit, $count); ?>433 <?php self::pagination('referrers', $offset, $limit, $count); ?> 409 434 410 435 <?php -
koko-analytics/tags/2.5.3/src/Dashboard_Public.php
r3581336 r3704935 20 20 $settings = get_settings(); 21 21 $today = (new DateTimeImmutable('now', wp_timezone()))->format('Y-m-d'); 22 if (!$settings['is_dashboard_public'] && !current_user_can('view_koko_analytics')) { 23 return; 22 23 // this dashboard is a read-only view, so only ever answer GET (and HEAD) requests 24 // any other method is uncacheable, and would be a way to force a full render past every cache 25 $method = wp_unslash($_SERVER['REQUEST_METHOD'] ?? 'GET'); 26 if ($method !== 'GET' && $method !== 'HEAD') { 27 status_header(405); 28 header('Allow: GET, HEAD'); 29 exit; 24 30 } 25 31 … … 32 38 } 33 39 40 // redirect to the canonical URL for this view, so that visitors arriving through a link that 41 // was shared or crawled hit the same cache entry as everyone else 42 $request_uri = wp_unslash($_SERVER['REQUEST_URI'] ?? ''); 43 $canonical = sort_query_string($request_uri); 44 if ($canonical !== $request_uri) { 45 wp_safe_redirect($canonical, 301); 46 exit; 47 } 48 49 if (! can_view_dashboard($settings)) { 50 do_action('koko_analytics_public_dashboard_access_denied', $settings); 51 return; 52 } 53 54 // the public dashboard is crawlable, so don't hand out paginated URLs for bots to follow 55 self::hide_pagination_links(); 56 34 57 do_action('koko_analytics_public_dashboard_headers'); 35 58 … … 37 60 header("X-Robots-Tag: noindex, nofollow"); 38 61 if (is_user_logged_in()) { 39 header("Cache-Control: no-store, must-revalidate, no-cache, max-age=0, private");62 $cache_control = 'no-store, must-revalidate, no-cache, max-age=0, private'; 40 63 } elseif (isset($_GET['end_date'], $_GET['start_date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', wp_unslash($_GET['start_date'])) && preg_match('/^\d{4}-\d{2}-\d{2}$/', wp_unslash($_GET['end_date'])) && wp_unslash($_GET['end_date']) < $today) { 41 header("Cache-Control: public, max-age=68400");64 $cache_control = 'public, max-age=68400'; 42 65 } else { 43 header("Cache-Control: public, max-age=60");66 $cache_control = 'public, max-age=60'; 44 67 } 68 $cache_control = (string) apply_filters('koko_analytics_public_dashboard_cache_control', $cache_control, $settings); 69 header("Cache-Control: {$cache_control}"); 45 70 46 71 require KOKO_ANALYTICS_PLUGIN_DIR . '/src/Resources/views/dashboard-public.php'; -
koko-analytics/tags/2.5.3/src/Pageview_Aggregator.php
r3581336 r3704935 17 17 class Pageview_Aggregator 18 18 { 19 private const DATABASE_BATCH_SIZE = 500; 20 19 21 protected wpdb $db; 20 22 protected DateTimeZone $timezone; … … 84 86 } 85 87 86 public function finish( ): void88 public function finish(bool $update_realtime = true): void 87 89 { 88 90 $this->commit_site_stats(); 89 91 $this->commit_post_stats(); 90 92 $this->commit_referrer_stats(); 91 $this->update_realtime_pageview_count(); 93 if ($update_realtime) { 94 $this->update_realtime_pageview_count(); 95 } 92 96 } 93 97 … … 103 107 private function commit_post_stats(): void 104 108 { 105 $upserter = new Upserter('paths', 'path' );109 $upserter = new Upserter('paths', 'path', $this->db); 106 110 107 111 // insert page-specific stats 108 112 foreach ($this->post_stats as $date => $stats) { 109 $path_ids = $upserter->upsert(array_keys($stats)); 110 $values = []; 111 foreach ($stats as $path => $r) { 112 array_push($values, $date, $path_ids[$path], $r->post_id, $r->visitors, $r->pageviews); 113 foreach (array_chunk($stats, self::DATABASE_BATCH_SIZE, true) as $batch) { 114 $path_ids = $upserter->upsert(array_keys($batch)); 115 $values = []; 116 foreach ($batch as $path => $r) { 117 array_push($values, $date, $path_ids[$path], $r->post_id, $r->visitors, $r->pageviews); 118 } 119 $placeholders = rtrim(str_repeat('(%s,%d,%d,%d,%d),', count($batch)), ','); 120 $this->db->query($this->db->prepare("INSERT INTO {$this->db->prefix}koko_analytics_post_stats(date, path_id, post_id, visitors, pageviews) VALUES {$placeholders} ON DUPLICATE KEY UPDATE visitors = visitors + VALUES(visitors), pageviews = pageviews + VALUES(pageviews)", $values)); 113 121 } 114 $placeholders = rtrim(str_repeat('(%s,%d,%d,%d,%d),', count($stats)), ',');115 $this->db->query($this->db->prepare("INSERT INTO {$this->db->prefix}koko_analytics_post_stats(date, path_id, post_id, visitors, pageviews) VALUES {$placeholders} ON DUPLICATE KEY UPDATE visitors = visitors + VALUES(visitors), pageviews = pageviews + VALUES(pageviews)", $values));116 122 } 117 123 … … 121 127 private function commit_referrer_stats(): void 122 128 { 123 $upserter = new Upserter('referrer_labels', 'value' );129 $upserter = new Upserter('referrer_labels', 'value', $this->db); 124 130 125 131 // insert referrer stats 126 132 foreach ($this->referrer_stats as $date => $stats) { 127 $referrer_ids = $upserter->upsert(array_keys($stats)); 128 $values = []; 129 foreach ($stats as $url => $r) { 130 array_push($values, $date, $referrer_ids[$url], $r->visitors, $r->pageviews); 133 foreach (array_chunk($stats, self::DATABASE_BATCH_SIZE, true) as $batch) { 134 $referrer_ids = $upserter->upsert(array_keys($batch)); 135 $values = []; 136 foreach ($batch as $url => $r) { 137 array_push($values, $date, $referrer_ids[$url], $r->visitors, $r->pageviews); 138 } 139 $placeholders = rtrim(str_repeat('(%s,%d,%d,%d),', count($batch)), ','); 140 $this->db->query($this->db->prepare("INSERT INTO {$this->db->prefix}koko_analytics_referrer_stats(date, id, unique_hits, hits) VALUES {$placeholders} ON DUPLICATE KEY UPDATE unique_hits = unique_hits + VALUES(unique_hits), hits = hits + VALUES(hits)", $values)); 131 141 } 132 $placeholders = rtrim(str_repeat('(%s,%d,%d,%d),', count($stats)), ',');133 $this->db->query($this->db->prepare("INSERT INTO {$this->db->prefix}koko_analytics_referrer_stats(date, id, unique_hits, hits) VALUES {$placeholders} ON DUPLICATE KEY UPDATE unique_hits = unique_hits + VALUES(unique_hits), hits = hits + VALUES(hits)", $values));134 142 } 135 143 -
koko-analytics/tags/2.5.3/src/Resources/external-strings.php
r3605984 r3704935 3 3 // i18n strings for koko-analytics-pro 4 4 5 // ../koko-analytics-pro/src/UTM/views/dashboard.php# 185 // ../koko-analytics-pro/src/UTM/views/dashboard.php#20 6 6 __('Top referrers', 'koko-analytics'); 7 7 8 // ../koko-analytics-pro/src/UTM/views/dashboard.php# 198 // ../koko-analytics-pro/src/UTM/views/dashboard.php#21 9 9 __('Where your visitors came from', 'koko-analytics'); 10 10 11 // ../koko-analytics-pro/src/UTM/views/dashboard.php#2 711 // ../koko-analytics-pro/src/UTM/views/dashboard.php#29 12 12 __('Referrers report type', 'koko-analytics'); 13 13 14 // ../koko-analytics-pro/src/UTM/views/dashboard.php#3 414 // ../koko-analytics-pro/src/UTM/views/dashboard.php#36 15 15 __('A visitor represents the number of sessions during which a page was viewed one or more times.', 'koko-analytics'); 16 16 17 // ../koko-analytics-pro/src/UTM/views/dashboard.php#3 417 // ../koko-analytics-pro/src/UTM/views/dashboard.php#36 18 18 __('Visitors', 'koko-analytics'); 19 19 20 // ../koko-analytics-pro/src/UTM/views/dashboard.php#3 520 // ../koko-analytics-pro/src/UTM/views/dashboard.php#37 21 21 __('A pageview is defined as a view of a page on your site. If a user clicks reload after reaching the page, this is counted as an additional pageview. If a visitor navigates to a different page and then returns to the original page, a second pageview is recorded as well.', 'koko-analytics'); 22 22 23 // ../koko-analytics-pro/src/UTM/views/dashboard.php#3 523 // ../koko-analytics-pro/src/UTM/views/dashboard.php#37 24 24 __('Pageviews', 'koko-analytics'); 25 25 26 // ../koko-analytics-pro/src/UTM/views/dashboard.php#5 226 // ../koko-analytics-pro/src/UTM/views/dashboard.php#54 27 27 __('There is nothing here. Yet!', 'koko-analytics'); 28 29 // ../koko-analytics-pro/src/UTM/views/dashboard.php#6130 __('Previous', 'koko-analytics');31 32 // ../koko-analytics-pro/src/UTM/views/dashboard.php#7333 __('Next', 'koko-analytics');34 28 35 29 // ../koko-analytics-pro/src/UTM/views/setting.php#8 … … 165 159 __('Pageviews', 'koko-analytics'); 166 160 167 // ../koko-analytics-pro/src/Password/views/password-form.php#8 0161 // ../koko-analytics-pro/src/Password/views/password-form.php#84 168 162 __('Enter password to access the dashboard', 'koko-analytics'); 169 163 170 // ../koko-analytics-pro/src/Password/views/password-form.php#82 171 __('Incorrect password. Please try again.', 'koko-analytics'); 172 173 // ../koko-analytics-pro/src/Password/views/password-form.php#84 164 // ../koko-analytics-pro/src/Password/views/password-form.php#89 174 165 __('Password', 'koko-analytics'); 175 166 176 // ../koko-analytics-pro/src/Password/views/password-form.php# 85167 // ../koko-analytics-pro/src/Password/views/password-form.php#90 177 168 __('Submit', 'koko-analytics'); 178 169 179 // ../koko-analytics-pro/src/Password/bootstrap.php#55 170 // ../koko-analytics-pro/src/Password/bootstrap.php#42 171 __('Unable to sign in. Check the password and try again.', 'koko-analytics'); 172 173 // ../koko-analytics-pro/src/Password/bootstrap.php#57 180 174 __('Dashboard password', 'koko-analytics'); 181 175 182 // ../koko-analytics-pro/src/Password/bootstrap.php#5 7176 // ../koko-analytics-pro/src/Password/bootstrap.php#59 183 177 __('Set a password to protect your public dashboard. Leave empty for no password protection.', 'koko-analytics'); 184 178 … … 234 228 __('Add event', 'koko-analytics'); 235 229 236 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php# 19230 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#22 237 231 __('Event:', 'koko-analytics'); 238 232 239 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#2 0233 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#23 240 234 __('Unique', 'koko-analytics'); 241 235 242 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#2 1236 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#24 243 237 __('Total', 'koko-analytics'); 244 238 245 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#5 2239 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#55 246 240 __('There is nothing here. Yet!', 'koko-analytics'); 247 248 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#58249 __('Previous', 'koko-analytics');250 251 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#61252 __('Next', 'koko-analytics');253 241 254 242 // ../koko-analytics-pro/src/Events/views/event-details.php#24 … … 309 297 __('Back to event settings', 'koko-analytics'); 310 298 311 // ../koko-analytics-pro/src/Events/bootstrap.php#5 1299 // ../koko-analytics-pro/src/Events/bootstrap.php#52 312 300 __('Events', 'koko-analytics'); 313 301 314 // ../koko-analytics-pro/src/Events/Rest.php# 59302 // ../koko-analytics-pro/src/Events/Rest.php#71 315 303 __('start_date must be before or equal to end_date.', 'koko-analytics'); 316 304 317 // ../koko-analytics-pro/src/Events/Rest.php# 63305 // ../koko-analytics-pro/src/Events/Rest.php#75 318 306 __('Date range is too large.', 'koko-analytics'); 319 307 … … 321 309 __('Pageviews', 'koko-analytics'); 322 310 323 // ../koko-analytics-pro/src/Devices/views/dashboard.php# 39311 // ../koko-analytics-pro/src/Devices/views/dashboard.php#43 324 312 __('There is nothing here. Yet!', 'koko-analytics'); 325 326 // ../koko-analytics-pro/src/Devices/views/dashboard.php#45327 __('Previous', 'koko-analytics');328 329 // ../koko-analytics-pro/src/Devices/views/dashboard.php#48330 __('Next', 'koko-analytics');331 313 332 314 // ../koko-analytics-pro/src/Devices/views/setting.php#8 … … 378 360 __('Send periodic email reports?', 'koko-analytics'); 379 361 380 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php# 18362 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#20 381 363 __('Daily', 'koko-analytics'); 382 364 383 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php# 19365 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#21 384 366 __('Weekly', 'koko-analytics'); 385 367 386 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#2 0368 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#22 387 369 __('Monthly', 'koko-analytics'); 388 370 389 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#2 2371 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#24 390 372 __('Select the timeframes for which you want to receive a summary of your most important statistics.', 'koko-analytics'); 391 373 392 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#2 6374 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#28 393 375 __('Send to these email addresses', 'koko-analytics'); 394 376 395 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php# 28377 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#30 396 378 __('Enter a comma separated list of email addresses to which the reports should be sent.', 'koko-analytics'); 397 379 398 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#3 2380 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#34 399 381 __('Save Changes', 'koko-analytics'); 400 382 … … 402 384 __('Email reports', 'koko-analytics'); 403 385 404 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#2 0386 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#21 405 387 __('Country', 'koko-analytics'); 406 388 407 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#2 1389 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#22 408 390 __('Count', 'koko-analytics'); 409 391 410 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#4 0392 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#41 411 393 __('There is nothing here. Yet!', 'koko-analytics'); 412 413 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#46414 __('Previous', 'koko-analytics');415 416 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#49417 __('Next', 'koko-analytics');418 394 419 395 // ../koko-analytics-pro/src/Geolocation/views/setting.php#8 -
koko-analytics/tags/2.5.3/src/Resources/functions/functions.php
r3581336 r3704935 55 55 } 56 56 57 /** 58 * Returns whether the current request may view the analytics dashboard. 59 */ 60 function can_view_dashboard(?array $settings = null): bool 61 { 62 if (current_user_can('view_koko_analytics')) { 63 return true; 64 } 65 66 $settings = $settings ?? get_settings(); 67 if (empty($settings['is_dashboard_public'])) { 68 return false; 69 } 70 71 return (bool) apply_filters('koko_analytics_can_view_public_dashboard', true, $settings); 72 } 73 74 /** 75 * Wrapper around add_query_arg() that returns the URL with its query string in a canonical order. 76 * 77 * All dashboard links should be created through this function. Since add_query_arg() keeps the query 78 * parameters of the current request in whatever order they came in, links pointing at the exact same 79 * view can otherwise end up with a different query string on every request. Sorting the parameters 80 * keeps the number of distinct dashboard URLs to a minimum, which helps any cache (browser, page 81 * cache or CDN) in front of the public dashboard. 82 * 83 * Takes the same arguments as add_query_arg(), so a value of null removes that parameter. 84 * 85 * @param array<string, mixed> $args 86 * @param string|null $url URL to add the query args to. Defaults to the URL of the current request. 87 */ 88 function dashboard_url(array $args = [], ?string $url = null): string 89 { 90 return sort_query_string($url === null ? add_query_arg($args) : add_query_arg($args, $url)); 91 } 92 93 /** 94 * Sorts the query string of the given URL by parameter name, leaving the rest of the URL untouched. 95 */ 96 function sort_query_string(string $url): string 97 { 98 $start = strpos($url, '?'); 99 if ($start === false) { 100 return $url; 101 } 102 103 $query = substr($url, $start + 1); 104 $fragment = ''; 105 $hash = strpos($query, '#'); 106 if ($hash !== false) { 107 $fragment = substr($query, $hash); 108 $query = substr($query, 0, $hash); 109 } 110 111 if ($query === '') { 112 return $url; 113 } 114 115 $pairs = explode('&', $query); 116 $names = array_map(function ($pair) { 117 return explode('=', $pair, 2)[0]; 118 }, $pairs); 119 $order = array_keys($pairs); 120 121 // sort by parameter name, keeping parameters that share a name in their original order 122 array_multisort($names, SORT_ASC, SORT_STRING, $order, SORT_ASC, SORT_NUMERIC, $pairs); 123 124 return substr($url, 0, $start + 1) . implode('&', $pairs) . $fragment; 125 } 126 57 127 function get_most_viewed_post_ids(array $args) 58 128 { -
koko-analytics/tags/2.5.3/src/Resources/views/dashboard-page.php
r3597329 r3704935 3 3 use KokoAnalytics\Chart_View; 4 4 use KokoAnalytics\Fmt; 5 6 use function KokoAnalytics\dashboard_url; 5 7 6 8 defined('ABSPATH') || exit; … … 14 16 * @var int $realtime 15 17 * @var string $date_format 16 * @var string $dashboard_url17 18 * @var array $next_dates 18 19 * @var array $prev_dates … … 35 36 — <?php echo esc_html(wp_date($date_format, $date_end->getTimestamp())); ?> 36 37 </div> 37 38 38 <div id="ka-datepicker-dropdown" class="rounded bg-white shadow" style="display: none; position: absolute; width:360px; z-index: 9992;"> 39 <div class="mb-3 bg-dark text-white p-3 rounded-top fw-bold d-flex justify-content-between"> 40 <?php 41 // only output pagination for date ranges between reasonable dates... to prevent ever-crawling bots from going wild 42 ?> 43 <?php if ($date_start > $total_start_date) { ?> 44 <a class="js-quicknav-prev text-decoration-none text-white me-2" href="" data-href="<?= esc_attr(add_query_arg(['start_date' => $prev_dates[0]->format('Y-m-d'), 'end_date' => $prev_dates[1]->format('Y-m-d')], $dashboard_url)); ?>" rel="nofollow">◂</a> 45 <?php } else { ?> 46 <a class="text-decoration-none text-white me-2">◂</a> 39 <?php $show_date_navigation = KokoAnalytics\Dashboard::shows_pagination_links(); ?> 40 <div class="mb-3 bg-dark text-white p-3 rounded-top fw-bold d-flex <?= $show_date_navigation ? 'justify-content-between' : 'justify-content-center'; ?>"> 41 <?php if ($show_date_navigation) { ?> 42 <?php 43 // only output pagination for date ranges between reasonable dates... to prevent ever-crawling bots from going wild 44 // the target URL is assembled in JS from these dates, so no crawlable URL ever appears in the document 45 ?> 46 <?php if ($date_start > $total_start_date) { ?> 47 <a class="js-quicknav-prev text-decoration-none text-white me-2" href="" data-start-date="<?= esc_attr($prev_dates[0]->format('Y-m-d')); ?>" data-end-date="<?= esc_attr($prev_dates[1]->format('Y-m-d')); ?>" rel="nofollow">◂</a> 48 <?php } else { ?> 49 <a class="text-decoration-none text-white me-2">◂</a> 50 <?php } ?> 47 51 <?php } ?> 48 52 <span><?php echo esc_html(wp_date($date_format, $date_start->getTimestamp())); ?> — <?= esc_html(wp_date($date_format, $date_end->getTimestamp())); ?></span> 49 <?php if ($date_end < $total_end_date) { ?> 50 <a class="js-quicknav-next text-decoration-none text-white ms-2" href="" data-href="<?= esc_attr(add_query_arg(['start_date' => $next_dates[0]->format('Y-m-d'), 'end_date' => $next_dates[1]->format('Y-m-d')], $dashboard_url)); ?>" rel="nofollow">▸</a> 51 <?php } else { ?> 52 <a class="text-decoration-none text-white ms-2">▸</a> 53 <?php if ($show_date_navigation) { ?> 54 <?php if ($date_end < $total_end_date) { ?> 55 <a class="js-quicknav-next text-decoration-none text-white ms-2" href="" data-start-date="<?= esc_attr($next_dates[0]->format('Y-m-d')); ?>" data-end-date="<?= esc_attr($next_dates[1]->format('Y-m-d')); ?>" rel="nofollow">▸</a> 56 <?php } else { ?> 57 <a class="text-decoration-none text-white ms-2">▸</a> 58 <?php } ?> 53 59 <?php } ?> 54 60 </div> … … 98 104 <?php $filtered_label = $filtered_post_id > 0 ? get_the_title($filtered_post_id) : $page; ?> 99 105 <a class="" href="<?= esc_url($filtered_permalink); ?>"><?= esc_html($filtered_label); ?></a> 100 <a class="text-decoration-none text-reset ms-2" aria-label="<?php esc_attr_e('Clear page filter', 'koko-analytics'); ?>" title="<?php esc_attr_e('Clear page filter', 'koko-analytics'); ?>" href="<?= esc_attr( remove_query_arg('p')); ?>">✕</a>106 <a class="text-decoration-none text-reset ms-2" aria-label="<?php esc_attr_e('Clear page filter', 'koko-analytics'); ?>" title="<?php esc_attr_e('Clear page filter', 'koko-analytics'); ?>" href="<?= esc_attr(dashboard_url(['p' => null])); ?>">✕</a> 101 107 </div> 102 108 … … 191 197 <?php } ?> 192 198 193 <?php $can_sort = current_user_can('manage_koko_analytics'); ?> 199 <?php // only offer drag & drop where the action that stores the new order runs: it is registered from the admin controller, so a save from the public dashboard would silently do nothing ?> 200 <?php $can_sort = is_admin() && current_user_can('manage_koko_analytics'); ?> 194 201 <div id="ka-components" class="ka-row ka-row-cols-1 ka-row-cols-xl-2 g-3 mb-3 <?= $page !== 0 ? 'page-filter-active' : ''; ?>" <?= $can_sort ? 'data-nonce="' . esc_attr(wp_create_nonce('koko_analytics_save_component_order')) . '"' : ''; ?>> 195 202 <?php foreach ($this->get_components() as $id => $callback) : ?> -
koko-analytics/tags/2.5.3/src/Resources/views/settings/help.php
r3581336 r3704935 24 24 <div class="mb-5"> 25 25 <ul class="ul-square"> 26 <li><a href="https://www.kokoanalytics.com/pricing/?utm_source=koko-analytics&utm_medium=link&utm_campaign=free-plugin-settings-help">Upgrade to Koko Analytics Pro</a> to get direct email support from the plugin builder.</li> 26 <?php if (defined('KOKO_ANALYTICS_PRO_VERSION')) : ?> 27 <?php /* translators: 1: opening anchor tag, 2: closing anchor tag. */ ?> 28 <li><?php printf(esc_html__('As a Koko Analytics Pro user, you can email us directly at %1$ssupport@kokoanalytics.com%2$s for help.', 'koko-analytics'), '<a href="mailto:support@kokoanalytics.com">', '</a>'); ?></li> 29 <?php else : ?> 30 <li><a href="https://www.kokoanalytics.com/pricing/?utm_source=koko-analytics&utm_medium=link&utm_campaign=free-plugin-settings-help">Upgrade to Koko Analytics Pro</a> to get direct email support from the plugin builder.</li> 31 <?php endif; ?> 32 27 33 <?php /* translators: 1: opening anchor tag, 2: closing anchor tag. */ ?> 28 34 <li><?php printf(esc_html__('Have a look at our %1$sknowledge base%2$s for help with configuring and using Koko Analytics.', 'koko-analytics'), '<a href="https://www.kokoanalytics.com/docs/#utm_source=koko-analytics&utm_medium=link&utm_campaign=free-plugin-settings-help-docs" target="_blank">', '</a>'); ?></li> -
koko-analytics/tags/2.5.3/src/Rest.php
r3581336 r3704935 113 113 public function permission_callback(): bool 114 114 { 115 $settings = get_settings(); 116 $is_dashboard_public = $settings['is_dashboard_public']; 117 return $is_dashboard_public || current_user_can('view_koko_analytics'); 115 return can_view_dashboard(); 118 116 } 119 117 -
koko-analytics/tags/2.5.3/src/Upserter.php
r3581336 r3704935 7 7 class Upserter 8 8 { 9 private const DATABASE_BATCH_SIZE = 500; 10 9 11 protected wpdb $db; 10 12 protected string $table; … … 23 25 /** 24 26 * @param array<string> $values 25 * @return array< string, int> Map of value to ID in database27 * @return array<array-key, int> Map of value to ID in database 26 28 */ 27 29 public function upsert(array $values): array … … 35 37 $values = array_unique($values); 36 38 37 // INSERT IGNORE all deduplicated values into the database table 38 $placeholders = rtrim(str_repeat('(%s),', count($values)), ','); 39 $this->db->query($this->db->prepare("INSERT IGNORE INTO {$this->table}({$this->column}) VALUES {$placeholders}", $values)); 39 $ids = []; 40 foreach (array_chunk($values, self::DATABASE_BATCH_SIZE) as $batch) { 41 // INSERT IGNORE all deduplicated values into the database table 42 $placeholders = rtrim(str_repeat('(%s),', count($batch)), ','); 43 $this->db->query($this->db->prepare("INSERT IGNORE INTO {$this->table}({$this->column}) VALUES {$placeholders}", $batch)); 40 44 41 // retrieve all entries from the database table to get their normalized ID's 42 $placeholders = rtrim(str_repeat('%s,', count($values)), ','); 43 $results = $this->db->get_results($this->db->prepare("SELECT id, {$this->column} FROM {$this->table} WHERE {$this->column} IN({$placeholders})", $values)); 45 // retrieve all entries from the database table to get their normalized ID's 46 $placeholders = rtrim(str_repeat('%s,', count($batch)), ','); 47 $results = $this->db->get_results($this->db->prepare("SELECT id, {$this->column} FROM {$this->table} WHERE {$this->column} IN({$placeholders})", $batch)); 48 $ids = array_replace($ids, array_column($results, 'id', $this->column)); 49 } 44 50 45 return array_column($results, 'id', $this->column);51 return $ids; 46 52 } 47 53 } -
koko-analytics/trunk/CHANGELOG.md
r3634028 r3704935 1 1 # Changelog 2 3 = 2.5.3 = 4 5 - security: enforce the same access checks for public dashboard pages and REST API requests, fixing bypasses of Pro password protection. 6 - dashboard: reduce crawler and cache load by only accepting read requests, canonicalizing query parameters, and hiding pagination and date navigation on public dashboards. 7 - dashboard: only enable component sorting in the admin dashboard, where changes can be saved. 8 - perf: process aggregation buffers in chunks to reduce peak memory usage on busy sites. 9 - docs: show the dedicated support address to Pro users on the help page. 10 2 11 3 12 = 2.5.2 = … … 7 16 - data: increase the aggregation interval to 65 seconds to reduce the frequency of scheduling conflicts. 8 17 - ux: show admin notice with location of analytics dashboard after initial plugin activation. 18 9 19 10 20 = 2.5.1 = -
koko-analytics/trunk/assets/css/dashboard.css
r3605984 r3704935 244 244 } 245 245 .page-filter-active tr, 246 .page-filter-active .ka-pagination {246 .page-filter-active .ka-pagination2 { 247 247 opacity: 0.5; 248 248 } … … 587 587 588 588 /* pagination */ 589 590 /* old style pagination, used in Pro components */591 .ka-pagination {592 display: flex;593 padding: 0.5rem 1rem;594 border-top: 1px solid rgb(240, 240, 240);595 }596 .ka-pagination--next {597 margin-left: auto;598 }599 589 600 590 /* new style pagination (2026-07-03) */ -
koko-analytics/trunk/assets/js/dashboard.js
r3548865 r3704935 1 (()=>{function l(t){this.root=t,this.barWidth=0,this.closeTooltipTimeout=null,this.root&&(this.tooltip=document.querySelector(".ka-chart--tooltip"),this.tooltip.remove(),document.body.appendChild(this.tooltip),[this.root,this.tooltip].forEach(i=>{i.addEventListener("mouseleave",this.closeTooltip.bind(this)),i.addEventListener("mouseenter",this.keepTooltip.bind(this))}),this.root.addEventListener("mouseover",i=>{i.target.tagName==="rect"&&this.showTooltip(i.target)}),this.redraw())}l.prototype.closeTooltip=function(){this.closeTooltipTimeout=window.setTimeout(()=>{this.tooltip.style.display="none"},20)};l.prototype.keepTooltip=function(){window.clearTimeout(this.closeTooltipTimeout)};l.prototype.showTooltip=function(t){let i=t.parentElement.dataset;this.tooltip.querySelector(".ka-chart--tooltip-heading").textContent=i.date,this.tooltip.querySelector(".ka--visitors").children[0].textContent=i.visitors,this.tooltip.querySelector(".ka--pageviews").children[0].textContent=i.pageviews;let o=document.body.clientWidth;this.tooltip.style.display="block";let e=t.parentElement.getBoundingClientRect(),r=Math.round(e.left-.5*this.tooltip.clientWidth+.5*this.barWidth),a=Math.round(e.top+window.scrollY-this.tooltip.clientHeight),s=0;r<12?s=-r+12:r+this.tooltip.clientWidth>o-12&&(s=o-(r+this.tooltip.clientWidth)-12),r+=s;let y=this.tooltip.querySelector(".ka-chart--tooltip-arrow");y.style.marginLeft=s===0?"auto":.5*this.tooltip.clientWidth-6-s+"px",this.tooltip.style.left=r+"px",this.tooltip.style.top=a+"px"};l.prototype.redraw=function(){let t=Array.from(this.root.querySelectorAll(".axes-y text")).reduce((e,r)=>Math.max(e,8+Math.max(5,r.textContent.length*8)),0),i=this.root.querySelectorAll(".bars g"),o=Math.max(1,(this.root.clientWidth-t)/i.length);this.barWidth=Math.max(1,o-2);for(let e=0;e<i.length;e++){let r=e*o+t+1,a=i[e].children;a[0].setAttribute("x",r),a[0].setAttribute("width",this.barWidth),a[1].setAttribute("x",r),a[1].setAttribute("width",this.barWidth);let s=e*o+t+.5*o;a[2].style.display=this.barWidth===1?"none":"",a[2].setAttribute("x1",s),a[2].setAttribute("x2",s)}};var n=document.getElementById("ka-components");if(n&&n.querySelector(".ka-col[data-sortable]")){let t=null,i=n.querySelectorAll(".ka-col[data-sortable]");n.addEventListener("mousedown",function(o){if(o.target.closest("th")){let e=o.target.closest(".ka-col");e&&(e.draggable=!0)}}),document.addEventListener("mouseup",function(){i.forEach(o=>{o.draggable=!1})}),n.addEventListener("dragstart",function(o){t=o.target.closest(".ka-col"),t&&(t.classList.add("ka-dragging"),o.dataTransfer.effectAllowed="move")}),n.addEventListener("dragover",function(o){o.preventDefault(),o.dataTransfer.dropEffect="move";let e=o.target.closest(".ka-col");!e||e===t||(t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING?n.insertBefore(t,e.nextSibling):n.insertBefore(t,e))}),n.addEventListener("dragend",function(){if(!t)return;t.classList.remove("ka-dragging"),t=null,i.forEach(function(r){r.draggable=!1});let o=Array.from(n.querySelectorAll(".ka-col[data-sortable]")).map(r=>r.id),e=new FormData;e.append("koko_analytics_action","save_component_order"),e.append("_nonce",n.dataset.nonce),o.forEach(r=>{e.append("component_order[]",r)}),window.fetch(window.location.href,{method:"POST",body:e})})}var d=window.setTimeout(c,61e3);function c(){document.hidden?d=window.setTimeout(c,61e3):window.location.reload()}document.addEventListener("mouseover",function(){window.clearTimeout(d),d=window.setTimeout(c,61e3)});var u=document.querySelector("#ka-date-presets"),h=document.querySelector("#ka-date-start"),p=document.querySelector("#ka-date-end");u&&u.addEventListener("change",function(){h.disabled=!0,p.disabled=!0,this.form.submit()});function f(){u.value="custom"}h&&h.addEventListener("change",f);p&&p.addEventListener("change",f);document.addEventListener("keydown",function(t){if(!t.defaultPrevented)switch(t.key){case"ArrowLeft":document.querySelector(".js-quicknav-prev").click();break;case"ArrowRight":document.querySelector(".js-quicknav-next").click();break}});document.querySelectorAll("a[data-href]").forEach(function(t){t.addEventListener("click",function(i){i.preventDefault(),window.location.href=t.getAttribute("data-href")})});document.addEventListener("DOMContentLoaded",()=>{new l(document.getElementById("ka-chart"))});function w(){sessionStorage.setItem("scrollX",window.pageXOffset),sessionStorage.setItem("scrollY",window.pageYOffset)}document.addEventListener("click",w);window.addEventListener("beforeunload",w);var m=parseInt(sessionStorage.getItem("scrollX")??0),g=parseInt(sessionStorage.getItem("scrollY")??0);(m!=0||g!=0)&&window.scroll(m,g);})();1 (()=>{function d(t){this.root=t,this.barWidth=0,this.closeTooltipTimeout=null,this.root&&(this.tooltip=document.querySelector(".ka-chart--tooltip"),this.tooltip.remove(),document.body.appendChild(this.tooltip),[this.root,this.tooltip].forEach(r=>{r.addEventListener("mouseleave",this.closeTooltip.bind(this)),r.addEventListener("mouseenter",this.keepTooltip.bind(this))}),this.root.addEventListener("mouseover",r=>{r.target.tagName==="rect"&&this.showTooltip(r.target)}),this.redraw())}d.prototype.closeTooltip=function(){this.closeTooltipTimeout=window.setTimeout(()=>{this.tooltip.style.display="none"},20)};d.prototype.keepTooltip=function(){window.clearTimeout(this.closeTooltipTimeout)};d.prototype.showTooltip=function(t){let r=t.parentElement.dataset;this.tooltip.querySelector(".ka-chart--tooltip-heading").textContent=r.date,this.tooltip.querySelector(".ka--visitors").children[0].textContent=r.visitors,this.tooltip.querySelector(".ka--pageviews").children[0].textContent=r.pageviews;let o=document.body.clientWidth;this.tooltip.style.display="block";let e=t.parentElement.getBoundingClientRect(),i=Math.round(e.left-.5*this.tooltip.clientWidth+.5*this.barWidth),a=Math.round(e.top+window.scrollY-this.tooltip.clientHeight),s=0;i<12?s=-i+12:i+this.tooltip.clientWidth>o-12&&(s=o-(i+this.tooltip.clientWidth)-12),i+=s;let b=this.tooltip.querySelector(".ka-chart--tooltip-arrow");b.style.marginLeft=s===0?"auto":.5*this.tooltip.clientWidth-6-s+"px",this.tooltip.style.left=i+"px",this.tooltip.style.top=a+"px"};d.prototype.redraw=function(){let t=Array.from(this.root.querySelectorAll(".axes-y text")).reduce((e,i)=>Math.max(e,8+Math.max(5,i.textContent.length*8)),0),r=this.root.querySelectorAll(".bars g"),o=Math.max(1,(this.root.clientWidth-t)/r.length);this.barWidth=Math.max(1,o-2);for(let e=0;e<r.length;e++){let i=e*o+t+1,a=r[e].children;a[0].setAttribute("x",i),a[0].setAttribute("width",this.barWidth),a[1].setAttribute("x",i),a[1].setAttribute("width",this.barWidth);let s=e*o+t+.5*o;a[2].style.display=this.barWidth===1?"none":"",a[2].setAttribute("x1",s),a[2].setAttribute("x2",s)}};var n=document.getElementById("ka-components");if(n&&n.querySelector(".ka-col[data-sortable]")){let t=null,r=n.querySelectorAll(".ka-col[data-sortable]");n.addEventListener("mousedown",function(o){if(o.target.closest("th")){let e=o.target.closest(".ka-col");e&&(e.draggable=!0)}}),document.addEventListener("mouseup",function(){r.forEach(o=>{o.draggable=!1})}),n.addEventListener("dragstart",function(o){t=o.target.closest(".ka-col"),t&&(t.classList.add("ka-dragging"),o.dataTransfer.effectAllowed="move")}),n.addEventListener("dragover",function(o){o.preventDefault(),o.dataTransfer.dropEffect="move";let e=o.target.closest(".ka-col");!e||e===t||(t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING?n.insertBefore(t,e.nextSibling):n.insertBefore(t,e))}),n.addEventListener("dragend",function(){if(!t)return;t.classList.remove("ka-dragging"),t=null,r.forEach(function(i){i.draggable=!1});let o=Array.from(n.querySelectorAll(".ka-col[data-sortable]")).map(i=>i.id),e=new FormData;e.append("koko_analytics_action","save_component_order"),e.append("_nonce",n.dataset.nonce),o.forEach(i=>{e.append("component_order[]",i)}),window.fetch(window.location.href,{method:"POST",body:e})})}var l=window.setTimeout(c,61e3);function c(){document.hidden?l=window.setTimeout(c,61e3):window.location.reload()}document.addEventListener("mouseover",function(){window.clearTimeout(l),l=window.setTimeout(c,61e3)});function m(t){var r=new URL(window.location.href),o=new URLSearchParams(new FormData(t));o.sort(),r.search=o.toString(),window.location.href=r.toString()}var f=document.querySelector("#ka-datepicker-dropdown form");f&&f.addEventListener("submit",function(t){t.preventDefault(),m(this)});var u=document.querySelector("#ka-date-presets"),h=document.querySelector("#ka-date-start"),p=document.querySelector("#ka-date-end");u&&u.addEventListener("change",function(){h.disabled=!0,p.disabled=!0,m(this.form)});function w(){u.value="custom"}h&&h.addEventListener("change",w);p&&p.addEventListener("change",w);document.addEventListener("keydown",function(t){if(!t.defaultPrevented){var r;switch(t.key){case"ArrowLeft":r=document.querySelector(".js-quicknav-prev");break;case"ArrowRight":r=document.querySelector(".js-quicknav-next");break}r&&r.click()}});document.querySelectorAll("a[data-start-date][data-end-date]").forEach(function(t){t.addEventListener("click",function(r){r.preventDefault();var o=new URL(window.location.href),e=new URLSearchParams;["page","koko-analytics-dashboard"].forEach(function(i){o.searchParams.has(i)&&e.set(i,o.searchParams.get(i))}),e.set("start_date",t.getAttribute("data-start-date")),e.set("end_date",t.getAttribute("data-end-date")),e.sort(),o.search=e.toString(),window.location.href=o.toString()})});document.addEventListener("DOMContentLoaded",()=>{new d(document.getElementById("ka-chart"))});function y(){sessionStorage.setItem("scrollX",window.pageXOffset),sessionStorage.setItem("scrollY",window.pageYOffset)}document.addEventListener("click",y);window.addEventListener("beforeunload",y);var g=parseInt(sessionStorage.getItem("scrollX")??0),v=parseInt(sessionStorage.getItem("scrollY")??0);(g!=0||v!=0)&&window.scroll(g,v);})(); -
koko-analytics/trunk/assets/js/src/imports/datepicker.js
r3500426 r3704935 1 2 // navigate to the date range form its target URL ourselves, so that we can sort the query string 3 // keeping the query args in a fixed order means the same view always maps to the same URL, which is 4 // friendlier to any cache sitting in front of the (public) dashboard 5 function submitDateForm(form) { 6 var url = new URL(window.location.href); 7 var params = new URLSearchParams(new FormData(form)); 8 params.sort(); 9 url.search = params.toString(); 10 window.location.href = url.toString(); 11 } 12 13 var dateForm = document.querySelector('#ka-datepicker-dropdown form'); 14 dateForm && dateForm.addEventListener('submit', function(evt) { 15 evt.preventDefault(); 16 submitDateForm(this); 17 }); 1 18 2 19 // update date_start and date_end <input>'s whenever a preset is selected … … 7 24 dateStartInput.disabled = true; 8 25 dateEndInput.disabled = true; 9 this.form.submit();26 submitDateForm(this.form); 10 27 }); 11 28 … … 19 36 20 37 // click "prev date range" or "next date range" when using arrow keys 38 // note these links are not always there, eg on the public dashboard or at the edges of the available date range 21 39 document.addEventListener('keydown', function (evt) { 22 40 if (evt.defaultPrevented) { … … 24 42 } 25 43 44 var quicknav; 26 45 switch (evt.key) { 27 46 case 'ArrowLeft': 28 document.querySelector('.js-quicknav-prev').click();47 quicknav = document.querySelector('.js-quicknav-prev'); 29 48 break; 30 49 case 'ArrowRight': 31 document.querySelector('.js-quicknav-next').click();50 quicknav = document.querySelector('.js-quicknav-next'); 32 51 break; 33 52 } 53 54 quicknav && quicknav.click(); 34 55 }) 35 56 36 57 // fake <a> elements to stop bots from crawling infinitely 37 document.querySelectorAll('a[data-href]').forEach(function(el) { 58 // the HTML only carries the target dates, we assemble the URL here so the document holds no crawlable link 59 // note we keep just the query args identifying the dashboard itself, dropping any active filter or pagination 60 document.querySelectorAll('a[data-start-date][data-end-date]').forEach(function(el) { 38 61 el.addEventListener('click', function(evt) { 39 62 evt.preventDefault(); 40 window.location.href = el.getAttribute('data-href'); 63 64 var url = new URL(window.location.href); 65 var params = new URLSearchParams(); 66 ['page', 'koko-analytics-dashboard'].forEach(function(key) { 67 if (url.searchParams.has(key)) { 68 params.set(key, url.searchParams.get(key)); 69 } 70 }); 71 params.set('start_date', el.getAttribute('data-start-date')); 72 params.set('end_date', el.getAttribute('data-end-date')); 73 params.sort(); 74 url.search = params.toString(); 75 window.location.href = url.toString(); 41 76 }); 42 77 }); -
koko-analytics/trunk/data/referrer-blocklist
r3597329 r3704935 489 489 concretepol.com 490 490 connectikastudio.com 491 considerin.net 491 492 constanceonline.top 492 493 cookie-law-enforcement-aa.xyz … … 1490 1491 oracle-patches.ru 1491 1492 orakul.spb.ru 1493 osdzc.com 1492 1494 osteochondrosis.ru 1493 1495 otdbiaxaem-vmeste.ru … … 1654 1656 rcb101.ru 1655 1657 rczhan.com 1658 readlife.net 1656 1659 realresultslist.com 1657 1660 recinziireale.com … … 1939 1942 suckoutlet.xyz 1940 1943 sudachitravel.com 1944 sunblog.asia 1941 1945 sundrugstore.com 1942 1946 super-seo-guru.com … … 2333 2337 zelena-mriya.com.ua 2334 2338 zexhgoods.xyz 2339 zeyao.net 2335 2340 zhcsapp.net 2336 2341 zhoobintravel.com -
koko-analytics/trunk/koko-analytics.php
r3634028 r3704935 4 4 Plugin Name: Koko Analytics 5 5 Plugin URI: https://www.kokoanalytics.com/#utm_source=koko-analytics&utm_medium=link&utm_campaign=free-plugin-plugin-header 6 Version: 2.5. 26 Version: 2.5.3 7 7 Description: Privacy-friendly and efficient statistics for your WordPress site. 8 8 Author: ibericode … … 43 43 PHP_VERSION_ID >= 70400 || exit; 44 44 45 define('KOKO_ANALYTICS_VERSION', '2.5. 2');45 define('KOKO_ANALYTICS_VERSION', '2.5.3'); 46 46 define('KOKO_ANALYTICS_PLUGIN_FILE', __FILE__); 47 47 define('KOKO_ANALYTICS_PLUGIN_DIR', __DIR__); -
koko-analytics/trunk/readme.txt
r3674437 r3704935 3 3 Tags: analytics, google analytics, privacy, statistics, website statistics 4 4 Requires at least: 6.2 5 Tested up to: 7. 16 Stable tag: 2.5. 25 Tested up to: 7.0 6 Stable tag: 2.5.3 7 7 License: GPL-3.0-or-later 8 8 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 14 14 15 15 Koko Analytics is a privacy-friendly and lightweight Google Analytics alternative for WordPress sites. It is a plug and play solution to get GDPR compliant insights into your website visitors without requiring any third-party services. 16 17 [youtube https://www.youtube.com/watch?v=4o0Rp3F4qh8]18 16 19 17 == Why choose Koko Analytics? == … … 126 124 == Changelog == 127 125 126 = 2.5.3 = 127 128 - security: enforce the same access checks for public dashboard pages and REST API requests, fixing bypasses of Pro password protection. 129 - dashboard: reduce crawler and cache load by only accepting read requests, canonicalizing query parameters, and hiding pagination and date navigation on public dashboards. 130 - dashboard: only enable component sorting in the admin dashboard, where changes can be saved. 131 - perf: process aggregation buffers in chunks to reduce peak memory usage on busy sites. 132 - docs: show the dedicated support address to Pro users on the help page. 133 134 128 135 = 2.5.2 = 129 136 … … 132 139 - data: increase the aggregation interval to 65 seconds to reduce the frequency of scheduling conflicts. 133 140 - ux: show admin notice with location of analytics dashboard after initial plugin activation. 141 134 142 135 143 = 2.5.1 = … … 156 164 - compatibility: raise the minimum supported WordPress version to 6.2. 157 165 158 159 = 2.4.0 =160 161 - tracking: hook into the visibilitychange event again to ignore prerender requests.162 - data: rewrite exporter and importer to use NDJSON instead of raw SQL.163 - import: fix column names for the referrer stats table.164 - rest: clamp date range for unauthenticated users to prevent large table scans.165 - review notice: simplify the review notice to one primary action.166 167 166 [View the full changelog on GitHub](https://github.com/ibericode/koko-analytics/blob/main/CHANGELOG.md) -
koko-analytics/trunk/src/Aggregator.php
r3581336 r3704935 13 13 class Aggregator 14 14 { 15 private const CHUNK_SIZE = 10000; 16 15 17 /** 16 * Reads the buffer file in to memory and moves data into the MySQL database (in bulk)18 * Reads the buffer file in chunks and moves data into the MySQL database. 17 19 * 18 20 * @throws Exception … … 29 31 // init pageview aggregator 30 32 $pageview_aggregator = new Pageview_Aggregator(); 33 $records_in_chunk = 0; 31 34 32 35 // rename file to temporary location so nothing new is written to it while we process it … … 71 74 // add-on aggregators 72 75 do_action('koko_analytics_aggregate_line', $type, $params); 76 77 ++$records_in_chunk; 78 if ($records_in_chunk === self::CHUNK_SIZE) { 79 $this->flush($pageview_aggregator); 80 $records_in_chunk = 0; 81 } 73 82 } 83 84 if ($records_in_chunk > 0) { 85 $this->flush($pageview_aggregator); 86 } 87 88 // Finalize state that remains bounded across chunks, such as realtime counts. 89 $pageview_aggregator->finish(); 74 90 75 91 // close file & remove it from filesystem … … 77 93 \unlink($tmp_filename); 78 94 79 // tell aggregators to write their results to the database 80 $pageview_aggregator->finish(); 95 // signal that the entire buffer file was processed 81 96 do_action('koko_analytics_aggregate_finish'); 82 97 83 98 update_option('koko_analytics_last_aggregation_at', \time(), false); 84 99 } 100 101 private function flush(Pageview_Aggregator $pageview_aggregator): void 102 { 103 $pageview_aggregator->finish(false); 104 105 // Allow add-on aggregators to persist and reset their in-memory state. 106 do_action('koko_analytics_aggregate_chunk_finish'); 107 } 85 108 } -
koko-analytics/trunk/src/Chart_View.php
r3597329 r3704935 44 44 <div class="ka-chart-group"> 45 45 <?php foreach ($group_options as $key => $option) : ?> 46 <a href="<?= esc_attr( add_query_arg(['group' => $key])); ?>" class="<?php echo $group === $key ? 'on' : ''; ?>" rel="nofollow"><?php echo esc_html($option['label']); ?></a>46 <a href="<?= esc_attr(dashboard_url(['group' => $key])); ?>" class="<?php echo $group === $key ? 'on' : ''; ?>" rel="nofollow"><?php echo esc_html($option['label']); ?></a> 47 47 <?php endforeach; ?> 48 48 </div> -
koko-analytics/trunk/src/Dashboard.php
r3597329 r3704935 17 17 public const MAX_OFFSET = 10000; 18 18 19 private static bool $show_pagination_links = true; 20 19 21 public static function clamp_limit($value, int $default = 10, int $minimum = 1): int 20 22 { … … 39 41 $items_per_page = (int) apply_filters('koko_analytics_items_per_page', 20); 40 42 $date_format = get_option('date_format', 'Y-m-d'); 41 $dashboard_url = $this->get_base_url();42 43 43 44 // parse query params … … 272 273 } 273 274 274 public function pagination(string $key, int $offset, int $limit, int $count): void 275 /** 276 * Hides the previous/next links in the datepicker and in every pagination component rendered from here on. 277 */ 278 public static function hide_pagination_links(): void 279 { 280 self::$show_pagination_links = false; 281 } 282 283 /** 284 * Whether previous/next links should be rendered at all. 285 */ 286 public static function shows_pagination_links(): bool 287 { 288 return self::$show_pagination_links; 289 } 290 291 /** 292 * Renders the pagination component for a dashboard component. 293 * 294 * @param string $key The query string key holding the offset & limit for this component. 295 * @param array $extra_args Additional query args to preserve in the previous/next links. 296 */ 297 public static function pagination(string $key, int $offset, int $limit, int $count, array $extra_args = []): void 275 298 { 276 299 if ($offset >= $limit || $offset + $limit < $count) { … … 283 306 ?> 284 307 </span> 308 <?php if (self::$show_pagination_links) : ?> 285 309 <span> 286 310 <?php if ($offset >= $limit) : ?> 287 <a href="<?php echo esc_attr( add_query_arg(['p' => null, $key => $offset >= $limit * 2 ? ['offset' => $offset - $limit, 'limit' => $limit] : null])); ?>" rel="nofollow">← <?php esc_html_e('Previous', 'koko-analytics'); ?></a>311 <a href="<?php echo esc_attr(dashboard_url(['p' => null, $key => $offset >= $limit * 2 ? array_merge($extra_args, ['offset' => $offset - $limit, 'limit' => $limit]) : ($extra_args ?: null) ])); ?>" rel="nofollow">← <?php esc_html_e('Previous', 'koko-analytics'); ?></a> 288 312 <?php else : ?> 289 313 <span class="ka-pagination2-muted">← <?php esc_html_e('Previous', 'koko-analytics'); ?></span> … … 291 315 <span> · </span> 292 316 <?php if ($offset + $limit < $count) : ?> 293 <a href="<?php echo esc_attr( add_query_arg(['p' => null, $key => ['offset' => $offset + $limit, 'limit' => $limit]])); ?>" rel="nofollow"><?php esc_html_e('Next', 'koko-analytics'); ?> →</a>317 <a href="<?php echo esc_attr(dashboard_url(['p' => null, $key => array_merge($extra_args, ['offset' => $offset + $limit, 'limit' => $limit])])); ?>" rel="nofollow"><?php esc_html_e('Next', 'koko-analytics'); ?> →</a> 294 318 <?php else : ?> 295 319 <span class="ka-pagination2-muted"><?php esc_html_e('Next', 'koko-analytics'); ?> →</span> 296 320 <?php endif; ?> 297 321 </span> 322 <?php endif; ?> 298 323 </div> 299 324 <?php … … 340 365 <td class="text-muted"><?= esc_html((string) ($offset + $i + 1)); ?></td> 341 366 <td class="text-truncate"> 342 <a href="<?= esc_attr( add_query_arg(['p' => $page_filter])); ?>"><?= esc_html($p->label); ?></a>367 <a href="<?= esc_attr(dashboard_url(['p' => $page_filter])); ?>"><?= esc_html($p->label); ?></a> 343 368 <a class="ka-visit-link" href="<?= esc_url($p->post_permalink); ?>" target="_blank" rel="noopener" title="<?php esc_attr_e('View page', 'koko-analytics'); ?>"><i class="icon icon-sm icon-external-link" aria-hidden="true"></i></a> 344 369 </td> … … 354 379 <?php } ?> 355 380 356 <?php $this->pagination('posts', $offset, $limit, $count); ?>381 <?php self::pagination('posts', $offset, $limit, $count); ?> 357 382 358 383 <?php … … 406 431 <?php } ?> 407 432 408 <?php $this->pagination('referrers', $offset, $limit, $count); ?>433 <?php self::pagination('referrers', $offset, $limit, $count); ?> 409 434 410 435 <?php -
koko-analytics/trunk/src/Dashboard_Public.php
r3581336 r3704935 20 20 $settings = get_settings(); 21 21 $today = (new DateTimeImmutable('now', wp_timezone()))->format('Y-m-d'); 22 if (!$settings['is_dashboard_public'] && !current_user_can('view_koko_analytics')) { 23 return; 22 23 // this dashboard is a read-only view, so only ever answer GET (and HEAD) requests 24 // any other method is uncacheable, and would be a way to force a full render past every cache 25 $method = wp_unslash($_SERVER['REQUEST_METHOD'] ?? 'GET'); 26 if ($method !== 'GET' && $method !== 'HEAD') { 27 status_header(405); 28 header('Allow: GET, HEAD'); 29 exit; 24 30 } 25 31 … … 32 38 } 33 39 40 // redirect to the canonical URL for this view, so that visitors arriving through a link that 41 // was shared or crawled hit the same cache entry as everyone else 42 $request_uri = wp_unslash($_SERVER['REQUEST_URI'] ?? ''); 43 $canonical = sort_query_string($request_uri); 44 if ($canonical !== $request_uri) { 45 wp_safe_redirect($canonical, 301); 46 exit; 47 } 48 49 if (! can_view_dashboard($settings)) { 50 do_action('koko_analytics_public_dashboard_access_denied', $settings); 51 return; 52 } 53 54 // the public dashboard is crawlable, so don't hand out paginated URLs for bots to follow 55 self::hide_pagination_links(); 56 34 57 do_action('koko_analytics_public_dashboard_headers'); 35 58 … … 37 60 header("X-Robots-Tag: noindex, nofollow"); 38 61 if (is_user_logged_in()) { 39 header("Cache-Control: no-store, must-revalidate, no-cache, max-age=0, private");62 $cache_control = 'no-store, must-revalidate, no-cache, max-age=0, private'; 40 63 } elseif (isset($_GET['end_date'], $_GET['start_date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', wp_unslash($_GET['start_date'])) && preg_match('/^\d{4}-\d{2}-\d{2}$/', wp_unslash($_GET['end_date'])) && wp_unslash($_GET['end_date']) < $today) { 41 header("Cache-Control: public, max-age=68400");64 $cache_control = 'public, max-age=68400'; 42 65 } else { 43 header("Cache-Control: public, max-age=60");66 $cache_control = 'public, max-age=60'; 44 67 } 68 $cache_control = (string) apply_filters('koko_analytics_public_dashboard_cache_control', $cache_control, $settings); 69 header("Cache-Control: {$cache_control}"); 45 70 46 71 require KOKO_ANALYTICS_PLUGIN_DIR . '/src/Resources/views/dashboard-public.php'; -
koko-analytics/trunk/src/Pageview_Aggregator.php
r3581336 r3704935 17 17 class Pageview_Aggregator 18 18 { 19 private const DATABASE_BATCH_SIZE = 500; 20 19 21 protected wpdb $db; 20 22 protected DateTimeZone $timezone; … … 84 86 } 85 87 86 public function finish( ): void88 public function finish(bool $update_realtime = true): void 87 89 { 88 90 $this->commit_site_stats(); 89 91 $this->commit_post_stats(); 90 92 $this->commit_referrer_stats(); 91 $this->update_realtime_pageview_count(); 93 if ($update_realtime) { 94 $this->update_realtime_pageview_count(); 95 } 92 96 } 93 97 … … 103 107 private function commit_post_stats(): void 104 108 { 105 $upserter = new Upserter('paths', 'path' );109 $upserter = new Upserter('paths', 'path', $this->db); 106 110 107 111 // insert page-specific stats 108 112 foreach ($this->post_stats as $date => $stats) { 109 $path_ids = $upserter->upsert(array_keys($stats)); 110 $values = []; 111 foreach ($stats as $path => $r) { 112 array_push($values, $date, $path_ids[$path], $r->post_id, $r->visitors, $r->pageviews); 113 foreach (array_chunk($stats, self::DATABASE_BATCH_SIZE, true) as $batch) { 114 $path_ids = $upserter->upsert(array_keys($batch)); 115 $values = []; 116 foreach ($batch as $path => $r) { 117 array_push($values, $date, $path_ids[$path], $r->post_id, $r->visitors, $r->pageviews); 118 } 119 $placeholders = rtrim(str_repeat('(%s,%d,%d,%d,%d),', count($batch)), ','); 120 $this->db->query($this->db->prepare("INSERT INTO {$this->db->prefix}koko_analytics_post_stats(date, path_id, post_id, visitors, pageviews) VALUES {$placeholders} ON DUPLICATE KEY UPDATE visitors = visitors + VALUES(visitors), pageviews = pageviews + VALUES(pageviews)", $values)); 113 121 } 114 $placeholders = rtrim(str_repeat('(%s,%d,%d,%d,%d),', count($stats)), ',');115 $this->db->query($this->db->prepare("INSERT INTO {$this->db->prefix}koko_analytics_post_stats(date, path_id, post_id, visitors, pageviews) VALUES {$placeholders} ON DUPLICATE KEY UPDATE visitors = visitors + VALUES(visitors), pageviews = pageviews + VALUES(pageviews)", $values));116 122 } 117 123 … … 121 127 private function commit_referrer_stats(): void 122 128 { 123 $upserter = new Upserter('referrer_labels', 'value' );129 $upserter = new Upserter('referrer_labels', 'value', $this->db); 124 130 125 131 // insert referrer stats 126 132 foreach ($this->referrer_stats as $date => $stats) { 127 $referrer_ids = $upserter->upsert(array_keys($stats)); 128 $values = []; 129 foreach ($stats as $url => $r) { 130 array_push($values, $date, $referrer_ids[$url], $r->visitors, $r->pageviews); 133 foreach (array_chunk($stats, self::DATABASE_BATCH_SIZE, true) as $batch) { 134 $referrer_ids = $upserter->upsert(array_keys($batch)); 135 $values = []; 136 foreach ($batch as $url => $r) { 137 array_push($values, $date, $referrer_ids[$url], $r->visitors, $r->pageviews); 138 } 139 $placeholders = rtrim(str_repeat('(%s,%d,%d,%d),', count($batch)), ','); 140 $this->db->query($this->db->prepare("INSERT INTO {$this->db->prefix}koko_analytics_referrer_stats(date, id, unique_hits, hits) VALUES {$placeholders} ON DUPLICATE KEY UPDATE unique_hits = unique_hits + VALUES(unique_hits), hits = hits + VALUES(hits)", $values)); 131 141 } 132 $placeholders = rtrim(str_repeat('(%s,%d,%d,%d),', count($stats)), ',');133 $this->db->query($this->db->prepare("INSERT INTO {$this->db->prefix}koko_analytics_referrer_stats(date, id, unique_hits, hits) VALUES {$placeholders} ON DUPLICATE KEY UPDATE unique_hits = unique_hits + VALUES(unique_hits), hits = hits + VALUES(hits)", $values));134 142 } 135 143 -
koko-analytics/trunk/src/Resources/external-strings.php
r3605984 r3704935 3 3 // i18n strings for koko-analytics-pro 4 4 5 // ../koko-analytics-pro/src/UTM/views/dashboard.php# 185 // ../koko-analytics-pro/src/UTM/views/dashboard.php#20 6 6 __('Top referrers', 'koko-analytics'); 7 7 8 // ../koko-analytics-pro/src/UTM/views/dashboard.php# 198 // ../koko-analytics-pro/src/UTM/views/dashboard.php#21 9 9 __('Where your visitors came from', 'koko-analytics'); 10 10 11 // ../koko-analytics-pro/src/UTM/views/dashboard.php#2 711 // ../koko-analytics-pro/src/UTM/views/dashboard.php#29 12 12 __('Referrers report type', 'koko-analytics'); 13 13 14 // ../koko-analytics-pro/src/UTM/views/dashboard.php#3 414 // ../koko-analytics-pro/src/UTM/views/dashboard.php#36 15 15 __('A visitor represents the number of sessions during which a page was viewed one or more times.', 'koko-analytics'); 16 16 17 // ../koko-analytics-pro/src/UTM/views/dashboard.php#3 417 // ../koko-analytics-pro/src/UTM/views/dashboard.php#36 18 18 __('Visitors', 'koko-analytics'); 19 19 20 // ../koko-analytics-pro/src/UTM/views/dashboard.php#3 520 // ../koko-analytics-pro/src/UTM/views/dashboard.php#37 21 21 __('A pageview is defined as a view of a page on your site. If a user clicks reload after reaching the page, this is counted as an additional pageview. If a visitor navigates to a different page and then returns to the original page, a second pageview is recorded as well.', 'koko-analytics'); 22 22 23 // ../koko-analytics-pro/src/UTM/views/dashboard.php#3 523 // ../koko-analytics-pro/src/UTM/views/dashboard.php#37 24 24 __('Pageviews', 'koko-analytics'); 25 25 26 // ../koko-analytics-pro/src/UTM/views/dashboard.php#5 226 // ../koko-analytics-pro/src/UTM/views/dashboard.php#54 27 27 __('There is nothing here. Yet!', 'koko-analytics'); 28 29 // ../koko-analytics-pro/src/UTM/views/dashboard.php#6130 __('Previous', 'koko-analytics');31 32 // ../koko-analytics-pro/src/UTM/views/dashboard.php#7333 __('Next', 'koko-analytics');34 28 35 29 // ../koko-analytics-pro/src/UTM/views/setting.php#8 … … 165 159 __('Pageviews', 'koko-analytics'); 166 160 167 // ../koko-analytics-pro/src/Password/views/password-form.php#8 0161 // ../koko-analytics-pro/src/Password/views/password-form.php#84 168 162 __('Enter password to access the dashboard', 'koko-analytics'); 169 163 170 // ../koko-analytics-pro/src/Password/views/password-form.php#82 171 __('Incorrect password. Please try again.', 'koko-analytics'); 172 173 // ../koko-analytics-pro/src/Password/views/password-form.php#84 164 // ../koko-analytics-pro/src/Password/views/password-form.php#89 174 165 __('Password', 'koko-analytics'); 175 166 176 // ../koko-analytics-pro/src/Password/views/password-form.php# 85167 // ../koko-analytics-pro/src/Password/views/password-form.php#90 177 168 __('Submit', 'koko-analytics'); 178 169 179 // ../koko-analytics-pro/src/Password/bootstrap.php#55 170 // ../koko-analytics-pro/src/Password/bootstrap.php#42 171 __('Unable to sign in. Check the password and try again.', 'koko-analytics'); 172 173 // ../koko-analytics-pro/src/Password/bootstrap.php#57 180 174 __('Dashboard password', 'koko-analytics'); 181 175 182 // ../koko-analytics-pro/src/Password/bootstrap.php#5 7176 // ../koko-analytics-pro/src/Password/bootstrap.php#59 183 177 __('Set a password to protect your public dashboard. Leave empty for no password protection.', 'koko-analytics'); 184 178 … … 234 228 __('Add event', 'koko-analytics'); 235 229 236 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php# 19230 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#22 237 231 __('Event:', 'koko-analytics'); 238 232 239 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#2 0233 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#23 240 234 __('Unique', 'koko-analytics'); 241 235 242 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#2 1236 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#24 243 237 __('Total', 'koko-analytics'); 244 238 245 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#5 2239 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#55 246 240 __('There is nothing here. Yet!', 'koko-analytics'); 247 248 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#58249 __('Previous', 'koko-analytics');250 251 // ../koko-analytics-pro/src/Events/views/dashboard-event-component.php#61252 __('Next', 'koko-analytics');253 241 254 242 // ../koko-analytics-pro/src/Events/views/event-details.php#24 … … 309 297 __('Back to event settings', 'koko-analytics'); 310 298 311 // ../koko-analytics-pro/src/Events/bootstrap.php#5 1299 // ../koko-analytics-pro/src/Events/bootstrap.php#52 312 300 __('Events', 'koko-analytics'); 313 301 314 // ../koko-analytics-pro/src/Events/Rest.php# 59302 // ../koko-analytics-pro/src/Events/Rest.php#71 315 303 __('start_date must be before or equal to end_date.', 'koko-analytics'); 316 304 317 // ../koko-analytics-pro/src/Events/Rest.php# 63305 // ../koko-analytics-pro/src/Events/Rest.php#75 318 306 __('Date range is too large.', 'koko-analytics'); 319 307 … … 321 309 __('Pageviews', 'koko-analytics'); 322 310 323 // ../koko-analytics-pro/src/Devices/views/dashboard.php# 39311 // ../koko-analytics-pro/src/Devices/views/dashboard.php#43 324 312 __('There is nothing here. Yet!', 'koko-analytics'); 325 326 // ../koko-analytics-pro/src/Devices/views/dashboard.php#45327 __('Previous', 'koko-analytics');328 329 // ../koko-analytics-pro/src/Devices/views/dashboard.php#48330 __('Next', 'koko-analytics');331 313 332 314 // ../koko-analytics-pro/src/Devices/views/setting.php#8 … … 378 360 __('Send periodic email reports?', 'koko-analytics'); 379 361 380 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php# 18362 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#20 381 363 __('Daily', 'koko-analytics'); 382 364 383 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php# 19365 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#21 384 366 __('Weekly', 'koko-analytics'); 385 367 386 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#2 0368 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#22 387 369 __('Monthly', 'koko-analytics'); 388 370 389 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#2 2371 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#24 390 372 __('Select the timeframes for which you want to receive a summary of your most important statistics.', 'koko-analytics'); 391 373 392 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#2 6374 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#28 393 375 __('Send to these email addresses', 'koko-analytics'); 394 376 395 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php# 28377 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#30 396 378 __('Enter a comma separated list of email addresses to which the reports should be sent.', 'koko-analytics'); 397 379 398 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#3 2380 // ../koko-analytics-pro/src/Emails/views/settings-email-reports.php#34 399 381 __('Save Changes', 'koko-analytics'); 400 382 … … 402 384 __('Email reports', 'koko-analytics'); 403 385 404 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#2 0386 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#21 405 387 __('Country', 'koko-analytics'); 406 388 407 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#2 1389 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#22 408 390 __('Count', 'koko-analytics'); 409 391 410 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#4 0392 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#41 411 393 __('There is nothing here. Yet!', 'koko-analytics'); 412 413 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#46414 __('Previous', 'koko-analytics');415 416 // ../koko-analytics-pro/src/Geolocation/views/dashboard.php#49417 __('Next', 'koko-analytics');418 394 419 395 // ../koko-analytics-pro/src/Geolocation/views/setting.php#8 -
koko-analytics/trunk/src/Resources/functions/functions.php
r3581336 r3704935 55 55 } 56 56 57 /** 58 * Returns whether the current request may view the analytics dashboard. 59 */ 60 function can_view_dashboard(?array $settings = null): bool 61 { 62 if (current_user_can('view_koko_analytics')) { 63 return true; 64 } 65 66 $settings = $settings ?? get_settings(); 67 if (empty($settings['is_dashboard_public'])) { 68 return false; 69 } 70 71 return (bool) apply_filters('koko_analytics_can_view_public_dashboard', true, $settings); 72 } 73 74 /** 75 * Wrapper around add_query_arg() that returns the URL with its query string in a canonical order. 76 * 77 * All dashboard links should be created through this function. Since add_query_arg() keeps the query 78 * parameters of the current request in whatever order they came in, links pointing at the exact same 79 * view can otherwise end up with a different query string on every request. Sorting the parameters 80 * keeps the number of distinct dashboard URLs to a minimum, which helps any cache (browser, page 81 * cache or CDN) in front of the public dashboard. 82 * 83 * Takes the same arguments as add_query_arg(), so a value of null removes that parameter. 84 * 85 * @param array<string, mixed> $args 86 * @param string|null $url URL to add the query args to. Defaults to the URL of the current request. 87 */ 88 function dashboard_url(array $args = [], ?string $url = null): string 89 { 90 return sort_query_string($url === null ? add_query_arg($args) : add_query_arg($args, $url)); 91 } 92 93 /** 94 * Sorts the query string of the given URL by parameter name, leaving the rest of the URL untouched. 95 */ 96 function sort_query_string(string $url): string 97 { 98 $start = strpos($url, '?'); 99 if ($start === false) { 100 return $url; 101 } 102 103 $query = substr($url, $start + 1); 104 $fragment = ''; 105 $hash = strpos($query, '#'); 106 if ($hash !== false) { 107 $fragment = substr($query, $hash); 108 $query = substr($query, 0, $hash); 109 } 110 111 if ($query === '') { 112 return $url; 113 } 114 115 $pairs = explode('&', $query); 116 $names = array_map(function ($pair) { 117 return explode('=', $pair, 2)[0]; 118 }, $pairs); 119 $order = array_keys($pairs); 120 121 // sort by parameter name, keeping parameters that share a name in their original order 122 array_multisort($names, SORT_ASC, SORT_STRING, $order, SORT_ASC, SORT_NUMERIC, $pairs); 123 124 return substr($url, 0, $start + 1) . implode('&', $pairs) . $fragment; 125 } 126 57 127 function get_most_viewed_post_ids(array $args) 58 128 { -
koko-analytics/trunk/src/Resources/views/dashboard-page.php
r3597329 r3704935 3 3 use KokoAnalytics\Chart_View; 4 4 use KokoAnalytics\Fmt; 5 6 use function KokoAnalytics\dashboard_url; 5 7 6 8 defined('ABSPATH') || exit; … … 14 16 * @var int $realtime 15 17 * @var string $date_format 16 * @var string $dashboard_url17 18 * @var array $next_dates 18 19 * @var array $prev_dates … … 35 36 — <?php echo esc_html(wp_date($date_format, $date_end->getTimestamp())); ?> 36 37 </div> 37 38 38 <div id="ka-datepicker-dropdown" class="rounded bg-white shadow" style="display: none; position: absolute; width:360px; z-index: 9992;"> 39 <div class="mb-3 bg-dark text-white p-3 rounded-top fw-bold d-flex justify-content-between"> 40 <?php 41 // only output pagination for date ranges between reasonable dates... to prevent ever-crawling bots from going wild 42 ?> 43 <?php if ($date_start > $total_start_date) { ?> 44 <a class="js-quicknav-prev text-decoration-none text-white me-2" href="" data-href="<?= esc_attr(add_query_arg(['start_date' => $prev_dates[0]->format('Y-m-d'), 'end_date' => $prev_dates[1]->format('Y-m-d')], $dashboard_url)); ?>" rel="nofollow">◂</a> 45 <?php } else { ?> 46 <a class="text-decoration-none text-white me-2">◂</a> 39 <?php $show_date_navigation = KokoAnalytics\Dashboard::shows_pagination_links(); ?> 40 <div class="mb-3 bg-dark text-white p-3 rounded-top fw-bold d-flex <?= $show_date_navigation ? 'justify-content-between' : 'justify-content-center'; ?>"> 41 <?php if ($show_date_navigation) { ?> 42 <?php 43 // only output pagination for date ranges between reasonable dates... to prevent ever-crawling bots from going wild 44 // the target URL is assembled in JS from these dates, so no crawlable URL ever appears in the document 45 ?> 46 <?php if ($date_start > $total_start_date) { ?> 47 <a class="js-quicknav-prev text-decoration-none text-white me-2" href="" data-start-date="<?= esc_attr($prev_dates[0]->format('Y-m-d')); ?>" data-end-date="<?= esc_attr($prev_dates[1]->format('Y-m-d')); ?>" rel="nofollow">◂</a> 48 <?php } else { ?> 49 <a class="text-decoration-none text-white me-2">◂</a> 50 <?php } ?> 47 51 <?php } ?> 48 52 <span><?php echo esc_html(wp_date($date_format, $date_start->getTimestamp())); ?> — <?= esc_html(wp_date($date_format, $date_end->getTimestamp())); ?></span> 49 <?php if ($date_end < $total_end_date) { ?> 50 <a class="js-quicknav-next text-decoration-none text-white ms-2" href="" data-href="<?= esc_attr(add_query_arg(['start_date' => $next_dates[0]->format('Y-m-d'), 'end_date' => $next_dates[1]->format('Y-m-d')], $dashboard_url)); ?>" rel="nofollow">▸</a> 51 <?php } else { ?> 52 <a class="text-decoration-none text-white ms-2">▸</a> 53 <?php if ($show_date_navigation) { ?> 54 <?php if ($date_end < $total_end_date) { ?> 55 <a class="js-quicknav-next text-decoration-none text-white ms-2" href="" data-start-date="<?= esc_attr($next_dates[0]->format('Y-m-d')); ?>" data-end-date="<?= esc_attr($next_dates[1]->format('Y-m-d')); ?>" rel="nofollow">▸</a> 56 <?php } else { ?> 57 <a class="text-decoration-none text-white ms-2">▸</a> 58 <?php } ?> 53 59 <?php } ?> 54 60 </div> … … 98 104 <?php $filtered_label = $filtered_post_id > 0 ? get_the_title($filtered_post_id) : $page; ?> 99 105 <a class="" href="<?= esc_url($filtered_permalink); ?>"><?= esc_html($filtered_label); ?></a> 100 <a class="text-decoration-none text-reset ms-2" aria-label="<?php esc_attr_e('Clear page filter', 'koko-analytics'); ?>" title="<?php esc_attr_e('Clear page filter', 'koko-analytics'); ?>" href="<?= esc_attr( remove_query_arg('p')); ?>">✕</a>106 <a class="text-decoration-none text-reset ms-2" aria-label="<?php esc_attr_e('Clear page filter', 'koko-analytics'); ?>" title="<?php esc_attr_e('Clear page filter', 'koko-analytics'); ?>" href="<?= esc_attr(dashboard_url(['p' => null])); ?>">✕</a> 101 107 </div> 102 108 … … 191 197 <?php } ?> 192 198 193 <?php $can_sort = current_user_can('manage_koko_analytics'); ?> 199 <?php // only offer drag & drop where the action that stores the new order runs: it is registered from the admin controller, so a save from the public dashboard would silently do nothing ?> 200 <?php $can_sort = is_admin() && current_user_can('manage_koko_analytics'); ?> 194 201 <div id="ka-components" class="ka-row ka-row-cols-1 ka-row-cols-xl-2 g-3 mb-3 <?= $page !== 0 ? 'page-filter-active' : ''; ?>" <?= $can_sort ? 'data-nonce="' . esc_attr(wp_create_nonce('koko_analytics_save_component_order')) . '"' : ''; ?>> 195 202 <?php foreach ($this->get_components() as $id => $callback) : ?> -
koko-analytics/trunk/src/Resources/views/settings/help.php
r3581336 r3704935 24 24 <div class="mb-5"> 25 25 <ul class="ul-square"> 26 <li><a href="https://www.kokoanalytics.com/pricing/?utm_source=koko-analytics&utm_medium=link&utm_campaign=free-plugin-settings-help">Upgrade to Koko Analytics Pro</a> to get direct email support from the plugin builder.</li> 26 <?php if (defined('KOKO_ANALYTICS_PRO_VERSION')) : ?> 27 <?php /* translators: 1: opening anchor tag, 2: closing anchor tag. */ ?> 28 <li><?php printf(esc_html__('As a Koko Analytics Pro user, you can email us directly at %1$ssupport@kokoanalytics.com%2$s for help.', 'koko-analytics'), '<a href="mailto:support@kokoanalytics.com">', '</a>'); ?></li> 29 <?php else : ?> 30 <li><a href="https://www.kokoanalytics.com/pricing/?utm_source=koko-analytics&utm_medium=link&utm_campaign=free-plugin-settings-help">Upgrade to Koko Analytics Pro</a> to get direct email support from the plugin builder.</li> 31 <?php endif; ?> 32 27 33 <?php /* translators: 1: opening anchor tag, 2: closing anchor tag. */ ?> 28 34 <li><?php printf(esc_html__('Have a look at our %1$sknowledge base%2$s for help with configuring and using Koko Analytics.', 'koko-analytics'), '<a href="https://www.kokoanalytics.com/docs/#utm_source=koko-analytics&utm_medium=link&utm_campaign=free-plugin-settings-help-docs" target="_blank">', '</a>'); ?></li> -
koko-analytics/trunk/src/Rest.php
r3581336 r3704935 113 113 public function permission_callback(): bool 114 114 { 115 $settings = get_settings(); 116 $is_dashboard_public = $settings['is_dashboard_public']; 117 return $is_dashboard_public || current_user_can('view_koko_analytics'); 115 return can_view_dashboard(); 118 116 } 119 117 -
koko-analytics/trunk/src/Upserter.php
r3581336 r3704935 7 7 class Upserter 8 8 { 9 private const DATABASE_BATCH_SIZE = 500; 10 9 11 protected wpdb $db; 10 12 protected string $table; … … 23 25 /** 24 26 * @param array<string> $values 25 * @return array< string, int> Map of value to ID in database27 * @return array<array-key, int> Map of value to ID in database 26 28 */ 27 29 public function upsert(array $values): array … … 35 37 $values = array_unique($values); 36 38 37 // INSERT IGNORE all deduplicated values into the database table 38 $placeholders = rtrim(str_repeat('(%s),', count($values)), ','); 39 $this->db->query($this->db->prepare("INSERT IGNORE INTO {$this->table}({$this->column}) VALUES {$placeholders}", $values)); 39 $ids = []; 40 foreach (array_chunk($values, self::DATABASE_BATCH_SIZE) as $batch) { 41 // INSERT IGNORE all deduplicated values into the database table 42 $placeholders = rtrim(str_repeat('(%s),', count($batch)), ','); 43 $this->db->query($this->db->prepare("INSERT IGNORE INTO {$this->table}({$this->column}) VALUES {$placeholders}", $batch)); 40 44 41 // retrieve all entries from the database table to get their normalized ID's 42 $placeholders = rtrim(str_repeat('%s,', count($values)), ','); 43 $results = $this->db->get_results($this->db->prepare("SELECT id, {$this->column} FROM {$this->table} WHERE {$this->column} IN({$placeholders})", $values)); 45 // retrieve all entries from the database table to get their normalized ID's 46 $placeholders = rtrim(str_repeat('%s,', count($batch)), ','); 47 $results = $this->db->get_results($this->db->prepare("SELECT id, {$this->column} FROM {$this->table} WHERE {$this->column} IN({$placeholders})", $batch)); 48 $ids = array_replace($ids, array_column($results, 'id', $this->column)); 49 } 44 50 45 return array_column($results, 'id', $this->column);51 return $ids; 46 52 } 47 53 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)