css/frontend.css000064400000002437147206624130007700 0ustar00/* Frontend CSS */ .post-views.entry-meta > span { margin-right: 0 !important; font: 16px; line-height: 1; } .post-views.entry-meta > span.post-views-icon.dashicons { display: inline-block; font-size: 16px; line-height: 1; text-decoration: inherit; vertical-align: middle; } .post-views.load-dynamic .post-views-count { color: rgba(0, 0, 0, 0); transition: color 0.3s ease-in-out; position: relative; } .post-views.load-dynamic.loaded .post-views-count { color: inherit; } .post-views.load-dynamic.loading .post-views-count, .post-views.load-dynamic.loading .post-views-count:after { box-sizing: border-box; } .post-views.load-dynamic .post-views-count:after { opacity: 0; transition: opacity 0.3s ease-in-out; position: relative; color: rgb(102, 16, 242); } .post-views.load-dynamic.loading .post-views-count:after { content: ''; display: block; width: 16px; height: 16pxpx; border-radius: 50%; border: 2px solid currentColor; border-color: currentColor transparent currentColor transparent; animation: pvc-loading 1s linear infinite; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); opacity: 1; } @keyframes pvc-loading { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }css/frontend.min.css000064400000002042147206624130010452 0ustar00.post-views.entry-meta>span{margin-right:0!important;font:16px;line-height:1}.post-views.entry-meta>span.post-views-icon.dashicons{display:inline-block;font-size:16px;line-height:1;text-decoration:inherit;vertical-align:middle}.post-views.load-dynamic .post-views-count{color:#fff0;transition:color 0.3s ease-in-out;position:relative}.post-views.load-dynamic.loaded .post-views-count{color:inherit}.post-views.load-dynamic.loading .post-views-count,.post-views.load-dynamic.loading .post-views-count:after{box-sizing:border-box}.post-views.load-dynamic .post-views-count:after{opacity:0;transition:opacity 0.3s ease-in-out;position:relative;color:#6610f2}.post-views.load-dynamic.loading .post-views-count:after{content:'';display:block;width:16px;height:16pxpx;border-radius:50%;border:2px solid currentColor;border-color:currentColor #fff0 currentColor #fff0;animation:pvc-loading 1s linear infinite;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);opacity:1}@keyframes pvc-loading{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}css/admin.min.css000064400000012215147206624130007726 0ustar00#pvc-reports-upgrade,.post-views-credits .pvc-sidebar-body .pvc-icon{left:0;top:0;position:absolute}.post-views-counter-settings.has-sidebar{display:flex;flex-direction:row;gap:30px;justify-content:space-between}.post-views-counter-settings form{min-width:463px;width:auto;position:relative}.post-views-sidebar{width:280px;min-width:280px;margin:15px 0;position:relative;order:1}.post-views-sidebar .inner{padding:2em}.post-views-sidebar>div:not(:last-child){margin-bottom:3em}.post-views-sidebar .inner img{max-width:80%;height:auto;display:block;margin:20px auto}.post-views-counter-settings p.description,.post-views-counter-settings p.help{font-size:13px;font-style:italic;line-height:1.6}.post-views-counter-settings div.ip-box{margin-bottom:3px}.post-views-counter-settings select{vertical-align:top}.post-views-counter-settings .available{color:#00a32a}.post-views-counter-settings .unavailable{color:#d63638}.pvc-subfield{margin-top:12px}#misc-publishing-actions #post-views #post-views-display:before{display:inline-block;font:400 20px/1 dashicons;left:-1px;padding:0 2px 0 0;position:relative;text-decoration:none!important;vertical-align:top;color:#888;content:"\f185";top:-1px}.edit-php .widefat th#post_views{width:5.5em}.edit-php .widefat th.column-post_views .dashicons,.edit-php .widefat th.column-post_views .dashicons:before{font-size:1.1em;vertical-align:middle}.edit-php .metabox-prefs .dash-icon,.edit-php .widefat th .dash-title,.upload-php .widefat th .dash-title{display:none}.edit-php .widefat td .dashicons,.edit-php .widefat td .dashicons:before{font-size:1.1em}.edit-php #inline-edit-post_views input{width:auto}.is-hidden{display:none!important;visibility:hidden!important}output{display:block;font-size:30px;font-weight:700;text-align:center;margin:30px 0;width:100%}.post-views-credits{background:#fff;box-shadow:0 0 0 1px rgba(0,0,0,.05)}.post-views-credits .inner{text-align:center;margin:0}.post-views-counter-settings .pvc-button{color:#fff;background-color:#6610f2;border-color:#6610f2}.post-views-counter-settings .pvc-button:active,.post-views-counter-settings .pvc-button:focus,.post-views-counter-settings .pvc-button:hover{color:#fff;background-color:#570ece;border-color:#570ece}.post-views-counter-settings .pvc-button:focus{box-shadow:0 0 0 1px #fff,0 0 0 3px #6610f2}.post-views-credits h2{border:none;padding-bottom:0;font-size:23px;font-weight:400;margin:.25em 0 .5em;color:#6610f2}.post-views-credits h3{font-size:18px;line-height:1.4;font-weight:400;margin:0;padding:0;color:#6610f2}.post-views-credits p:first-child{margin-top:0}.post-views-credits .pvc-sidebar-body{font-size:14px;text-align:left;margin:2em 0;padding:0}.post-views-credits .pvc-sidebar-footer{margin:1em 0}.post-views-credits .pvc-sidebar-body p{padding-left:30px;margin:.75em 0;position:relative}.post-views-credits .pvc-sidebar-body b{font-weight:700;color:#000}.post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right{box-sizing:border-box;display:block;transform:scale(1);width:22px;height:22px}.post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right::after,.post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right::before{content:"";display:block;box-sizing:border-box;position:absolute;right:3px}.post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right::after{width:8px;height:8px;border-top:2px solid #ffc107;border-right:2px solid #ffc107;transform:rotate(45deg);bottom:7px}.post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right::before{width:16px;height:2px;bottom:10px;background:#ffc107}#post_views_counter_other_license_setting .pvc-icon{box-sizing:border-box;position:relative;display:inline-block;transform:scale(1);width:22px;height:22px;border:2px solid transparent;border-radius:40px;vertical-align:middle;margin-left:3px;top:-2px;color:#b32d2e}#post_views_counter_other_license_setting:not(.valid) .pvc-icon:after,#post_views_counter_other_license_setting:not(.valid) .pvc-icon:before{content:"";display:block;box-sizing:border-box;position:absolute;width:16px;height:2px;background:currentColor;transform:rotate(45deg);border-radius:5px;top:8px;left:1px}#post_views_counter_other_license_setting:not(.valid) .pvc-icon:after{transform:rotate(-45deg)}#post_views_counter_other_license_setting.valid .pvc-icon{color:#1da867}#post_views_counter_other_license_setting.valid .pvc-icon:after{content:"";display:block;box-sizing:border-box;position:absolute;left:3px;top:-1px;width:6px;height:10px;border-width:0 2px 2px 0;border-style:solid;transform-origin:bottom left;transform:rotate(45deg)}#pvc-reports-upgrade{height:100%;width:100%;overflow:hidden;box-sizing:border-box;min-height:400px}#pvc-reports-bg{width:100%;height:auto;opacity:.8;filter:blur(2px)}#pvc-reports-modal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);padding:1.5em 3em;box-shadow:0 0 25px 10px rgba(0,0,0,.1);border-radius:3px;background-color:#fff;text-align:center;width:26em}#pvc-reports-modal p{margin:0}#pvc-reports-modal h2{font-size:21px;font-weight:400;margin:0 0 10px;padding:9px 0 4px;line-height:1.3}#pvc-reports-modal .button{margin-top:25px;margin-bottom:10px}@media only screen and (max-width:960px){.post-views-counter-settings{flex-wrap:wrap}.post-views-counter-settings .post-views-sidebar{width:100%}}css/admin.css000064400000015710147206624130007147 0ustar00/* Post Views Counter settings */ .post-views-counter-settings.has-sidebar { display: flex; flex-direction: row; gap: 30px; justify-content: space-between; } .post-views-counter-settings form { min-width: 463px; width: auto; position: relative; } .post-views-sidebar { width: 280px; min-width: 280px; margin: 15px 0; position: relative; order: 1; } .post-views-sidebar .inner { padding: 2em; } .post-views-sidebar>div:not(:last-child) { margin-bottom: 3em; } .post-views-sidebar .inner img { max-width: 80%; height: auto; display: block; margin: 20px auto; } .post-views-counter-settings p.help, .post-views-counter-settings p.description { font-size: 13px; font-style: italic; line-height: 1.6; } .post-views-counter-settings div.ip-box { margin-bottom: 3px; } .post-views-counter-settings select { vertical-align: top; } .post-views-counter-settings .available { color: #00a32a; } .post-views-counter-settings .unavailable { color: #d63638; } .pvc-subfield{ margin-top: 12px; } /* Single post edit screen */ #misc-publishing-actions #post-views #post-views-display:before { display: inline-block; font: 400 20px/1 dashicons; left: -1px; padding: 0 2px 0 0; position: relative; text-decoration: none !important; vertical-align: top; color: #888; content: "\f185"; top: -1px; } /* Listing edit screen */ .edit-php .widefat th#post_views { width: 5.5em; } .edit-php .widefat th.column-post_views .dashicons, .edit-php .widefat th.column-post_views .dashicons:before { font-size: 1.1em; vertical-align: middle; } .edit-php .widefat th .dash-title, .upload-php .widefat th .dash-title { display:none; } .edit-php .metabox-prefs .dash-icon { display:none; } .edit-php .widefat td .dashicons, .edit-php .widefat td .dashicons:before { font-size: 1.1em; } .edit-php #inline-edit-post_views input { width: auto; } .is-hidden { display:none !important; visibility:hidden !important } output { display: block; font-size: 30px; font-weight: bold; text-align: center; margin: 30px 0; width: 100%; } .post-views-credits { background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.05); } .post-views-credits .inner { text-align: center; margin: 0; } .post-views-counter-settings .pvc-button { color: #fff; background-color: #6610f2; border-color: #6610f2; } .post-views-counter-settings .pvc-button:active, .post-views-counter-settings .pvc-button:focus, .post-views-counter-settings .pvc-button:hover { color: #fff; background-color: #570ece; border-color: #570ece; } .post-views-counter-settings .pvc-button:focus { box-shadow: 0 0 0 1px #fff,0 0 0 3px #6610f2; } .post-views-credits h2 { border: none; padding-bottom: 0; font-size: 23px; font-weight: normal; margin: 0.25em 0 0.5em; color: #6610f2; } .post-views-credits h3 { font-size: 18px; line-height: 1.4; font-weight: normal; margin: 0; padding: 0; color: #6610f2; } .post-views-credits p:first-child { margin-top: 0; } .post-views-credits .pvc-sidebar-body { padding-bottom: 0; font-size: 14px; text-align: left; margin: 2em 0; padding: 0; } .post-views-credits .pvc-sidebar-footer { margin: 1em 0; } .post-views-credits .pvc-sidebar-body p { padding-left: 30px; margin: 0.75em 0; position: relative; } .post-views-credits .pvc-sidebar-body b { font-weight: bold; color: #000; } .post-views-credits .pvc-sidebar-body .pvc-icon { position: absolute; top: 0; left: 0; } .post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right { box-sizing: border-box; display: block; transform: scale(1); width: 22px; height: 22px; } .post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right::after, .post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right::before { content: ""; display: block; box-sizing: border-box; position: absolute; right: 3px; } .post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right::after { width: 8px; height: 8px; border-top: 2px solid #ffc107; border-right: 2px solid #ffc107; transform: rotate(45deg); bottom: 7px; } .post-views-credits .pvc-sidebar-body .pvc-icon-arrow-right::before { width: 16px; height: 2px; bottom: 10px; background: #ffc107; } #post_views_counter_other_license_setting .pvc-icon { box-sizing: border-box; position: relative; display: inline-block; transform: scale(1); width: 22px; height: 22px; border: 2px solid transparent; border-radius: 40px; vertical-align: middle; margin-left: 3px; top: -2px; color: #b32d2e; } #post_views_counter_other_license_setting:not(.valid) .pvc-icon:before, #post_views_counter_other_license_setting:not(.valid) .pvc-icon:after { content: ""; display: block; box-sizing: border-box; position: absolute; width: 16px; height: 2px; background: currentColor; transform: rotate(45deg); border-radius: 5px; top: 8px; left: 1px } #post_views_counter_other_license_setting:not(.valid) .pvc-icon:after { transform: rotate(-45deg) } #post_views_counter_other_license_setting.valid .pvc-icon { color: #1da867; } #post_views_counter_other_license_setting.valid .pvc-icon:after { content: ""; display: block; box-sizing: border-box; position: absolute; left: 3px; top: -1px; width: 6px; height: 10px; border-width: 0 2px 2px 0; border-style: solid; transform-origin: bottom left; transform: rotate(45deg); } #pvc-reports-upgrade { position: absolute; left: 0; top: 0; height: 100%; width: 100%; overflow: hidden; box-sizing: border-box; min-height: 400px; } #pvc-reports-bg { width: 100%; height: auto; opacity: 0.8; filter: blur(2px); } #pvc-reports-modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 1.5em 3em; box-shadow: 0 0 25px 10px rgba(0,0,0,0.1); border-radius: 3px; background-color: #fff; text-align: center; width: 26em; } #pvc-reports-modal p { margin: 0; } #pvc-reports-modal h2 { font-size: 21px; font-weight: 400; margin: 0 0 10px 0; padding: 9px 0 4px; line-height: 1.3; } #pvc-reports-modal .button { margin-top: 25px; margin-bottom: 10px; } /* All Mobile Sizes (devices and browser) */ @media only screen and (max-width: 960px) { .post-views-counter-settings { flex-wrap: wrap; } .post-views-counter-settings .post-views-sidebar { width: 100%; } }css/admin-dashboard.css000064400000010047147206624130011072 0ustar00.pvc-dashboard-container { min-height: 260px; margin: 0 -4px; text-align: center; position: relative; display: flex; flex-direction: column; } .pvc-dashboard-container .spinner { position: absolute; left: 50%; top: 40%; margin-left: -10px; z-index: 10; } .pvc-dashboard-container.loading { pointer-events: none; } .pvc-dashboard-container.loading:after { position: absolute; content: ''; display: block; height: 100%; width: 100%; top: 0; left: 0; background-color: rgba(255,255,255,0.8); z-index: 1; transition: all 0.2s; } .pvc-dashboard p.sub { color: #8f8f8f; font-size: 14px; text-align: left; padding-bottom: 3px; border-bottom: 1px solid #ececec; } .pvc-dashboard-container .pvc-months { display: flex; justify-content: space-between; margin: 6px 10px 0 10px; color: #aaa; } .pvc-dashboard-container .pvc-months .current { color: #212529; } .pvc-data-container { min-height: 230px; } .pvc-table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; } .pvc-table { caption-side: bottom; border-collapse: collapse; width: 100%; margin-bottom: 1rem; color: inherit; vertical-align: top; border-color: #dee2e6; text-align: left; } .pvc-table > thead { vertical-align: bottom; color: #212529; } .pvc-table > tbody { vertical-align: inherit; } .pvc-table tbody, .pvc-table td, .pvc-table tfoot, .pvc-table th, .pvc-table thead, .pvc-table tr { border-color: inherit; border-style: solid; border-width: 0; } .pvc-table th, .pvc-table td { text-align: inherit; text-align: -webkit-match-parent; } .pvc-table th:first-child, .pvc-table td:first-child { width: 1px; white-space: nowrap; } .pvc-table th:last-child, .pvc-table td:last-child { text-align: right; } .pvc-table .no-posts :last-child { text-align: left; } .pvc-table > :not(caption) > * > * { padding: .5rem .5rem; background-color: transparent; border-bottom-width: 1px; box-shadow: inset 0 0 0 9999px transparent; } #pvc_dashboard .inside { margin: 0; padding: 0; } .pvc-accordion-toggle { background-color: #fafafa; border-bottom: 1px solid #eee; cursor: pointer; line-height: 1; position: relative; font-size: 14px; font-weight: 400; margin: 0; padding: 11px 12px; color: #23282c; } .pvc-accordion-toggle::before { color: #72777c; content: "\f142"; display: inline-block; font: normal 20px dashicons; line-height: 1; position: absolute; right: 8px; text-decoration: none !important; text-indent: -1px; transform: rotate(0deg); speak: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; top: 8px; } .pvc-accordion-title { display: inline-block; padding-right: 10px; } .pvc-accordion-actions { position: absolute; top: 0; right: 0; z-index: 1; padding: 11px 30px 11px 0; height: 14px; line-height: 1; } .pvc-accordion-actions .pvc-accordion-action, .pvc-accordion-actions .pvc-accordion-action::before { font-size: 14px; height: 14px; width: 14px; color: #72777c; } .pvc-tooltip { position: relative; } .pvc-tooltip-icon { display: inline-block; width: 16px; cursor: help; } .pvc-tooltip-icon::before { color: #b4b9be; content: "\f14c"; display: inline-block; font: normal 16px dashicons; line-height: 1; position: absolute; text-decoration: none !important; speak: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; left: 0; top: 2px; } .pvc-according-header { display: flex; align-items: center; justify-content: space-between; } .pvc-accordion-content { padding: 11px 12px; border-bottom: 1px solid #eee; height: 100%; } .pvc-collapsed .pvc-accordion-toggle::before { transform: rotate(180deg); } .pvc-collapsed .pvc-accordion-content { display: none; } .pvc-dashboard-block { display: flex; justify-content: center; align-items: center; background: #fafafa; color: #787c82; font-size: 13px; font-style: italic; padding: 13px; margin-top: 0; }css/page-reports.png000064400000531503147206624130010466 0ustar00PNG  IHDR (tEXtSoftwareAdobe ImageReadyqe<'iTXtXML:com.adobe.xmp LeIDATxxTUIII=@E늻\;bVAJ ̤I"R&ɔ'9 3$S[[[p(J@ P oPx%(7@ (J@ P oPx%(7@ (J@ P oPx%(7@ (J`@TS~~ڵk{.Zݣ5JRņ@ X|Ν׽u+uld?uy:VZ5$::\޽=oݔKlu]z_>--{{2kMEt>RPq;vbb]zf͇:-&)WZ]%E@`]9w\KOJ"\~ c=Sd.wX_^3A))]q;B;1ŷd2i~*ѽ]o*b[qw8/~g[w%66VRJ*Θ1[ \hԹ8t,g f3lذfW5\hh]_%zghWd Ϝ;w"RTONq ))IREM&Mػ툮s7QLvjtň7j@"@x#jjjZYYي|3dFtG}d=W}|cc%&&¸@3xGx+˿+_2T,=6v_TT-ƍMJfr77KYر˿zyD,czv;oT]~~֭[z݈z*TXje=Kǩ^I$'oڵkoj@D=...:z֮]+H/_qy{Rb [n\֯_߳Y@@L}؋.Pz@uYT*#Xd[ry|U/_jJ)~沵ufHŻ:~#]ͺb@`v{&Ո"4g]ťEqh)Zɭo"R (jڵGwS@ѭ#61$%(7@ (J@ P oPx%(7@ (J@ P oPx%0JL8ņD"Y+h_t@ X|9@uo%&&ٳjʀs)׬P,woi\~?Lfj22rP5X"wor77O~~T*Pl#xɤ{[.oݺu%sK*TVV*{( BK86t\siIE^v@@U+ #+G_]~[AKƎv?VZZrj[[[<, :,\йg\йgA=KjOvO yo];6v_~~^|W^R˧t,/ַY:t.YйsAҹsAς z%M53DѨ(6Ν$Ghx.]}u귯h˗O).o連;wrO\Q翕9:O5 Qx%(7@ (U7%6e7h"%Pm͍wE[/`N, JL |4*a|ޓE,%(Kxz@Ŷ˳ks7PF+cC}KEήޓ[HD(l.>:ر=wr(R\rv-Qw%7<e6P4NXw#}eS7!̓7w<0̥no0E_W_ԋ)k)o̩h̭h233773201_Fu.\xl0[fib莋UUVV BHgZ;e+gFiPm=4܅~UF @B^S:O sP172x{OI-qN3 5g*&@5vڙ7BK(-DbDq캰X0G]٥a޶#}DŽ(ytG i{`)X=7y_a7p@oBHj`y|sTd-ޟV`˔PpLzooC]voXwa*Ⴁ.:YS*gKlGuogUnnL\kb:6ILFکྍ [}r nD THE}s[Sg&x6v{cW9hqhdrs#v)=UMg ◳ew}Xyc'{lꎌ֎NU$, Fpn"iaϥ@Qxڎ?M sxvҸwa.F+ΤwZ'Uz[yNՍO_j(@?KȫsmBzISBTg?|/\ЉW4כv^E}+*o>MrՊ~"37gkGG[KO'bWG;Y~?^B G}KDzm)_.8Qbɡ^6_ݑVF43Ս퓂E"w$ȩ@uPx~[2yClTyW,v/^hoOJ' -lJ';Pm8V36˦ LT,u bS*r+&;nϋt4oу"(@ߩkXOE{=Y]vt^Ӈ9]t"@#mN 73W66П5-1c&/<k^ PwîBn|l9ٲ),p WLն - n6݅'sWf4 BPx%*k|fٙibLڜt7@mM)4X g pp7O˪>Qy8up֖&ʩX}W/iM Ÿ ##]m͍_QxeulCl媫fg61= ~:]5_1G4so>0#+uv]8Qu pfe[Džn#ʽ#C}#\7~kA[G‘" ϔN>:(/}51XG:s3[:[6sjk[b@M{ʞhH>] tK.tٞeFzݬ6J7O[k^@nESkGP8UPsc^k& oWw)J_lkej03jXVujqyhqOZwo{Εs|YWx3vM, /wFUrk<6ZNNU7JkR NCm:1|Ʌu/Ĥ[QҰpEE1ﺥS|$&lH>SCoj+ oK.ѤW'6tY6GdiQZ).ObƟ73=ԊZzhV}vӍ{vWZhq'7t ݪ\(o-·+Ώ%5>4X8@b=X( a&kO2[t.\!}$g[`37ڂ=K΅,k[Fz ^vv4ilQXƸؽ##GVuo{Jȫ|몭>>ޱzftw|k5*8::]+k} ꎞsw;[9/P]_ǵnvtn~i+~mAUqUV7wNp36Wg[sӛϥ  z ٭ -ry`OZiΉӅB{wd[uSMƮZ{CqH$IIIs\*4|~ҋK&bJ beD}SuwB a뎲^kG纣;¡^:/In]LHyt߈hn%W{\)Ec\\@ X|u@#U4KydPQXrqHRO֬3H{yFo:[:5ԑQGׅNj6(259aH7I͹p“wxPGjry誋 p^ݗ=flS#)oʼoЌ4i{b`k˴0uG )@mڛCd:YM{k:ڏQDrAo+3 j<]t0_S+&$au9N"=]ŖMOԌ@ucѬƞykZ;:5/ éJVzOx\t9?Zcja[BOn8V49ݻ-HldϿO}ye #hO}q¸u݃O*˭od\|n\@WM z$ٔPڗGI x[&\y(uѾvD0=a"Ym Q%he}aƆbk¹A GmL^>(T..uoʜ6ul߾+Pឌ%9-́nH_;sc-RO9Z-Bauc z@_pk.rzb{Uc۫?e*3 Z^,t,:Ɲ|+:~s.D_1s"ȡǴ0wf=X%2' 2yQus /MɂvO{^cqtɣD5QxھhH2mD;K~8U<{3iɨ…~Bl9cE' ήl)^4󷱁'**0Bq\lM:}I!xXd K7I:=ʓ[ߙdxQE ЏO*OJW~gsD@=2*ٛmlTKӿˮN-r9/֔nMͽC$+~Hۘ^QxJZ7(' pxų NĦVDHkJ反v'+xGڭ?VNύp|޶~]B!ݮ).!GZJ#FV/S>8q^&v~T@ѶQŶD(_;[sނ^P y5Ds|+gryp+?e@s̭ycW5'KL~~@7kbGEI|kUM)wp"IlM-$ ([d:>P4_O9GF?9G*&h̲oOv$ p^6'Uxpk|q}*&$E`A2mߞ?Y5׎4pJVϙl͍l_Gqw2@_IĒEM Mȭh$ @inܞ(G=zDSC6cxٽ':[@խ/Z3՟ ICEPxkrN5v6h'jW9ID3`O[a)})D[ֹ⇴e‘zDצ;Zbo*s4Mguy͹zx3a ܞP\N-x?9+SianM|^=%U6>'`4sGFT6ޝEu[Z#{ݻjgJ,*nl["}aϹq"d5-?.! ܬ/0mL?:uKG^۫"/t(qb‘dFmM=:ʍ(m{RB}&-94poP4]L8(P]?'I⛻2C$ 3҄qj\hkj[BH_;1Qܔ)Ԗx}v?&xٚyhKE}9Dя(PE?*q >/dN>q¼D%E{ϗp61^Rk1jo{W;9j26{h7Nj~MS~e{bRys"C}[@%,fp+9?Zp`!pƶLϯlze/U[݃ؕbeoɨ~{*b{bO~v(!iq95gDP-vF E\XWۺ^AM=w7_s:Q >v)6ᓃycD%7}=]b<&üm'ɈtٙGhQUCQ% ןeizjcuG QxПUEқdp~{2lD;ϔjZe6ftv] ۡpst|Q܎!63@󧌇F7ԅ4ԋ\>-٧jDN ꦇ;>=HLE⧣=I;]zOVaU%'s-ښ S͔j+MqAGSGG˅ :6.5*,M 3D]sNJ zsSdN`sl|n_ zтfʭh|!&@_3odD}?Eѕr|_d6?Z/b𝻃xFQ6'oO*9 ڦ暦Ǝ6ťA]Xxkb -mOMnx籬_S+2Mۑ\R1J2sF8X˭y~{j1tHw>LG( 'ejNiq(PYŵ-MUMyy͊6+SC+S+3CKPpqbGtttu.~UW{[-5M.5b6s },肛9N6!Pt1|(|#kyg;5ؙ44Cr{#mL; z7nkB/LU'TlPG:Jʦ܊Ƃʦ. c};gk܋"ύ8Yv\b/r΁Բ֎{>M=qgS#iMN& %9[RdVX-Xњ_s:!5]Kޓbi ozQ\n'r ftb(ŕ޶%Sx_՝'f5:Z8Ylmbmfث;0:zid;}KZ뾍~;kNΜr=[EƬL_-=ڝTHwk< !=fr: ozEI]ΖN uX<Ԉ6NϮmK@gׅsRYVQhe`12ў7mms|`ŕL). jeTE(_#B]t$~0?(nǧ=LHv)g+x3f/"Y;3JdaL (WG ?;"Z9ޜ@Gfw&(UecYGgW+L:{ٛ/{;ۚ͋tSYY) @7kT~"u:."WtBx6YQ, f;;Z@ @ֵ>޵|ˆ@4Pտd}Wi* oԊO䵴w>>Ɲ o\2JPsR#U0Ŗu 2>P#l$ Gf95G޽weFӁM'BT7ٳ t D=8\l[wIPx ol9?.4tޖ(]>e](A[G#YDz!AՃ\$~h^L8[$L[<ΓH OELN9hi/~zGh-D wygo kO>PoY17͎YpV~|;'rjv+sH/j^sf@qٟVO pN?U}"zѡ/<;ы44ɶ7weΏ0ֽ4;IPxp[:.7xٝγ:nJ'Ƹ%i\3>N 4Elyңp`R=mgpf^MšO=>m4C]^>7(u'skޝ _?GK-('(7}fpv+lٱj?Gq޶FZ1[e*LѪʤ5 B].!^64[~fjd0kӧ;:/z9[TgK'{kDpWI!aI%j[ݐ\T~üj%$:'ά$ @xI#]tu6fVQߺ|(ռnp7ַt,,EHK>,C-M?>GjpfF[=1֝4G qil\k.Q(7Tٽq"/O讘!dpBۓ zZauȼ窽¸dnsF=V xcN_E:}0(Žҧ75.չwdñ<&T obͯ9#uٓ?_2tfyGeR >1x@{YNrqٕU "Nis:/'6մhAU7}r o@G&GV|>ɰjㅯppGuJSCfI3tWR=S#s¢[G E: zx zҙº|]P|ۙ#ߝ,xiaVs \Vљybljѧ-IxM_L)Q|xq"Z0D٥xRPx;Mmo>[`er%S*H}`BPFe(ןwOsd57&~y kZ(12 cl[GsRdY͍p^k^kGiL~=mI7ޡ'TcYr^ZojfӢ86d̆z| }M@68]X솖%]-%|HF9ڙ;R^t?{:ŧ ,PxP9֬ݟ/P)m]KU64WlcB qAΖh.3F'θ*O{sA߈Etq\n eOW7IlMd ҄` uq5% @$3s|h$/C}^,9ήv6kejš :3>Usg1idrknKapiv;Y!^6ge#>!1>C憶F:zz%u% m a.VCS>vt%0?Jfgoy% ˧n+ug&\XRX' ph,۞AlI{gWe5_>L?cr*^wU|4?tk}y#|8Y `1'i6fFrV\r2&!&)SddNJP_Ol\d<Ƞ:_I94WK{Y-ysW4v46_[G+?eO+_6'T" (Y;{_i7M o2eT]lM,* p4镣wv=[21?)Ճx1&X,I*[j՝+y[:Y013ҽ4ή_ΔL Pa^E| ppxBOu~1ꦶu $1Y1o #듟}=Y]tVNV{s>x,/T"yMy5<XU|\~}ʼnCQ 4 d~髦҉Iy5{ٳVl6Pl}\q~uS!QJʯVB.2%{U85AlyED3GZ{pa ߞSY5+)9 o S5%eXnJ(/$JXT^ qߟ.q܁c9KfZS\QzF 2ʚ @9Qi hhw }[3*ڨ d \cKhAMlY} $Aa@;קh]dsfǫ}m;v E@@̙5q˒%K\-U/;_XhlG9X1֖i4L fkmϞr9U?{tڛIp#TL1M{._y.^cCY!?؂ C@h~Cy"LjsH=|p(ߚh6/c*w^Wk"&;.GªllK̭yj$oZ5ϖcԓRx<ȍnnl<*2̱ɞdo>dTHX^0@ EJqe$(MM_ j<%Ǝ`.-v\ض菝O 3bkA¯NSQZӯ9s&::#srr^}ٳTT(N8^YY<9n\bJuU???*!+԰'L/?9Zpf ,ڍJ8=*3,-qߝ)q[~oD<"tI^ji ;mwlzNm7RB|]"*):@B36FMo=_mCӠhk2d.3ݙ\Qvx h+pTB=[ь42bf.~W&k\1ޞgsߜ.,Y"JwOO7у"PBL/7̨DJg[&&w ȶ ˺.;eNPBY02ܓ-Un=½> sSKN֊ wb oSRʣBC\}>xEcZ@`ٳŵ-iRU; NGq=tE!?" "##6oތX]]ՏzH[zT}Et?q \d.dn գiFF]_Ъ4Kc]+n|%}8s1S7XVG?5moCb U?ɝAӶgַ$=ӯ߿Μ9;%\vý__t_(j8_PwS+. 8SB=/VӎW=V΂a\X'neĖSw\u03܉( ƟRu ?<NIU|~8'W7SގI1 ΄J)6rQpyO,p0#9{RK'?t2K5/ʵJK/<`(]DP>v]s_Wc{y y{2[GI s,xDcR_ l{V`P*?,mu+ly֓IRY6H!2ZlIR({ ;PRa׭S!^jzv)=f}yNl̚ p1F266FZ=HWLE*3pxmZ?-fiu=u IȫP^0X6 y`bbǙL8S|pDgcn 5|'a uUi:Uo4TG;v˽֭[|?p 655)H&yw=])iȭTGzLBQo۵2z )|\ 3];_ :1dYLVkG\WW3ކر{r3ie>xHR=` ~lFM4'Tmr_,_ޒn52-t=ȽQJӍaWXTJa`raݲIlRӱx?iWm5" 2q+~zyB ."\!r-"UΕi:w,ƛBŊ_/7&H[@n ;me eumemuq&$٩.k,ώO݄JUS\*~_RL)L_F VOsk2.տ?w }ѓ݆~R*_qÙNZ]wQm IOS.*غX̭T(s+5 -dK%xٜA&D׮wti܍8RUwOOVE3µ w9pn٘?*p+ MCFyOJߎ{^70dںt1) yu%05ugr%Ť*QPGmލvui\1i30A4x,vTTVgKb3/GVZ=x~*t_wHy4tij.µ_$ 1D<ΠnM4'qF0hhH$X] )NpZڈT[;u|bݿnoo'T#}i N$$\$"H&C o.~\&se*՝?&He*0pys ӉCbGgX5iw\(551.Vr0Kډ4%j;fTUihfb4 SD:3x*kT4=q҃o\ 87i7,bMS'rOK80aCV|cAեRyLb .uhkTr+IrFR(a9T bMT0vɐ\rYL4o<d!4;gb4Au}W\)WkyTY??.۳XdBNE ,E?+T#ik',m of%"#Ub%XXӒ.kƂk2϶ %K#u@U$sa}KD, #Ee19-.g$ -DS4gsjYcbb-)iK~!st=)d>̉CV7K׷1%I ]xVPJȩΔ~D$ر>KB-_9 XT|af%uy;sk5/G؄;QNoi mL(j@I.4Q GCdf͊}9HkaHD VyM< R?ߒ>8>GYO!om)4Xv }ҘԊͳ|fð/NթKE ;HJ*|`tk ˖eo=ٓ.Bޡչ'۳IMZC'J%Itͤ:I #ŔY<3JRh1 dOΛc=.@zssqZ[<;JZ E5+~92glHԌwcy(>8BD-%M`Pthם*sZ Rq&6҅9ڙG:3[~Xz#ô|w$܉N :U=V(Q+h|ֵU֨*3I7R!z0:U5.Z;&fF̬9~P0nJ=ƅAěn>Wnbk4AoHh XJ ڦD+DbksIv|*Ϯ/{&)ֆn3K۝\T>D)<"VV3Gr쨉yuY( Ob E.#.$bݙG(fݠ{_$sKsܖ$$ɝ`(}yHq?~pH^1ǟ7k+12!N%!l&hwbAdܡZɢmƸ@ӛfi&ahz-‰ˆtf"㋙dB{n_Jl0 yI2v< KMI@@1 ]+Kz"QjRҠOc @΂ctWJU)>)ԑCe\pc7*Tӈ=F˿>\GP$k=|jSujo=Zla)MS4u8IXcߐKJM'NТ[Һnv|9E5-cG`NKhv:vcbqv~6vv[Dϣu+I4 bi>931ph52wmP!^h[̍f0"qH~9sVvT|sZ>]lO9x`6t׫[0#'g| \\Y0~8+ISo6cG* ፍi) w.L(}:X6eTl?/kn{vp9;\-ƝW)f&)^l%Yt jif]8c1I=?@s"zs&JZgK1ʭT\ޥ~v@"a#YIh]9s҃ӽΔ;¤7D%#m $-4,ghZZ">,ܝ1z6DHR- 9](Ij fC|ɾʕ]$=5WYW4' 0*>9_ܹf3q qٵ@;72q aZ/4nMh Jl D;T:PO%o8[ dT/bxy"km1gK۽%3221qŻR*ޛn̨,LD]UJ`m(H"mBHGáeWLGO[0=v#0m~<ݩ swr=N8W4()έT|$RՎ Odg/vm/n_gt/l  sa}TؾXzBYrt2]BIz~$!o Y5&:p܉N0@oR_)&><*mJ%,Ky}L?_/;>Wiȇtȼ4ŕFxr'QX5gskWX^;q$#m:4SD;L8pk](lxw`pZ:%!E Kp.8Xt]º]$tfb%$n理Rur ke[ӫTkg8Aq?s_Sץ8B6ٱԎ]so$Y~_RZڵm:C>8 fpsK i: [!CTV퇇$F1LMƺ2+%Iz>TҩD!' Կ%Xþn@QhlbtU1@>QZ?w/ڝb?~%!1~'&Z$9sI+[GQ 푝/7n9/;4C:Hhm1уvzx=!C^ثY^cW)%LWѲ?+_#թڷ$6tK0LRe뾴hw)F?#8?ϭEj f?'f7z Jr_Nk7G{7TOLϖ,/@@\nEߜ.+K&1Ε9у1Ba=:Shjb,NQ?>/U0R3|H5} Ub^݉پйX԰*6SX[O!#0ƅy!L1υ>;VU%q'nVcB0\_R.Gpfvrq#S+t~u m+R`uG<+1rܟɴnx(]wgKfpt(thu[d?+tf,P, 3 >܉7=IBZJQn̷fմ %X){:^jN`gQ&y7%JQÝ 4?%^ar옖KBl0AɽY><ʧeMsNݱ-:=ɦZ>;vmr_^ Ѵk3}a Y$d<܏ 8SFtpy m c\m]='3Q |oVEc;<+$N+0XuLD2q&zd4R? s EQh3Y(|u En9T @gZMdmq}zSE/ yX7[|h/MCXF)|y_Ytfsf4jӽ9Hy_p]yY>ǏSן*v q)P;]lԡ'U/ e1YUt+xJw^X>ޅn!eH Oj+ӑMlCPǻáXb_V8;8Sue0!R`KzBtg&dʪy+&;]4ϟ8X(-)$I8v3,_ :8Ѡ2:Pϖ#¤7`ߟsezAUT`^iIp b |us;V(Ys~NonB~me+$n|utvmJG!q?^5 H1A $`{T7u9T_fLzq'u=Q̨ZKFd-=p.կܗ}"6ҙ4f7>HMSFxr>:TwN@oVg+!V7nꏏ)ƈ`sȬQl=+MGQ_ `۱\קa4=zOJGI<0h4re+l?k931^(6Ѐ.&M)Lfr+w5%Lpg+0uo0Ml ol邌_Y5fz .(|WT1|xI#=2Ջ|r|1Flo(\@L`W`qpM'bV<Ѷ0rjg[uio?ө6B e`!҆h[R8W=GI~7QԅNgaI@-e2*g'8Z̰~v~vZ_2§3`ywCt6Vk򆶍 O-vcYgK1Az#έ;/ˏ߬1L{cF0\)iƧlJN`KX$Jʯ۝RکCG:3_:Y$^ǙGHba}:f, o>W\Pk$]/kj2_0ۗgElyϜq#} *(fS9IFy ѻ<Ƀ>8U?8JIT H$PoVn5SDO%S_([;Q?;^ХY;َI̚3"EP-E83EOrD!:ލ%opޞ|{%QxR9t>\)iX9^xB2/e_W4~VLdtЁ E!pNGoT͞EZ7kD}( lnԊe=6tXcž7>yA@~&>W\Р>uUӡ1tG4eߝ)y; *h~mG\\f͚o@x\nT¥b[!|)쏏, :q`30U'Ǝ`. A4(Dx:ku(\oHWOrS L6kzࣩNz0C=fy\%6wnN,ÛO*!n$6$ΉKy^my|uPGC+x5rP{;ۗĀhvUz6r@wcq)JQB;%Qܹs333n~mߓrȑ#{O655!uϿ$keRu>(O.ۛoK[,hJ.K *>W9Kz{ξb.7ܗ[\B(ʕilKv=8Ç< )^O!#vxä  `қٔ(Êa醼i͡W+CFeOfʷZ1^4:Mǂ6} 6#&4<)-)_ !2@ -bs%}`؛Q B_|vI{ɱ[+̺ӵk׼yqcc#}- %//T$w>JSS{Ruw[Tɪh~cw(uf1څ+~Ϝ e$TK3H?(?-ٔXgK}>TdCcCܘP*U3/Էt;?u~x$ vcNI rUvLw{Ji$v0^iNe&9Buǿ7ۅ>y=%@..BmɅuيf 'SY~RՁJ&rQ )`F=yyy! ll;99󕷟~zffiw>o]]%!P[Ba=j狀)<څ5S&{r s:ن8XC@)̑-"pVڀtx󡢽i+#ޝ.S].nP=y}Dl_ L~$]=)֥{c]+vCXdDŽIoC]|DJU{n"cPʫt:Zq#俯$,?kUɅL!BX,XC~[vڵ~ۛJ޹۝T=mu]7#`=igƮ+0^ĦW.ؒnM6VHH$'yZ%&{rV?&H{zzP<6iIh<7<6reI`cϖlFə{3+ gXV5tvR [ .d ]DŽK F-̠wMSЈfPz(G6qgK,֦(4ƅ-$+̺PDf3`X[;1v^Aؖ"m%]ZS.ΰy>;^x6W9??݇CO051˫TvCE=kao&"{'Cy{z6&"WC> n>R9WxTqNg6C(Rś2aks|sIk"c+ם*Z!v!ioZEfˀ8*ʕ)YlFX`xi>Z s" 򠩩ylkk+ʾ޲ 6[vϿxL%bR+{&X4ٓ xh7VR~]fy3~J*}^B~8U@kma/!9hPѩZxt &x/5\$w)Q31 #{5DڎwcL!&h&"_'a {oìFU3򛹮d ,z/J@޼~#;5Mc]amt}oNuqESp;wnlg/_(MvWĜ?><<ww>3%_m%#-W`5H-/~w&CX@FL_  z0{)ݓRqWCDHtL TCgs4nKRs]+U J_2ڀ2\ic0 QZiW]:Hgy<JE%-]*v ihwQ~oک{z5;G?\0`!Uwܟ3ٓc3M qi;Pmh7LTvޡխ3woiUA5o8O47^7kUe9nŴV\Xq[e*,^l`{݆-r.v~ui7-C[?4k7&H(ʚ^7v4j^= O1ta"āvz/U^/SC(uMSzEE:jlOKȫjP  R' ֤/Q4n.21.dV9<[dj&dž:T1xBBBZK~L7őE#ڕu&GWP\raV41sD30WJW[|}8̍&vBm4zMwe*aqnnL,' wUޛ}L(H0˗K'CL0Ď7B1Y_/vC4ɡk;/_ J)F09XJznJ5t0yu`7Dbcc7nWJ7%JR{* @o!uv𱡾9 y"[NPwgJd8XBc~A1AT 2HPO|2E?"ܘ(TTB/zO*z)8dxE5/O(X/` sJB1 q&F_[#qCE#xwMaNvcq(MRt&QiZ)D6'J_uBmFp 8@ϨZٟKrc s۵E. 50l\=<H?WiVh^ q1,_ܩz7ӣJ52]j*:F4zlh4$02W^-F@VKKMC;׋"Ed@C=jiVQ>R  ik$tew \=YN̩~[7%N2P1tXJہ!^)i,U즓xˢVkdA_s|)H;2awC9JuB^PRT)mQqdll~ odכ==zvĦWvj{rxku ,`3KbsVoGIGWtN.$-e9.='KsBؗ`ZmO2+*RJ&1U2 6 i NQ|4 in,'u&GU>T$fI}lO۞,X[@4 bI" JXWw0JTj|aA l =Z%`f1359Kr '{k5_(q;dx6rW݉A+8v†nth{Z:t - 36љk+9vFے.շkwz~J,ݕRHL=crU;N\rJq݊=׭=ntZ;#όس Req5-o A@AT:??FG6qgKA2G:χ>:\9Qh;:wcO m*fR^8;EfEW+~-̎b%R~Ȭy9B Jc\ gXWwhDEv jOv_򕹕B(uasD&ώ}p([r a6Yzz;u9џ dcKWY}Vs Hgf##304x`nBB46M>sնN5 B2?LUt}GeH8sI"ʞMIoTԾ1tI`feRXH_3` l& rq%_.^?)?!8ڃkfjrF+5tR1ns1pNj"g^"/e _Kdf?>>z[-robьF [ߏ Y' ?ř!`]-U%ů-P,KP̗GcAGJI㕒ƲV<혖>bJsDf (DtꦡP|ӣO `_Sdѕ%'_/ʓ&z$re5j7}<]iQ-@>딚O2* X1ԝOA8tF}uΥLuK"k 7>dV5b!mԅAK (mzd;WNUXShRÜKQ-mn)XںJZz{?is3Kre=+[D}!LiNJhBkq"j`V6Z ؞g+ofuEC鲘K .\hEA `{\+'pnV~}|V5ҬfHwtXPwT7\i]7z{P͑*[_C6+EaڦzqOc"VTPVET,2cЅ7((yGUe?{5I Emeu,`AQQk =@ɤML2o2EJ~?c2s߽{=s$#h'ü%/֕5<ˇa<঱e;Z҂Xz;w`.pILJuN&c+kۯ5j:=rWt(l^.%?YPQk9]ݙA#ll~E7/+A۶mx!bt6u!8]f]o͙8%f"!d{O’Aeg; 9+a(X.4ݖW[*[G@Iƣ}Sgo^dQ"SryǬ:VCa soUq ?y?hߝTA^=f{V+<>B49WYԴ:O(c&W~&'(*K:X1ٜ@Az/ecCKhx7 D"Jhx|ys d+:Xws8 }AXĄ_apzkCi n#i"O X(>n4喴 Iozu| /Loh\0YōF^w[PSU BW5$ #8A1;0 m--u 9OK^yu.`,PXC)+b0"㵋5_{oOe mu@tĠQORl,jH-3R3I2[]Pj VxZ{'%xh:QkΎ˞빇K5/o.u\LM "vdxArMsy-إ,9d/F+m8~ub( _5 nzh|:Q264 !FW)&p٫ufq1e7о8Z(6٧'&CEw~9Ģ`To @N9Z{VhQe 䐞)hu"VX;|Uߩ{]6!!]xqQQMrss??~[JJʚ5kn+V5*99gϟdɒ}@[믿>iҤѣGxG Fo>cm[Ŧ̙B#}ՖugU 3eOfɼc )j535{( 9Lʛz~9X5"Cj ?n1g#C2^DTRwW<768^!szqFۧjKLK ?NUu|F!mL@n&Oy$YIBH!m:VB[!] A^T ̾%g;4wxx}ϪGEq)VEL1ª>$F *O]õ@_1?nA {!8_@ wPͫh ރN&9~c%e} 6-Et[lq?alݺu}Io󓒒ܷg]'Psk*J'wm4XtqQgE?!-i;򀘬m`Eo.xDkgFF {b4}4+?#V=?2g6<qAB,ʒ {+Z{PxP$c9ay}W[gipV 뇓oEfCv1FGq:^eFZN@fVOّIBRշv-H}Yabx<!Z@¼ X_5IX0LWD/4@Ze]f+?)))ݻ ܹsq_WXq?/**RTׯ$$$\v'^]6Nu#Y@(6mVPPfsIo,!(Onnn@P.kҚu&A {q>͎?3zrcuPxyTמQuլNYg~8c =.2us9ƧJAuU%y钩J#H(Y\~U ^\|qeGƅM õ7hYAhn:́- l[ O?-Y/EBpAk@8+9sK-_zuKGSt\DH]No@V8;FLVX]KyH 䖬?&`1c`>AL"'S{U0d|))h޸#wv{]1c%nӦMK,YlWz\?rܹ@믿5//|?> N&L> ^9;|UnWF s__>_hэ&l{൲wO 5YB͋ _Tꖲ=|!whu*t5|[̈\xscs", tuǛ}r+ɓIrƲ55wX -E>ª/n*1XLh[uesb''P;_n~_U0@ Z.!!h= Fg7]mAfnx;rHOOe+Izz:(H;\ a ecenռ!cu6ٿS8R+??68RH$3UPUK4+͗ZXTUǏUOdʈXgk:_kSQGj4ݟ L}JgoMJJ\._W饗 ƍ^e}.Ry9|̼rʍ? |?<+%qu3f2M0w\C]q8]r DH#3%f6̮H勣u: ߩ5T"9da u] Yij1(V+6ǐ D ?QI٤0/(t8]'Dkjto ̨"vuq`)))FZxqrreUIIIO)>HH[ݪZo6۞%T˴KdS{.2ۜ7^؜8Y(,6wU'Z ESjo3՝H+۟s VGV[.]XPP0bH:x fV8[L)?mLJ["b0`Yooj :O1gVrKjJ)02l۝-9sljds#x,`\*Qǯf ڋ/pv_o7oޝjݺu cԨQgBӹ|/_v']̬ݹji^ݻwk֬YlY|݊?h|JUXti tn\f(=/l(.o\Q9zos>ՅON˛K^lRu33UNZ n'F(CĪpݕ2Ki(̣)B0=) % f; 3<ZKmkϮߪHѹ2V€5sOהƿ+rǝyՉdI,4jq׋M;{{~֭OHKvr;tAF+A@{|^sk4m #}#bᝢV.jZEDA S M̺P n*DաiX{a09%ix1g=w(Ө;<)V#mV}JC=AfLٚ&Bz٤iqwv3AzY X6)~e*r:72eʔRwhE/*snܸQRY\裏x9sVPPPjLҥK?|2PdɒӧO8իWo7l/Kp_/,,~p9Et^߻[ @awL!FsDʴ /|4x^^0Ym{nizl;xm¢Q MƲ)X46$.lDu1aޜp ?|`{P!ǯoF x?.N^P$W7}\VjGCvvvfSZɐ'jQV}itQo騀VppWX;$-7.]ܺu9s[ݮG 1.BѭKQ|B@5 MDyW(4ƌ^;'|vi-7~bkoY7u~2+͛]o8ef5[$l6Ϫ`c/ʄMlUmLb$u% f!s\4oR||SԬ37kEg:KL%`x4gQRH*Ksx6+vl8>X5w8N2i3f1&'ncaǻDhZGF5ڴ4.6=q )lv,NhҚ+[z:=ӦGF aVR1|Ao?9Rʣ ~/'%3Ҙ^lPΌ;r.0m7>۾a(dla]t=s7ZT*-,,'%%1A&"5 O@nկ;m3$1"lqH?Ɏ`~J$#͈0Rs / 1L/.mmZK%ɘ ivll7[VbwYNj-6W_B(P)x ("XMĢ 8 + %`+JЈ nVmH@SlwfvbE}B)5'sS?gUקDS+;L[ TۏuC[z,B:mutph>=TsdP0/FsFu[Q4>;*Vij3צ.pb@@|:Es!C58J7>^_Գ8[ub]gԯMf) 쪘&Iara0vDp\hu/l*h|"KAAiG0ʯc\( tv;+[tD,ppDx]j+˖ns?^\䵂:[o!dbL]gJQplF%a|v]nbSF(L -?l >+r34 S_7L¥p(#Ribܐ\IՉD9Ykڄiz:K-}GR1tk8ۜ4  x, A"Ec(3tz'8oNg |m]Vjss ^e4"Nā^k K#D ¢$lҸ)׬?q(XtK7.4@ʎŚr<tYܒ5y-xkR;5Ą^%HJYۢ}m]>f]cp$OE&G]l|˳$ØW#DutMd4թַ鶀BO#x1 yxP6^Hlj x_S*xNSr"?VRժ@u{ͩb>i6|NcS)r&^sJrD쪈Ӑ nx{ꩧn!?!dl4oqcmY[#a% ֔ugZt('A'gkFfX0+\YNPXٚ]%?k#E+_wN=͠5ژd{\*k#a: 0Zk'zS^ "p8 JΦYp'`.ϧ>YhZv5X FohO*Nqɰ6Mz6'tŧʛz6WSQH £<#pU[| o* h*-jIkfL_3ف#;}q1!%q+ Yu#f0 rH,(bkh6~<-!c &7WKbNNOsKάp6| +-KUe_UI^Eڳ QBP qn;wxN=ͣ!1=WmPx8ARnɫ(W~r/cV7kGwfőZ9$a' ,}hp if8ћchvL6XtNi8A N 1bڌd'T"&-)M6:4gR}11 l& .O{|vZ꓊W4*fNSySOYSOys8q8]m=?m:12٩6#uR6iV*|Sum_ΩLTId5vl*P/ʒOڸ?4_Ϋ5)EeHsI6WӉر@WtS/`!i֞Q ƠP~vBџEg^w4Q"@y`FE/w+<ÇeRqڳ%amxmԝWaCB%ZMy,S'' "ƭm&d/(YP]% ( ὬW`1iXBZhu|}\4T+i;{A!-]E-3EPQZ;dQx8L3ȼrX땉BY(%U?]ynLfmoor'.jMgGVe2vU-FLɀt{Cr6?#aezwVq՝EJCetҐ cjj5js&)A& o1:MJd cZt0OO< cuV'|Q jiiptmrMO{HW@Ry|Ln5, fgrs"ԦA/+uGWRFbĴ!%JD޿E)@fdW[<Ȅ{N{mx2S钭e)ɪTɋic:^ش9-91OSPN?֟S;aBjYҙll/Ti=iӒ.t)Ȥ`n`jY֜TX̒BG7]{Fb>ͤ8(fgP_ܲt0D@0mu2Jm8IxlNG|o-7՛ ЎC0x(֗61fr|9ZҠKۦ @Ŋiʤ8A v]nb8 ݇=hz8 Z@ls|[ˤff\z)%@F(c,)=:\*AHdU8n|h~fk;Gv?f{A]׮-ߞp6%Cxɢ?GsE40 E6_h}Li`?} ImԜ,gs|?Zraw:w|X*oOHbtCů"AW٨5hCug^ȥhRpAF( ,k LPtcWuT~c{yNocD# =Dz`ML.\QVc=Drx? d?B5jN–wG MAGqD=PwEMoZGV{q\p0'%Zr={ۉ CMe!gl4CJ568ަ ف( B/{r?'TN3c5*zsBD(lp0ch}@ F SHĩ0t[ǧS$00H$R8Wuca쉱H.=Wr`qȸ 6`~`JeysO[e< PeCxd6t#9(oy"K] @vsHg_\yf9s`I ws( sh?\l -&7_hDqAת3E 眠vLw~ކ"riJ(pp֎k ҥC|n9}-:ZFF(jΉ`p*" 7_P{?3h7lWu)(:L! baRsuن@f_J& Vm#ub&Oi"4/<ᦂF) 2|*!a0!wX ӷ'3U)LaDp#8:^wEeonJ'ݏ1@mo7Cak3(M`=)O 摧& ChWVU! x\Uu(jx@0S䛼)"oU#Qr vbs8/)sox?&D>Ef EQD)=(LwgG"x"ƈ驁Dٮ7TuBiCf_\yo&MZGEreţ8TeG28 f^Vv^nY?^g|,FhxkϨhOrZR͑2̚;\L!@8U ˜(pp]u ߟTf$|GRz0OO G"ymLZXFgj wG!=)GPm/l^[#d4=֟mi0)v7ei.Lޢ(: v &Y$(#"&I$B#p>;\ۮ.ʒI7&Bcv8C38ޙoO(^ƾ-̉9GIޜXD8x5EP f(3U3dHמQtHD Yale=M<K$;҈X˵ZQ0[Ccݑ^& X|e[_T"e2umyJ24 !6iDp224]wax0+ .by{:xPvOUs%>Z~uu:M b6!,"8&Z GHxLZ #%5#W@ǯ9Nv=Ti| FPWTѬ;\k:ϔY68 튐rFREVa,IOrk6!"O}1D_Կ1?)BxphugpBox387ףpy[ @Oޠ&5@C!6\|uK){alER@ƴEvEժʩЉ2 (CXΫߞ¼ rQ oϧ~tOC_ܺj OC!aЗ\- 573\r he2w'8\yysFxubI=T4?p^ͥIe$,be*kr(":S5A9y4.+elK.+Z<3 @OdJ<-Kz j-CS8 C!`q@h4aѽra+\w~ @{ bN(l_-"1hnEj sx0|Gb`m7n)6ٗLlϕ-| #A |1-|0C:;>@C ?QM{D/%AdKó8 ^>%^jLe0"W#?pmA  ۑ,gMoyCamxq1׶5!iҚDy*^mZV8;3 l8Z--Z=U8 *JHR\`@TPqk"x_=I bjz?u1;膾O¿9XҺiѰ*|pmd!ޤP]y.+ $Yf=WZDaёRu'\ࣵWiEzB7X+{Mnv3- ^6Ƅ 9ҍ{cN"a e=ͳ̈́Xޑ2ͣ+N"GJzR3<>;k60vk3,wZNe5Y. Lq4\*Kűx! '琦'V{bw,Z42!AUח7,ΖG3LeLMg\"b&1=N+䌔@&zk[a3FFFQ0rPD@'#'$jEQn.Tv5;.%Y$ $,#`o^t[].^Qv6_k҉`gH@xg)_M xzͥOL @[uYnc8@?6O11?2|(b9]V=xA_o.h.^?'a1PS-%u]<(OhQrC|vTtZH'sbמQ]oWw<=IP?(.W@3Wh+k2w aJ(29'vʣu/ CҖ78]2_S"|Qum'd*kT8uZ?L cxۘ .+v7@x9T:"Y4kސ!b0% wVkܪ5ukzzw'1NgtCY؝4"O'ih1mJӏtjefc{4OO!TVōū&&谙eB)pЀ@Cug& C̀EƏ 9_8BᚌPVgvXWt|L xklg^Eve)5̨@9'[]ߟTA-WQ3C00,ʒ/ vE [cPD[j뱪]ugMh*K Ph1폫NKm2|iae59d9|qkVrx<*1b p^7p]'dds*/RZ(QA6_ΩS:l헚&/Pa>Eȭq{HoxxcY.t_zf N8-@K-h9]*Ĝ.Q@ x=tpx0kZpMIr,l£ܷp9uIe.R2Ff( X teORyc}#u1!H)HRIڿdms 9;&VOrkz, GD8@|5'U"amxќvԷw5{*rmȡe0MG…f]#Dp֜R}4+!ަͰ<2>\Ñ60FHc#ػ.7{'ca_6K^!?ih R;n`;Sq/JEüwaYlG^3iMKy?;0wRu(IbZz+JD2NT(jt[*Mv꧇1-V"-?P)e>"ZLUb;ԬT?[p/Ntq% Z5cRݬnfHWm\&Š]m jm!F2y&A\V6)ձz 3=YƽBvL4oZ2l/8&{2 [GB&}e(Y*Xr SaTИΡ߰ v((AH׀2{W yh…|,z|oVЄ+琎_1A\OJK'apMG\ffcu`^JKデød0,_܊=;*0Kv(yN)k @sUxyõ=OThjD8kƣ2*;5Q +@qo/錶Fe¨30n*P?  D ƦR_e5j̓[wU'Tw $x@RCY$<6R{HCwfGj`?QZҠuh>\k,v1X 3eOLKTk敨/wWb$[z̎Op^@ -rS3ϫ+{VrR\٢w_1Z<nz_2:+;'GJk' +mU{WKdl;!u&;+ymJBy4NjUkU-q17E< a;~'KPM:%I b=,zooU0(̱`sJe>'3OB%X~eeGQ@!N?TΎ1Q;|.V56g }1޸Eo+\A=tm-|XTV^\ |cPN?ɭ1IeH'\ rNWwL(gi Ji^Tt^61sumIe%NOf " PNt#=f5GH{,gA$+ݑW16FAqj jR9 @=3U[m9YձWu hCNK0Xbn0XlhqlMWe>O̊Hй <4j^_C!X@&D,Paz0FJc{aӲ-:3˼8[~E._1ZJi_3 jw~zAAď0s7t!L2J h78#Zi~Ri̯6;k 2:ܓl(H_^Pȸؠ 8!ηt{wx^ХʃIɭ*ɧgI{}|qa\8勊&La Y?՟&KXrhGuh=94[2A+Wovoo8E-)I` {M4"V&ѠPQSXX7YDsooS@&!Mcdt&xgVim񶡅oӳlVVN=T-f{h`XJIP B)By}hF"@x@ct&W^EZ+=tITd;tXr6d! o:WqHil>澙R4PP E#ܮ6X )9;tvC!A=R6xᇰ?Ii{^;%cT?n3wNnRNv~\9WnuQmnb(W (X h2$RpSY bШCPRZ]H.Of%d˛ud(BMoo:֮Ӆ? {}ckKt>RJВ&6( ӌD6B@G#\T<(p5O*\|I3w"][, l \YH-܅!++^yOƠQq1kG F QZh̘T1-2@=9L~cnu9t{HxŒI,oshN%)H^A~"_Ơ_=r{ckQk{ٝ[ӣ΍-N`J\Ntg_@P ;ĉ 3ԡ =s "Rᐦ0F]Vc ~bo6H*\raѨh&]zB rk>p|quE/ȧY[,גBP B)rXtZcɍaM5\D\L _S${ Vۛg#IX r6P(!Z 2ZəR:{gb^d{d!ͿC6 g'ݿ &oTEř(ŠQn875p4œ&CXIY͑|[*lNϑf'_ۮO?6/)!5Ʈ6eW:kz{(A=@ )AG}^="h)9-fs7G}7\fH`SO(6ת=zyʹU2׳;)M:MHu(8]Uc( \ASe9F(MYVE$5h=1ġs?ޒ.ا:ue7Ru}o<41vdIwJ^ۖ"QPiı. Js'y4 _9g&6(;j'#9A,Uj=P"=Q0Hו6k,o?ǧm>ʓ }+K침+9u Q['כAH8tq<@6o3ٯdh{X'g4Fh$`269OҤo8+چ}\9kyN @Xγ={,?E#e {/mi dP |֡69h#Id}n )T#= j8%_[,[/ W85r V Xg=xI 4.;Ea4hf%q;gBMoTM!E\S>94;/&sk :ԝ.·pO/G>:dwy1U \]r{}oE`Ϗ<'?o A<j!*o; Ϣ$lQ{ho$h_09ުWئrŌ|eقIk &)2Eߣ8>X=hJ&"LA[ĘN8=2:-l1ju<ިY08EġI8 !dp{љƷ9h]^>_˚ü^刔Gϗ3mϛ.3_ -Ι%EGǜgFtT){^*Ca5rO*YdoLe!.zo_WuQ䂸K!>EYLYr02PفQ{noڥ>1 fRE$w5_P88D&Iڌ fk=b_Թ^䓢x-5ݟQ& -[_7:K(}MUâ :{FǜWFfX z옼K*SMyd(+g҉x7(M{dNbnA}6ךln>P"6 ]P' (RMn04?zx;؆ǜHpшX: mW'*foFkڪ1kRHK7TAJa[CY2gw:޶Vhd5 7L3^=9N=?nǫ3;&Ooګ<*A&& sRbZ:ř|7 u Rviר T'/z3#UD4"BǕMTkS?ݭk EcgK( ^<%62.Oɋe!oX< ~췎i)BbO#T } !WF(G 0~O`7SQxsMwh7tgH y F6+|E"M}*@8/xrA!#nL2s^+n|pӊ)\,dHHq;^0'%jH;FpT͌.4u/pm#>'p9<Ǣ,1T"FĈ K%`%⠃-Th1̃_Hk'FBO*)litH|YD~=?i|hjEY:ނY[,+:%L}o -PArk1נٮ39s=nCC*!SUO~<qM{/G Ai1ҟ%5]ۿVsfB0E'E]%۫7;M4 Vezr2.a_O9x Od6Ɉ񬷏vƐJ; ١5hP7-A.˒ҭϻ'z)垈Yɒ1?Uį;:*Wt8x X4gԯ|zI JƢGHHB6,n62R} x[|(;0b[QQEzfpvի#msjQ;i0hUg: BZmD < t9O(\RZ;b(]Btvw/aQBIB66w [xzG?;8THUΘfí1š39vi/ f81B!Oi管!ACΊl̋ԸyT)n_N4Oj>e<9#[ƀ\(p{,< Xǧz0h}%MgկzfiD4nX j JZ!G[ͳ9[:@˒ו7DX #zIQe16Րqv2'Cql[9LVp$o+7aC]:s% Mk~珷-ϕMj^ܺuc=nkТ1:h42Ʀȓ3T<+TzoY5/r5_^PJL4*H#d?C ?,ksyוyiM rxO4W03ٛN5cnʊ+L} lTЌZd2-&{PY[" x:N$! dnX|/^c&ED=9Ҡ0͘lwfG֬ܿm+egONX)/˸4I}|J=@΂KPTn5?L)9=ڽ Z( #vA^E'Hqh 3pQ)O''r_czIcRi8V( !R uzCmə7fxD߅^ê\QQ@hǺcyd U ;@b^?9uhs]$ x@'ɾ/_7;[3189)\ѦoQ[f'c5Z*hx8n=xI)|Gj͍ow 9=r@AP ےE>1Iif"ے~5[~4an O) SêQ;$ 6941@[@Ȧ(vzr"iǠ |GHB^7 DhNaU;ܞHHEݰʼnx"+KJtݣQ^Pis eMhYRƝb8?AXmWv>YþF#OZc|r!7ɡ7g̊6&]/ۻ`,NB$!!WX-$pX.%slS"OH$T\c, OocLҩk:%x7}F.e d9 ~>YmZ X!&Ǻ={rLvt¯CDnjU]P{O9bȋ3 yq<2r&(̓>ݦKSW- @{Xd3~#_Ѯ \\DmEmHeX jUӀ) G%aq/䟟XޅSy79ŐLsgM%/eZ*W\52`nrva*5 Y3;GVv/wjd ܸ }fS1G9:#ښTɭtQa}vWkq "c^E[.]3N>8an*>uk4pYI}H5jA)8P&Orgӳ)op&U(Z*oAj:p{bAL[Vזj}Ny^oR%sL?Ow{/UL+gY 7ia:N~x/lF)kM**ҮN\Ƿ4Ȍc L=#=&?i21`#<9{{r ɲقw+.*SU+{-UmCI0.bзNohIj^ ب4UdyێZi|8E/n\Rvֆ l1/H5\wy~O{pL?hu?4IȀ ʎj{DNEqwb!$J0u(Glb8p(57hOnjƫo;vlx^zT*O?>W)7-Wah 9Ω+$H R6T*dmRNQ?:' w\コsвVswQ,2~Uc Ēg#f^܂t󾋺^-X/ɐЧpZ5槿lfs݉6FȘ8ԗ8/6>M4>e1t"R<gN Ye ~}n r[oP"nܸr12f >S7 Z><81 bfG%mTNg%q\^߾m[\ޢ_f?=*cAd!uI&X8㟻-yúmmꙚfٮ|)Š ݑf[WgHOߒ0'n $$NLzCRi,/m1C` ܒ)solw,on`` 55M&SLwhT\}ffFkk da(g-qg J3a n['/WYww}umR!Emmnј%<8'J9}v',0YH}|~?H`P~<݊ln\pF4o_\nFc^2ZPƢ 9.0ğ^ar2WHc,ߩІN4nzc Jna\Q<,BRitT-, tP4bbwJ.GxSSs裏󟯦 OoN wFH nQxqSgr?Cơ!A,%s7V*<0O#1wj&>=(tJHFgekкz K](y{mbEw. /J |nuɞ)ː^yI~۳NئKË2ϋ͏eB%e<-wN.Jr4juX9bd{G^̙;\srV ds`Kpc6«:]*fMn(,)Yt n1koM-\o z1]keG TjȠp(!Ϳ]g?ܤļI2=p26 cq4^2' )Qypт؈XpceΑJcd}@sk $[(] LƧ~ yO'zO<[+?AZk$CĦX~#=q\BH @X5U0;F^s{(tsD[tQ*8?dVw$F'k}砥];֮5֙f{Oz}1^ s܂%g>נ=2(J+LpIDfәދ^s#l y_ޭmmCΩT|>1 I}W7::Ԥ;ܬ1+ q\3_k#-j& 41mtGT"vRo(CzoLY;%8?44 iJ}CCCjz4Lo&ߒ%}6 0;\AB~OƲJ8`Uk>}dz޽7Կ2U7?[e ld.,0)EN4NͅBEQ(䅼iQm~$Y45pܑX#c} tIBJI;7^1݇.>/G^!3 7HG?֭Hϧ2cFk#)\ޢ ☁r{2Spb}t8ҭ5:9<\r273spKo."_0*")l:z`38HhTTT5Wr~eIh=q!9ӳYI}^= ~j 1+s+ >$2!!sBy˫A<\n4kq Iee)q{2S%C:=x0Kyn,&"$ʅ@Nx>e<ܬˋa 7x#b1yS!rS(18!5/]s]nZ$wR[}C֐K(f%q~{YI\ 0sSy>ӣԸ$uQ EvRyaoAx|=AD To,I o:5|Wm$02W(a#e MV6GԮ5}K3O ]obo}'FǁN1y ՆFElV4J J7;[PR|toQ_꧰ZZ}4. N2Nu=0?b_;GAT&-H=1?i6b1O(xq}EE1Zov UG?kZ1O-_-Ҡ- C08iьۦ g^n:""!YeӼv;?yW!E$${[aG'|^ t8ڢ7.t5t -CBs |]w"8ܞY^S ^ sK%|~MSv 3Fi]y[Uyiܧ'K>.aC"!P?sݣꚕ^V$<.͟Qq%V剒v6L~dAHG][$-K@BѼTd?Ҥfq??2 M)xlQiB!$4 ugȺt?F8iךu՜,=,q~o5rn/R]Wvz|_<\ Kr=mY%M@`SwHNoQkXHֈ4[Hc_98$kT6V0Xuǀ}ǢȦ0kg8|IՈ ZMi"GȀhKtD T"nbꎷ?:(I`/!`ly9:>:5GHh@3]Ju{$mSY1]4=C!`Yhsuh:-L2nz sz = }~Jf$pűƝ ;j;4k <MTB* iR Ӄ3dH١1HX&rzK}rFN4cz 2X]êQ{,(j|]Xgpbڢt^EC4'Ye~,+!tTSoD(^p)['Do *7AcvԪu&/g!AB9b9˟lVkāEea&KΓ3-&^ZogCrE_Y_@E̓p fdccfEs0>R<)#,Z*{x415YHMѮMk~pO,fkzD-.PgEmޚ#HR(lܤ$ VW,X$>@+c]uG{,)I@@,LV=GΣ"4wQa\=! "Q;4+fG:#XW2{dv'Qu;܎BrKِ>p a˒ػ4~x 3[m`dokkYRzg@Șty֟#0qLHר4Yޓ+gDrP Jkmh]d<18 ἾYqG>nP E‘,2.& ^R=%c$ o{d` @"ʒL{H׳0H7r&Z/.oֿGE> DIDC %sk/0 )_~ϐ'UMf',d]Bqp@h)c=vzN,TٮMI1ksí7;] n<$RI XU'{VkcVLnxH׳˒|'p-H!g R5A 2w+(6$ !dz7=>lsK*gtvt6N;+ˣhq=39KhZG[HuYzݮI%)2FI{a:ehM8Q@}X5s~ll_o4 \4٫SCm^a"`Q"ڃ3|8o"׬$'iheY)x{I-Mhu.g LdHhBɶ:=^H)tU%2hje}Bєxx}dV4LLHwH>=$K2t2?-co?ytѲl4n;r)!w\H㹮XohPX|Xσ=}26)7Nm4ޒ%Y>=A!IU T1푙/kÖDxې䐲爊Y>i_"Gï.RЉE/VsIkh ~ XS tNI!`f%];/tPXS ToM3u<8eQH92®&{o4u,ڱUm"ba$\UY ݵDNlTLa"i~Z!Zޢ@FxK?٩bzEK{X{ L98vOp*.W'dG@*UY2w~r.CBf{WkoIӐW|W_|$1\#2\AkܭPQ1\r,4;%:ȸI5[XެQY;aUרrg6N%ciGtdsP"q|:nŰ) !݊oO&0uvӊ>y,XOA+d9ԧ%A@.rQPlj]kr=>FA,;C`h$,]SW,G?cypg:/ O>7G织u46"yds-QH@[+RΉGNWoRY΃&j^w_W-8йW tT|`[-R[ߎƜhf,jҩ+D:C Kq(x6J`{ itY}1Ն 9\|#Jݬ$V\uYݑ/yxL$P^Ԝh~4mLHzt!!օ /_6<*+]8ޢ}~gG#~2Xw9ݾJ XY gg&wȺJ7l[/Ur-ˑDGrB?"#O ӯX: JR;]pt~,sI췜vW5j4ڌvOR2X]'"͡4A5.7hov혘E]fxgv_Mc,A4q ˡ(hà3_$uWFƜ龻P*b9Wd5+g yZ~qdU;[,ā#Xt% z¦ YXUư{c vhBqx<n\T#Rq>y]5H `umnQQyE3;GEtBGƣ %S$}װapi_\'IpƑn921^/4-$6rR;,XQQU,6h+~wJ 5M?T*~AGyKlw](MS!y& Mڇ'~яs7pzŠ9{skC7p):6VyVZıHϪzVmp2!V{ܼ2<>y`0G~3{;o/X0]m OTM3+xޞK\.ӫg F^J<ӫZ]Ymխj8+oRyjۖW|3wb#7~}gh8w=88uX= HOvolp}pcA$[.ё=ѷvR7窴jFWnJ@hY5ȅP2{h(rr<.ʲy3.7p6:~wN\;?tm~uK|匐_U~FBݓr]ntuT1sڋg;8'nX$7L:?>wd,=#=#:¤UYF%n\@ \*Sk?PƷ/Dg=3>g[՘9881KF]v ~w6Ժ͜ =o=wS{k;Q78%զxǎ`_zXMZ `!.ݖ6y֡/{6 iqV]9u+wO>oFkqW8ܽP{2Q|L}oVT\l'7,;s}.6: p[jr&o5Kk(o%p?YUc |gؒ*.˷ sPs` rK{Z]A_ԉ<7k*64u7ݹ*ZI&rH+liu>zw2Q޷k܆x&_wJ9Ze~zoy?ё6\5^_0i~o$"O=2޽\$N^i"s7pYDoU_h*KX:V_H??n(Ʀ hO}?;mnܽo#zuormis mrnkKq? G]vR/Ia UX4_WMuc}#0kٓSRQ7xmlh?thU)ŪW~3uvww ~3yɏv669>y}l|9J*QKZaؖ[WsLh|LߜHz/ @lms47ƦͭwERw&opcwywO5I>Z\릦%U=;O/;|Tf ?,T*\>^!ZL5m柿8}Z鿿m֊[_㗟 ' \v 2Z"SxVx(n@\Z`{TzK[.yt$NVQXcss4~!kYX`욉 jbs{gZ)_m u<4^iխQ2UXYu*W~s߿zzЫ[uois_Y^kyUZS︦VU g=2k(hlporKk__O;|,lnu?ϚU 'zxeݕ8>9{bum0ΞwSuj<0^iǪWݼ} NwyOLM3#VO =@emot'zէ{}RTʿXm:>"\zr׶v g4Pe,(+4sCVǶJ7PN/9{55zH}b2*g4olWt/-mN?-mͭf\~=ÿ>i{krbC ٖvǃo\kf6\t?ٛ)YU.RXk(86JVg/MdǶ{]7Pf/u~y`lG۬:"ECǎLͭmΥ9 <4~u_QʙQ@miuϾ;]Nf^GOd -m0Xtm.NO$G鑞nkݨZj몗Z(&,f@uV=tsL၃c,@sMkQWTv 3uϾ 5 4VÕҿ?@xftٱ߿}&c+_h4X)Mܤ(mv4Lu8vT4W7X*|ɤݨ^Uou_Ջo2d䳧Y>sSkqʅ^ae4? c\WOQz^[™S?;hKM ]o;:}g'֯o^~i ԉGk,o`lkw~cg_Pde4v1[<0T۴"mpFRddGmlpzˠ(/> 6_ϜΓ=D4~ǍMn3"fˤyx:<"`!WM@ =N'0EE[e9I4iAd*B˥A&#4u>kL7uJAE ̢M=Sڜc荝nAM I+Lu0ь/%rTn,d>[(/1hUJ7Q׷;.B "ZQWy)UX"b4dϞ?03(vJj.!Kuŧe?0vC'Q7bU Fi[!/=#.d&j̶mxiҴBlhOиj \jϜ]s睟…Wzkp/}|k\GO?K nD>=}Ɔ u]X FnXu2=G9mL`K7%K:ϮYfX^|ىoM_MK.ͫowsGzŭMGλ5J<mh*u񛖺.,W^ܱƒ\l8YmpTeGo  .(ًw?yΧ;5ι\Wae% .(YP.(YPsInl}MogGzaV Uof#6_ނDRpA^eUyFn^橭QpAɂKɂ JXtoЛ 568p@,=zlɒ7Z p1u{͑sϾz~}#~\7;>o߾;G? 7uGO\O/; .y|} +_vޕoJQܺsϾk,udݼ]iR(| \y+j-m?ya*~붴:)7puܺ=L=z 8c#Wx( 7pu4[;2L]?7ᷯ4ʅjtw=?.:E2"[{G_鞑D& (;oj2T\UyϾѮsLۯTd>ʋʖVwk`2?tb<] d;ʎv,q58 58{b2976po`NxʩD/ ƛo=_ߴMV`xsFxǏ:1Y7V=\Qk׿{u:4.޽w/rL! XYO|*AM`VxsZ_{LD2#Ov?4msRۗ98xcN~oSݓ"+AE9F{>-v ޑp2g*,Z2W70Gmkw5| 죛W Rx}Wg'/ {]mӨd#o`Zh=yo<}"_3Si?7I]'5H2-TɗR9q[}LMI}v|{-晨Ʋ58 4-wx{א3xy,ʅSp2NBI)EĚd>_(*2RV)TrJP\mJ{}S۟}ߍ!>o0odhz,2c?J*¢Su*N2iNIkᇃGLW@rM.c&_ G1)=}x:\&ǢvncUX8o`Kx690 X:/;RAm( ץ6jbG*%j*r\Ia]J#Vqw4zUͤU3G}Lʪm0-2;ݜI}?f'"P2_n[Y{ nbPV4O.Ҫשro0wY_4㋈$ &JiRLFqMn/]Cz=ku)NN.671ʹ*H5/td?<45v]C_+]orɗ&cH/nY4=cVtB]YmPެ4ʪ]Yo{ojuߞU7C#IDv, r5˪-j-\0GR䙉` Lfűi8*- op<&s7/CMR\Xa68 .C`4!|i8 #t"S0TNt.oMeX_{K65kp:ܼ~4JEE+5Ba"G3޾`:WΡ[Bpdh2l(SnP; è6 ٜb`cڪ31èiY%/]5vZ+Ab"d&EO:ӫ{Պ{TMxG-1Ldg9uIfm[60b/XNkl:i9Hf4F),Q+,ZY)LKxFs^,MD3hf#*44^\D8NYsU(鱰4/"RX*9Am7IckfZY; "MMoJYm))ǻlt՘Eb+vF|LC1np]R#|uYfavާ41áްJ)4uVf/)=zr/\]\ eL ij\ARyT6j:-[cTMRpU[h*?0{*k[l*K|))$2dMRB 7Ԟ}u.o{{aT5vڪW+t)r`Ġ}թ0+VDOLKR6f_1ԲMNw7;u 5q*!-0'Gw,j=<eМ$ꎢ"58JAf .(ٷXG|Ҭ9U w^3 2٥v9K;ʪZs^#uS ^ ܺƳ-f&ݶH*55ͭZ<H0]/߱F,cyQeu Q\Qk|o5uVAV\WD4owrgſ` iѫE2nN괪StE:e5XƬS96/5ʙ{r&_Ed&wV|SPth@Hln^a&6@aCvfKs~l4p M Mvb^rϙDk]*-T%3s㣱np7VZ,6lA4j} LmӫfMu{'l/ƴ,<<~ACKA0O6 R+ƢW9MJʪhlZ/QH(mXv,q]?-ټ4WQ&Mv+ȤpJB$ ,6Ҍ6WzKw j/J)SDA]Vʦȵ*Ź(:45uN}T૏vJL-p8rg}˹y^?<{+m&2qP?޶Ő@8ړn* GC"FÙ|?d}e035dB@ X ?vwud^ukZڦӨȓ9R'b], JAQ d,GEL͢7#9Z՘s<&2cqQ‸ɱ&9.`|q $&cQ5*m8L9<|d¢ ZGL}F?!b0Bɬh^)rYSmWZ4U6]Uc[c wы_wDzmp"(U`)ɲRfzL>ΕrB:_ʊ\1/[q\IJ,_(g椠4'N._,ohH{_jzrzN89 &tX( y7ZW)A LOl<7I2`]0U ox+>%fD^۶v͠f[ gމP(mzˬsn`ti/olJ/ڵkv2m!Y7#ֿM,QAUecxǬkڮ[&O:K##ѮhV[*L%Zı,1+DhT/Bn9FG[{4zb,gDc]wAT?>JkUZTl ov拥:~Sn+_ R &rb ƳDN=şw띆z0lpA.F꿹}@j6BhT6B-|[)Ce:RrJV*4irJQ*UL35M.ISrR_7RQ 蟓>d)ϧt./UBԪOsvZQ0F8 j^f;Ox["CvC<IL$3~bJڮ& DvשTPi-ɑ{ bS]'|M/62tDM24B.ikl:]u4>6gye򥮡p04x fYyl>|q^޲kPP|X]Va &O}^{Gp'b'tީjjlzY{շh: 'bOjp S_}lA]̊R6'ɦS)[g \8ӷLH+ 9R爅H͍^Su}>'iZF#dBf噻j)&Ӥ[B!GO,G+^W|I2E pRYɨUVY*"f |Am ^']lKxS8OJ|&&cYQScU۵6s SPczZ dQ d<7Hqxn*U)6̛zdڜrf"5=2ԭckɬcHŸO.J=q_$YmZUc3l``$uV*9||9چˬY絭ZJ;oӅFѥot:loBq<ь/-u} RۨQQlpA^ ӣ5 ՘Jc9sT"gl^EX-jE|3'S*LRZyxt{z"3/L [[2-sϟ:3|׺ZZ &xdSl ƬFRyimU& f;g] Fni5r{,T`x{kIlyq)_x?.d`.u拾V]4욂D %_rEY[Jp.W,NƲhz2;9%sZڦU0CCp2^i}EE]G,fJ,?陈ҨIM Ve 80ڎJSGY%Hy Z뚬ԯW[WY2s@ yl,&jTm\5y9gP+}+uT,다L(ln:^NEpJ%)KID6Hl!+&y>- )AJVHrZQVt2XTUrB*44o-AQEwҊp2wo{.k{t{|il\Z瘴*/Wsb .V=O&'Luuvr|U, E+M׷jWמ)#/u3K1otZ(ܷDԐb@,IBKƦJ}6[ɤ)܆gjZi$>USD/6ɈagyI-E-X<JuƎł4dYu)Y5LH$*+Ve1P87m卷]-7ό32b2z]FC2-(\ weo}Щt(t>腱T~z$\鋯Zw-W#M+d=D$'ޡS$f0 rsxv2 $rXVTgt8֫wdڜop05=<U)dJ@No..JcY25zul<>LP,4K\l}cMų .שor*%%{d󅞉,2_SgYZHpLtdD ND{clT Br'HΌRR24168lAR 3';J:r{sSOrDn* %rx6̊r:'W3hFʨSFaҪ Q+Ě9}Sa2-MD2XfbzXJ5}0wjfx[#CC`N9+OOON2ysfvnFY*-TI@<ec鼨إjl+cL"%2ᮡx$1z.#>~p05JkTFcw=Vj'Oh}S{++W5Xϩؔ&dd2Pkl:ϻx;3)_\VeguQjkmN(#AR!۬z͢r"4X(R)8v{]FfK.Ϝ?rp 흧nXd H'2h& EJaӫzE'NopVE_$;II 48 hC5 r6+ۢ;885^+BN'NEO Zeδnv(Q'+燛Jm"S0jq*Ʈis)q9<>6s5-c[ɠV/_TzyheJajYSaՙGO|VvXfX/njqbWfn몥]ʪd\sß8O VQejun_UVʟT>쐒L1]8\rK2͌RRpZV6ncPmcrn6[ Ň{Kx3wEm]~"SЩ.i:j^%`sEECL4cxF ҙ=16td$K-C_8b _|fzj'77M?xj:,*KG㽓\X78 [̿?ѓZ*-BN7م2)MW \Q:-L_yV6y݆&i\Ax[lMwTDknp] [渱p?>-Jm<)Y* 'X6v E?/dbW 5v} /n\q(-]'GF"FcŒZ;/qniʻ &'/}e}SP)Dv*;8JsV.fP; Im F5,fb]xE^i|5 F/׫IxE̋}ɘ4۬ %sk[] .C$yG fpHԨU6 7vkl:ynЈOw9Y:졻f uh8=9|иMjt&Ѫ'`T73ƩĞ3Zkd\3LMƲ5v]sNY ,:6FW^eSn` S5VtN+nm:xDQ&Jۖ{-*tXh,ȶxL:NZk[UE*ʊ =N\]{r` &Q J-ْ^={}y[;;{ﷶwed^EJdI$H 0gsWW}zA ޟW=U,ܿ)L|)g-ލzD$5mnSw&\>[j7 p<}Pĸ'#",aW`W[L))&2ӂGn'h\L0H SѬ9;S B\{! tQPPP\.l7=VP߽#?*>[{b Xf((+$ɩa6&5sn5i;n?#FAA;¾rni4 72JZOeφfęY} i0(5lMY/T5H|,rՠXæ@6Rnٮò/c{+ eymˠjnymm[^x*QIP%>*QSDDHY9qՑցBOkoݔҨ+\:2<2Ӫ#V#r.p/M6;O/h1E{Z,PT1 1WRWLZ*ŕv1䡩dEK+;c1 5ʺQDE~spuX7{n!!tE\rz x[G??D/̓H}Xv+|2کy|Kl䞍\T+kB举ݶ.R:2=vs'޳S ɿ[kiz3Wu}/r>.bR!axQ+MUi:y\t36{72 J< ][s lK3]X7t!7_}yy1Qjvs;{@c8 S>?^ٚy>9MW*[/rZ3eLn)6!ܛ" /L4RQ*n"aGyE~x[?m1+mi~t+o'0E=7tE޾+F}DVI?WoC$6B .uݩN]'+FE]0򄁃m.&IFza)(+v`|{PAG=ԮrTT+U@Y UJXb*֍cVtAVHvIE"]gjύ?yWzԡRzh X;4鱼,oQEbTMz`6rt:JP(((1 ex9fTL=<|Td\4LN s lZ>g4_!`6"B:?\y8w-s}ݦ8c{v~|7_Ks1ETe>Lǔubjok7\\D?vg*C5;GRZ=XH / hWZ CS'\0Y+9UxDAteąņ B `8",KC3ٚrOW.yy{'7wiQƛ8Wa`ńbM"Ę g.4'OTm pz-?R;zEs* nҗ88Sw hkǧ*e:-˞Em|[dcR:\oJT(((9~b{'ڐ=ƔbÜ?<Q>.mmhA~z +gRU%ʺQPPPa*( g9PetZհ\߰ZK 0cW"#BBc&bVu~ŎW)U`:vrNK\ʇh~C,Gґ ; ݭx'OEl\'LT$DQ\[`6XsK{~`ؾ@sL$g[8x6;oo/bzdy֚n^X ]ԍ`0Q;2M+wmHo*5X'X"P,4ԴV}ۗOƑ|ijZ;̴w')13K}]= pqn}𥅦NWٚ ZtJu0՛ ō2:Wć6gLjW͊8U4 {jj왪(ppcAݔDNJn{ڧS n53zX_&gs$IMPYv{_3wlE_s L_ny-Wmpz~W $1NHA%8X_;xW_Ua;uݿT4 \Y?oe뭛2(OCMB;gz۩vCw+-\]]D˪b.&eT) -*dc4M%9|඾6> g:ş2j&۞\ [`\9 G\hZӴB<)8LJ }EQAⰞ(DZ1[HbP 0 cD mxmaFeL|x]_|v:J] )I$|腉Ɓչ'z ^ueм ۧ& eS}i7%#7b7/? q\!؜vΑԞzJּ0YkKɶ@0 @g芋] 7)gԵ3hQ zsn3UnX^a^R>5:9ߞ(뉈YсZ] xd|wк nNыFd zg)n ` }d\+oNN_퇀M3]ۄfymkn\䄄"2WxʒH -N}#+Um/Y YBG.<~H5 ez-: bZ2Q!*eUIyZfib<?:=?'_[JHYaOd3k:ac!xhED "d+p"ujJ*/d}j'oS+wmQ1ƴv / e9l# VձixGk{b]$į͵TġXwڶjLŘɈ̶7f=]Oη-;)#CH.vyIqp!MLґKA,Mzpwz m[lf6< ą%B*.b"KSyI-+ V䱖Va B-p61mapKO޷vt/O^Y )|<Ƚ nB6r:t\ FZNtTzEϔG_YXhZ:5|=acŖ#]чwz+='KozyIx%0RNu?SҾԎ\w`UgYAA!'a hH0r ;YޞLe=9 [\Ӝs aHCYe\\ V骱\T7l퉍Ѭtått;=UZ{vٞаm{iÁڛ܃Tܪ } F*R:^l}龴rPz[o\/ZAydeu%ԪaRaee.H*r¯ۛ{cd_f0 4U1&+?Pfsc6wǣOyM!S m r`6J+n+cuZ #"au~Y  tgVj!-9/A_OTIEVxemBmٕUiN-k=.z,/kd8Rlum iMEE!,6C'^F*֬/>;ߒ/_mk{Ō*OE \ Y1? \Öy>cc3d/LYnoD"Ϳ11#DL <# qbDT3[m2]8xcה.[ RIP@ZUjYź *s 2||ЂWX^UJ PPd;QNT2oɒ`Y@R-kb,7SZEA&pHORJ΋4a8^mm}mXr<9RC鹆?wgb.*Ҷ֩j UKFI ~uW}ﭽp£N읽OMKOͥXwvKSn8ù薞zK}m1װ,h'۠$tRЛK,Ǹ)+Ć,)2dI.Zh?uq+Iq'_/pw7mma`(QlfcV7UbyU[RNb;7dעp{}H鳿zw#1<,J8>arp@!ȓ]LǸ0|cDCbEz i3v2>]"ϖO ˡq,RerR,Uq# q2Iٚ ^\ɒZ\7% sjnMl펽X[ қ{aueX-8IJUV_8M5Iq-9Q1' e瓑?}+PϬuݭXۙt\B.dTl Z$zhzs_*Ho.:e͚$V>.#W?`NͷOͷΔ5y? f_5$t@,+ _gbRCw %aIRmѰVkb. ǭxOkN""b.4)0*x $nLX&&va84"uM,h+ y\BJa6א yOblqGkc@_ḇxʂx!Uxq8}`Hk:Y/{Rʃst0EfdYձBlko8W=nK}Ƕ!oJAq3W ʘ޼I0~hEfC*.fU+.CX4L|n`Ej̪R6MHB>&KX6""*ԫ%t|qu;] 526Mm-#bȮ2Oj2me/Eͦ1Y֛DY*_i&gB7w0cْv/+2oMIxh2" BJDfPPVlUR1o-4O+ ֮я=( gӳ}QQ*1ʺ]=z Tp2f빹';6WEmʼn  rξTm+a=Ѯήe *-L]naY|\M#WH( _o$hI7'SUٓB_]6L6ÎX.O*xLMX(>C/\S ?,r ވ}W%.QP%>*W Aͥw5PlX6hhն=l6u8S3,hDY|FKGJ(鰥#bꆻ{80ؖ ])谥7cf@C((@eIB&g.鶧\O d{R&Vaݽ!Tx-ec}iۺe*`>D03Ucakx9lt( ʛ |RT Wb݄9[35r$V}`s3=X{hnݰ[ `bX6QBQX8正#/ZHDP 3. 3ϰ}.J .C 2<02s-& Ϋ_?vlލ'+J]KZ?^~׎dW~^Z bm`V%4ti:kATB,Z&V,!) \/}ϿKTYH\ ƅīq + jXȨmbǸ6ӪHR\UWfU[Dz-;6NV{e~c>f>4lӸU4'`r>ɨ=dTq$k `N vMw-Nh6|"й@ZA4zuED8DhdWJ.& Áɰ7)}i9Pbk Dĭ=hef+3Ͷ$RWL_A^Yw[{Di|Q/9ga0좮@:r`?Hoŵ$h]sTI/j"`ޅG(v-گm=@w7<{򎭴8̈́i` #ֶS7 `@(ID\ҝ`# ,r "@LfBNWRPCw_r|~euڞ-} z((V2NJZEmCLPoPd_1'z7jf "7Q1[h-^@&җPŊB**A I8IݶSSfX,p.&I|,dQޱ#g>Uɨ! (H}V#ʚxcar}4s$E ŪÞw-| ߒw\/wRv9x{UuҶ4k^taahMeq,ϲ< q#Ѷ0L0@ 4x5B!銋GQY؞Lt-n<#Ɉ0Fz:t՜.7mrۊ<+<+]RFBΝkM YJ!@~%>X5[igƼ#|Ggc=Xxyqd޶q }qg'޳[pl5@<`H4[LFa`0ǠyYXN9EQFKV5Բ'suYR 6GAAAAAA#@ڦk:jI ]lVߔpD8=oݲaē'nwb Gf[֞gŮ=#KN[0W meUu~*F%>ĺyy!Z*scП*_ynb8)gb,olpLpd8|?R!RV荠B4 gi7 *\:*"Xw,p7 ɾW[GZ.b l-GQ,԰51k>~x2ڡ XhZ(Yba=Yh_7]Եc=7^_lyKzQםbkg8[ok#ЏbApmlW3 qqYl"apub_aM-=1- ÃSN38L^% G/.2W{s e9~M?]z4o_=\îkŇn|kb,ԴJ-Ý9]IjחnÀ|5/OT/-$H.̺UD!Ɨ4u><Ӭmb@6ڟh#3d7tDDHGlցDZ]3*^/׶ܖiEظ$Id^{il]8G=iJQ:=SB[/~mHVɨȿm Xϟ[X,6޾w,!jr?]/.GZ̏N=S{.QtxȬb+ 6 <ȭXg4 l,}}/<= w Ԩ̋41 yO(?w83_x%]1/ r]*Ü L .jpY:^sp|C¢8r<RwLw>"xd- ӎ@ fTvN͵,8eb[7epSͣh@Gf[?>]u~Bq 8)((((((V@(?o/3e-)64 p[㎇2)"1;L乨Iˊbw8;5ߚhi s@"" Q7ПvخH M Qp* 1ExUX#Pi 5hwe7Ŗ <t'2QQ>e 9wmQ)0酉'ܮ4nܛ Gp6 7"(ZV&.<q ..M[3;*e ⾑0UZc4Q\_${鸺D\ m=aiVh9Al,K,\`kXM)>q2O"YU`)H* KDlHAqc0^2`pJ!!;$XqqBEq,H!vُ abO4DŚ_TVqo@" fXxآ*)lEV[Heכ:ږW6}g*IrD9A81ٮߗS-b%hʨ؞) pƒ{xin@)J<rzUnW\900R$dMw\Fsڲ 8[6$,b.m[w]?xoJ+\Oc, )&|ao^Y-۾=(V֠eI"]\j *$m=lL\D@N) )UjsuT繡Lt8 4^a IrUh9UT)bL`/?@򄶵}2[Vi?uME޴ҝTR\t' vHtx%UUINrψ_1cAB>_7S?U-=f_'Y19DV$o(c2g'=αygx-rr<'/YA_!"~aT\84 d~ݛB"~<{5̑C@5mt=AZT-@v3#'("+|Br1M<sr#^JN'k[Uקg͝𢑷 h3TeXsowznQg >\/`ƍK5ȇd[L7QtrH,1ߴ"$.u({*kX'vi5]m۲bR<F4׃۶ܐ-5/>{nk7s=Ұ-O#TDx .sqObRRQ!R['#X7O̷ږcm`k[vIeᵖ2 <"tZD""OR8s {%U'V>,5$q{~۴u'hN@"q 8#4oʪ2 IFz!&ɀ+2z"& yJǰ_G}?yD)uJMܲj2]XzԓRol/l4CS?alB^qDLx%jkXN6Пc:Xp`y WĵM@QxE܍<*o<7ڝĽp՛Hku(kYY8''n[q#1UuKe_Ze[tfkad }2"dcr6&vťwRkM̫Ym;psёh&zCG^2a+Ԇ9ܲbˁQ`..%W87po紆4ҟa]!vXm"AB 6MD"nR *A6NXiY=~v#'jW}4P$\P'4VĆZQ(Qrh$rUoذE (< F ixU :qnWn640 ޸QsI g 6j3 UU$+Б1=,׊S?jI֋[4ê`PTtrm t x컓rwBeh/t|^;2{wͥ9fԴmk]VIG:l&/gWPN=:xd#bkyrחRƦ R9]esŎ׉䄲H:W#fxQ[l9Y9X43RqKauXfWgj;{4~x>x>X#s^^Er`;`Akœaw(gR./'7Y1.ԟ|X۴]ޖSU{K銝k>~t÷k\X[]f5奢b6&!ن8$AsuԲ2*2pXJ.`0wi77J 2KMMÑ>S7%WNk|ѐq[ItRgARRݬTf֍[#N[7<|kO;1Qn;<sď)*݅c!|s-򔆫nT j\RQ!ÏZl)谽m w-TeRRǒ/U2C:Qki@nE&&L ItwrfWtuݳ]O Dzi/7onL'!_K892/|ük4{ V,i(WZN6znQېW߾%GE)U/=?;?z@9<]ZύWM7rn s7 \mp 8* !FxV`E*'_c<}zܯ=4#&{Fx 5sgMLZN`8&а X[Q % 8h)(vdhIAiL쥦/IFs/NT), ۡ,g٘vPrCJ5r|km|.w~h>.]WUe}\ ^i mao G bQGfz e.\#Φ2O+(LeX„& 1h ,rf 5`%2pÃ4*Ct>17×?by ٶ冖9/~yVaRyX 30 3AS%sJU%T OQwx 7JȡCӍ@TxU545Uʪ®DmWVQhbze 膼:/$S.k_sihc$H/O"d# b0$DهMR2f&ZVBgJmXL1W'F&v`XX,tǷW`aVp|Xa8X'0Tߺ)1<#΍ӰPXBRDŇ_Q\DI.e(`y$題*ZZK98$.[7|řo4G6r;y UD+(>`~D\% ((V|Ws\j?[[벊䬴Ĉ< 30T$5`` ؕLԽ48oΩ/~wl \~`e;஍ٷl7^MG` %0LtLc>ڴд1fkEG g3 ^K.dk<8'4zǣ 894y϶0l%?q=PA ݀<ltϷI<@$"3,XȊg;EyPk۰l,!;}X #3pD㧳˒]Ό`|bv}ytH|.\r%%C'廖!VΠ#847H !&fĦ,$$ipL) ,|csѿu)'G<$%:.$pPtTM$p-tBJA&&mkVCTTxǶ.~m\L׏=C ??vd|ۻVH# i!t~GhŶ2N(kM/eiy qpR\h MsaU8NgT.o '])6 ?<`VLG*d6 n mNUsv_\%>ۇ<] )(b  #t1)Y\[,tU'<B1Ѯ|;nڒG kpe82k{LRDEVQUHFX8C ;]bgnwm\IމޔLm+bu/TLO'e6,>HlaY@V5+*q[|[z+[?x-3(q,<8 I dE, f7 "+ 5ñ/ï0;uE!)i~O/<9#<{IxAp@2,_F-pHtf&~qLbdNbMG^ X3.' %ւ$$0$t =/ʣkxдXx%ȜYnp e{xؤKhٹM?h|m˫h>~&-tV!g Î|)yN6cbR5$}b_:aN^v~z3u~3/yE _pamkGB#* 7:Ƃǘw3kb nuPH7&}HO3,K<,h+lߑDNĚU3(򡮇RQ@)H>/4Tɓn>!8ǻG!BwHn71gp$"X)&rhW{y6W7JMꭃqʺQ\s` Ęf.k6rTmMWwZ n JA8Y) > ôk_m |CYMHBOań1HVuE- ۮ3ē+c8(Ğ']IDu%;?bMߐn+KTEIA'߰0  ]_.⚄u .r6,-`KL*`u$b2wXv[sKPh<<ȥɲq]xT>-%V@UHRRe$Z=SKŤLL:6Gl?>-`D^.yHpAYX:ECsy" M,$?aEhd .#^XSK.~'Oݗ9)Gw(FAD!4t߀erh؟ID#/N5A¹?31ÚX"PD'w)kSGd"09'qN"2 WTI둟sw{~,|}̳kUٻ*{}_IB ;Y 6Y"(u^-H׊ro}m+ tګ-r[^z("Z( -@B$}_<'䜓C@2}?O&3gy?<3y㎢umޒe?CK5{gh9Ef±u]vQWf֓;"O+%oyK<_S ą Yځ{&&5~xBhUnڥ?~Vt)yXH9Yx[s]qG{ګQY/Ul   uhY:L!Hvt_*~}?ڭ'B\o֩1`(jbYNOП+| WM׹5+#r}[emq~󄤰m/ʌnh}й0nq,RQӘ5662xݓy&q+9SѠzqk+o%_ E}x`7:;s0p 5--1a#"Gh9OƢ'cbƒGŅDE+]֬V ~p`6R +MAyVb >$:766_dŗ|˓~#E!~6FrqΪԛ[$jy|Zv dugbte>>:V9!9|9PgǸޡhrQFwujOv~votR/)^mm ]19>AUKQJ-iOVڳ\vS?:xv1LAg%Ǟ]yq[{G}x˵ݏ\|ugĤHu-#+:Huϭu~Þ]t̔s侲FחGQP΂j+@{.Eڦ?@}x\e1)t{8{ҋs[YTj]::ڵ!IӮ iW䵛qHu]Phs_(:Vw-GѴ )>7WZ7o%3S)!>"; [{5:znRZQT(}~}?pPXXY`Ս6f5FG6lszGqbn'ҡB}|By?%n *]n JbzƦ7Kpbv>.Ɔw_T{aHfqGy7:E&9Gv0GF ,ظ7FUCGKf?51xwjx<ژg>:zqA\;[=wL 7pƓM[/շU776qD8GŇGĈ6fXilhokVoUslMu-jS^.\F%D@kj)+0}Aɞ] xӕ33z8*c |ӣ[jx8sT} ]c\]]C˧kCsux6JלD}392vK>8긺 SBTDŽ#`^iD%T5Ęx;S8a^]Ôč+WNBA``I=ZVMGmuSnFZorںɞ]c؇бQSڐ&6ʏy]6K+,@?`eޅex#4ײ}_׍Y~ |ءr)02ط?Purn_RsM:qq!~~Y7n=VqyRJTe}ӥgǦ [i:n]#utul9wmTM>ps6klXEc9(7:[>YWQ ]N *ovF_XE8w;^O0)9rټĈƜPܖIS1;*[.5/ij R W'~ՋtZcO?8|.):fGGc:rEcK\֎Fu Nht*-~>GŇ؛ a '*_l:V4J QhϬN>" ?lשpYQϥa7GWW:/]EUuMmLyB@?@Ԫvئ>ܺE{luc[s[SmN o SO׏1+GvrpqM]s=~sTz΁89&ȯw*v!ڳa6P6зESѼםD=!RCP@mYo?m9gMSӗ˪%-<*6$sx0##cGF?6; ;ԤXY$Eʆjlkк[ſ~ߘq{iX[Kk6992?@C{FZjۺk{[S4"E[RՇW3x6%y`Zw[O䀓^ͻ+Sgj_ǐca];nb ;GuhOVGA':Y)דgjW||mL8apGm$5-ؗWT]S|>;_vhv[q? ]2uOq&{Dٕ_Lm 6rhzn˯T ɗŕ}ml. ^E}jM{΍,oAޚTG#\#8Uil N N4Fcicb[ia:P0ԡ.٦u*E #)j[m[ziW.] _&"bm03MZ$4`/{AFrkOrLhK.:m5kDĆO x$W46G[?ێTА4,H|_+Grٺ"c'$[= +E$G7oHjh;E+ZZˊ%k[::;cš'&EY9*E0׈EߘaVn'W7啩W=ʑ,,PnSNJ~$Hnli̊i:]@_oѺaH>t|UKMLH n/5:YvE;9kDHK.j:j\4u򡒚 5n'W6T6T7G6eN t$:C쮜efվӧ=hb񺵵u޽Q.gWڼQ!UM.6Xksuݔ0gLl?^h'*oN{R%?;u3.ܲwϤ#˪Mlz&|MN5;{{~yLX@HE#mʩHNrsk#?{'UVt|}Ze#Y|)anv׶aV+#󂪦֍䝧.;iԨkGY8/4`w>j$DŽX$5HNU#9YDmm<}I "/4;WgH>q!|h]-bXh5#Y}_99%ɉ1!{E$[8=YA&D_n>RZg (,Npx?֍*f{rBJLhnaUMSFj//5'EWԶ䝭g|b%euh{۸?ߏˣB},G+'&GذUU#tUM'A7FUֵ.g|֍CgkE$1>>w[~ydE䆖+r"E$ņW#muɻNWۼD$OI|:56ۢ7[h:XR`BG||<<7<Кw7v~x";1➉ T]oiNQ''NIVк&%&Ǣ,"#E$H f$7v<2[F9!OϊsycbC:_Z[m\b9ZT٘jŌE{e.~M 贝Peͫ'按orx=*64<}|*+>Ԓ=+l3ecu\B/9VXhɌEM9ZVļtQwHlj¬y [bxS52o퇧FX3^n7Ȓ_nlBijqqTٔ#eu:*'1Rک(JcìY'P7+3)H qɬ0K;[ݻ3}Pw)=Κw)}|(qErgz 32cjP|F%{ϔV55v~gA=!#Κ]ߍ(#9F/[ۉ 7$Ѐ5Ȋ Es-[86Pv#_S:Jr%s\p$g<ɱar|t|%o-PZՋ$#ĩ! "yɌԛRd$ *GD h$7LM,QUDr%oȻXRBƌMSVx}\C3RE+cԬ̮S#-['MH\ts=#ᡡVK mhzņ1#,N>Whzh=Ԭ剑<=~>'))dK+*ZuX|3"l퇧N_;2̒71HRv/Oh$oNǠQAb="32 7 ._=U>95Z6&Dw~_Q%K[8OOWU #fg6XcOJr^4;T[lHxqiqª2E'cn2Da/zx֕6Exu#ks㍑3l$Ί}N5J}J${{{m˿hHyF#g=|Ѫ[vFưi\圞ܳKj}q oɯ(xPU8Ք'Ό}koɡ5V_uV8j$,f?ÖdѴ:33ͽ%yu䙩]#9 G)"y癿Y6'Fߒi )l3SB=hHjr3[9Id]dOY6H3OA/p$/#iG*e$DgUt\UCS#]ܢB7Y:~[@1~z\r疣='5V+BMc揍^q5ӫ;;EA)0zxɴdxSuMmr g[;>2A߻݅54>:+ FrCKpcwSQs3BVޗeH5DrqeCs[ob~ۏWt1/T7}eF[6mݡ9eɊ.mHH>{W_Ή ;NV~|1kye[;@}K컩T1?(1D&Ѿj$uH~p:9_[N jbyM67&lkF؈]5_['+6>ysE:Ȍ$?~p䨏^g|ߜfH~tV7Ȩ%6cs9.Ϊ~wf^ظlZ0k='NNşX5%W$F?8-%# [6]X~`n9|'odQg|iQ۴H^:;ɺur^94,/շb'2/|P?H漴g[N~Eq^P&^1VVaK8#"Hٙ,q̫Hԯ]얼rq"츧`H.l ɉ"JeH^a+u[Ӟʑ?}U-;V4w'ޮJE]KEEQVkm,jj3|—ol>8DgM",Zu Q>C"Z.KO _Rcaek{x|zRߐEs}'Jf :aDdPh]\nh=wbNFЈ΢KCNwXmF\piUKf*urƸ ɭ/u䂊CN=Q<6-)HՍmmCN)ݒ:D"RSLШkڊ.Lu8=uatP(0$"YMm#Fl) :927ذ!RC'&$Gc\FrJCj ho"_{d݄H!u :+u(i1CNo 21H!S'[#3oaCNN u=DːE$?DnY;RcM4tlD_ܐNM7DnN2[kɉQ=Kf$\[PP0mIlY6.ظ`˂ 6.زl\ql\eٸU]]C)׈&xL@ 0u}9}O?J7sn={ċUʌ38KQYBvإK[UӧO/sUwΟ?٦O{]JnBҾ}{7l _,^xi=YTTvZ9qٲei."g3W1ГAT'~)=Y^._kU̖0dbpn2N1dN,V'8'Ja\OTݷ]L{6|[Kbty;;)p9q)u=Y =q+[VM,'&b6q)uڹp1X:tEqd:ٸ6?u\׮ @Vu`=E %&&$$c9Eˉ %ku>*4nD#KL'eBDDQb 4.b7"bA,C'" ٸuĆ+++c9ׯ_/~n(^pu~ϗ?EO!AW'U84Y:lQr;CkS.=MD0dr9Nv$޴iu V(1Q4zj]x1NT[G-DWSʉnڮ =C0|[KM1wY ʉ8X|ya,bGL}D<ԋf~u䞦9_u銢kb_Nh3N&W-'z;`p?% qh\g@Wo""abbwSLǏϑ*shjk\]YYB{]G+"77Ww%;ӜK.Ȼ*+++⧟^1} DR",ޑl:bW9QodSbܕIJk׮qd zѓAT'fqffqӜ.Ţߔw y,#E*/CBEӜN͞U^J#kdEW\QtsMR84ԅN64>Я=b:Oqӝ~i=:ouU3D' ee's}NRG5. {Gh/E|ilBn^RvBD;r?2n;8sꊋ]bSl8OKSEOOB`0R8j*w|xBԍtvbrnse1l:_sۧ ݯD :u΅ ,V'x!RJ",S''$$FDUUܛ) ҭ>g:L<,]a^^Rp`<=Kלr0iǃ9E) \ ?vhڰaCC]ahDHlyaNqXW5ޯ"WE,Zk@n:9??45puzm۶unZ\9_# ]b}{KyQDzr"rNBvџzUOu)RE=\):QQ<Թd)Y;+:켞\#u򑶢Xzg@O6b눝Z>ځR* :<uú/++]zؚu/͆L,|8uUgXCڵkɣ'{󜢄ŏ:Ob/͕O)nv>qvXmF}c=yNFcUU|\<[c]4GAP{]J:?V"M 8Mmm~ǐFr|mQc[9L2E؂^^'Gȇ (;; V.a)?]r]?E켮b9U^l:9+DVŜr43nP:|Kr/<:i$Yԓb)y]XR+rTpO{tS<\8ȘM&8,qsy˦b8F”WXV9i,]>]r].'bp.% :yֵX؅|EP\\t=UG jڵ~s(^Q-R}W9h .߿׷u#w:\%E<3*I1gO޼Z.׎%{Cq/)Tp$m.?>󄮖˵NJ,\utV䲲2v~ۇ_cj˖- ȫF?iQӳ =ͩ1A,.*Y=&Rn>*\T?agzz_]~ԇqWT,n=YJ_vϞ$zmt]DZ+n=ͩV܎dKСpsERW54d.t["-W・ԹuS hHr*u~}* :庌: :&1YNx,Y?|!X:?}ǹ5aU -X:R %}?+[hӸ9ō[y=\ .P6ze|`uS]ݟºɖ-[yf%AayX oKM.0~uï֭[KKK5ofYYO)**\veˌo}E?x+埒~zÔŋO:ď$VbENzTlAs-x7ڳgsUt V[['[n%֭i\( ;85_vv(ŋ)e˶m.{:?>G7EK+1!FQ[ݕ ׯ_/q(N3>#z,)ZD f }6qCE[~ЙJ'xYYsO%>)J8<<\~Aи{RcbA9/vӫu0АxPhӫWW_ p,;{>}{ſx)))SLY~k~~Lee2#xc4+**AGИ1_YU#>wR7mЧP̖-["DubSN)[nŔD1^_nn 2?!>[Or)饞PQ3C 몸C#xa5_2AxUdd Y# Li>dWg}pڵbo߾z*W݉EdyVVV~guU_]335k'k+;)>z|"xYCVMgKIIJ:s*2A؇@W(_4W5:tt<֭7n|Cd׍>̹]HYYxC 񺶶VjҷQ)))Q|e./=1!!A(I=ec!_p_aٲe>Yxq~~.ò${bF^J">(^cME5rbnn1`=//OϊkyKYko)<u괪j9}ɒt貞(s! .TAb1i7[G|-[lݺU|`W߱!xmڂhsܙ]K(ӟ}=bqV]RR]/{y6-:6  ׎enTz,wC&[O2&CMɊbOC\.RWx_ -*'孊ꊔОGok:>jqWM'~eEyǪSgj?_S6cgUw?dm5+'K>h.0 Q}+(3;ET_irY;}o%Pt^HqYТs\[Nd-8pzy֏J ׯvsnGМ._@ɣFRWx^׍T\qry&k`]kul˾XE+_8qohg>ٲUqÊ:uk2~sսrv6n pN\6NRrʯb7g}]yZjFz{_mx1ŏ2H}VˇHҽ>,Y(*=9bwZ&\JW( k @pΙs ʓ=*w︗!`Yj_} syHޞsze\ŻՆHytwFEMW9~^g~s0e}O,^J޻2w~b4Mc~⣷!Y}檓w>3uF?>4c_nӵ4I493=ׁASw%[K=ڶGft>EG]Ds~q_h&_}/oՄӎ5/CMC{oҍW麳@m\@Wiʾ۷Oe|;vv/|!ǵtxGvejPp|[kJ^{,?yh>i5m '._<>\ju%_~d {_T 4ŞZ1~e}=3USQ83SMkC7it9\o&5?9G:*^x8yOny@fYY~rFquNt-::A>aߜryGsnZ\0Q*._@]oAAWk移fQ6$$xL@ 07$xL@ 07$xL@ 0o移)Z)S&Q=fI$xL@ 07$xL@ 07$xL@ 07tUXWq}O+b;`fp{+BdEZ(Xȑ">+K"aVKfM p4N<@jjjLTէܗwz3us.]P9ݻ{&[~>/"Ήu^047[|o{AՉ.v,g=@*Z0и 5n@#coNYpѵS{xH۫bZ5Tm ۲r[ۏsacZM\X&sg "sbˀ#зxijl+ݽH{; WQPQ4|KsVn݉kX^*s>6^>8_Z|Y'gSKlWFIE81e@czo::kuPK5 hv,g=^/bqI)gKmlޡ^ZeBM\,/-uwLSnݾr>L3VV݆*QEj#_5GN/]T[ 6~H^TPQ4Mo|kle~cǎ# x@ o7H$@   x/oB7H$@   x@ o7H$@   x& ӷ&:k+G 1sőQ7_}~qN]"4oc_oJ7~]aYDd =cwƂǭhPqR(Nmo6JZt4d2?<蜅jË=Ftְ`~uZdNS"4`b[}hzrvۊʃOd+Yg "; )klr|r=V"@ *5n@ Yjf+[\;Ms>Z\3o mD ߭ >[Ҿ')Fj\ցR#s]&: `@#Ǖ6gmJ@JBqx+7R|A.KvkBK.3DOg+ߴިuJjnٞ7CBw>gFD'1h6ڛD&4РRG[E\ukBA^(&snŵ.9ppMw66_e skeG' ~ 5J7 4Ըi#VWз ߪ;><4;bq%E%Bc#Ň+R!(zl+T©훑 4Ըicb񗹩1 i0ʍ9Z h8ȊQE%B㍝gO}z9n -; (vlsX8UҺW"@ *5n@?Ww ~cǎ1x& x@ o7H$@   x@_r!$@   x@ o7H$@   x@TɏkÎmW^n.LݜcW/T}-'4wk9vTznt=%=5>?N .L^M 7f1SV\?Y]&.]d,SIeϝm.N,"Ή[-9:cu$rŪ8کkNi@ h$X@y_>T;Mi=\neMxW,5yf,R\/9W$;:9ޞZb[GG2=w&/oO."Ɖӟ./戈zGUx-<_7a 4],Eؼ\=[n܂.^Qc3"ޝ^ZeBM\,/-uw-޹rR]vk4KP,>M6\(n\Qvk[$lOܡNyL?Fڧn噧An<.ptld[fgzOVtw@@ciՅ|u=p̗wz#m CqoKm/gkCF4mDM9Cۺ,]9?]G,NMvv7+W :ƵK@363#Ko_k8w901 4֒*';ZE%+ޡXrl4p)\dQ,67g6tJF8vN -X @JEuƀFTSn܀'BdkCpZt4d2?<۵蜅jbldk0_}YDC|r#DWr}!6 Xd bM܎EuƀFTOT'Og+7,R76;^R|TZ j2EAfu^/'v:'Z_V\S?<4F;sHxc?>,VOW뤵Z\sR{䳹[>_?6 wzE駯/?u*-§:@9: G1e[q/~nsRfo|ܪ.AP\;Ms>Z\3o欴z-n5!QIJΏL\#O?1EoVrbn4}X>5~z)AǏ:c@ |52XƘx #X;Fr_J妭۵B+3>4=9To'7,N}OG]x|G)rA{ݣK}@zERf@cuƀV1}tr<8689ԩXKM;akӻ{zJ1Eqַڜu{]r;Ւӷk@$06HQg hb0xEg{sI=7PfzbB03urf6w*>%Eʃ+K GOdʶuoUl"z)cuƀ &gk?<܀cp˨-21|?L9}kroTtlnin26#(re`Ag { Bbl[σc?IsouwpP?-;vXs7`oo7H$@   x@ o7H$x/~ O@ o7H$@   x@ o7H$xLٻV9VK~ƒs_͹! H)N;vcTؘ^ K6~~=3hAey`=7Pmhgd]Q>8<m:9ןͼjgٸjEđ['?Z(l[[7 y<]nWR!N;cThhb5];H2rX 8l1x }\eZSnݾr>L, 3VV]1<RQEP^R5>Jr̝?|"@ZHvޱ4ܠzn!?~om}.ʫ?wǎ b-5 G   x@ o7H$@   8^օ_o7H$@   x@ o7H$@   8'SgM*oMu֒W /b [7Ë#so-'#2A"?X}b߄YK z@j16HrPcK7~ٙ{:"NmoZp9[t4d2k6<۵蜅jba62 Pi7/ W_Eɡu[wEV7hwр|d * *FS؃Od+Og+aX>ql6::OJQiy.jwBurZbgze"bS-7 9"+WdbV(^@ X @EI<؋?uז"(w҂Rs:{䳹L>_?6 wzE?<,lȈ\b`kll:`o56HCE(Ͷ;iBqGryK7nU-n9_-7s6wpٶ uīSDi_7?NX×#eWJǕJjl 4CE>˝::PYjrd.x'b:݉x e;vr_JAPj -]Ȝ\^e'7,N}Oܴ|0GGD媓XD}."b:݉dE\Bmn(B>sS_-uQEnRʵw Ra9o(l! @* )z,= v7nHB[i^A;:*VQAl٩[O-/-,EW`Õ5\6z>&)`4ymCˇHH&_~_@63Ôӷ&+7Dj~`oT;+h-1q沁 4_36HCEEσO}z#OG$7y\8;v,77H$@   x@ o7H$@ p ' o7H$@   x@ o7H$@ ph+]'V+֕R?R@{4r57(O IϘq t,ۧ;Hl,YZ]XW dخh8[3fL[H- 5KAд9]w4E <1~9,{~k㏮Yb"=1lgo/Dr| 8"*V}{~w=FGw@{撹ή練7z1\ř1sZjxN5`spgbss~MmCWZT쇵4sf {Wֆ\fcfz)Xx[^oY8LegB>'4f\3x[6ZuԩOkA[{Ng3q\>"bȶ?}sW F4vng{xk+|~=@jg= `2yz[6yEJ$s3w*R:O/l|.\}n_9t W)ΙYeEݨ"b&ό\|ҕOy[DV |marh8oY8h;v귗X84 4If3f@*?Ϸj}VV^;vXs7`oo7H$@   x@ o7H.x@ o7H$@   x@ o7H8O?hP9<}kr`p ٺ9^ |UOn9\yЗ _v`hto֑ x14N;,Z핟9=qqU ӓOÎt{ex,6a9 Ň?z$F"sʃOd+'~:k"ҷV}3$T'ڽc: )kl]韂}GD1X/G"1]Ra{jq͵ێ$xcԷfw^6~QsTapb|6 fqaUX' Ehiy.+|z0zPf~F{pN;pObzaƏhr8  fo|ܪDP,ȿ֠9_-Gƪ]kK~FA.wbnzT+s NT{2=Y|Ymj$CƢm@EtRsޚ{-*kDa;3_Ho&w rͿ,[LkBKW&2g}hzr+*ޘ|rc f㖁 "U-KuN.t,ߨ,c=9m@YD' u4c#龝mWyh47M-Vx MZi |l:? v[UD,x'?(;3%).-0f{/>1! Ixzlp֧~V:8I`4șhdi^})V[|tNԋvp\s̗Y->]uGS{f'=sb-|6ZoIxǒWM ¯[7<?5~yH0p(b{%e}[߯G'GhyN]7[؍7L( wz˱ @;cƏ@㈜y69 r!~[wر R~ o7H$@   x@ o7H_.x@ o7H$@   x@ o7H8Jw?qr1<~Kͅ羼ӛsyQ3ٺp󑞦~oڠYGKQ+\t[vݟ)m 1Uwr ]i'j31~ֺ*o 'NUh&,:g1wX}2$Fsg "xGoT{㏲g炠ۅRݎmiGgz2?Hy`#M ,5yf,R\/9W9:9ޞZb[GG2=w&/oO._4:RFLÑ]gKm\6 Z](\& 199SP :3F@pj.~Prsnqe+_ ew~}zgc/˧x{H+s]2ד3u 73R?5 W-Zrl&k.7SMo4p»_o7A}bG.y֩3F82W9j櫞\r?:8//"kkJ7~y>iYIITz><0isqo++OO:dW?.O}:|2^iņa,:g!G_"⼶ȿ m}Zn>yPVz *@JD~:3 "O'U~PYfxp(,5zcYI @*ק~ny 8<X%m?e9Y\e+"FpTd[;/^k*o796h6mDFTZ ^ _ x oiŵޭ4=f抺|rz 'EQ>qWpd*LG a HIhu~o.B6e^݊x W}P\RԆkBKW&2g}hzr(lCCu7OnYb"2oQ"b'`u[@4*ԲHx^@Zj \5zB!Ϲ{SUDGlt ^ݛ޽ȁ+clqeINLhɉT[i^{/S85}y:['givVn\s̗YDGL,~Zy>WyY*m깕f'Ng݊d*T ᪸? 5Y7*:gPVݛ*]TQEzmCσc?I}֕ 16H*Q}u`l. n?Ww ~cǎ1x& x@ o7H$@   x@_r!$@   x@ o7H$@   x@TɏkÎmW^n.LݜcW/T}-'4`hto֑ xRT 3];>F=6H}8?`fҷ}451 Z[sKF$%zl!7&P ! m?qs{`ӳ#{8Orٸ׬wVdrfG$.\tԊ~+\q`f޻MnTa[caNX5n.¢kskAga\m/<+l"ιE![pTolDʘ߅b_Og}M XL5 *c稷:5~L 8KMv^iƲrrTةʓ&5zawOO6c3,n"ƹEBkG*È`4rP^Xsـx7P=.(@YtrTti?GYvJ+=ʥLvAu5jOjD p-rsYgk̹D@JU77M4glb/$g&jA5hY }1. }-/^FkKS͵[kV;T;/^{M؊Zܥ cy5_{>nWW^QWHLcya~s堿EZ@KHݗf.U;Y h 1rԃ7j)9 jq'jz/}m@t3Y3tŠ\m +SWvmIm/`DM 7ަ:{~]|+<3 Lվk=ȸ(@%həC|r HX93,݅ Z&*=D̍cp!jU,gltz'<&D&|A>ϭFHW4hz/f{/"@K&əBWXuim-8A1s}#+O.vsW'oʼncsr~t4Y^9.,qg"snpd>}g{zWfѾp 6k?Ξ!J&iٸ36H@9lww;l,{d xb# 4APT]KzFOfl֫Ս\xTq-49|o\svH@m͏}uxhqמ @5K,Pϟ H9pFjN k m}}78rHkI`/& & & & & & & & & & G+ǟBoooooooooooooq*~BĵzO߻s٦5W{|zq/ 䚶OϏ> 7b{/ޛAx̷@&c `&icW>jV~l<0pLpf~c@zuV|"j6:;蚅Zal0ow>YYd-NЂcۣ+V#OܿgTH8 @ڽO*H&cDLr8`q7zz_/Rpj#uW3m)!UD}/Bɕ ZclWLr8GQɷҞ?-㙗kX`{k4i{N.ٮsե&Ǘw#GuY{8@ޏ}\~QcB$5v2/tPe"k_yg3/TZR44ZC';pG{?EHuw Ke Hq%h>V{@TJv32sO6b) t͆ޏ좚unEh-a7R{>[} *B@ʣ]R$Fl"JqBtǏ{$c⭴pps fZ[6Al۝/Ds-BZnalwSZt/=BqbT[z(LmZ:cdLr8h1x ʝ=¥r3' KN\_} fP£unEh-ϯ4`i͵ɑ[գ7Nf_͛wn~<:u_4KHyO_j@Ϊa@zc_QDxRr8q&63ӝmrcRxk~,fU;wsag]\+\l2S-7Cg "snB%0K-^G6\_;xz-\6 ر3s=@JʼnM-*T4)*kP)9 8KMv^ife;RؑQpLac+O ==Uz;qnvb{ϯC"y7PںrkA FD?UYl b&i*>BHyȠRr88\zdwr5wv=nYߚ~ć's~#jV(- _?wP^-62i*ulTJi?okhɂRԑ{n6^(nE =[حlq&M4[\Q]d;bepts9Qbk @ ӡ=y *%u4fpP3tj5|O[dPcݵɑWb&^[|rVpy&1zWKGC}Ae:)#6Hy?&TZZ<H~QW`7z#Gb ` ` ` ` w7Q\y+,2߀v[EɊGe[,X E8CEF4+"0pV$rGƲF3|n &Wwڔs]2RQ>ݧ(:% xH@$ o?xu'~w@$ o7H$@   xH@$ o7H$hBGhP<9<>Q۽|{Γp#ٺ%l5ewn= slvAɏ[Os(h#+Clo/ W\isހ1`MͶw4:s[`r 8(qo?&[jr%6i&akqoh4蒅?LUfӹ}U9)!CطF>|jtzsbhI!8q@CDZC]F1Fgl.ٖbT 1՝̃J&[G\{RUZ Tlk}Xd5RgKАbM?m*F4Sر8:\Ll ݷWn?ו< hH P8G78hov34h.RqkB>~u_>5ĞU"ֱT*4#ơj,d?])8m52s~e`䶮а֯ΩШbMڄ"~;#ծ"pb$J7 }?z8bwtwA:%Kjp|񧘇YEc o  k( MVt&?AzwhhwBglи zEduMg>hm)xj-|67U9x㉹v/WgKiq|eigl"]AeĔ׫F)~W&u{ә+z;|pOX蒙Z޽zUEc/ oQoj;Uy`ooq|eiclq,%9=b߾/[ͧ?8`߽ϟg[Z;W~#G1N   xH@$ o7H$@   8Z_w"p$@   xH@$ o7H$@   xH@)T|׏ ͓˷< 7n}ޟ[rVs_vLPWhW˼=!78 lIhh\}pMpR&rrl _&s:[\W*^YcizރCcf׆I8.bkKw^pU,x輑&liWp~4+ŵ-dhk"y/Fm5VO;#PyU3,MFIh8wFngy H?OkOt3T[v|=>XcrM̕[-;3%C]{-[?ku|aD3'9<4\Pm4N@e#ތZyJLtRwH8`,Gm |>W于/Z)?*b[ا\|nEf4=w.bk?3w*K ehMpe (VY#MHono**|n0CS'uJ'7\SC"αUXC%~3p&N>-pWZ8܀T恗o]~9ކa KMLl=L_ \h]2[ =UEc *LrXoq@}n<̝pPb 4:ثg~#G1N   xH@$ o7H$@   x-:{   xH@$ o7H$@   xH@$ G*Mf؅f&h:LݒKoU7o[Oӟt W_{!: AXt?nT w4 *}{_7+f'|pxC6{1u'*Qڷ0]d4ٍtgN7^"ޱ#^,rD4H xtDZz3]_:TpBm;ԿuoN8q<~i7mmm.@٠3;ޙ:%c=ݵv_/GɶͻqlB{G-]R3F"W q,z'@R/zrT\ujT*7,M=_^bj2r'ppJ.ݼQrKn ]\W*?ִ1~xՂ ŦLZ?עx展zw;=oKp],P_zZ7 =l(m|N W1okG]p3s}ږSrzޫ4==] ǖmm ~猊9"Wbr<7ѕANnO̵wp}cpUGg6t&\Xϟ{]<ip]2Z+V}Z£ul\.߷NStp],P`ڤJP律={bW<Ңk{Q۽|{Γp#ٺ%l5ewn= ul+Cl?`HmdqͯycKyHOl @#3]ɮpp6OI-Pou.O~\^4em|¢K;˝f+;t W^"α-v5 >Hmmpr|QudtH8ccQ3* *jrlWOFWn3F&[wroJVii&S sͳO 2'Ց|r|p˯qlOj_ϖGzTdog bMYL4@uզo>8znmVb|6y{d~nTg˞WE+/,9vo#@c ~@DODO4w{S-߸L*U>K4}`r1TFc ZdJ8_yڹnۋƶUys7f&'Jk+á{3i"xը hHəuۦqc;>7H򏹯7`Y.\Biy\闹R9UZ(4wf"K:5쪏m2ʮ5֩"kv߾Z:]>1\W>|0ڥә4 wDL(b -GG˓[;f+GOf+ Xr'fpi˭!,\-[+K T[Tm7YC靏GsHk#8?@C ~@l}M8m,2s~e``>X.=u`(R雅.h9LP[*w껠dNGOsM1-8Ƕ;+=hpF=3"n}tӦ}Ulq^[h#ոt?uqɹ7E!phdƏ@JE2PKǫ[Z[lW<{g tv׫#}؅ͻ&cR&>,\څ< "{8$HoU3J+LvFX.W&K ]\W*_ibN:u H@C(C ǏznAKm`reAXjν|T6T(,z/ٽXyuocCdiolY:mZ}Դ` 1G܀w4Ps' 7.dk+PXjXdЉ .T:yN޼n68~i]kSű/ (a֏wݑ˥.~_l hm)xFpgf"qlo煐zzn8V .F)~թVdbƏi?-okG/ |q'sϽ. [2Z+.\PYg2vpgDy¡b߾/[ wsp^d?elKq'jyoyȑ RH@$ o7H$@   xH@$ GNxH@$ o7H$@   xH@$ o7H8xyrx|{'FܭuK=?2Sj\z7\ݛ WM7nv?Ѿf7 h8VРl @#?)M_Eg H2"Z:;vwyħw&*lh4Ƶ蒅r'*nM+/EVMG Nnyt,ƍc=glq,jdmWܮOwZQqp,5re},FT cL޶sͳO 2'Ց:m< >կh\?{Fc=kl"ǏwjJqyR6|E39=>X%mި%7GS-{^p C/А1c+ tf8fr 8 GP4}`Tq|oZЖYjꞺJ-m< amc*J__S@:l94W :~92~Rg8fr 8(qoO;\闹R9UZ(4wf"K՝{#|[?kulaě3'M)sh0 R<~ }4udr!X[39XKMfpir[NX |>W于/m*!o %v94MR1~,..6%N[ifv֫z?ϝ.8S4=&4bZd;79?y%EP @Ə`|j{錫 %P&_f&f6?wl/W{NW.T.-ԊUWڍI{T73N;A@Ë98/@16H1}hSWi n1 ϟgD^<3[o9r$7`wo7H$@   xH@$ o7H$hϿՉo7H$@   xH@$ o7H$@   8Pi'?V6ۇ.v?73Fә/d\xkylz7,lثvnնÈBHOoYھ5 5RҫKerlG+t'p$ޞOy/\D%]6Ƶ܇՝N'*MW*0R&S]OfoϾQ]4.*i>EMn@DM=wmG28vqxa'p,5yz,\\+1_Ao߉/?S28]^lr@lsÈBHO`h]u@XzlNH], &g7 U"&Ǟ-tv-DZ`iq8x{|Zi%nިr%7G_upHlų?i@*b@jb.px܀CVW ?wte +IK͹6~5/v uZXC|8oÀígt6OZj4}Nm=-[lj/uîB 0࿣om޺#;Q -\@*bŖ Hύ\61^;W|4Y}Գ|e\{%2srxbF4 ?t܀4pVOXj2|ԣso:̱?_x6>4V-dfV,f~Àzn|x?lnО558@:]X@EOn@ڽ V6b lK?zȑ#AgxH@$ o7H$@   xH@ם= xH@$ o7H$@   xH@$ oq Be`Gm;O[g떜{~dݹo20r7_0 hW[Wi/>@Ķ;RF9jO`I-PoE˓&>\}0V͇!,d!n32n6ȯjVAbGs(}=`S>{|׭~:k`߈N@E;Ǣ&g7 Mskhv'p0,5re=qkxP*JK3A*{z5>]S2vvtWG1vnÀtjk?ۺOjWE0?[r :=H Hsԥwj;NW<`o/lnF/Do>MDo>=7܎ E 4 HW;oDz+ k@ 쳽$JW +Wo_ݔϯ"KVh51ڻd v{; 8|`rs9 ޘ(՚^m':v:H3J92ck;\rӎWrM̕QB3Y2689>Tߖ+½>w-t_X}7toumhY H]?srFp&uiJJn`t,뎷rD\(N[+\:>{? 8$r7heGSf6]pD' բierFp#uqa>T{x{,#VYݻsr :Nf,MO,n'͹?amgnWFt'r"38s:OJ,?qUb,5YyTg?K}នݹ%Zpi -ay9|26w+#2@v;R$zrFp#ur>03 :ثg~#G1N   xH@$ o7H$@   x-:{   xH@$ o7H$@   xH@$ G*Mf؅f&h:LݒKoU7o[Oӟt{vkԈ l/ #Ug Ј"C֦AoN8yx{>Y~8vFW׋azX[Ͼ={\KsVw;yfq3{tSU;]ͷ,:q؅#|_9!b5̻#Kg]>})8s@ ."q.!+[\{g[ր+Vhw…tWb܋b)S_.`Ա 7FS;DNF ݧqʫB֋B#bٹs ?Qg=SNw&x4Y}|e\{Wwئ7IazGF=o)b1Px(@   o7@ b1PxnS'O*=cS]C7~g}6sQ_>O_?ٯq c緂ntA^uM9@"X 8#ҙTL |ۯ־eL~( Lձk3l?d37:ˢ{of+{UZ k-r i QC4IHgcX?iM*;gkǥPXu3s^Flٚ:^-a [ߩL:=ZcWwOȝl3J4иK0j3~4!"$3t8#RT>tG==ފGlnAe3-.uQjþ0&v] ^@@cTTg@S(Mmfnv9F ƪA7^o%_/=C[]6;ͱ7ǯg%?a=;8 q iIR!bqF47sfn@24Zx нp5~r-KAP XBkw&ghkݥg~·mdoF ceb<8sB0+H{Hg?hk{2fn@B4Tx+jpa*K[e\('C+/aZ:̟]nI3z+O:$UTF[3#VV~1s{N:?TK2'[ginzŻ3?Zb~02_ܞ}[(5Ҝ"8 q "TͻZ?Mb+܀O槟*ȟ`63فgl/WgP}&c5 7}9/8;yr/vq%Z?3mfnK/.ۉvС&(@   o7@ b1Px(@   oÛ_r"7@ b1Px(@   o7@ b1PxnSi_~ڨvNk5?oÃS_:qb(ꕷu' '?;_GFtwg4M!rxQW9x1 Hjێ`[ZG=y@Y XxHrkt4Rx{8S3y&VXg/kwUV.} CXtbjLd"`h=u_n7Єs.͗LlD9G$9;wg\|oRirPu\id q5jVG/lŦl{{k\<*s!81Pl{T~yAZc{gg]3y]~4L0)6 qٻVwGM /IcϱF ppoz.y:rog/?Oݪ2jsn/W_yxo=~+ h[@~@Zc@oZx?GhSRkds-ؑ{cwwO]3[wÖė=rIriҸ2[zu o8E@J :?H}4%WlǞo4Zx +XW'ekQ,8l%#=C[wpY8zD v e{Xwo])4Dvu;?k{w$ HK\HR$b q֨JC8u#ӝ+sp{ܣXli﹥}wJ11A0vxQvє2 \0;O/n5-/t\Hkm ;+6icϿF 5pž >ܝS?;2uz>slUaxiJ~M/vv;֝hw#p vy@zD=5F p[xO>@ #u'.?tN3Qn֢i0gG:oޭMHK--yWd+6 83W~uMi 7^СCAC[MQx(@   o7@ b1Px(@ o?'  o7@ b1Px(@   o7@ bpNŻ?UM w՚׮9<ȟ`nϥg+G_ї<_>4~Dfƽ3 <)SCindtўk3 ql!z/|I`y@ Y푎&@"Y5p~( Lբ͵ʢXy0ltTJ\0E,/Vn}f6rM7[ k-rs_OVo{.Wӭݾ^}|LuJAfDsN|635@.(O)6\.Razqn#@2XQ(83@ DfsWwgH+n#x{΃P&sŅݱn!9ͬ8+||v`(.%ieۋ>++?vtmHʞXr.7HWKa@s"Loin`yƫrБyghvk;ھAn0D#(>_hd=hN/Ƨ?>;e^m3T򦻟ow\ I`׳w;{92xh-\pu9,kyX ZQ(vg"{:g+!5#e{Xg[moޘZ.E0M9]v{&[9lt˝p7 >?21\6ޕ&/[C͈v50hnVqB!?Y疮.ZL\cD #.9qfj,/Cj+پ޳W[]|z˛n@bE?)Qq!X_j?>4~ݪ4*OwC 2o_,tJ:d Ãy:7?wLR $RC͈v#ӝ+^FG+1qҒsK{ﮕ6Qè). D/LuO5 ʇڳ]Tx<;Ood|G MW%@4dލbw<7;S%?;2uz>slUaT?9x\\پ(;HՉo 'nw"dw+$ï0?)&OU~򮨠3G|baK[ٺ=3n~+L:w|zK--~4"wm^y/V `g/BxzqU/Hyp?i&_~]hk0xnkkxC m5 G b1Px(@   o7@ b18?'~x(@   o7@ b1Px(@   o7F:~Tr365Uk^>t gٺ=n~E_>j0s> nt ;.V87Hg4?iTg08~( Lեk3l?'j3ՉD> a= lqJQP~}"Qj׫=H.+efszq?3Sõo␬ HgZ(2HLu>lqHFlZuvZ( |0NmgZdO{=GgwFfzqϙԋj]gz#$Òu׏cIw.Zd8 $sX|vT^>_i8=l~;^Yđe_jþ|TB5Uؚy$W8 XTyH'D:}Ks#_cӒϯAG鞕W4{lサNa ~1>ف/1?"s0Hzj:Z-jn ݙȞM×nz˶#e{5p_kg*Đ0^\+ř- MY 5Տc  .#O/2,IPx/vO»LU B!?Y疮.ZL\c<5R0ta0[no.&rO]\C׫yTs#hόcI4üKͮEf@4pU3rgtd,Moxxw\KÏF+9a|>o. ~q{f5~~CS߳@D?M1?lqH>V,>~ g#g–\ݞB[uSݾX]0}?:[9vWޕ)waм^d8 $+n'ׯoڡC1Px(@   o7@ b1Px(@ oˉ'  o7@ b1Px(@   o7@ bpN+ir9:ywZN}~?SkW}/,t~{gAإ{ iP8$ loϜw!.x1 HXy8 T|Lͯ'K&V^da[߳{8ZcD6.+9s.z@BXC;Ǟ1*;C@5xow-/M#Vr\N T uSuzGj흝#w乎wƃ[P7Օ4q9y$$56Ix?*߻~)ή̭PRxs\Ze;*.sΣ& #lPx RR-I)w=jϟ؀͞nGZ&mV;}na*K͵cGY pߝ'ۅo agc#v=p rŹ'|wm1zRKHI@v޵:ׅO3tHOg/?}z7oZޕW'ekfv}fJD7G"{3y2;zzˣ&o]-adX:}A[Ou|NSR@$ qxk{w4 5Zy.;J'p*mF\= 7=ZqŖL[ڇ}w/v|B.5M'N^@ڽI;R\V=nN}*;S*:=flǹ|6a:m@RRN0HH[w&k_.mn@X+lpi['ʥŻOrHO]::ll 3QVT7-< Qj̼ԁ{wwWÛ|wދހ/.$0bl~{_findt  q`R ~ߊ @ʕ/v:,oݞ騄щ|¢{of+3Cx`ؠ r=/:Wӭݾ^}<'w]R ~ @pnc۹}"idɶؔzP*nVw:*gZdO{=Gg3S= 4AE]W\fq|ɍc x@'o:x9V,>gs;*|~w;6h>O^JϽދ jh ^|فJCE;}Ks#_cO7^o%_/=C[H]On?pAN2v^q/Ƨ7'_V78^R I4/V׍?y/=4Zx ýp \ˣRR͍B;31<35Tv z˶#e?c&`6Knd+CCn@H1TiQW\+]yBR @xe{'i7;D<VV?nppd@36Uy@tl֭zǩxAؠD_q}?:[m:ABR @'i_\>T ead+˯ v"y::t(ho)@   o7@ b1Px(@   oÛ~_t"x_s{СC67T7ުLJ7Pu(@ ?z=^?IENDB`css/block-editor.min.css000064400000000326147206624130011214 0ustar00.edit-post-post-views-popover .components-popover__content{padding:10px;min-width:260px}.edit-post-post-views-popover .components-popover__content legend{font-weight:600;margin-bottom:1em;margin-top:.5em;padding:0}css/admin-dashboard.min.css000064400000006430147206624130011655 0ustar00.pvc-dashboard-container{min-height:260px;margin:0 -4px;text-align:center;position:relative;display:flex;flex-direction:column}.pvc-dashboard-container .spinner{position:absolute;left:50%;top:40%;margin-left:-10px;z-index:10}.pvc-dashboard-container.loading{pointer-events:none}.pvc-dashboard-container.loading:after{position:absolute;content:'';display:block;height:100%;width:100%;top:0;left:0;background-color:rgb(255 255 255 / .8);z-index:1;transition:all 0.2s}.pvc-dashboard p.sub{color:#8f8f8f;font-size:14px;text-align:left;padding-bottom:3px;border-bottom:1px solid #ececec}.pvc-dashboard-container .pvc-months{display:flex;justify-content:space-between;margin:6px 10px 0 10px;color:#aaa}.pvc-dashboard-container .pvc-months .current{color:#212529}.pvc-data-container{min-height:230px}.pvc-table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.pvc-table{caption-side:bottom;border-collapse:collapse;width:100%;margin-bottom:1rem;color:inherit;vertical-align:top;border-color:#dee2e6;text-align:left}.pvc-table>thead{vertical-align:bottom;color:#212529}.pvc-table>tbody{vertical-align:inherit}.pvc-table tbody,.pvc-table td,.pvc-table tfoot,.pvc-table th,.pvc-table thead,.pvc-table tr{border-color:inherit;border-style:solid;border-width:0}.pvc-table th,.pvc-table td{text-align:inherit;text-align:-webkit-match-parent}.pvc-table th:first-child,.pvc-table td:first-child{width:1px;white-space:nowrap}.pvc-table th:last-child,.pvc-table td:last-child{text-align:right}.pvc-table .no-posts :last-child{text-align:left}.pvc-table>:not(caption)>*>*{padding:.5rem .5rem;background-color:#fff0;border-bottom-width:1px;box-shadow:inset 0 0 0 9999px #fff0}#pvc_dashboard .inside{margin:0;padding:0}.pvc-accordion-toggle{background-color:#fafafa;border-bottom:1px solid #eee;cursor:pointer;line-height:1;position:relative;font-size:14px;font-weight:400;margin:0;padding:11px 12px;color:#23282c}.pvc-accordion-toggle::before{color:#72777c;content:"\f142";display:inline-block;font:normal 20px dashicons;line-height:1;position:absolute;right:8px;text-decoration:none!important;text-indent:-1px;transform:rotate(0deg);speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;top:8px}.pvc-accordion-title{display:inline-block;padding-right:10px}.pvc-accordion-actions{position:absolute;top:0;right:0;z-index:1;padding:11px 30px 11px 0;height:14px;line-height:1}.pvc-accordion-actions .pvc-accordion-action,.pvc-accordion-actions .pvc-accordion-action::before{font-size:14px;height:14px;width:14px;color:#72777c}.pvc-tooltip{position:relative}.pvc-tooltip-icon{display:inline-block;width:16px;cursor:help}.pvc-tooltip-icon::before{color:#b4b9be;content:"\f14c";display:inline-block;font:normal 16px dashicons;line-height:1;position:absolute;text-decoration:none!important;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;left:0;top:2px}.pvc-according-header{display:flex;align-items:center;justify-content:space-between}.pvc-accordion-content{padding:11px 12px;border-bottom:1px solid #eee;height:100%}.pvc-collapsed .pvc-accordion-toggle::before{transform:rotate(180deg)}.pvc-collapsed .pvc-accordion-content{display:none}.pvc-dashboard-block{display:flex;justify-content:center;align-items:center;background:#fafafa;color:#787c82;font-size:13px;font-style:italic;padding:13px;margin-top:0}includes/class-columns.php000064400000036002147206624130011654 0ustar00post_type, (array) $pvc->options['general']['post_types_count'] ) ) return; // break if current user can't edit this post if ( ! current_user_can( 'edit_post', $post->ID ) ) return; // get total post views $count = (int) pvc_get_post_views( $post->ID ); ?>
' . number_format_i18n( $count ) . ''; ?> options['general']['restrict_edit_views']; if ( $restrict === false || ( $restrict === true && current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) ) { ?>


options['general']['post_types_count']; // get post type if ( is_null( $post ) ) $post_type = get_post_type( $post_id ); else $post_type = $post->post_type; // invalid post type? if ( ! in_array( $post_type, $post_types, true ) ) return; // break if views editing is restricted if ( (bool) $pvc->options['general']['restrict_edit_views'] === true && ! current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) return; // validate data if ( ! isset( $_POST['pvc_nonce'] ) || ! wp_verify_nonce( $_POST['pvc_nonce'], 'post_views_count' ) ) return; // update post views pvc_update_post_views( $post_id, $post_views ); do_action( 'pvc_after_update_post_views_count', $post_id ); } /** * Register post views column for specific post types. * * @return void */ public function register_new_column() { // get main instance $pvc = Post_Views_Counter(); // is posts views column active? if ( ! $pvc->options['general']['post_views_column'] ) return; // get post types $post_types = $pvc->options['general']['post_types_count']; // any post types? if ( ! empty( $post_types ) ) { foreach ( $post_types as $post_type ) { if ( $post_type === 'attachment' ) { // actions add_action( 'manage_media_custom_column', [ $this, 'add_new_column_content' ], 10, 2 ); // filters add_filter( 'manage_media_columns', [ $this, 'add_new_column' ] ); add_filter( 'manage_upload_sortable_columns', [ $this, 'register_sortable_custom_column' ] ); } else { // actions add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'add_new_column_content' ], 10, 2 ); // filters add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_new_column' ] ); add_filter( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'register_sortable_custom_column' ] ); // bbPress? if ( class_exists( 'bbPress' ) ) { if ( $post_type === 'forum' ) add_filter( 'bbp_admin_forums_column_headers', [ $this, 'add_new_column' ] ); elseif ( $post_type === 'topic' ) add_filter( 'bbp_admin_topics_column_headers', [ $this, 'add_new_column' ] ); } } } } } /** * Register sortable post views column. * * @param array $columns * @return array */ public function register_sortable_custom_column( $columns ) { // add new sortable column $columns['post_views'] = 'post_views'; return $columns; } /** * Add post views column. * * @param array $columns * @return array */ public function add_new_column( $columns ) { // date column exists? if ( isset( $columns['date'] ) ) { // store date column $date = $columns['date']; // unset date column unset( $columns['date'] ); } // comments column exists? if ( isset( $columns['comments'] ) ) { // store comments column $comments = $columns['comments']; // unset comments column unset( $columns['comments'] ); } // add post views column $columns['post_views'] = '' . esc_attr__( 'Post Views', 'post-views-counter' ) . ''; // restore date column if ( isset( $date ) ) $columns['date'] = $date; // restore comments column if ( isset( $comments ) ) $columns['comments'] = $comments; return $columns; } /** * Add post views column content. * * @param string $column_name * @param int $id * @return void */ public function add_new_column_content( $column_name, $id ) { if ( $column_name === 'post_views' ) { // get total post views $count = pvc_get_post_views( $id ); echo esc_html( $count ); } } /** * Handle quick edit. * * @global string $pagenow * * @param string $column_name * @param string $post_type * @return void */ function quick_edit_custom_box( $column_name, $post_type ) { global $pagenow; if ( $pagenow !== 'edit.php' ) return; if ( $column_name !== 'post_views' ) return; // get main instance $pvc = Post_Views_Counter(); if ( ! $pvc->options['general']['post_views_column'] || ! in_array( $post_type, $pvc->options['general']['post_types_count'] ) ) return; // break if views editing is restricted $restrict = (bool) $pvc->options['general']['restrict_edit_views']; if ( $restrict === true && ! current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) return; ?>
options['general']['restrict_edit_views']; if ( $restrict === true && ! current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) exit; // any post ids? if ( ! empty( $post_ids ) ) { foreach ( $post_ids as $post_id ) { // break if current user can't edit this post if ( ! current_user_can( 'edit_post', $post_id ) ) continue; // insert or update db post views count $wpdb->query( $wpdb->prepare( "INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count) VALUES (%d, %d, %s, %d) ON DUPLICATE KEY UPDATE count = %d", $post_id, 4, 'total', $count, $count ) ); } } exit; } /** * Add admin bar stats to a post. * * @return void */ public function maybe_load_admin_bar_menu() { // get main instance $pvc = Post_Views_Counter(); // statistics disabled? if ( ! apply_filters( 'pvc_display_toolbar_statistics', $pvc->options['display']['toolbar_statistics'] ) ) return; // skip for not logged in users if ( ! is_user_logged_in() ) return; // skip users with turned off admin bar at frontend if ( ! is_admin() && get_user_option( 'show_admin_bar_front' ) !== 'true' ) return; if ( is_admin() ) add_action( 'admin_init', [ $this, 'admin_bar_maybe_add_style' ] ); else add_action( 'wp', [ $this, 'admin_bar_maybe_add_style' ] ); } /** * Add admin bar stats to a post. * * @global string $pagenow * @global string $post * * @param object $admin_bar * @return void */ public function admin_bar_menu( $admin_bar ) { // get main instance $pvc = Post_Views_Counter(); // set empty post $post = null; // admin? if ( is_admin() && ! wp_doing_ajax() ) { global $pagenow; $post = ( $pagenow === 'post.php' && ! empty( $_GET['post'] ) ) ? get_post( (int) $_GET['post'] ) : $post; // frontend? } elseif ( is_singular() ) global $post; // get countable post types $post_types = $pvc->options['general']['post_types_count']; // whether to allow this post type or not if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) ) return; $dt = new DateTime(); // get post views $views = pvc_get_views( [ 'post_id' => $post->ID, 'post_type' => $post->post_type, 'fields' => 'date=>views', 'views_query' => [ 'year' => $dt->format( 'Y' ), 'month' => $dt->format( 'm' ) ] ] ); $graph = ''; // get highest value $views_copy = $views; arsort( $views_copy, SORT_NUMERIC ); $highest = reset( $views_copy ); // find the multiplier $multiplier = $highest * 0.05; // generate ranges $ranges = []; for ( $i = 1; $i <= 20; $i ++ ) { $ranges[$i] = round( $multiplier * $i ); } // create graph foreach ( $views as $date => $count ) { $count_class = 0; if ( $count > 0 ) { foreach ( $ranges as $index => $range ) { if ( $count <= $range ) { $count_class = $index; break; } } } $graph .= ''; } $admin_bar->add_menu( [ 'id' => 'pvc-post-views', 'title' => '' . $graph . '', 'href' => false, 'meta' => [ 'title' => false ] ] ); } /** * Maybe add admin CSS. * * @global string $pagenow * @global string $post * * @return void */ public function admin_bar_maybe_add_style() { // get main instance $pvc = Post_Views_Counter(); // set empty post $post = null; // admin? if ( is_admin() && ! wp_doing_ajax() ) { global $pagenow; $post = ( $pagenow === 'post.php' && ! empty( $_GET['post'] ) ) ? get_post( (int) $_GET['post'] ) : $post; // frontend? } elseif ( is_singular() ) global $post; // get countable post types $post_types = $pvc->options['general']['post_types_count']; // whether to allow this post type or not if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) ) return; // add admin bar add_action( 'admin_bar_menu', [ $this, 'admin_bar_menu' ], 100 ); // backend if ( current_action() === 'admin_init' ) add_action( 'admin_head', [ $this, 'admin_bar_css' ] ); // frontend elseif ( current_action() === 'wp' ) add_action( 'wp_head', [ $this, 'admin_bar_css' ] ); } /** * Add admin CSS. * * @return void */ public function admin_bar_css() { $html = ' '; echo wp_kses( $html, [ 'style' => [] ] ); } } includes/class-update.php000064400000013144147206624130011460 0ustar00options['general']; if ( $general['reset_counts']['number'] > 0 ) { // unsupported data reset in minutes/hours if ( in_array( $general['reset_counts']['type'], [ 'minutes', 'hours' ], true ) ) { // set type to date $general['reset_counts']['type'] = 'days'; // new number of days if ( $general['reset_counts']['type'] === 'minutes' ) $general['reset_counts']['number'] = $general['reset_counts']['number'] * MINUTE_IN_SECONDS; else $general['reset_counts']['number'] = $general['reset_counts']['number'] * HOUR_IN_SECONDS; // how many days? $general['reset_counts']['number'] = (int) round( ceil( $general['reset_counts']['number'] / DAY_IN_SECONDS ) ); // force cron to update $general['cron_run'] = true; $general['cron_update'] = true; // update settings update_option( 'post_views_counter_settings_general', $general ); // update general options $pvc->options['general'] = $general; } // update cron job for all users $pvc->cron->check_cron(); } } // update 1.3.13+ if ( version_compare( $current_db_version, '1.3.13', '<=' ) ) { // get general options $general = $pvc->options['general']; // disable strict counts $general['strict_counts'] = false; // get default other options $other_options = $pvc->defaults['other']; // set current options $other_options['deactivation_delete'] = isset( $general['deactivation_delete'] ) ? (bool) $general['deactivation_delete'] : false; // add other options add_option( 'post_views_counter_settings_other', $other_options, null, false ); // update other options $pvc->options['other'] = $other_options; // remove old setting unset( $general['deactivation_delete'] ); // flush cache enabled? if ( $general['flush_interval']['number'] > 0 ) { if ( $pvc->counter->using_object_cache( true ) ) { // flush data from cache $pvc->counter->flush_cache_to_db(); } // unschedule cron event wp_clear_scheduled_hook( 'pvc_flush_cached_counts' ); // disable cache $general['flush_interval'] = [ 'number' => 0, 'type' => 'minutes' ]; } // update general options $pvc->options['general'] = $general; // update general options update_option( 'post_views_counter_settings_general', $general ); } if ( isset( $_POST['post_view_counter_update'], $_POST['post_view_counter_number'] ) ) { if ( $_POST['post_view_counter_number'] === 'update_1' ) { $this->update_1(); // update plugin version update_option( 'post_views_counter_version', $pvc->defaults['version'], false ); } } // get current database version $current_db_version = get_option( 'post_views_counter_version', '1.0.0' ); // new version? if ( version_compare( $current_db_version, $pvc->defaults['version'], '<' ) ) { // is update 1 required? if ( version_compare( $current_db_version, '1.2.4', '<=' ) ) { $update_1_html = '

' . __( 'Post Views Counter - this version requires a database update. Make sure to back up your database first.', 'post-views-counter' ) . '

'; $pvc->add_notice( $update_1_html, 'notice notice-info', false ); } else // update plugin version update_option( 'post_views_counter_version', $pvc->defaults['version'], false ); } } /** * Database update for 1.2.4 and below. * * @global object $wpdb * * @return void */ public function update_1() { global $wpdb; // get index $old_index = $wpdb->query( "SHOW INDEX FROM `" . $wpdb->prefix . "post_views` WHERE Key_name = 'id_period'" ); // check whether index already exists if ( $old_index > 0 ) { // drop unwanted index which prevented saving views with identical weeks and months $wpdb->query( "ALTER TABLE `" . $wpdb->prefix . "post_views` DROP INDEX id_period" ); } // get index $new_index = $wpdb->query( "SHOW INDEX FROM `" . $wpdb->prefix . "post_views` WHERE Key_name = 'id_type_period_count'" ); // check whether index already exists if ( $new_index === 0 ) { // create new index for better performance of sql queries $wpdb->query( 'ALTER TABLE `' . $wpdb->prefix . 'post_views` ADD UNIQUE INDEX `id_type_period_count` (`id`, `type`, `period`, `count`) USING BTREE' ); } Post_Views_Counter()->add_notice( __( 'Thank you! Datebase was successfully updated.', 'post-views-counter' ), 'updated', true ); } } includes/class-query.php000064400000046452147206624130011353 0ustar00query_vars['orderby'] ) ) return; if ( is_string( $query->query_vars['orderby'] ) ) { // simple order by post_views if ( $query->query_vars['orderby'] === 'post_views' ) $query->pvc_orderby = true; // multisort post_views as string elseif ( strpos( $query->query_vars['orderby'], 'post_views' ) !== false ) { // explode orderby $sort = explode( ' ', $query->query_vars['orderby'] ); // make sure only full string is available if ( ! empty( $sort ) ) { // clear it $sort = array_filter( $sort ); if ( in_array( 'post_views', $sort, true ) ) $query->pvc_orderby = true; } } // post_views in array } elseif ( is_array( $query->query_vars['orderby'] ) && array_key_exists( 'post_views', $query->query_vars['orderby'] ) ) $query->pvc_orderby = true; } /** * Modify the database query to use post_views parameter. * * @global object $wpdb * * @param string $join * @param object $query * @return string */ public function posts_join( $join, $query ) { $sql = ''; $query_chunks = []; // views query? if ( ! empty( $query->query['views_query'] ) ) { if ( isset( $query->query['views_query']['inclusive'] ) ) $query->query['views_query']['inclusive'] = (bool) $query->query['views_query']['inclusive']; else $query->query['views_query']['inclusive'] = true; // check after and before dates foreach ( [ 'after' => '>', 'before' => '<' ] as $date => $type ) { $year_ = null; $month_ = null; $week_ = null; $day_ = null; // check views query date if ( ! empty( $query->query['views_query'][$date] ) ) { // is it a date array? if ( is_array( $query->query['views_query'][$date] ) ) { // check views query $date date year if ( ! empty( $query->query['views_query'][$date]['year'] ) ) $year_ = str_pad( (int) $query->query['views_query'][$date]['year'], 4, 0, STR_PAD_LEFT ); // check views query date month if ( ! empty( $query->query['views_query'][$date]['month'] ) ) $month_ = str_pad( (int) $query->query['views_query'][$date]['month'], 2, 0, STR_PAD_LEFT ); // check views query date week if ( ! empty( $query->query['views_query'][$date]['week'] ) ) $week_ = str_pad( (int) $query->query['views_query'][$date]['week'], 2, 0, STR_PAD_LEFT ); // check views query date day if ( ! empty( $query->query['views_query'][$date]['day'] ) ) $day_ = str_pad( (int) $query->query['views_query'][$date]['day'], 2, 0, STR_PAD_LEFT ); // is it a date string? } elseif ( is_string( $query->query['views_query'][$date] ) ) { $time_ = strtotime( $query->query['views_query'][$date] ); // valid datetime? if ( $time_ !== false ) { // week does not exists here, string dates are always treated as year + month + day list( $day_, $month_, $year_ ) = explode( ' ', date( "d m Y", $time_ ) ); } } // valid date? if ( ! ( $year_ === null && $month_ === null && $week_ === null && $day_ === null ) ) { $query_chunks[] = [ 'year' => $year_, 'month' => $month_, 'day' => $day_, 'week' => $week_, 'type' => $type . ( $query->query['views_query']['inclusive'] ? '=' : '' ) ]; } } } // any after, before query chunks? if ( ! empty( $query_chunks ) ) { $valid_dates = true; // check only if both dates are in query if ( count( $query_chunks ) === 2 ) { // before and after dates should be the same foreach ( [ 'year', 'month', 'day', 'week' ] as $date_type ) { if ( ! ( ( $query_chunks[0][$date_type] !== null && $query_chunks[1][$date_type] !== null ) || ( $query_chunks[0][$date_type] === null && $query_chunks[1][$date_type] === null ) ) ) $valid_dates = false; } } // after and before dates should be both valid if ( $valid_dates ) { foreach ( $query_chunks as $chunk ) { // year if ( isset( $chunk['year'] ) ) { // year, week if ( isset( $chunk['week'] ) ) $sql .= " AND pvc.type = 1 AND pvc.period " . $chunk['type'] . " '" . $chunk['year'] . $chunk['week'] . "'"; // year, month elseif ( isset( $chunk['month'] ) ) { // year, month, day if ( isset( $chunk['day'] ) ) $sql .= " AND pvc.type = 0 AND pvc.period " . $chunk['type'] . " '" . $chunk['year'] . $chunk['month'] . $chunk['day'] . "'"; // year, month else $sql .= " AND pvc.type = 2 AND pvc.period " . $chunk['type'] . " '" . $chunk['year'] . $chunk['month'] . "'"; // year } else $sql .= " AND pvc.type = 3 AND pvc.period " . $chunk['type'] . " '" . $chunk['year'] . "'"; // month } elseif ( isset( $chunk['month'] ) ) { // month, day if ( isset( $chunk['day'] ) ) $sql .= " AND pvc.type = 0 AND RIGHT( pvc.period, 4 ) " . $chunk['type'] . " '" . $chunk['month'] . $chunk['day'] . "'"; // month else $sql .= " AND pvc.type = 2 AND RIGHT( pvc.period, 2 ) " . $chunk['type'] . " '" . $chunk['month'] . "'"; // week } elseif ( isset( $chunk['week'] ) ) $sql .= " AND pvc.type = 1 AND RIGHT( pvc.period, 2 ) " . $chunk['type'] . " '" . $chunk['week'] . "'"; // day elseif ( isset( $chunk['day'] ) ) $sql .= " AND pvc.type = 0 AND RIGHT( pvc.period, 2 ) " . $chunk['type'] . " '" . $chunk['day'] . "'"; } } } // standard query if ( $sql === '' ) { // check year if ( isset( $query->query['views_query']['year'] ) ) $year = (int) $query->query['views_query']['year']; // check month if ( isset( $query->query['views_query']['month'] ) ) $month = (int) $query->query['views_query']['month']; // check week if ( isset( $query->query['views_query']['week'] ) ) $week = (int) $query->query['views_query']['week']; // check day if ( isset( $query->query['views_query']['day'] ) ) $day = (int) $query->query['views_query']['day']; // year if ( isset( $year ) ) { // year, week if ( isset( $week ) && $this->is_date_valid( 'yw', $year, 0, 0, $week ) ) $sql = " AND pvc.type = 1 AND pvc.period = '" . str_pad( $year, 4, 0, STR_PAD_LEFT ) . str_pad( $week, 2, 0, STR_PAD_LEFT ) . "'"; // year, month elseif ( isset( $month ) ) { // year, month, day if ( isset( $day ) && $this->is_date_valid( 'ymd', $year, $month, $day ) ) $sql = " AND pvc.type = 0 AND pvc.period = '" . str_pad( $year, 4, 0, STR_PAD_LEFT ) . str_pad( $month, 2, 0, STR_PAD_LEFT ) . str_pad( $day, 2, 0, STR_PAD_LEFT ) . "'"; // year, month elseif ( $this->is_date_valid( 'ym', $year, $month ) ) $sql = " AND pvc.type = 2 AND pvc.period = '" . str_pad( $year, 4, 0, STR_PAD_LEFT ) . str_pad( $month, 2, 0, STR_PAD_LEFT ) . "'"; // year } elseif ( $this->is_date_valid( 'y', $year ) ) $sql = " AND pvc.type = 3 AND pvc.period = '" . str_pad( $year, 4, 0, STR_PAD_LEFT ) . "'"; // month } elseif ( isset( $month ) ) { // month, day if ( isset( $day ) && $this->is_date_valid( 'md', 0, $month, $day ) ) { $sql = " AND pvc.type = 0 AND RIGHT( pvc.period, 4 ) = '" . str_pad( $month, 2, 0, STR_PAD_LEFT ) . str_pad( $day, 2, 0, STR_PAD_LEFT ) . "'"; // month } elseif ( $this->is_date_valid( 'm', 0, $month ) ) $sql = " AND pvc.type = 2 AND RIGHT( pvc.period, 2 ) = '" . str_pad( $month, 2, 0, STR_PAD_LEFT ) . "'"; // week } elseif ( isset( $week ) && $this->is_date_valid( 'w', 0, 0, 0, $week ) ) $sql = " AND pvc.type = 1 AND RIGHT( pvc.period, 2 ) = '" . str_pad( $week, 2, 0, STR_PAD_LEFT ) . "'"; // day elseif ( isset( $day ) && $this->is_date_valid( 'd', 0, 0, $day ) ) $sql = " AND pvc.type = 0 AND RIGHT( pvc.period, 2 ) = '" . str_pad( $day, 2, 0, STR_PAD_LEFT ) . "'"; } if ( $sql !== '' ) $query->pvc_query = true; } // is it sorted by post views? if ( ( $sql === '' && isset( $query->pvc_orderby ) && $query->pvc_orderby ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) $sql = ' AND pvc.type = 4'; // add date range if ( $sql !== '' ) { global $wpdb; $join .= " LEFT JOIN " . $wpdb->prefix . "post_views pvc ON pvc.id = " . $wpdb->prefix . "posts.ID" . $sql; $this->join_sql = $join; } return $join; } /** * Group posts using the post ID. * * @global object $wpdb * @global string $pagenow * * @param string $groupby * @param object $query * @return string */ public function posts_groupby( $groupby, $query ) { // is it sorted by post views or views_query is used? if ( ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) || ( isset( $query->pvc_query ) && $query->pvc_query ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) { global $pagenow; // needed only for sorting if ( $pagenow === 'upload.php' || $pagenow === 'edit.php' ) $query->query['views_query']['hide_empty'] = false; global $wpdb; $groupby = trim( $groupby ); $groupby_aliases = []; $groupby_values = []; $groupby_sql = ''; $groupby_set = false; // standard group by if ( strpos( $groupby, $wpdb->prefix . 'posts.ID' ) === false ) $groupby_aliases[] = $wpdb->prefix . 'posts.ID'; else $groupby_set = true; // tax query group by $groupby_aliases[] = $this->get_groupby_meta_aliases( $query ); // meta query group by if ( $this->join_sql ) { $groupby_aliases[] = $this->get_groupby_tax_aliases( $query, $this->join_sql ); // clear join to avoid possible issues $this->join_sql = ''; } // any group by aliases? if ( ! empty( $groupby_aliases ) ) { foreach ( $groupby_aliases as $alias ) { if ( is_array( $alias ) ) { $groupby_values = array_merge( $groupby_values, $alias ); } else $groupby_values[] = $alias; } } // any group by values? if ( ! empty( $groupby_values ) ) { $groupby = ( $groupby !== '' ? $groupby . ', ' : '' ) . implode( ', ', $groupby_values ); // set group by flag $groupby_set = true; } if ( $groupby_set ) $query->pvc_groupby = true; // hide empty? if ( ! isset( $query->query['views_query']['hide_empty'] ) || $query->query['views_query']['hide_empty'] === true ) $groupby .= ' HAVING post_views > 0'; } return $groupby; } /** * Order posts by post views. * * @global object $wpdb * * @param string $orderby * @param object $query * @return string */ public function posts_orderby( $orderby, $query ) { // is it sorted by post views? if ( ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) ) { global $wpdb; // get order $order = $query->get( 'order' ); // get original orderby (before parsing) $org_orderby = $query->get( 'orderby' ); // orderby as string if ( is_string( $org_orderby ) ) { if ( $org_orderby === 'post_views' ) $orderby = 'post_views ' . $order; elseif ( strpos( $org_orderby, 'post_views' ) !== false ) { // explode orderby $sort = explode( ' ', $org_orderby ); if ( ! empty( $sort ) ) { // clear it $sort = array_values( array_filter( $sort ) ); // make sure only full string is available if ( in_array( 'post_views', $sort, true ) ) { // sort only by post views if ( count( $sort ) === 1 ) $orderby = 'post_views ' . $order; else { // post_views as first value if ( $sort[0] === 'post_views' ) $orderby = 'post_views ' . $order . ', ' . $orderby; else { //todo find a way to recognize other sorting options based on original order and parsed order by wordpress $orderby = 'post_views ' . $order . ', ' . $orderby; } } } } } // orderby as array } elseif ( is_array( $org_orderby ) && array_key_exists( 'post_views', $org_orderby ) ) { // sort only by post views if ( count( $org_orderby ) === 1 ) $orderby = 'post_views ' . $order; else { // post_views as first key if ( array_key_first( $org_orderby ) === 'post_views' ) { $sanitized_orderby = sanitize_sql_orderby( 'post_views ' . strtoupper( $org_orderby['post_views'] ) ); if ( $sanitized_orderby !== false ) $orderby = $sanitized_orderby . ', ' . $orderby; else $orderby = 'post_views ' . $order . ', ' . $orderby; } else { //todo find a way to recognize other sorting options based on original order and parsed order by wordpress $sanitized_orderby = sanitize_sql_orderby( 'post_views ' . strtoupper( $org_orderby['post_views'] ) ); if ( $sanitized_orderby !== false ) $orderby = $sanitized_orderby . ', ' . $orderby; else $orderby = 'post_views ' . $order . ', ' . $orderby; } } } } return $orderby; } /** * Add DISTINCT clause. * * @param string $distinct * @param object $query * @return string */ public function posts_distinct( $distinct, $query ) { if ( ( ( isset( $query->pvc_groupby ) && $query->pvc_groupby ) || ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) || ( isset( $query->pvc_query ) && $query->pvc_query ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) && ( strpos( $distinct, 'DISTINCT' ) === false ) ) $distinct = $distinct . ' DISTINCT '; return $distinct; } /** * Return post views in queried post objects. * * @param string $fields * @param object $query * @return string */ public function posts_fields( $fields, $query ) { if ( ( ! isset( $query->query['fields'] ) || $query->query['fields'] === '' || $query->query['fields'] === 'all' ) && ( ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) || ( isset( $query->pvc_query ) && $query->pvc_query ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) ) $fields = $fields . ', SUM( COALESCE( pvc.count, 0 ) ) AS post_views'; return $fields; } /** * Get tax table aliases from query. * * @global object $wpdb * * @param object $query * @param string $join_sql * @return array */ private function get_groupby_tax_aliases( $query, $join_sql ) { global $wpdb; $groupby = []; // trim join sql $join_sql = trim( $join_sql ); // any join sql? valid query with tax query? if ( $join_sql !== '' && is_a( $query, 'WP_Query' ) && ! empty( $query->tax_query ) && is_a( $query->tax_query, 'WP_Tax_Query' ) ) { // unfortunately there is no way to get table_aliases by native function // tax query does not have get_clauses either like meta query does // we have to find aliases the hard way $chunks = explode( 'JOIN', $join_sql ); // any join clauses? if ( ! empty( $chunks ) ) { $aliases = []; foreach ( $chunks as $chunk ) { // standard join if ( strpos( $chunk, $wpdb->prefix . 'term_relationships ON' ) !== false ) $aliases[] = $wpdb->prefix . 'term_relationships'; // alias join elseif ( strpos( $chunk, $wpdb->prefix . 'term_relationships AS' ) !== false && preg_match( '/' . $wpdb->prefix . 'term_relationships AS ([a-z0-9]+) ON/i', $chunk, $matches ) === 1 ) $aliases[] = $matches[1]; } // any aliases? if ( ! empty( $aliases ) ) { foreach ( array_unique( $aliases ) as $alias ) { $groupby[] = $alias . '.term_taxonomy_id'; } } } } return $groupby; } /** * Get meta table aliases from query. * * @param object $query * @return array */ private function get_groupby_meta_aliases( $query ) { $groupby = []; // valid query with meta query? if ( is_a( $query, 'WP_Query' ) && ! empty( $query->meta_query ) && is_a( $query->meta_query, 'WP_Meta_Query' ) ) { // get meta clauses, we can't use table_aliases here since it's protected value $clauses = $query->meta_query->get_clauses(); // any meta clauses? if ( ! empty( $clauses ) ) { $aliases = []; foreach ( $clauses as $clause ) { $aliases[] = $clause['alias']; } // any aliases? if ( ! empty( $aliases ) ) { foreach ( array_unique( $aliases ) as $alias ) { $groupby[] = $alias . '.meta_id'; } } } } return $groupby; } /** * Extend query object with total post views. * * @param array $posts * @param object $query * @return array */ public function the_posts( $posts, $query ) { if ( ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) || ( isset( $query->pvc_query ) && $query->pvc_query ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) { $sum = 0; // any posts found? if ( ! empty( $posts ) ) { foreach ( $posts as $post ) { if ( ! empty( $post->post_views ) ) $sum += (int) $post->post_views; } } // pass total views $query->total_views = $sum; } return $posts; } /** * Check whether date is valid. * * @param string $type * @param int $year * @param int $month * @param int $day * @param int $week * @return bool */ private function is_date_valid( $type, $year = 0, $month = 0, $day = 0, $week = 0 ) { switch ( $type ) { case 'y': $bool = ( $year >= 1 && $year <= 32767 ); break; case 'yw': $bool = ( $year >= 1 && $year <= 32767 && $week >= 0 && $week <= 53 ); break; case 'ym': $bool = ( $year >= 1 && $year <= 32767 && $month >= 1 && $month <= 12 ); break; case 'ymd': $bool = checkdate( $month, $day, $year ); break; case 'm': $bool = ( $month >= 1 && $month <= 12 ); break; case 'md': $bool = ( $month >= 1 && $month <= 12 && $day >= 1 && $day <= 31 ); break; case 'w': $bool = ( $week >= 0 && $week <= 53 ); break; case 'd': $bool = ( $day >= 1 && $day <= 31 ); break; } return $bool; } } includes/class-settings-api.php000064400000062501147206624130012606 0ustar00prefix = $args['prefix']; $this->domain = $args['domain']; // empty slug? if ( empty( $args['slug'] ) ) $this->slug = $args['domain']; else $this->slug = $args['slug']; // empty short name? if ( empty( $args['short'] ) ) { $short = ''; // prepare short name based on prefix $parts = explode( '_', $this->prefix ); foreach ( $parts as $string ) { $short .= substr( $string, 0, 1 ); } // set short name $this->short = $short; } else $this->short = $args['short']; $this->object = $args['object']; $this->plugin = $args['plugin']; $this->plugin_url = $args['plugin_url']; // actions add_action( 'admin_menu', [ $this, 'admin_menu_options' ], 11 ); add_action( 'admin_init', [ $this, 'register_settings' ], 11 ); add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); } /** * Get prefix. * * @return string */ public function get_prefix() { return $this->prefix; } /** * Get pages. * * @return array */ public function get_pages() { return $this->pages; } /** * Get settings. * * @return array */ public function get_settings() { return $this->settings; } /** * Get current input settings during saving. * * @return array */ public function get_input_settings() { return $this->input_settings; } /** * Get already validated setting fields during saving. * * @return array */ public function get_validated_settings() { return $this->validated_settings; } /** * Load default scripts and styles. * * @return void */ public function admin_enqueue_scripts() { $handler = $this->short . '-settings-api-style'; // register and enqueue styles wp_register_style( $handler, false ); wp_enqueue_style( $handler ); // add styles wp_add_inline_style( $handler, '.nav-tab-wrapper span.nav-span-disabled { cursor: not-allowed; float: left; } body.rtl .nav-tab-wrapper span.nav-span-disabled { float: right; } .nav-tab-wrapper a.nav-tab.nav-tab-disabled { pointer-events: none; } .nav-tab-wrapper a.nav-tab.nav-tab-disabled:hover { cursor: not-allowed; }' ); } /** * Add menu pages. * * @return void */ public function admin_menu_options() { $this->pages = apply_filters( $this->prefix . '_settings_pages', [] ); $types = [ 'page' => [], 'subpage' => [], 'settings_page' => [] ]; foreach ( $this->pages as $page => $data ) { // skip invalid page types if ( empty( $data['type'] ) || ! array_key_exists( $data['type'], $types ) ) continue; if ( $data['type'] === 'page' ) { add_menu_page( $data['page_title'], $data['menu_title'], $data['capability'], $data['menu_slug'], ! empty( $data['callback'] ) ? $data['callback'] : [ $this, 'options_page' ], $data['icon'], $data['position'] ); // add page type $types['page'][$data['menu_slug']] = $page; // menu subpage? } elseif ( $data['type'] === 'subpage' ) { add_submenu_page( $data['parent_slug'], $data['page_title'], $data['menu_title'], $data['capability'], $data['menu_slug'], ! empty( $data['callback'] ) ? $data['callback'] : [ $this, 'options_page' ] ); // add subpage type $types['subpage'][$data['menu_slug']] = $page; // menu settings page? } elseif ( $data['type'] === 'settings_page' ) { add_options_page( $data['page_title'], $data['menu_title'], $data['capability'], $data['menu_slug'], ! empty( $data['callback'] ) ? $data['callback'] : [ $this, 'options_page' ] ); // add settings type $types['settings_page'][$data['menu_slug']] = $page; } } // set page types $this->page_types = $types; } /** * Render settings. * * @global string $pagenow * * @return void */ public function options_page() { global $pagenow; $valid_page = false; // get current screen $screen = get_current_screen(); // display top level settings page? if ( $pagenow === 'admin.php' && preg_match( '/^toplevel_page_(' . implode( '|', $this->page_types['page'] ) . ')$/', $screen->base, $matches ) === 1 && ! empty( $matches[1] ) ) { $valid_page = true; $page_type = 'page'; $url_page = 'admin.php'; } // display sub level settings page? if ( ! $valid_page && $pagenow === 'admin.php' && preg_match( '/^(?:toplevel|' . $this->prefix . ')_page_' . $this->prefix . '-(' . implode( '|', $this->page_types['subpage'] ) . ')-settings$/', $screen->base, $matches ) === 1 && ! empty( $matches[1] ) ) { $valid_page = true; $page_type = 'subpage'; $url_page = 'admin.php'; } // display settings page? if ( ! $valid_page && $pagenow === 'options-general.php' && preg_match( '/^(?:settings_page_)(' . implode( '|', array_keys( $this->page_types['settings_page'] ) ) . ')$/', $screen->base, $matches ) === 1 ) { $valid_page = true; $page_type = 'settings_page'; $url_page = 'options-general.php'; } // skip invalid pages if ( ! $valid_page ) return; echo '

' . esc_html( $this->settings[$matches[1]]['label'] ) . '

'; $tab_key = ''; // any tabs? if ( array_key_exists( 'tabs', $this->pages[$this->page_types[$page_type][$matches[1]]] ) ) { // get tabs $tabs = $this->pages[$this->page_types[$page_type][$matches[1]]]['tabs']; // reset tabs reset( $tabs ); // get first default tab $first_tab = key( $tabs ); // get current tab $tab_key = ! empty( $_GET['tab'] ) && array_key_exists( $_GET['tab'], $tabs ) ? $_GET['tab'] : $first_tab; // check current tab if ( ! empty( $_GET['tab'] ) ) $tab_key = sanitize_key( $_GET['tab'] ); // invalid tab? if ( ! array_key_exists( $tab_key, $tabs ) ) $tab_key = $first_tab; echo ' '; } // skip for internal options page if ( $page_type !== 'settings_page' ) settings_errors(); // get settings page classes $settings_class = apply_filters( $this->prefix . '_settings_page_class', [ $this->slug . '-settings', $tab_key . '-settings' ] ); // sanitize settings page classes $settings_class = array_unique( array_filter( array_map( 'sanitize_html_class', $settings_class ) ) ); echo '
'; $display_form = true; // check form attribute if ( ! empty( $this->settings[$matches[1]]['form'] ) && ! empty( $tab_key ) && ! empty( $this->settings[$matches[1]]['form'][$tab_key] ) ) { $form = $this->settings[$matches[1]]['form'][$tab_key]; if ( isset( $form['buttons'] ) && ! $form['buttons'] ) $display_form = false; } // any tabs? if ( ! empty( $tab_key ) ) { if ( ! empty( $tabs[$tab_key]['option_name'] ) ) $setting = $tabs[$tab_key]['option_name']; else $setting = $this->prefix . '_' . $tab_key . '_settings'; } else $setting = $this->prefix . '_' . $matches[1] . '_settings'; do_action( $this->short . '_settings_sidebar', $setting, $page_type, $url_page, $tab_key ); if ( $display_form ) { echo '
'; } settings_fields( $setting ); if ( $display_form ) do_action( $this->short . '_settings_form', $setting, $page_type, $url_page, $tab_key ); do_settings_sections( $setting ); if ( $display_form ) { echo '

'; submit_button( '', 'primary', 'save_' . $setting, false ); echo ' '; submit_button( __( 'Reset to defaults', $this->domain ), 'secondary reset_' . $setting . ' reset_' . $this->short . '_settings', 'reset_' . $setting, false ); echo '

'; } echo '
'; } /** * Register settings. * * @return void */ public function register_settings() { $this->settings = apply_filters( $this->prefix . '_settings_data', [] ); // check settings foreach ( $this->settings as $setting_id => $setting ) { // tabs? if ( is_array( $setting['option_name'] ) ) { foreach ( $setting['option_name'] as $tab => $option_name ) { $this->register_setting_fields( $tab, $setting, $option_name ); } } else $this->register_setting_fields( $setting_id, $setting ); } } /** * Register setting with sections and fields. * * @return void */ public function register_setting_fields( $setting_id, $setting, $option_name = '' ) { if ( empty( $option_name ) ) $option_name = $setting['option_name']; // register setting register_setting( $option_name, $option_name, ! empty( $setting['validate'] ) ? $setting['validate'] : [ $this, 'validate_settings' ] ); // register setting sections if ( ! empty( $setting['sections'] ) ) { foreach ( $setting['sections'] as $section_id => $section ) { // skip unwanted sections if ( ! empty( $section['tab'] ) && $section['tab'] !== $setting_id ) continue; add_settings_section( $section_id, ! empty( $section['title'] ) ? esc_html( $section['title'] ) : '', ! empty( $section['callback'] ) ? $section['callback'] : null, ! empty( $section['page'] ) ? $section['page'] : $option_name ); } } // register setting fields if ( ! empty( $setting['fields'] ) ) { foreach ( $setting['fields'] as $field_key => $field ) { // skip unwanted fields if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id ) continue; // set field ID $field_id = implode( '_', [ $this->prefix, $setting_id, $field_key ] ); // skip rendering this field? if ( ! empty( $field['skip_rendering'] ) ) continue; add_settings_field( $field_id, ! empty( $field['title'] ) ? esc_html( $field['title'] ) : '', [ $this, 'render_field' ], $option_name, ! empty( $field['section'] ) ? esc_attr( $field['section'] ) : '', array_merge( $this->prepare_field_args( $field, $field_id, $field_key, $setting_id, $option_name ), $field ) ); } } } /** * Prepare field arguments. * * @param array $args * @return array */ public function prepare_field_args( $field, $field_id, $field_key, $setting_id, $setting_name ) { // get field type $field_type = ! empty( $field['type'] ) ? $field['type'] : ''; return [ 'id' => $field_id, 'name' => $setting_name . '[' . $field_key . ']', 'class' => ! empty( $field['class'] ) ? $field['class'] : '', 'type' => $field_type, 'label' => ! empty( $field['label'] ) ? $field['label'] : '', 'description' => ! empty( $field['description'] ) ? $field['description'] : '', 'text' => ! empty( $field['text'] ) ? $field['text'] : '', 'min' => ! empty( $field['min'] ) ? (int) $field['min'] : 0, 'max' => ! empty( $field['max'] ) ? (int) $field['max'] : 0, 'options' => ! empty( $field['options'] ) ? $field['options'] : [], 'callback' => ! empty( $field['callback'] ) ? $field['callback'] : null, 'validate' => ! empty( $field['validate'] ) ? $field['validate'] : null, 'callback_args' => ! empty( $field['callback_args'] ) ? $field['callback_args'] : [], 'default' => $field_type !== 'custom' ? $this->object->defaults[$setting_id][$field_key] : null, 'value' => $field_type !== 'custom' ? $this->object->options[$setting_id][$field_key] : null /* after_field before_field */ ]; } /** * Render settings field. * * @param array $args * @return void|string */ public function render_field( $args ) { if ( empty( $args ) || ! is_array( $args ) ) return; $html = '
'; if ( ! empty ( $args['before_field'] ) ) $html .= $args['before_field']; switch ( $args['type'] ) { case 'boolean': if ( empty( $args['disabled'] ) ) $html .= ''; $html .= ''; break; case 'radio': foreach ( $args['options'] as $key => $name ) { $html .= ' '; } break; case 'checkbox': // possible "empty" value if ( $args['value'] === 'empty' ) $args['value'] = []; $display_type = ! empty( $args['display_type'] ) && in_array( $args['display_type'], [ 'horizontal', 'vertical' ], true ) ? $args['display_type'] : 'horizontal'; $html .= ''; foreach ( $args['options'] as $key => $name ) { $html .= '' . ( $display_type === 'horizontal' ? ' ' : '
' ); } break; case 'select': $html .= ''; break; case 'range': $html .= '' . ( (int) $args['value'] ) . ''; break; case 'number': $html .= ( ! empty( $args['prepend'] ) ? wp_kses_post( $args['prepend'] ) : '' ); $html .= ''; $html .= ( ! empty( $args['append'] ) ? wp_kses_post( $args['append'] ) : '' ); break; case 'custom': $html .= call_user_func( $args['callback'], $args ); break; case 'info': $html .= '' . esc_html( $args['text'] ) . ''; break; case 'class': case 'input': default: $empty_disabled = empty( $args['disabled'] ); $html .= ( ! empty( $args['prepend'] ) ? wp_kses_post( $args['prepend'] ) : '' ); $html .= ''; $html .= ( ! empty( $args['append'] ) ? wp_kses_post( $args['append'] ) : '' ); if ( ! $empty_disabled ) $html .= '
'; if ( ! empty( $args['return'] ) ) return $html; else echo $html; } /** * Validate settings field. * * @param mixed $value * @param string $type * @param array $args * @return mixed */ public function validate_field( $value = null, $type = '', $args = [] ) { if ( is_null( $value ) ) return null; switch ( $type ) { case 'boolean': // possible value: 'true' or 'false' $value = ( $value === 'true' || $value === true ); break; case 'radio': $value = is_array( $value ) ? $args['default'] : sanitize_key( $value ); // disallow disabled radios if ( ! empty( $args['disabled'] ) && in_array( $value, $args['disabled'], true ) ) $value = $args['default']; break; case 'checkbox': // possible value: 'empty' or array if ( $value === 'empty' ) $value = []; else { if ( is_array( $value ) && ! empty( $value ) ) { $value = array_map( 'sanitize_key', $value ); $values = []; foreach ( $value as $single_value ) { if ( array_key_exists( $single_value, $args['options'] ) ) $values[] = $single_value; } $value = $values; } else $value = []; } break; case 'number': $value = (int) $value; // is value lower than? if ( isset( $args['min'] ) && $value < $args['min'] ) $value = $args['min']; // is value greater than? if ( isset( $args['max'] ) && $value > $args['max'] ) $value = $args['max']; break; case 'info': $value = ''; break; case 'custom': // do nothing break; case 'class': $value = trim( $value ); // more than 1 class? if ( strpos( $value, ' ' ) !== false ) { // get unique valid HTML classes $value = array_unique( array_filter( array_map( 'sanitize_html_class', explode( ' ', $value ) ) ) ); if ( ! empty( $value ) ) $value = implode( ' ', $value ); else $value = ''; // single class } else $value = sanitize_html_class( $value, $args['default'] ); break; case 'input': case 'select': default: $value = is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : sanitize_text_field( $value ); break; } return stripslashes_deep( $value ); } /** * Validate settings. * * @param array $input * @return array */ public function validate_settings( $input ) { // check capability if ( ! current_user_can( 'manage_options' ) ) return $input; // check option page if ( empty( $_POST['option_page'] ) ) return $input; // try to get setting name and ID foreach ( $this->settings as $id => $setting ) { // tabs? if ( is_array( $setting['option_name'] ) ) { foreach ( $setting['option_name'] as $tab => $option_name ) { // found valid setting? if ( $option_name === $_POST['option_page'] ) { // assign setting ID $setting_id = $tab; // assign setting name $setting_name = $option_name; // assign setting key $setting_key = $id; // already found setting, no need to check the rest break 2; } } } else { // found valid setting? if ( $setting['option_name'] === $_POST['option_page'] ) { // assign setting ID and key $setting_key = $setting_id = $id; // assign setting name $setting_name = $setting['option_name']; // already found setting, no need to check the rest break; } } } // check setting id, no need to check $setting_name since it was at the same stage if ( empty( $setting_id ) ) return $input; // save settings if ( isset( $_POST['save_' . $setting_name] ) ) { $input = $this->validate_input_settings( $setting_id, $setting_key, $input ); add_settings_error( $setting_name, 'settings_saved', __( 'Settings saved.', $this->domain ), 'updated' ); // reset settings } elseif ( isset( $_POST['reset_' . $setting_name] ) ) { // get default values $input = $this->object->defaults[$setting_id]; // check custom reset functions if ( ! empty( $this->settings[$setting_key]['fields'] ) ) { foreach ( $this->settings[$setting_key]['fields'] as $field_id => $field ) { // skip invalid tab field if any if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id ) continue; // custom reset function? if ( ! empty( $field['reset'] ) ) { // valid function? no need to check "else" since all default values are already set if ( $this->callback_function_exists( $field['reset'] ) ) { if ( $field['type'] === 'custom' ) $input = call_user_func( $field['reset'], $input, $field ); else $input[$field_id] = call_user_func( $field['reset'], $input[$field_id], $field ); } } } } add_settings_error( $setting_name, 'settings_restored', __( 'Settings restored to defaults.', $this->domain ), 'updated' ); } return $input; } /** * Validate input settings. * * @param string $setting_id * @param array $input * @return array */ public function validate_input_settings( $setting_id, $setting_key, $input ) { if ( ! empty( $this->settings[$setting_key]['fields'] ) ) { foreach ( $this->settings[$setting_key]['fields'] as $field_id => $field ) { // skip saving this field? if ( ! empty( $field['skip_saving'] ) ) continue; // skip invalid tab field if any if ( ! empty( $field['tab'] ) && $field['tab'] !== $setting_id ) continue; // custom validate function? if ( ! empty( $field['validate'] ) ) { // valid function? if ( $this->callback_function_exists( $field['validate'] ) ) { if ( $field['type'] === 'custom' ) $input = call_user_func( $field['validate'], $input, $field ); else $input[$field_id] = isset( $input[$field_id] ) ? call_user_func( $field['validate'], $input[$field_id], $field ) : $this->object->defaults[$setting_id][$field_id]; } else $input[$field_id] = $this->object->defaults[$setting_id][$field_id]; } else { // field data? if ( isset( $input[$field_id] ) ) { // make sure default value is available if ( ! isset( $field['default'] ) ) $field['default'] = $this->object->defaults[$setting_id][$field_id]; $input[$field_id] = $this->validate_field( $input[$field_id], $field['type'], $field ); } else $input[$field_id] = $this->object->defaults[$setting_id][$field_id]; } // update input data $this->input_settings = $input; // add this field as validated $this->validated_settings[] = $field_id; } } return $input; } /** * Check whether callback is a valid function. * * @param string|array $callback * @return bool */ public function callback_function_exists( $callback ) { // function as array? if ( is_array( $callback ) ) { list( $object, $function ) = $callback; // check function existence $function_exists = method_exists( $object, $function ); // function as string? } elseif ( is_string( $callback ) ) { // check function existence $function_exists = function_exists( $callback ); } else $function_exists = false; return $function_exists; } /** * Get value based on minimum and maximum. * * @param array $data * @param string $setting_name * @param int $default * @param int $min * @param int $max * @return void */ public function get_int_value( $data, $setting_name, $default, $min, $max ) { // check existence of value $value = array_key_exists( $setting_name, $data ) ? (int) $data[$setting_name] : $default; if ( $value > $max || $value < $min ) $value = $default; return $value; } } includes/class-cron.php000064400000005552147206624130011143 0ustar00 1, 'weeks' => 7, 'months' => 30, 'years' => 365 ]; // get main instance $pvc = Post_Views_Counter(); // default where clause $where = [ 'type = 0', 'CAST( period AS SIGNED ) < CAST( ' . date( 'Ymd', strtotime( '-' . ( (int) ( $counter[$pvc->options['general']['reset_counts']['type']] * $pvc->options['general']['reset_counts']['number'] ) ) . ' days' ) ) . ' AS SIGNED)' ]; // update where clause $where = apply_filters( 'pvc_reset_counts_where_clause', $where ); // delete views $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'post_views WHERE ' . implode( ' AND ', $where ) ); } /** * Add new cron interval from settings. * * @param array $schedules * @return array */ public function cron_time_intervals( $schedules ) { // get main instance $pvc = Post_Views_Counter(); $schedules['post_views_counter_interval'] = [ 'interval' => DAY_IN_SECONDS, 'display' => __( 'Post Views Counter reset daily counts interval', 'post-views-counter' ) ]; return $schedules; } /** * Check whether WP Cron needs to add new task. * * @return void */ public function check_cron() { // only for backend if ( ! is_admin() ) return; // get main instance $pvc = Post_Views_Counter(); // set wp cron task if ( $pvc->options['general']['cron_run'] ) { // not set or need to be updated? if ( ! wp_next_scheduled( 'pvc_reset_counts' ) || $pvc->options['general']['cron_update'] ) { // task is added but need to be updated if ( $pvc->options['general']['cron_update'] ) { // remove old schedule wp_clear_scheduled_hook( 'pvc_reset_counts' ); // set update to false $general = $pvc->options['general']; $general['cron_update'] = false; // update settings update_option( 'post_views_counter_settings_general', $general ); } // set schedule wp_schedule_event( current_time( 'timestamp', true ) + DAY_IN_SECONDS, 'post_views_counter_interval', 'pvc_reset_counts' ); } } else { // remove schedule wp_clear_scheduled_hook( 'pvc_reset_counts' ); remove_action( 'pvc_reset_counts', [ $this, 'reset_counts' ] ); } } } includes/class-settings.php000064400000140623147206624130012041 0ustar00'; // submenu referer $submenu = ''; if ( $pvc->options['other']['menu_position'] === 'sub' ) echo $topmenu . $submenu; else echo $submenu . $topmenu; } /** * Display settings sidebar. * * @return void */ public function settings_sidebar() { // get main instance $pvc = Post_Views_Counter(); $license_data = get_option( 'post_views_counter_pro_license', [] ); $is_pro = class_exists( 'Post_Views_Counter_Pro' ); if ( ! $is_pro ) { echo '

' . esc_html__( "You're using", 'post-views-counter' ) . '

Post Views Counter

Lite

' . __( 'Get more accurate information about the number of views of your site, regardless of what the user is visiting.', 'post-views-counter' ) . '

' . __( 'Unlock optimization features and speed up view count tracking.', 'post-views-counter' ) . '

' . __( 'Take your insights to the next level with dedicated, customizable reporting.', 'post-views-counter' ) . '

'; } } /** * Update counter mode. * * @return void */ public function update_counter_mode() { // get main instance $pvc = Post_Views_Counter(); // get settings $settings = $pvc->settings_api->get_settings(); // fast ajax as active but not available counter mode? if ( $pvc->options['general']['counter_mode'] === 'ajax' && in_array( 'ajax', $settings['post-views-counter']['fields']['counter_mode']['disabled'], true ) ) { // set standard javascript ajax calls $pvc->options['general']['counter_mode'] = 'js'; // update database options update_option( 'post_views_counter_settings_general', $pvc->options['general'] ); } } /** * Get available counter modes. * * @return array */ public function get_counter_modes() { // counter modes $modes = [ 'php' => __( 'PHP', 'post-views-counter' ), 'js' => __( 'JavaScript', 'post-views-counter' ), 'rest_api' => __( 'REST API', 'post-views-counter' ), 'ajax' => __( 'Fast AJAX', 'post-views-counter' ) ]; return apply_filters( 'pvc_get_counter_modes', $modes ); } /** * Add settings data. * * @param array $settings * @return array */ public function settings_data( $settings ) { // get main instance $pvc = Post_Views_Counter(); // time types $time_types = [ 'minutes' => __( 'minutes', 'post-views-counter' ), 'hours' => __( 'hours', 'post-views-counter' ), 'days' => __( 'days', 'post-views-counter' ), 'weeks' => __( 'weeks', 'post-views-counter' ), 'months' => __( 'months', 'post-views-counter' ), 'years' => __( 'years', 'post-views-counter' ) ]; // user groups $groups = [ 'robots' => __( 'robots', 'post-views-counter' ), 'users' => __( 'logged in users', 'post-views-counter' ), 'guests' => __( 'guests', 'post-views-counter' ), 'roles' => __( 'selected user roles', 'post-views-counter' ) ]; // get user roles $user_roles = $pvc->functions->get_user_roles(); // get post types $post_types = $pvc->functions->get_post_types(); // add settings $settings['post-views-counter'] = [ 'label' => __( 'Post Views Counter Settings', 'post-views-counter' ), 'form' => [ 'reports' => [ 'buttons' => false ] ], 'option_name' => [ 'general' => 'post_views_counter_settings_general', 'display' => 'post_views_counter_settings_display', 'reports' => 'post_views_counter_settings_reports', 'other' => 'post_views_counter_settings_other' ], 'validate' => [ $this, 'validate_settings' ], 'sections' => [ 'post_views_counter_general_settings' => [ 'tab' => 'general' ], 'post_views_counter_display_settings' => [ 'tab' => 'display' ], 'post_views_counter_reports_settings' => [ 'tab' => 'reports', 'callback' => [ $this, 'section_reports_placeholder' ] ], 'post_views_counter_other_settings' => [ 'tab' => 'other' ] ], 'fields' => [ 'post_types_count' => [ 'tab' => 'general', 'title' => __( 'Post Types Count', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'checkbox', 'display_type' => 'horizontal', 'description' => __( 'Select post types for which post views will be counted.', 'post-views-counter' ), 'options' => $post_types ], 'taxonomies_count' => [ 'tab' => 'general', 'title' => __( 'Taxonomies Count', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'custom', 'label' => __( 'Enable to count taxonomy terms visits.', 'post-views-counter' ), 'class' => 'pvc-pro', 'skip_saving' => true, 'callback' => [ $this, 'setting_taxonomies_count' ] ], 'users_count' => [ 'tab' => 'general', 'title' => __( 'Authors Count', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'custom', 'label' => __( 'Enable to count authors archive visits.', 'post-views-counter' ), 'class' => 'pvc-pro', 'skip_saving' => true, 'callback' => [ $this, 'setting_users_count' ] ], 'other_count' => [ 'tab' => 'general', 'title' => __( 'Other Count', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'boolean', 'label' => __( 'Enable to count visits of front page, post type and date archives, 404 and search pages.', 'post-views-counter' ), 'class' => 'pvc-pro', 'disabled' => true, 'skip_saving' => true, 'value' => false ], 'counter_mode' => [ 'tab' => 'general', 'title' => __( 'Counter Mode', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'radio', 'description' => __( 'Select the method of collecting post views data. If you are using any of the caching plugins select JavaScript, REST API or Fast AJAX (up to 10+ times faster!).', 'post-views-counter' ), 'class' => 'pvc-pro-extended', 'options' => $this->get_counter_modes(), 'disabled' => [ 'ajax' ] ], 'post_views_column' => [ 'tab' => 'general', 'title' => __( 'Post Views Column', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'boolean', 'description' => '', 'label' => __( 'Enable to display post views count column for each of the selected post types.', 'post-views-counter' ) ], 'data_storage' => [ 'tab' => 'general', 'title' => __( 'Data Storage', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'radio', 'class' => 'pvc-pro', 'skip_saving' => true, 'description' => __( "Choose how to store the content views data in the user's browser - with or without cookies.", 'post-views-counter' ), 'options' => [ 'cookies' => __( 'Cookies', 'post-views-counter' ), 'cookieless' => __( 'Cookieless', 'post-views-counter' ) ], 'disabled' => [ 'cookies', 'cookieless' ], 'value' => 'cookies' ], 'amp_support' => [ 'tab' => 'general', 'title' => __( 'AMP Support', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'boolean', 'class' => 'pvc-pro', 'disabled' => true, 'skip_saving' => true, 'value' => false, 'label' => __( 'Enable to support Google AMP.', 'post-views-counter' ), 'description' => __( 'This feature requires official WordPress Google AMP plugin to be installed and activated.', 'post-views-counter' ) ], 'restrict_edit_views' => [ 'tab' => 'general', 'title' => __( 'Restrict Edit', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'boolean', 'description' => '', 'label' => __( 'Enable to restrict post views editing to admins only.', 'post-views-counter' ) ], 'time_between_counts' => [ 'tab' => 'general', 'title' => __( 'Count Interval', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'custom', 'description' => '', 'min' => 0, 'max' => 999999, 'options' => $time_types, 'callback' => [ $this, 'setting_time_between_counts' ], 'validate' => [ $this, 'validate_time_between_counts' ] ], 'reset_counts' => [ 'tab' => 'general', 'title' => __( 'Cleanup Interval', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'custom', 'description' => sprintf( __( 'Delete single day post views data older than specified above. Enter %s if you want to preserve your daily views data regardless of its age.', 'post-views-counter' ), '0' ), 'min' => 0, 'max' => 999999, 'options' => $time_types, 'callback' => [ $this, 'setting_reset_counts' ], 'validate' => [ $this, 'validate_reset_counts' ] ], 'object_cache' => [ 'tab' => 'general', 'title' => __( 'Object Cache Support', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'boolean', 'class' => 'pvc-pro', 'disabled' => true, 'value' => false, 'skip_saving' => true, 'label' => sprintf( __( 'Enable to use object cache optimization.', 'post-views-counter' ), 'Redis', 'Memcached' ), 'description' => sprintf( __( 'This feature requires a persistent object cache like %s or %s to be installed and activated.', 'post-views-counter' ), 'Redis', 'Memcached' ) ], 'exclude' => [ 'tab' => 'general', 'title' => __( 'Exclude Visitors', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'custom', 'description' => '', 'options' => [ 'groups' => $groups, 'roles' => $user_roles ], 'callback' => [ $this, 'setting_exclude' ], 'validate' => [ $this, 'validate_exclude' ] ], 'exclude_ips' => [ 'tab' => 'general', 'title' => __( 'Exclude IPs', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'custom', 'description' => '', 'callback' => [ $this, 'setting_exclude_ips' ], 'validate' => [ $this, 'validate_exclude_ips' ] ], 'strict_counts' => [ 'tab' => 'general', 'title' => __( 'Strict counts', 'post-views-counter' ), 'section' => 'post_views_counter_general_settings', 'type' => 'boolean', 'class' => 'pvc-pro', 'disabled' => true, 'skip_saving' => true, 'value' => false, 'description' => '', 'label' => __( 'Enable to prevent bypassing the counts interval (for e.g. using incognito browser window or by clearing cookies).', 'post-views-counter' ) ], 'label' => [ 'tab' => 'display', 'title' => __( 'Views Label', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'input', 'description' => __( 'Enter the label for the post views counter field.', 'post-views-counter' ), 'subclass' => 'regular-text', 'validate' => [ $this, 'validate_label' ], 'reset' => [ $this, 'reset_label' ] ], 'display_period' => [ 'tab' => 'display', 'title' => __( 'Views Period', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'select', 'class' => 'pvc-pro', 'disabled' => true, 'skip_saving' => true, 'description' => __( 'Select the time period to be included when displaying the number of views. The default display is the total number of views of the post.', 'post-views-counter' ), 'options' => [ 'total' => __( 'Total Views', 'post-views-counter' ) ] ], 'display_style' => [ 'tab' => 'display', 'title' => __( 'Display Style', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'custom', 'description' => __( 'Choose how to display the post views counter.', 'post-views-counter' ), 'callback' => [ $this, 'setting_display_style' ], 'validate' => [ $this, 'validate_display_style' ], 'options' => [ 'icon' => __( 'icon', 'post-views-counter' ), 'text' => __( 'label', 'post-views-counter' ) ] ], 'icon_class' => [ 'tab' => 'display', 'title' => __( 'Icon Class', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'class', 'default' => '', 'description' => sprintf( __( 'Enter the post views icon class. Any of the Dashicons classes are available.', 'post-views-counter' ), 'https://developer.wordpress.org/resource/dashicons/' ), 'subclass' => 'regular-text' ], 'position' => [ 'tab' => 'display', 'title' => __( 'Position', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'select', 'description' => sprintf( __( 'Select where would you like to display the post views counter. Use %s shortcode for manual display.', 'post-views-counter' ), '[post-views]' ), 'options' => [ 'before' => __( 'before the content', 'post-views-counter' ), 'after' => __( 'after the content', 'post-views-counter' ), 'manual' => __( 'manual', 'post-views-counter' ) ] ], 'dynamic_loading' => [ 'tab' => 'display', 'title' => __( 'Dynamic Loading', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'boolean', 'class' => 'pvc-pro', 'label' => __( 'Enable dynamic loading and prevent caching of the displayed views count.', 'post-views-counter' ) ], 'use_format' => [ 'tab' => 'display', 'title' => __( 'Format Number', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'boolean', 'label' => __( 'Enable to display the views number formatted based on the locale (using the WP number_format_i18n function).', 'post-views-counter' ) ], 'taxonomies_display' => [ 'tab' => 'display', 'title' => __( 'Taxonomies', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'custom', 'class' => 'pvc-pro', 'skip_saving' => true, 'options' => $pvc->functions->get_taxonomies( 'labels' ), 'callback' => [ $this, 'setting_taxonomies_display' ] ], 'user_display' => [ 'tab' => 'display', 'title' => __( 'Authors', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'boolean', 'class' => 'pvc-pro', 'disabled' => true, 'skip_saving' => true, 'value' => false, 'label' => __( 'Display number of views on authors archive pages.', 'post-views-counter' ) ], 'post_types_display' => [ 'tab' => 'display', 'title' => __( 'Post Type', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'checkbox', 'display_type' => 'horizontal', 'description' => __( 'Select post types for which the views count will be displayed.', 'post-views-counter' ), 'options' => $post_types ], 'page_types_display' => [ 'tab' => 'display', 'title' => __( 'Page Type', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'checkbox', 'display_type' => 'horizontal', 'description' => __( 'Select page types where the views count will be displayed.', 'post-views-counter' ), 'options' => apply_filters( 'pvc_page_types_display_options', [ 'home' => __( 'Home', 'post-views-counter' ), 'archive' => __( 'Archives', 'post-views-counter' ), 'singular' => __( 'Single pages', 'post-views-counter' ), 'search' => __( 'Search results', 'post-views-counter' ), ] ) ], 'restrict_display' => [ 'tab' => 'display', 'title' => __( 'User Type', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'custom', 'description' => '', 'options' => [ 'groups' => $groups, 'roles' => $user_roles ], 'callback' => [ $this, 'setting_restrict_display' ], 'validate' => [ $this, 'validate_restrict_display' ] ], 'toolbar_statistics' => [ 'tab' => 'display', 'title' => __( 'Toolbar Chart', 'post-views-counter' ), 'section' => 'post_views_counter_display_settings', 'type' => 'boolean', 'description' => __( 'The post views chart will be displayed for the post types that are being counted.', 'post-views-counter' ), 'label' => __( 'Enable to display the post views chart at the toolbar.', 'post-views-counter' ) ], 'license' => [ 'tab' => 'other', 'title' => __( 'License', 'post-views-counter' ), 'section' => 'post_views_counter_other_settings', 'disabled' => true, 'value' => $pvc->options['other']['license'], 'type' => 'input', 'description' => __( 'Enter your Post Views Counter Pro license key (requires Pro version to be installed and active).', 'post-views-counter' ), 'subclass' => 'regular-text', 'validate' => [ $this, 'validate_license' ], 'append' => '' ], 'menu_position' => [ 'tab' => 'other', 'title' => __( 'Menu Position', 'post-views-counter' ), 'section' => 'post_views_counter_other_settings', 'type' => 'radio', 'options' => [ 'top' => __( 'Top menu', 'post-views-counter' ), 'sub' => __( 'Settings submenu', 'post-views-counter' ) ], 'description' => __( "Choose where to display the plugin's menu.", 'post-views-counter' ), ], 'import_views' => [ 'tab' => 'other', 'title' => __( 'Import Views', 'post-views-counter' ), 'section' => 'post_views_counter_other_settings', 'type' => 'custom', 'description' => '', 'skip_saving' => true, 'callback' => [ $this, 'setting_import_views' ] ], 'delete_views' => [ 'tab' => 'other', 'title' => __( 'Delete Views', 'post-views-counter' ), 'section' => 'post_views_counter_other_settings', 'type' => 'custom', 'description' => '', 'skip_saving' => true, 'callback' => [ $this, 'setting_delete_views' ] ], 'deactivation_delete' => [ 'tab' => 'other', 'title' => __( 'Deactivation', 'post-views-counter' ), 'section' => 'post_views_counter_other_settings', 'type' => 'boolean', 'description' => __( 'If you deactivate the plugin with this option enabled all plugin data will be deleted along with the number of post views.', 'post-views-counter' ), 'label' => __( 'Enable to delete all plugin data on deactivation.', 'post-views-counter' ) ] ] ]; return $settings; } /** * Add settings page. * * @param array $pages * @return array */ public function settings_page( $pages ) { // get main instance $pvc = Post_Views_Counter(); // default page $pages['post-views-counter'] = [ 'menu_slug' => 'post-views-counter', 'page_title' => __( 'Post Views Counter Settings', 'post-views-counter' ), 'menu_title' => $pvc->options['other']['menu_position'] === 'sub' ? __( 'Post Views Counter', 'post-views-counter' ) : __( 'Post Views', 'post-views-counter' ), 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ), 'callback' => null, 'tabs' => [ 'general' => [ 'label' => __( 'General', 'post-views-counter' ), 'option_name' => 'post_views_counter_settings_general' ], 'display' => [ 'label' => __( 'Display', 'post-views-counter' ), 'option_name' => 'post_views_counter_settings_display' ], 'reports' => [ 'label' => __( 'Reports', 'post-views-counter' ), 'option_name' => 'post_views_counter_settings_reports' ], 'other' => [ 'label' => __( 'Other', 'post-views-counter' ), 'option_name' => 'post_views_counter_settings_other' ] ] ]; // submenu? if ( $pvc->options['other']['menu_position'] === 'sub' ) { $pages['post-views-counter']['type'] = 'settings_page'; // topmenu? } else { // highlight submenus add_filter( 'submenu_file', [ $this, 'submenu_file' ], 10, 2 ); // add parameters $pages['post-views-counter']['type'] = 'page'; $pages['post-views-counter']['icon'] = 'dashicons-chart-bar'; $pages['post-views-counter']['position'] = '99.301'; // add subpages $pages['post-views-counter-general'] = [ 'menu_slug' => 'post-views-counter', 'parent_slug' => 'post-views-counter', 'type' => 'subpage', 'page_title' => __( 'General', 'post-views-counter' ), 'menu_title' => __( 'General', 'post-views-counter' ), 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ), 'callback' => null ]; $pages['post-views-counter-display'] = [ 'menu_slug' => 'post-views-counter&tab=display', 'parent_slug' => 'post-views-counter', 'type' => 'subpage', 'page_title' => __( 'Display', 'post-views-counter' ), 'menu_title' => __( 'Display', 'post-views-counter' ), 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ), 'callback' => null ]; $pages['post-views-counter-reports'] = [ 'menu_slug' => 'post-views-counter&tab=reports', 'parent_slug' => 'post-views-counter', 'type' => 'subpage', 'page_title' => __( 'Reports', 'post-views-counter' ), 'menu_title' => __( 'Reports', 'post-views-counter' ), 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ), 'callback' => null ]; $pages['post-views-counter-other'] = [ 'menu_slug' => 'post-views-counter&tab=other', 'parent_slug' => 'post-views-counter', 'type' => 'subpage', 'page_title' => __( 'Other', 'post-views-counter' ), 'menu_title' => __( 'Other', 'post-views-counter' ), 'capability' => apply_filters( 'pvc_settings_capability', 'manage_options' ), 'callback' => null ]; } return $pages; } /** * Settings page CSS class(es). * * @param array $class * @return array */ public function settings_page_class( $class ) { $is_pro = class_exists( 'Post_Views_Counter_Pro' ); if ( ! $is_pro ) $class[] = 'has-sidebar'; return $class; } /** * Highlight submenu items. * * @param string|null $submenu_file * @param string $parent_file * @return string|null */ public function submenu_file( $submenu_file, $parent_file ) { if ( $parent_file === 'post-views-counter' ) { $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general'; if ( $tab !== 'general' ) return 'post-views-counter&tab=' . $tab; } return $submenu_file; } /** * Validate options. * * @global object $wpdb * * @param array $input * @return array */ public function validate_settings( $input ) { // check capability if ( ! current_user_can( 'manage_options' ) ) return $input; global $wpdb; // get main instance $pvc = Post_Views_Counter(); // use internal settings api to validate settings first $input = $pvc->settings_api->validate_settings( $input ); // update meta key on save changes if ( isset( $_POST['post_views_counter_import_meta_key'] ) && isset( $_POST['save_post_views_counter_settings_other'] ) ) $input['import_meta_key'] = sanitize_key( $_POST['post_views_counter_import_meta_key'] ); // import post views data from another plugin if ( isset( $_POST['post_views_counter_import_wp_postviews'] ) ) { // make sure we do not change anything in the settings $input = $pvc->options['other']; // get views key $meta_key = sanitize_key( apply_filters( 'pvc_import_meta_key', ( isset( $_POST['post_views_counter_import_meta_key'] ) ? $_POST['post_views_counter_import_meta_key'] : $pvc->options['other']['import_meta_key'] ) ) ); // set meta_key option $input['import_meta_key'] = $meta_key; // get views $views = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM " . $wpdb->postmeta . " WHERE meta_key = %s", $meta_key ), ARRAY_A, 0 ); // any views? if ( ! empty( $views ) ) { $sql = []; foreach ( $views as $view ) { $sql[] = $wpdb->prepare( "(%d, 4, 'total', %d)", (int) $view['post_id'], (int) $view['meta_value'] ); } $wpdb->query( "INSERT INTO " . $wpdb->prefix . "post_views(id, type, period, count) VALUES " . implode( ',', $sql ) . " ON DUPLICATE KEY UPDATE count = " . ( isset( $_POST['post_views_counter_import_wp_postviews_override'] ) ? '' : 'count + ' ) . "VALUES(count)" ); add_settings_error( 'wp_postviews_import', 'wp_postviews_import', __( 'Post views data imported successfully.', 'post-views-counter' ), 'updated' ); } else add_settings_error( 'wp_postviews_import', 'wp_postviews_import', __( 'There was no post views data to import.', 'post-views-counter' ), 'updated' ); // delete all post views data } elseif ( isset( $_POST['post_views_counter_reset_views'] ) ) { // make sure we do not change anything in the settings $input = $pvc->options['other']; if ( $wpdb->query( 'TRUNCATE TABLE ' . $wpdb->prefix . 'post_views' ) ) add_settings_error( 'reset_post_views', 'reset_post_views', __( 'All existing data deleted successfully.', 'post-views-counter' ), 'updated' ); else add_settings_error( 'reset_post_views', 'reset_post_views', __( 'Error occurred. All existing data were not deleted.', 'post-views-counter' ), 'error' ); // save general settings } elseif ( isset( $_POST['save_post_views_counter_settings_general'] ) ) { $input['update_version'] = $pvc->options['general']['update_version']; $input['update_notice'] = $pvc->options['general']['update_notice']; $input['update_delay_date'] = $pvc->options['general']['update_delay_date']; // reset general settings } elseif ( isset( $_POST['reset_post_views_counter_settings_general'] ) ) { $input['update_version'] = $pvc->options['general']['update_version']; $input['update_notice'] = $pvc->options['general']['update_notice']; $input['update_delay_date'] = $pvc->options['general']['update_delay_date']; } return $input; } /** * Setting: taxonomies count. * * @param array $field * @return string */ public function setting_taxonomies_count( $field ) { $html = ' '; return $html; } /** * Setting: users count. * * @param array $field * @return string */ public function setting_users_count( $field ) { // get base instance $pvc = Post_Views_Counter(); $html = ' '; return $html; } /** * Setting: taxonomies count. * * @param array $field * @return string */ public function setting_taxonomies_display( $field ) { // get base instance $pvc = Post_Views_Counter(); $html = ''; foreach ( $field['options'] as $taxonomy => $label ) { $html .= ' '; } $html .= '

' . esc_html__( 'Select taxonomies for which the views count will be displayed.', 'post-views-counter' ) . '

'; return $html; } /** * Validate label. * * @param array $input * @param array $field * @return array */ public function validate_label( $input, $field ) { // get main instance $pvc = Post_Views_Counter(); if ( ! isset( $input ) ) $input = $pvc->defaults['display']['label']; // use internal settings API to validate settings first $input = $pvc->settings_api->validate_field( $input, 'input', $field ); if ( function_exists( 'icl_register_string' ) ) icl_register_string( 'Post Views Counter', 'Post Views Label', $input ); return $input; } /** * Restore post views label to default value. * * @param array $default * @param array $field * @return array */ public function reset_label( $default, $field ) { if ( function_exists( 'icl_register_string' ) ) icl_register_string( 'Post Views Counter', 'Post Views Label', $default ); return $default; } /** * Setting: display style. * * @param array $field * @return string */ public function setting_display_style( $field ) { // get main instance $pvc = Post_Views_Counter(); $html = ' '; foreach ( $field['options'] as $key => $label ) { $html .= ' '; } return $html; } /** * Validate display style. * * @param array $input * @param array $field * @return array */ public function validate_display_style( $input, $field ) { // get main instance $pvc = Post_Views_Counter(); $data = []; foreach ( $field['options'] as $value => $label ) { $data[$value] = false; } // any data? if ( ! empty( $input['display_style'] && $input['display_style'] !== 'empty' && is_array( $input['display_style'] ) ) ) { foreach ( $input['display_style'] as $value ) { if ( array_key_exists( $value, $field['options'] ) ) $data[$value] = true; } } $input['display_style'] = $data; return $input; } /** * Setting: count interval. * * @param array $field * @return string */ public function setting_time_between_counts( $field ) { // get main instance $pvc = Post_Views_Counter(); $html = '

' . sprintf( __( 'Enter the time between single user visit count. Enter %s if you want to count every page view.', 'post-views-counter' ), '0' ) . '

'; return $html; } /** * Validate count interval. * * @param array $input * @param array $field * @return array */ public function validate_time_between_counts( $input, $field ) { // get main instance $pvc = Post_Views_Counter(); // number $input['time_between_counts']['number'] = isset( $input['time_between_counts']['number'] ) ? (int) $input['time_between_counts']['number'] : $pvc->defaults['general']['time_between_counts']['number']; if ( $input['time_between_counts']['number'] < $field['min'] || $input['time_between_counts']['number'] > $field['max'] ) $input['time_between_counts']['number'] = $pvc->defaults['general']['time_between_counts']['number']; // type $input['time_between_counts']['type'] = isset( $input['time_between_counts']['type'], $field['options'][$input['time_between_counts']['type']] ) ? $input['time_between_counts']['type'] : $pvc->defaults['general']['time_between_counts']['type']; return $input; } /** * Setting: reset data interval. * * @param array $field * @return string */ public function setting_reset_counts( $field ) { // get main instance $pvc = Post_Views_Counter(); $html = ' '; return $html; } /** * Validate reset data interval. * * @param array $input * @param array $field * @return array */ public function validate_reset_counts( $input, $field ) { // get main instance $pvc = Post_Views_Counter(); // number $input['reset_counts']['number'] = isset( $input['reset_counts']['number'] ) ? (int) $input['reset_counts']['number'] : $pvc->defaults['general']['reset_counts']['number']; if ( $input['reset_counts']['number'] < $field['min'] || $input['reset_counts']['number'] > $field['max'] ) $input['reset_counts']['number'] = $pvc->defaults['general']['reset_counts']['number']; // type $input['reset_counts']['type'] = isset( $input['reset_counts']['type'], $field['options'][$input['reset_counts']['type']] ) ? $input['reset_counts']['type'] : $pvc->defaults['general']['reset_counts']['type']; // run cron on next visit? $input['cron_run'] = ( $input['reset_counts']['number'] > 0 ); // cron update? $input['cron_update'] = ( $input['cron_run'] && ( $pvc->options['general']['reset_counts']['number'] !== $input['reset_counts']['number'] || $pvc->options['general']['reset_counts']['type'] !== $input['reset_counts']['type'] ) ); return $input; } /** * Setting: object cache. * * @param array $field * @return string */ public function setting_object_cache( $field ) { // get main instance $pvc = Post_Views_Counter(); // check object cache $wp_using_ext_object_cache = wp_using_ext_object_cache(); $html = ' ' . __( 'minutes', 'post-views-counter' ) . '

' . __( 'Persistent Object Cache', 'post-views-counter' ) . ': ' . ( $wp_using_ext_object_cache ? __( 'available', 'post-views-counter' ) : __( 'unavailable', 'post-views-counter' ) ) . '

' . sprintf( __( 'How often to flush cached view counts from the object cache into the database. This feature is used only if a persistent object cache like %s or %s is detected and the interval is greater than %s. When used, view counts will be collected and stored in the object cache instead of the database and will then be asynchronously flushed to the database according to the specified interval. The maximum value is %s which means 24 hours.%sNotice:%s Potential data loss may occur if the object cache is cleared/unavailable for the duration of the interval.', 'post-views-counter' ), 'Redis', 'Memcached', '0', '1440', '
', '' ) . '

'; return $html; } /** * Setting: exclude visitors. * * @param array $field * @return string */ public function setting_exclude( $field ) { // get main instance $pvc = Post_Views_Counter(); $html = ''; foreach ( $field['options']['groups'] as $type => $type_name ) { $html .= ' '; } $html .= '

' . __( 'Use it exclude specific user groups from post views count.', 'post-views-counter' ) . '

options['general']['exclude']['groups'], true ) ? '' : ' style="display: none;"' ) . '>'; foreach ( $field['options']['roles'] as $role => $role_name ) { $html .= ' '; } $html .= '

' . __( 'Use it to exclude specific user roles from post views count.', 'post-views-counter' ) . '

'; return $html; } /** * Validate exclude visitors. * * @param array $input * @param array $field * @return array */ public function validate_exclude( $input, $field ) { // any groups? if ( isset( $input['exclude']['groups'] ) ) { $groups = []; foreach ( $input['exclude']['groups'] as $group => $set ) { if ( isset( $field['options']['groups'][$group] ) ) $groups[] = $group; } $input['exclude']['groups'] = array_unique( $groups ); } else $input['exclude']['groups'] = []; // any roles? if ( in_array( 'roles', $input['exclude']['groups'], true ) && isset( $input['exclude']['roles'] ) ) { $roles = []; foreach ( $input['exclude']['roles'] as $role => $set ) { if ( isset( $field['options']['roles'][$role] ) ) $roles[] = $role; } $input['exclude']['roles'] = array_unique( $roles ); } else $input['exclude']['roles'] = []; return $input; } /** * Setting: exclude IP addresses. * * @return string */ public function setting_exclude_ips() { // get ip addresses $ips = Post_Views_Counter()->options['general']['exclude_ips']; $html = ''; // any ip addresses? if ( ! empty( $ips ) ) { foreach ( $ips as $key => $ip ) { $html .= ' '; } } else { $html .= ' '; } $html .= '

' . esc_html__( 'Enter the IP addresses to be excluded from post views count.', 'post-views-counter' ) . '

'; return $html; } /** * Validate exclude IP addresses. * * @param array $input * @param array $field * @return array */ public function validate_exclude_ips( $input, $field ) { // any ip addresses? if ( isset( $input['exclude_ips'] ) ) { $ips = []; foreach ( $input['exclude_ips'] as $ip ) { if ( strpos( $ip, '*' ) !== false ) { $new_ip = str_replace( '*', '0', $ip ); if ( filter_var( $new_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) $ips[] = $ip; } elseif ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) $ips[] = $ip; } $input['exclude_ips'] = array_unique( $ips ); } return $input; } /** * Setting: tools. * * @return string */ public function setting_import_views() { $html = '

' . esc_html__( 'Enter the meta key from which the views data is to be retrieved during import.', 'post-views-counter' ) . '

' . esc_html__( 'Click Import Views to start importing the views data.', 'post-views-counter' ) . '

'; return $html; } /** * Setting: delete views. * * @return string */ public function setting_delete_views() { $html = '

' . esc_html__( 'Delete ALL the existing post views data. Note that this is an irreversible process!', 'post-views-counter' ) . '

'; return $html; } /** * Setting: user type. * * @param array $field * @return string */ public function setting_restrict_display( $field ) { // get main instance $pvc = Post_Views_Counter(); $html = ''; foreach ( $field['options']['groups'] as $type => $type_name ) { if ( $type === 'robots' ) continue; $html .= ' '; } $html .= '

' . __( 'Use it to hide the post views counter from selected type of visitors.', 'post-views-counter' ) . '

options['display']['restrict_display']['groups'], true ) ? '' : ' style="display: none;"' ) . '>'; foreach ( $field['options']['roles'] as $role => $role_name ) { $html .= ' '; } $html .= '

' . __( 'Use it to hide the post views counter from selected user roles.', 'post-views-counter' ) . '

'; return $html; } /** * Validate user type. * * @param array $input * @param array $field * @return array */ public function validate_restrict_display( $input, $field ) { // any groups? if ( isset( $input['restrict_display']['groups'] ) ) { $groups = []; foreach ( $input['restrict_display']['groups'] as $group => $set ) { if ( $group === 'robots' ) continue; if ( isset( $field['options']['groups'][$group] ) ) $groups[] = $group; } $input['restrict_display']['groups'] = array_unique( $groups ); } else $input['restrict_display']['groups'] = []; // any roles? if ( in_array( 'roles', $input['restrict_display']['groups'], true ) && isset( $input['restrict_display']['roles'] ) ) { $roles = []; foreach ( $input['restrict_display']['roles'] as $role => $set ) { if ( isset( $field['options']['roles'][$role] ) ) $roles[] = $role; } $input['restrict_display']['roles'] = array_unique( $roles ); } else $input['restrict_display']['roles'] = []; return $input; } /** * Reports page placeholder. */ public function section_reports_placeholder() { echo '
Post Views Counter - Reports

' . esc_html__( 'Display Reports and Export Views to CSV/XML', 'post-views-counter' ) . '

' . esc_html__( 'View detailed stats about the popularity of your content.', 'post-views-counter' ) . '

' . esc_html__( 'Generate views reports in any date range you need.', 'post-views-counter' ) . '

' . esc_html__( 'Export, download and share your website views data.', 'post-views-counter' ) . '

' . esc_html__( 'Upgrade to Pro', 'post-views-counter' ) . '

'; } /** * Validate license. * * @param array $input * @param array $field * @return array */ public function validate_license( $input, $field ) { // save value from database return $field['value']; } } includes/class-functions.php000064400000010700147206624130012201 0ustar00 true ], 'objects', 'and' ) as $key => $post_type ) { $post_types[$key] = $post_type->labels->name; } // remove bbPress replies if ( class_exists( 'bbPress' ) && isset( $post_types['reply'] ) ) unset( $post_types['reply'] ); // filter post types $post_types = apply_filters( 'pvc_available_post_types', $post_types ); // sort post types alphabetically asort( $post_types, SORT_STRING ); return $post_types; } /** * Get all user roles. * * @global object $wp_roles * * @return array */ public function get_user_roles() { global $wp_roles; $roles = []; foreach ( apply_filters( 'editable_roles', $wp_roles->roles ) as $role => $details ) { $roles[$role] = translate_user_role( $details['name'] ); } // sort user roles alphabetically asort( $roles, SORT_STRING ); return $roles; } /** * Get taxonomies available for counting. * * @param bool $mode * @return array */ public function get_taxonomies( $mode = 'labels' ) { // get public taxonomies $taxonomies = get_taxonomies( [ 'public' => true ], $mode === 'keys' ? 'names' : 'objects', 'and' ); // only keys if ( $mode === 'keys' ) $_taxonomies = array_keys( $taxonomies ); // objects elseif ( $mode === 'objects' ) $_taxonomies = $taxonomies; // labels else { $_taxonomies = []; // prepare taxonomy labels foreach ( $taxonomies as $name => $taxonomy ) { $_taxonomies[$name] = $taxonomy->label; } } return $_taxonomies; } /** * Get color scheme. * * @global array $_wp_admin_css_colors * * @return string */ public function get_current_scheme_color( $default_color = '' ) { // get color scheme global global $_wp_admin_css_colors; // set default color; $color = '#2271b1'; if ( ! empty( $_wp_admin_css_colors ) ) { // get current admin color scheme name $current_color_scheme = get_user_option( 'admin_color' ); if ( empty( $current_color_scheme ) ) $current_color_scheme = 'fresh'; $wp_scheme_colors = [ 'coffee' => 2, 'ectoplasm' => 2, 'ocean' => 2, 'sunrise' => 2, 'midnight' => 3, 'blue' => 3, 'modern' => 1, 'light' => 1, 'fresh' => 2 ]; // one of default wp schemes? if ( array_key_exists( $current_color_scheme, $wp_scheme_colors ) ) { $color_number = $wp_scheme_colors[$current_color_scheme]; // color exists? if ( isset( $_wp_admin_css_colors[$current_color_scheme] ) && property_exists( $_wp_admin_css_colors[$current_color_scheme], 'colors' ) && isset( $_wp_admin_css_colors[$current_color_scheme]->colors[$color_number] ) ) $color = $_wp_admin_css_colors[$current_color_scheme]->colors[$color_number]; } } return $color; } /** * Convert HEX to RGB color. * * @param string $color * @return bool|array */ public function hex2rgb( $color ) { if ( ! is_string( $color ) ) return false; // with hash? if ( $color[0] === '#' ) $color = substr( $color, 1 ); if ( sanitize_hex_color_no_hash( $color ) !== $color ) return false; // 6 hex digits? if ( strlen( $color ) === 6 ) list( $r, $g, $b ) = [ $color[0] . $color[1], $color[2] . $color[3], $color[4] . $color[5] ]; // 3 hex digits? elseif ( strlen( $color ) === 3 ) list( $r, $g, $b ) = [ $color[0] . $color[0], $color[1] . $color[1], $color[2] . $color[2] ]; else return false; return [ 'r' => hexdec( $r ), 'g' => hexdec( $g ), 'b' => hexdec( $b ) ]; } /** * Get default color. * * @return array */ public function get_colors() { // get current color scheme $color = $this->get_current_scheme_color(); // convert it to rgb $color = $this->hex2rgb( $color ); // invalid color? if ( $color === false ) { // set default color $color = [ 'r' => 34, 'g' => 113, 'b' => 177 ]; } return $color; } } includes/class-frontend.php000064400000017520147206624130012017 0ustar00 get_the_ID(), 'type' => 'post' ]; // main item? if ( ! in_the_loop() ) { // get current object $object = get_queried_object(); // post? if ( is_a( $object, 'WP_Post' ) ) { $defaults['id'] = $object->ID; $defaults['type'] = 'post'; // term? } elseif ( is_a( $object, 'WP_Term' ) ) { $defaults['id'] = $object->term_id; $defaults['type'] = 'term'; // user? } elseif ( is_a( $object, 'WP_User' ) ) { $defaults['id'] = $object->ID; $defaults['type'] = 'user'; } } // combine attributes $args = shortcode_atts( $defaults, $args ); // default type? if ( $args['type'] === 'post' ) { $views = pvc_post_views( $args['id'], false ); } return apply_filters( 'pvc_post_views_shortcode', $views, $args ); } /** * Display number of post views. * * @return void */ public function run() { if ( is_admin() && ! wp_doing_ajax() ) return; $filter = apply_filters( 'pvc_shortcode_filter_hook', Post_Views_Counter()->options['display']['position'] ); // valid filter? if ( ! empty( $filter ) && in_array( $filter, [ 'before', 'after' ] ) ) { // post content add_filter( 'the_content', [ $this, 'add_post_views_count' ] ); // bbpress support add_action( 'bbp_template_' . $filter . '_single_topic', [ $this, 'display_bbpress_post_views' ] ); add_action( 'bbp_template_' . $filter . '_single_forum', [ $this, 'display_bbpress_post_views' ] ); // custom } elseif ( $filter !== 'manual' && is_string( $filter ) ) add_filter( $filter, [ $this, 'add_post_views_count' ] ); } /** * Add post views counter to forum/topic of bbPress. * * @return void */ public function display_bbpress_post_views() { $post_id = get_the_ID(); // check only for forums and topics if ( bbp_is_forum( $post_id ) || bbp_is_topic( $post_id ) ) echo $this->add_post_views_count( '' ); } /** * Add post views counter to content. * * @param string $content * @return string */ public function add_post_views_count( $content = '' ) { // get main instance $pvc = Post_Views_Counter(); $display = false; // post type check if ( ! empty( $pvc->options['display']['post_types_display'] ) ) $display = is_singular( $pvc->options['display']['post_types_display'] ); // page visibility check if ( ! empty( $pvc->options['display']['page_types_display'] ) ) { foreach ( $pvc->options['display']['page_types_display'] as $page ) { switch ( $page ) { case 'singular': if ( is_singular( $pvc->options['display']['post_types_display'] ) ) $display = true; break; case 'archive': if ( is_archive() ) $display = true; break; case 'search': if ( is_search() ) $display = true; break; case 'home': if ( is_home() || is_front_page() ) $display = true; break; } } } // get groups to check it faster $groups = $pvc->options['display']['restrict_display']['groups']; // whether to display views if ( is_user_logged_in() ) { // exclude logged in users? if ( in_array( 'users', $groups, true ) ) $display = false; // exclude specific roles? elseif ( in_array( 'roles', $groups, true ) && $pvc->counter->is_user_role_excluded( get_current_user_id(), $pvc->options['display']['restrict_display']['roles'] ) ) $display = false; // exclude guests? } elseif ( in_array( 'guests', $groups, true ) ) $display = false; // we don't want to mess custom loops if ( ! in_the_loop() && ! class_exists( 'bbPress' ) ) $display = false; if ( (bool) apply_filters( 'pvc_display_views_count', $display ) === true ) { $filter = apply_filters( 'pvc_shortcode_filter_hook', $pvc->options['display']['position'] ); switch ( $filter ) { case 'after': $content = $content . do_shortcode( '[post-views]' ); break; case 'before': $content = do_shortcode( '[post-views]' ) . $content; break; case 'manual': default: break; } } return $content; } /** * Get frontend script arguments. * * @return array */ public function get_frontend_script_args() { return $this->script_args; } /** * Enqueue frontend scripts and styles. * * @return void */ public function wp_enqueue_scripts() { // get main instance $pvc = Post_Views_Counter(); // enable styles? if ( (bool) apply_filters( 'pvc_enqueue_styles', true ) === true ) { // load dashicons wp_enqueue_style( 'dashicons' ); // load style wp_enqueue_style( 'post-views-counter-frontend', POST_VIEWS_COUNTER_URL . '/css/frontend' . ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.min' : '' ) . '.css', [], $pvc->defaults['version'] ); } // skip special requests if ( is_preview() || is_feed() || is_trackback() || ( function_exists( 'is_favicon' ) && is_favicon() ) || is_customize_preview() ) return; // get countable post types $post_types = $pvc->options['general']['post_types_count']; // whether to count this post type or not if ( empty( $post_types ) || ! is_singular( $post_types ) ) return; // get current post id $post_id = (int) get_the_ID(); // allow to run check post? if ( ! (bool) apply_filters( 'pvc_run_check_post', true, $post_id ) ) return; // get counter mode $mode = $pvc->options['general']['counter_mode']; // specific counter mode? if ( in_array( $mode, [ 'js', 'rest_api' ], true ) ) { wp_enqueue_script( 'post-views-counter-frontend', POST_VIEWS_COUNTER_URL . '/js/frontend' . ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.min' : '' ) . '.js', [], $pvc->defaults['version'], false ); // prepare args $args = [ 'mode' => $mode, 'postID' => $post_id, 'requestURL' => '', 'nonce' => '', 'dataStorage' => $pvc->options['general']['data_storage'], 'multisite' => ( is_multisite() ? (int) get_current_blog_id() : false ), 'path' => empty( COOKIEPATH ) || ! is_string( COOKIEPATH ) ? '/' : COOKIEPATH, 'domain' => empty( COOKIE_DOMAIN ) || ! is_string( COOKIE_DOMAIN ) ? '' : COOKIE_DOMAIN ]; switch ( $mode ) { // rest api case 'rest_api': $args['requestURL'] = rest_url( 'post-views-counter/view-post/' . $args['postID'] ); $args['nonce'] = wp_create_nonce( 'wp_rest' ); break; // javascript case 'js': default: $args['requestURL'] = admin_url( 'admin-ajax.php' ); $args['nonce'] = wp_create_nonce( 'pvc-check-post' ); } // make it safe $args['requestURL'] = esc_url_raw( $args['requestURL'] ); // set script args $this->script_args = apply_filters( 'pvc_frontend_script_args', $args, 'standard' ); wp_add_inline_script( 'post-views-counter-frontend', 'var pvcArgsFrontend = ' . wp_json_encode( $this->script_args ) . ";\n", 'before' ); } } } includes/class-admin.php000064400000012647147206624130011275 0ustar00 [ 'POST' ], 'callback' => [ $this, 'block_editor_update_callback' ], 'permission_callback' => [ $this, 'check_rest_route_permissions' ], 'args' => [ 'id' => [ 'sanitize_callback' => 'absint', ] ] ] ); } /** * Check whether user has permissions to perform post views update in block editor. * * @param object $request WP_REST_Request * @return bool|WP_Error */ public function check_rest_route_permissions( $request ) { // break if current user can't edit this post if ( ! current_user_can( 'edit_post', (int) $request->get_param( 'id' ) ) ) return new WP_Error( 'pvc-user-not-allowed', __( 'You are not allowed to edit this item.', 'post-views-counter' ) ); // break if views editing is restricted if ( (bool) Post_Views_Counter()->options['general']['restrict_edit_views'] === true && ! current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) return new WP_Error( 'pvc-user-not-allowed', __( 'You are not allowed to edit this item.', 'post-views-counter' ) ); return true; } /** * REST API callback for block editor endpoint. * * @param array $data * @return string|int */ public function block_editor_update_callback( $data ) { // get main instance $pvc = Post_Views_Counter(); // cast post ID $post_id = ! empty( $data['id'] ) ? (int) $data['id'] : 0; // cast post views $post_views = ! empty( $data['post_views'] ) ? (int) $data['post_views'] : 0; // get countable post types $post_types = $pvc->options['general']['post_types_count']; // check if post exists $post = get_post( $post_id ); // whether to count this post type or not if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) ) return wp_send_json_error( __( 'Invalid post ID.', 'post-views-counter' ) ); // break if current user can't edit this post if ( ! current_user_can( 'edit_post', $post_id ) ) return wp_send_json_error( __( 'You are not allowed to edit this item.', 'post-views-counter' ) ); // break if views editing is restricted if ( (bool) $pvc->options['general']['restrict_edit_views'] === true && ! current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) return wp_send_json_error( __( 'You are not allowed to edit this item.', 'post-views-counter' ) ); // update post views pvc_update_post_views( $post_id, $post_views ); do_action( 'pvc_after_update_post_views_count', $post_id ); return $post_id; } /** * Enqueue frontend and editor JavaScript and CSS. * * @global string $pagenow * @global string $wp_version * * @return void */ public function block_editor_enqueue_scripts() { global $pagenow, $wp_version; // get main instance $pvc = Post_Views_Counter(); // skip widgets and customizer pages if ( $pagenow === 'widgets.php' || $pagenow === 'customize.php' ) return; // enqueue the bundled block JS file wp_enqueue_script( 'pvc-block-editor', POST_VIEWS_COUNTER_URL . '/js/block-editor.min.js', [ 'wp-element', 'wp-components', 'wp-edit-post', 'wp-data', 'wp-plugins' ], $pvc->defaults['version'] ); // restrict editing $restrict = (bool) $pvc->options['general']['restrict_edit_views']; // prepare script data $script_data = [ 'postID' => get_the_ID(), 'postViews' => pvc_get_post_views( get_the_ID() ), 'canEdit' => ( $restrict === false || ( $restrict === true && current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'wpGreater53' => version_compare( $wp_version, '5.3', '>=' ), 'textPostViews' => esc_html__( 'Post Views', 'post-views-counter' ), 'textHelp' => esc_html__( 'Adjust the views count for this post.', 'post-views-counter' ), 'textCancel' => esc_html__( 'Cancel', 'post-views-counter' ) ]; wp_add_inline_script( 'pvc-block-editor', 'var pvcEditorArgs = ' . wp_json_encode( $script_data ) . ";\n", 'before' ); // enqueue frontend and editor block styles wp_enqueue_style( 'pvc-block-editor', POST_VIEWS_COUNTER_URL . '/css/block-editor.min.css', '', $pvc->defaults['version'] ); } } includes/class-counter.php000064400000115057147206624130011663 0ustar00 false, 'visited_posts' => [], 'expiration' => 0 ]; /** * Class constructor. * * @return void */ public function __construct() { // actions add_action( 'plugins_loaded', [ $this, 'check_cookie' ], 1 ); add_action( 'init', [ $this, 'init_counter' ] ); add_action( 'deleted_post', [ $this, 'delete_post_views' ] ); } /** * Get storage data. * * @return array */ public function get_storage() { return $this->storage; } /** * Set storage data. Used only for additional authors counting. * * @return bool */ /* COUNT_POST_AS_AUTHOR_VIEW | removed function public function set_storage( $data, $class ) { if ( ! is_a( $class, 'Post_Views_Counter_Pro_Counter' ) ) return false; if ( ! $class->is_main_storage_allowed() ) return false; // is it active content type? if ( ! $class->is_content_type_active( 'user', 'posts' ) ) return false; if ( $this->storage_type === 'cookies' ) $this->storage = $data; else $this->storage['user'] = $data; $this->storage_modified = true; return true; } */ /** * Get storage type. * * @return array */ public function get_storage_type() { return $this->storage_type; } /** * Set storage type. Used only for fast ajax requests. * * @param string $storage_type * @return array */ public function set_storage_type( $storage_type, $class ) { // allow only from pro counter class if ( ! is_a( $class, 'Post_Views_Counter_Pro_Counter' ) ) return false; // allow only fast ajax requests if ( ! ( defined( 'SHORTINIT' ) && SHORTINIT ) ) return false; // check post data if ( ! isset( $_POST['action'], $_POST['content'], $_POST['type'], $_POST['subtype'], $_POST['storage_type'], $_POST['storage_data'], $_POST['pvcp_nonce'] ) ) return false; // verify nonce if ( ! wp_verify_nonce( $_POST['pvcp_nonce'], 'pvcp-check-post' ) ) return false; // allow only valid storage type if ( in_array( $storage_type, [ 'cookies', 'cookieless' ], true ) ) { $this->storage_type = $storage_type; return true; } return false; } /** * Add Post ID to queue. * * @param int $post_id * @return void */ public function add_to_queue( $post_id ) { $this->queue[] = (int) $post_id; } /** * Run manual pvc_view_post queue. * * @return void */ public function queue_count() { // check conditions if ( ! isset( $_POST['action'], $_POST['ids'], $_POST['pvc_nonce'] ) || ! wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-view-posts' ) || $_POST['ids'] === '' || ! is_string( $_POST['ids'] ) ) exit; // get post ids $ids = explode( ',', $_POST['ids'] ); $counted = []; if ( ! empty( $ids ) ) { $ids = array_filter( array_map( 'intval', $ids ) ); if ( ! empty( $ids ) ) { // turn on queue mode $this->queue_mode = true; foreach ( $ids as $id ) { $counted[$id] = ! ( $this->check_post( $id ) === null ); } // turn off queue mode $this->queue_mode = false; } } echo wp_json_encode( [ 'post_ids' => $ids, 'counted' => $counted ] ); exit; } /** * Print JavaScript with queue in the footer. * * @return void */ public function print_queue_count() { // any ids to "view"? if ( ! empty( $this->queue ) ) { echo " "; } } /** * Initialize counter. * * @return void */ public function init_counter() { // admin? if ( is_admin() && ! wp_doing_ajax() ) return; // get main instance $pvc = Post_Views_Counter(); // actions add_action( 'wp_ajax_pvc-view-posts', [ $this, 'queue_count' ] ); add_action( 'wp_ajax_nopriv_pvc-view-posts', [ $this, 'queue_count' ] ); add_action( 'wp_print_footer_scripts', [ $this, 'print_queue_count' ], 11 ); // php counter if ( $pvc->options['general']['counter_mode'] === 'php' ) add_action( 'wp', [ $this, 'check_post_php' ] ); // javascript (ajax) counter elseif ( $pvc->options['general']['counter_mode'] === 'js' ) { add_action( 'wp_ajax_pvc-check-post', [ $this, 'check_post_js' ] ); add_action( 'wp_ajax_nopriv_pvc-check-post', [ $this, 'check_post_js' ] ); } // rest api add_action( 'rest_api_init', [ $this, 'rest_api_init' ] ); } /** * Check whether to count visit. * * @param int $post_id * @param array $content_data * @return void|int */ public function check_post( $post_id = 0, $content_data = [] ) { // force check cookie in short init mode if ( defined( 'SHORTINIT' ) && SHORTINIT ) $this->check_cookie(); // get post id $post_id = (int) ( empty( $post_id ) ? get_the_ID() : $post_id ); // empty id? if ( empty( $post_id ) ) return; // get main instance $pvc = Post_Views_Counter(); // get user id, from current user or static var in rest api request $user_id = get_current_user_id(); // get user ip address $user_ip = $this->get_user_ip(); // before visit action do_action( 'pvc_before_check_visit', $post_id, $user_id, $user_ip, 'post', $content_data ); // check all conditions to count visit add_filter( 'pvc_count_conditions_met', [ $this, 'check_conditions' ], 10, 6 ); // check conditions - excluded ips, excluded groups $conditions_met = apply_filters( 'pvc_count_conditions_met', true, $post_id, $user_id, $user_ip, 'post', $content_data ); // conditions failed? if ( ! $conditions_met ) return; // do not count visit by default $count_visit = false; // cookieless data storage? if ( $pvc->options['general']['data_storage'] === 'cookieless' && $this->storage_type === 'cookieless' ) { $count_visit = $this->save_data_storage( $post_id, 'post', $content_data ); } elseif ( $pvc->options['general']['data_storage'] === 'cookies' && $this->storage_type === 'cookies' ) { // php counter mode? if ( $pvc->options['general']['counter_mode'] === 'php' ) { if ( $this->cookie['exists'] ) { // update cookie $count_visit = $this->save_cookie( $post_id, $this->cookie ); } else { // set new cookie $count_visit = $this->save_cookie( $post_id ); } } else $count_visit = $this->save_cookie_storage( $post_id, $content_data ); } // filter visit counting $count_visit = (bool) apply_filters( 'pvc_count_visit', $count_visit, $post_id, $user_id, $user_ip, 'post', $content_data ); // count visit if ( $count_visit ) { // before count visit action do_action( 'pvc_before_count_visit', $post_id, $user_id, $user_ip, 'post', $content_data ); return $this->count_visit( $post_id ); } } /** * Check whether counting conditions are met. * * @param bool $allow_counting * @param int $post_id * @param int $user_id * @param string $user_ip * @param string $content_type * @param array $content_data * @return bool */ public function check_conditions( $allow_counting, $post_id, $user_id, $user_ip, $content_type, $content_data ) { // already failed? if ( ! $allow_counting ) return false; // get main instance $pvc = Post_Views_Counter(); // get ips $ips = $pvc->options['general']['exclude_ips']; // whether to count this ip if ( ! empty( $ips ) && filter_var( preg_replace( '/[^0-9a-fA-F:., ]/', '', $user_ip ), FILTER_VALIDATE_IP ) ) { // check ips foreach ( $ips as $ip ) { if ( strpos( $ip, '*' ) !== false ) { if ( $this->ipv4_in_range( $user_ip, $ip ) ) return false; } else { if ( $user_ip === $ip ) return false; } } } // get groups to check them faster $groups = $pvc->options['general']['exclude']['groups']; // whether to count this user if ( ! empty( $user_id ) ) { // exclude logged in users? if ( in_array( 'users', $groups, true ) ) return false; // exclude specific roles? elseif ( in_array( 'roles', $groups, true ) && $this->is_user_role_excluded( $user_id, $pvc->options['general']['exclude']['roles'] ) ) return false; // exclude guests? } elseif ( in_array( 'guests', $groups, true ) ) return false; // whether to count robots if ( in_array( 'robots', $groups, true ) && $pvc->crawler->is_crawler() ) return false; return $allow_counting; } /** * Check whether real home page is displayed. * * @param object $object * @return bool */ public function is_homepage( $object ) { $is_homepage = false; // get show on front option $show_on_front = get_option( 'show_on_front' ); if ( $show_on_front === 'posts' ) $is_homepage = is_home() && is_front_page(); else { // home page $homepage = (int) get_option( 'page_on_front' ); // posts page $postspage = (int) get_option( 'page_for_posts' ); // both pages are set if ( $homepage && $postspage ) $is_homepage = is_front_page(); // only home page is set elseif ( $homepage && ! $postspage ) $is_homepage = is_front_page(); // only posts page is set elseif( ! $homepage && $postspage ) $is_homepage = is_home() && ( empty( $object ) || get_queried_object_id() === 0 ); } return $is_homepage; } /** * Check whether posts page (archive) is displayed. * * @param object $object * @return bool */ public function is_posts_page( $object ) { // get show on front option $show_on_front = get_option( 'show_on_front' ); // get page for posts option $page_for_posts = (int) get_option( 'page_for_posts' ); // check page $result = ( $show_on_front === 'page' && ! empty( $object ) && is_home() && is_a( $object, 'WP_Post' ) && (int) $object->ID === $page_for_posts ); return apply_filters( 'pvc_is_posts_page', $result, $object ); } /** * Check whether to count visit via PHP request. * * @return void */ public function check_post_php() { // do not count admin entries if ( is_admin() && ! wp_doing_ajax() ) return; // skip special requests if ( is_preview() || is_feed() || is_trackback() || ( function_exists( 'is_favicon' ) && is_favicon() ) || is_customize_preview() ) return; // get main instance $pvc = Post_Views_Counter(); // do we use php as counter? if ( $pvc->options['general']['counter_mode'] !== 'php' ) return; // get countable post types $post_types = $pvc->options['general']['post_types_count']; // whether to count this post type if ( empty( $post_types ) || ! is_singular( $post_types ) ) return; // get current post id $post_id = (int) get_the_ID(); // allow to run check post? if ( ! (bool) apply_filters( 'pvc_run_check_post', true, $post_id ) ) return; $this->check_post( $post_id ); } /** * Check whether to count visit via JavaScript (AJAX) request. * * @return void */ public function check_post_js() { // check conditions if ( ! isset( $_POST['action'], $_POST['id'], $_POST['storage_type'], $_POST['storage_data'], $_POST['pvc_nonce'] ) || ! wp_verify_nonce( $_POST['pvc_nonce'], 'pvc-check-post' ) ) exit; // get post id $post_id = (int) $_POST['id']; if ( $post_id <= 0 ) exit; // get main instance $pvc = Post_Views_Counter(); // do we use javascript as counter? if ( $pvc->options['general']['counter_mode'] !== 'js' ) exit; // get countable post types $post_types = $pvc->options['general']['post_types_count']; // check if post exists $post = get_post( $post_id ); // whether to count this post type or not if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) ) exit; // get storage type $storage_type = sanitize_key( $_POST['storage_type'] ); // invalid storage type? if ( ! in_array( $storage_type, [ 'cookies', 'cookieless' ], true ) ) exit; // set storage type $this->storage_type = $storage_type; // cookieless data storage? if ( $storage_type === 'cookieless' && $pvc->options['general']['data_storage'] === 'cookieless' ) { // sanitize storage data $storage_data = $this->sanitize_storage_data( $_POST['storage_data'] ); // cookies? } elseif ( $storage_type === 'cookies' && $pvc->options['general']['data_storage'] === 'cookies' ) { // sanitize cookies data $storage_data = $this->sanitize_cookies_data( $_POST['storage_data'] ); } else $storage_data = []; echo wp_json_encode( [ 'post_id' => $post_id, 'counted' => ! ( $this->check_post( $post_id, $storage_data ) === null ), 'storage' => $this->storage, 'type' => 'post' ] ); exit; } /** * Check whether to count visit via REST API request. * * @param object $request * @return int|WP_Error */ public function check_post_rest_api( $request ) { // get main instance $pvc = Post_Views_Counter(); // get post id (already sanitized) $post_id = $request->get_param( 'id' ); // do we use REST API as counter? if ( $pvc->options['general']['counter_mode'] !== 'rest_api' ) return new WP_Error( 'pvc_rest_api_disabled', __( 'REST API method is disabled.', 'post-views-counter' ), [ 'status' => 404 ] ); //TODO get current user id in direct api endpoint calls // check if post exists $post = get_post( $post_id ); if ( ! $post ) return new WP_Error( 'pvc_post_invalid_id', __( 'Invalid post ID.', 'post-views-counter' ), [ 'status' => 404 ] ); // get countable post types $post_types = $pvc->options['general']['post_types_count']; // whether to count this post type if ( empty( $post_types ) || ! in_array( $post->post_type, $post_types, true ) ) return new WP_Error( 'pvc_post_type_excluded', __( 'Post type excluded.', 'post-views-counter' ), [ 'status' => 404 ] ); // get storage type $storage_type = sanitize_key( $request->get_param( 'storage_type' ) ); // invalid storage type? if ( ! in_array( $storage_type, [ 'cookies', 'cookieless' ], true ) ) return new WP_Error( 'pvc_invalid_storage_type', __( 'Invalid storage type.', 'post-views-counter' ), [ 'status' => 404 ] ); // set storage type $this->storage_type = $storage_type; // cookieless data storage? if ( $storage_type === 'cookieless' && $pvc->options['general']['data_storage'] === 'cookieless' ) { // sanitize storage data $storage_data = $this->sanitize_storage_data( $request->get_param( 'storage_data' ) ); // cookies? } elseif ( $storage_type === 'cookies' && $pvc->options['general']['data_storage'] === 'cookies' ) { // sanitize cookies data $storage_data = $this->sanitize_cookies_data( $request->get_param( 'storage_data' ) ); } else $storage_data = []; return [ 'post_id' => $post_id, 'counted' => ! ( $this->check_post( $post_id, $storage_data ) === null ), 'storage' => $this->storage, 'type' => 'post' ]; } /** * Initialize cookie session. * * @param array $cookie Use this data instead of real $_COOKIE * @return void */ public function check_cookie( $cookie = [] ) { // do not run in admin except for ajax requests if ( is_admin() && ! wp_doing_ajax() ) return; if ( empty( $cookie ) || ! is_array( $cookie ) ) { // assign cookie name $cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' ); // is cookie set? if ( isset( $_COOKIE[$cookie_name] ) && ! empty( $_COOKIE[$cookie_name] ) ) $cookie = $_COOKIE[$cookie_name]; } // cookie data? if ( $cookie && is_array( $cookie ) ) { $visited_posts = $expirations = []; foreach ( $cookie as $content ) { // is cookie valid? if ( preg_match( '/^(([0-9]+b[0-9]+a?)+)$/', $content ) === 1 ) { // get single id with expiration $expiration_ids = explode( 'a', $content ); // check every expiration => id pair foreach ( $expiration_ids as $pair ) { $pair = explode( 'b', $pair ); $expirations[] = (int) $pair[0]; $visited_posts[(int) $pair[1]] = (int) $pair[0]; } } } // update cookie $this->cookie = [ 'exists' => true, 'visited_posts' => $visited_posts, 'expiration' => empty( $expirations ) ? 0 : max( $expirations ) ]; } } /** * Sanitize storage data. * * @param string $storage_data * @return array */ public function sanitize_storage_data( $storage_data ) { try { // strip slashes $storage_data = stripslashes( $storage_data ); // decode storage data $storage_data = json_decode( $storage_data, true, 2 ); } finally { // valid data? if ( json_last_error() === JSON_ERROR_NONE && is_array( $storage_data ) && ! empty( $storage_data ) ) { $content_data = []; foreach ( $storage_data as $content_id => $content_expiration ) { $content_data[(int) $content_id] = (int) $content_expiration; } return array_unique( $content_data, SORT_NUMERIC ); } else return []; } } /** * Sanitize cookies. * * @param string $storage_data * @return array */ public function sanitize_cookies_data( $storage_data ) { $content_data = $expirations = []; // is cookie valid? if ( preg_match( '/^(([0-9]+b[0-9]+a?)+)$/', $storage_data ) === 1 ) { // get single id with expiration $expiration_ids = explode( 'a', $storage_data ); // check every expiration => id pair foreach ( $expiration_ids as $pair ) { $pair = explode( 'b', $pair ); $expirations[] = (int) $pair[0]; $content_data[(int) $pair[1]] = (int) $pair[0]; } } return [ 'visited' => array_unique( $content_data, SORT_NUMERIC ), 'expiration' => empty( $expirations ) ? 0 : max( $expirations ) ]; } /** * Save data storage. * * @param int $content * @param string $content_type * @param array $content_data * @return bool */ private function save_data_storage( $content, $content_type, $content_data ) { // get base instance $pvc = Post_Views_Counter(); // set default flag $count_visit = true; // get expiration $expiration = $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'] ); // is this a new cookie? if ( empty( $content_data ) ) { $storage = [ $content => $expiration ]; } else { // get current gmt time $current_time = current_time( 'timestamp', true ); // post already viewed but not expired? if ( in_array( $content, array_keys( $content_data ), true ) && $current_time < $content_data[$content] ) $count_visit = false; // create copy for better foreach performance $content_data_tmp = $content_data; // check whether viewed id has expired - no need to keep it anymore foreach ( $content_data_tmp as $content_id => $content_expiration ) { if ( $current_time > $content_expiration ) unset( $content_data[$content_id] ); } // add new id or change expiration date if id already exists if ( $count_visit ) $content_data[$content] = $expiration; $storage = $content_data; } $this->storage[$content_type] = $storage; return $count_visit; } /** * Save cookie storage. * * @param int $content * @param array $content_data * @return bool */ private function save_cookie_storage( $content, $content_data ) { // early return? //TODO check this filter in js // if ( apply_filters( 'pvc_maybe_set_cookie', true, $content, $content_type, $content_data ) !== true ) // return; // get base instance $pvc = Post_Views_Counter(); // set default flag $count_visit = true; // get expiration $expiration = $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'] ); // assign cookie name $cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' ); $cookies_data = [ 'name' => [], 'value' => [], 'expiry' => [] ]; // is this a new cookie? if ( empty( $content_data['visited'] ) ) { $cookies_data['name'][] = $cookie_name . '[0]'; $cookies_data['value'][] = $expiration . 'b' . $content; $cookies_data['expiry'][] = $expiration; } else { // get current gmt time $current_time = current_time( 'timestamp', true ); if ( in_array( $content, array_keys( $content_data['visited'] ), true ) && $current_time < $content_data['visited'][$content] ) { $count_visit = false; } else { // add new id or change expiration date if id already exists $content_data['visited'][$content] = $expiration; } // create copy for better foreach performance $visited_expirations = $content_data['visited']; // check whether viewed id has expired - no need to keep it in cookie (less size) foreach ( $visited_expirations as $content_id => $content_expiration ) { if ( $current_time > $content_expiration ) unset( $content_data['visited'][$content_id] ); } // set new last expiration date if needed $content_data['expiration'] = empty( $content_data['visited'] ) ? 0 : max( $content_data['visited'] ); $cookies = $imploded = []; // create pairs foreach ( $content_data['visited'] as $id => $exp ) { $imploded[] = $exp . 'b' . $id; } // split cookie into chunks (3980 bytes to make sure it is safe for every browser) $chunks = str_split( implode( 'a', $imploded ), 3980 ); // more then one chunk? if ( count( $chunks ) > 1 ) { $last_id = ''; foreach ( $chunks as $chunk_id => $chunk ) { // new chunk $chunk_c = $last_id . $chunk; // is it full-length chunk? if ( strlen( $chunk ) === 3980 ) { // get last part $last_part = strrchr( $chunk_c, 'a' ); // get last id $last_id = substr( $last_part, 1 ); // add new full-lenght chunk $cookies[$chunk_id] = substr( $chunk_c, 0, strlen( $chunk_c ) - strlen( $last_part ) ); } else { // add last chunk $cookies[$chunk_id] = $chunk_c; } } } else { // only one chunk $cookies[] = $chunks[0]; } foreach ( $cookies as $key => $value ) { $cookies_data['name'][] = $cookie_name . '[' . $key . ']'; $cookies_data['value'][] = $value; $cookies_data['expiry'][] = $content_data['expiration']; } } /* COUNT_POST_AS_AUTHOR_VIEW | removed additional data if ( $this->storage_modified && ! empty( $this->storage ) ) { foreach ( $this->storage as $key => $value ) { foreach ( $value as $subkey => $subvalue ) { $cookies_data[$key][] = $subvalue; } } } */ $this->storage = $cookies_data; return $count_visit; } /** * Save cookie function. * * @param int $id * @param array $cookie * @return bool */ private function save_cookie( $id, $cookie = [] ) { // early return? if ( apply_filters( 'pvc_maybe_set_cookie', true, $id, 'post', $cookie ) !== true ) return; // get main instance $pvc = Post_Views_Counter(); // set default flag $count_visit = true; // get expiration $expiration = $this->get_timestamp( $pvc->options['general']['time_between_counts']['type'], $pvc->options['general']['time_between_counts']['number'] ); // assign cookie name $cookie_name = 'pvc_visits' . ( is_multisite() ? '_' . get_current_blog_id() : '' ); // check whether php version is at least 7.3 $php_at_least_73 = version_compare( phpversion(), '7.3', '>=' ); // is this a new cookie? if ( empty( $cookie ) ) { if ( $php_at_least_73 ) { // set cookie setcookie( $cookie_name . '[0]', $expiration . 'b' . $id, [ 'expires' => $expiration, 'path' => COOKIEPATH, 'domain' => COOKIE_DOMAIN, 'secure' => is_ssl(), 'httponly' => false, 'samesite' => 'LAX' ] ); } else { // set cookie setcookie( $cookie_name . '[0]', $expiration . 'b' . $id, $expiration, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); } if ( $this->queue_mode ) $this->check_cookie( [ 0 => $expiration . 'b' . $id ] ); } else { // get current gmt time $current_time = current_time( 'timestamp', true ); // post already viewed but not expired? if ( in_array( $id, array_keys( $cookie['visited_posts'] ), true ) && $current_time < $cookie['visited_posts'][$id] ) $count_visit = false; else { // add new id or change expiration date if id already exists $cookie['visited_posts'][$id] = $expiration; } // create copy for better foreach performance $visited_posts_expirations = $cookie['visited_posts']; // check whether viewed id has expired - no need to keep it in cookie (less size) foreach ( $visited_posts_expirations as $post_id => $post_expiration ) { if ( $current_time > $post_expiration ) unset( $cookie['visited_posts'][$post_id] ); } // set new last expiration date if needed $cookie['expiration'] = empty( $cookie['visited_posts'] ) ? 0 : max( $cookie['visited_posts'] ); $cookies = $imploded = []; // create pairs foreach ( $cookie['visited_posts'] as $id => $exp ) { $imploded[] = $exp . 'b' . $id; } // split cookie into chunks (3980 bytes to make sure it is safe for every browser) $chunks = str_split( implode( 'a', $imploded ), 3980 ); // more then one chunk? if ( count( $chunks ) > 1 ) { $last_id = ''; foreach ( $chunks as $chunk_id => $chunk ) { // new chunk $chunk_c = $last_id . $chunk; // is it full-length chunk? if ( strlen( $chunk ) === 3980 ) { // get last part $last_part = strrchr( $chunk_c, 'a' ); // get last id $last_id = substr( $last_part, 1 ); // add new full-lenght chunk $cookies[$chunk_id] = substr( $chunk_c, 0, strlen( $chunk_c ) - strlen( $last_part ) ); } else { // add last chunk $cookies[$chunk_id] = $chunk_c; } } } else { // only one chunk $cookies[] = $chunks[0]; } foreach ( $cookies as $key => $value ) { if ( $php_at_least_73 ) { // set cookie setcookie( $cookie_name . '[' . $key . ']', $value, [ 'expires' => $cookie['expiration'], 'path' => COOKIEPATH, 'domain' => COOKIE_DOMAIN, 'secure' => is_ssl(), 'httponly' => false, 'samesite' => 'LAX' ] ); } else { // set cookie setcookie( $cookie_name . '[' . $key . ']', $value, $cookie['expiration'], COOKIEPATH, COOKIE_DOMAIN, is_ssl(), false ); } } if ( $this->queue_mode ) $this->check_cookie( $cookies ); } return $count_visit; } /** * Count visit. * * @param int $id * @return int */ private function count_visit( $id ) { // increment amount $increment_amount = (int) apply_filters( 'pvc_views_increment_amount', 1, $id, 'post' ); if ( $increment_amount < 1 ) $increment_amount = 1; // get day, week, month and year $date = explode( '-', date( 'W-d-m-Y-o', current_time( 'timestamp', true ) ) ); foreach ( [ 0 => $date[3] . $date[2] . $date[1], // day like 20140324 1 => $date[4] . $date[0], // week like 201439 2 => $date[3] . $date[2], // month like 201405 3 => $date[3], // year like 2014 4 => 'total' // total views ] as $type => $period ) { //TODO investigate queueing these queries on the 'shutdown' hook instead of running them instantly? // hit the database directly $this->db_insert( $id, $type, $period, $increment_amount ); } do_action( 'pvc_after_count_visit', $id, 'post' ); return $id; } /** * Remove post views from database when post is deleted. * * @global object $wpdb * * @param int $post_id * @return void */ public function delete_post_views( $post_id ) { global $wpdb; $data = [ 'where' => [ 'id' => $post_id ], 'format' => [ '%d' ] ]; $data = apply_filters( 'pvc_delete_post_views_where_clause', $data, $post_id ); $wpdb->delete( $wpdb->prefix . 'post_views', $data['where'], $data['format'] ); } /** * Get timestamp convertion. * * @param string $type * @param int $number * @param bool $timestamp * @return int */ public function get_timestamp( $type, $number, $timestamp = true ) { $converter = [ 'minutes' => MINUTE_IN_SECONDS, 'hours' => HOUR_IN_SECONDS, 'days' => DAY_IN_SECONDS, 'weeks' => WEEK_IN_SECONDS, 'months' => MONTH_IN_SECONDS, 'years' => YEAR_IN_SECONDS ]; return (int) ( ( $timestamp ? current_time( 'timestamp', true ) : 0 ) + $number * $converter[$type] ); } /** * Check if object cache is in use. * * @param bool $only_interval * @return bool */ public function using_object_cache( $only_interval = false ) { $using = wp_using_ext_object_cache(); // is object cache active? if ( $using ) { // get main instance $pvc = Post_Views_Counter(); // check object cache if ( ! $only_interval && ! $pvc->options['general']['object_cache'] ) $using = false; // check interval if ( $pvc->options['general']['flush_interval']['number'] <= 0 ) $using = false; } return $using; } /** * Flush views data stored in the persistent object cache into * our custom table and clear the object cache keys when done. * * @return bool */ public function flush_cache_to_db() { // get keys $key_names = wp_cache_get( 'cached_key_names', 'pvc' ); if ( ! $key_names ) $key_names = []; else { // create an array out of a string that's stored in the cache $key_names = explode( '|', $key_names ); } // any data? if ( ! empty( $key_names ) ) { foreach ( $key_names as $key_name ) { // get values stored within the key name itself list( $id, $type, $period ) = explode( '.', $key_name ); // get the cached count value $count = wp_cache_get( $key_name, 'pvc' ); // store cached value in the database $this->db_prepare_insert( $id, $type, $period, $count ); // clear the cache key we just flushed wp_cache_delete( $key_name, 'pvc' ); } // flush values to database $this->db_commit_insert(); // delete the key holding the list wp_cache_delete( 'cached_key_names', 'pvc' ); } // remove last flush wp_cache_delete( 'last-flush', 'pvc' ); return true; } /** * Insert or update views count. * * @global object $wpdb * * @param int $id * @param int $type * @param string $period * @param int $count * @return int|bool */ private function db_insert( $id, $type, $period, $count ) { // check whether skip single query $skip_single_query = (bool) apply_filters( 'pvc_skip_single_query', false, $id, $type, $period, $count, 'post' ); // skip query? if ( $skip_single_query ) return false; global $wpdb; return $wpdb->query( $wpdb->prepare( "INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count) VALUES (%d, %d, %s, %d) ON DUPLICATE KEY UPDATE count = count + %d", $id, $type, $period, $count, $count ) ); } /** * Prepare bulk insert or update views count. * * @param int $id * @param int $type * @param string $period * @param int $count * @return void */ private function db_prepare_insert( $id, $type, $period, $count = 1 ) { // cast count $count = (int) $count; if ( ! $count ) $count = 1; // any queries? if ( ! empty( $this->db_insert_values ) ) $this->db_insert_values .= ', '; // append insert queries $this->db_insert_values .= sprintf( '(%d, %d, "%s", %d)', $id, $type, $period, $count ); if ( strlen( $this->db_insert_values ) > 25000 ) $this->db_commit_insert(); } /** * Insert accumulated values to database. * * @global object $wpdb * * @return int|bool */ private function db_commit_insert() { if ( empty( $this->db_insert_values ) ) return false; global $wpdb; $result = $wpdb->query( "INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count) VALUES " . $this->db_insert_values . " ON DUPLICATE KEY UPDATE count = count + VALUES(count)" ); $this->db_insert_values = ''; return $result; } /** * Check whether user has excluded roles. * * @param int $user_id * @param string $option * @return bool */ public function is_user_role_excluded( $user_id, $option = [] ) { // get user by ID $user = get_user_by( 'id', $user_id ); // no user? if ( empty( $user ) ) return false; // get user roles $roles = (array) $user->roles; // any roles? if ( ! empty( $roles ) ) { foreach ( $roles as $role ) { if ( in_array( $role, $option, true ) ) return true; } } return false; } /** * Check if IPv4 is in range. * * @param string $ip IP address * @param string $range IP range * @return bool */ public function ipv4_in_range( $ip, $range ) { $start = str_replace( '*', '0', $range ); $end = str_replace( '*', '255', $range ); $ip = (float) sprintf( "%u", ip2long( $ip ) ); return ( $ip >= (float) sprintf( "%u", ip2long( $start ) ) && $ip <= (float) sprintf( "%u", ip2long( $end ) ) ); } /** * Get user real IP address. * * @return string */ public function get_user_ip() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : ''; foreach ( [ 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' ] as $key ) { if ( array_key_exists( $key, $_SERVER ) === true ) { foreach ( explode( ',', $_SERVER[$key] ) as $ip ) { // trim for safety measures $ip = trim( $ip ); // attempt to validate IP if ( $this->validate_user_ip( $ip ) ) continue; } } } return (string) $ip; } /** * Ensure an IP address is both a valid IP and does not fall within a private network range. * * @param $ip string IP address * @return bool */ public function validate_user_ip( $ip ) { if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false ) return false; return true; } /** * Register REST API endpoints. * * @return void */ public function rest_api_init() { // view post route register_rest_route( 'post-views-counter', '/view-post/(?P\d+)|/view-post/', [ 'methods' => [ 'POST' ], 'callback' => [ $this, 'check_post_rest_api' ], 'permission_callback' => [ $this, 'view_post_permissions_check' ], 'args' => apply_filters( 'pvc_rest_api_view_post_args', [ 'id' => [ 'default' => 0, 'sanitize_callback' => 'absint' ], 'storage_type' => [ 'default' => 'cookies' ], 'storage_data' => [ 'default' => '' ] ] ) ] ); // get views route register_rest_route( 'post-views-counter', '/get-post-views/(?P(\d+,?)+)', [ 'methods' => [ 'GET', 'POST' ], 'callback' => [ $this, 'get_post_views_rest_api' ], 'permission_callback' => [ $this, 'get_post_views_permissions_check' ], 'args' => apply_filters( 'pvc_rest_api_get_post_views_args', [ 'id' => [ 'default' => 0, 'sanitize_callback' => [ $this, 'validate_rest_api_data' ] ] ] ) ] ); } /** * Get post views via REST API request. * * @param object $request * @return int */ public function get_post_views_rest_api( $request ) { return pvc_get_post_views( $request->get_param( 'id' ) ); } /** * Check if a given request has access to get views. * * @param object $request * @return bool */ public function get_post_views_permissions_check( $request ) { return (bool) apply_filters( 'pvc_rest_api_get_post_views_check', true, $request ); } /** * Check if a given request has access to view post. * * @param object $request * @return bool */ public function view_post_permissions_check( $request ) { return (bool) apply_filters( 'pvc_rest_api_view_post_check', true, $request ); } /** * Validate REST API incoming data. * * @param int|array $data * @return int|array */ public function validate_rest_api_data( $data ) { // POST array? if ( is_array( $data ) ) $data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC ); // multiple comma-separated values? elseif ( strpos( $data, ',' ) !== false ) { $data = explode( ',', $data ); if ( is_array( $data ) && ! empty( $data ) ) $data = array_unique( array_filter( array_map( 'absint', $data ) ), SORT_NUMERIC ); else $data = []; // single value? } else $data = absint( $data ); return $data; } } includes/class-dashboard.php000064400000056133147206624130012132 0ustar00setup_widget_items(); // do it only on dashboard page if ( $pagenow === 'index.php' ) { // filter user_can_see_stats if ( ! apply_filters( 'pvc_user_can_see_stats', current_user_can( 'publish_posts' ) ) ) return; add_action( 'wp_dashboard_setup', [ $this, 'wp_dashboard_setup' ], 1 ); add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts_styles' ] ); // ajax endpoints } elseif ( $pagenow === 'admin-ajax.php' ) { add_action( 'wp_ajax_pvc_dashboard_post_most_viewed', [ $this, 'dashboard_post_most_viewed' ] ); add_action( 'wp_ajax_pvc_dashboard_post_views_chart', [ $this, 'dashboard_post_views_chart' ] ); add_action( 'wp_ajax_pvc_dashboard_user_options', [ $this, 'update_dashboard_user_options' ] ); } } /** * Add dashboard widget. * * @return void */ public function wp_dashboard_setup() { // add dashboard widget wp_add_dashboard_widget( 'pvc_dashboard', __( 'Post Views Counter', 'post-views-counter' ), [ $this, 'dashboard_widget' ] ); } /** * Enqueue admin scripts and styles. * * @return void */ public function admin_scripts_styles() { // get main instance $pvc = Post_Views_Counter(); // styles wp_enqueue_style( 'pvc-admin-dashboard', POST_VIEWS_COUNTER_URL . '/css/admin-dashboard.min.css', [], $pvc->defaults['version'] ); wp_enqueue_style( 'pvc-microtip', POST_VIEWS_COUNTER_URL . '/assets/microtip/microtip.min.css', [], '1.0.0' ); // scripts wp_enqueue_script( 'pvc-admin-dashboard', POST_VIEWS_COUNTER_URL . '/js/admin-dashboard.js', [ 'jquery', 'pvc-chartjs' ], $pvc->defaults['version'], true ); // prepare script data $script_data = [ 'ajaxURL' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'pvc-dashboard-widget' ), 'nonceUser' => wp_create_nonce( 'pvc-dashboard-user-options' ) ]; wp_add_inline_script( 'pvc-admin-dashboard', 'var pvcArgs = ' . wp_json_encode( $script_data ) . ";\n", 'before' ); } /** * Setup dashboard widget items. * * @return void */ private function setup_widget_items() { // standard items $items = [ [ 'id' => 'post-views', 'title' => __( 'Post Views', 'post-views-counter' ), 'description' => __( 'Displays a chart of most viewed post types.', 'post-views-counter' ), 'content' => '', 'position' => 2 ], [ 'id' => 'post-most-viewed', 'title' => __( 'Top Posts', 'post-views-counter' ), 'description' => __( 'Displays a list of most viewed single posts or pages.', 'post-views-counter' ), 'content' => '
', 'position' => 3 ] ]; // filter items, do not allow to remove main items $new_items = apply_filters( 'pvc_dashboard_widget_items', [] ); // any new items? if ( is_array( $new_items ) && ! empty( $new_items ) ) { foreach ( $new_items as $item ) { // add new item array_push( $items, $item ); } } // sort dashboard items by position array_multisort( array_column( $items, 'position' ), SORT_ASC, SORT_NUMERIC, $items ); // set widget items $this->widget_items = $items; } /** * Calculate canvas height based on number of legend items. * * @param array $data * @param bool $expression * @return int */ public function calculate_canvas_size( $data, $expression = true ) { if ( $expression && ! empty( $data ) ) { // treat every 4 legend items as 1 line - 23 pixels $height = 23 * ( (int) ceil( count( $data ) / 4 ) - 1 ); } else $height = 0; return (int) ( 170 + $height ); } /** * Render dashboard widget. * * @return void */ public function dashboard_widget() { // get user options $user_options = get_user_meta( get_current_user_id(), 'pvc_dashboard', true ); // empty options? if ( empty( $user_options ) || ! is_array( $user_options ) ) $user_options = []; // sanitize options $user_options = map_deep( $user_options, 'sanitize_text_field' ); // get menu items $menu_items = ! empty( $user_options['menu_items'] ) ? $user_options['menu_items'] : []; // generate months $months_html = wp_kses_post( $this->generate_months( current_time( 'timestamp', false ) ) ); // get widget items $items = $this->widget_items; $html = '
'; foreach ( $items as $item ) { $html .= $this->generate_dashboard_widget_item( $item, $menu_items, $months_html ); } $html .= '
' . esc_html__( 'Powered by', 'post-views-counter' ) . ' Post Views Counter
'; echo $html; } /** * Generate dashboard widget item HTML. * * @param array $item * @param array $menu_items * @param string $esc_months_html * @return string */ public function generate_dashboard_widget_item( $item, $menu_items, $esc_months_html ) { // get allowed html tags $allowed_html = wp_kses_allowed_html( 'post' ); $allowed_html['canvas'] = [ 'id' => [], 'height' => [] ]; return '
' . esc_html( $item['title'] ) . '
' . wp_kses( $item['content'], $allowed_html ) . '
' . $esc_months_html . '
'; } /** * Render dashboard widget with post views. * * @return void */ public function dashboard_post_views_chart() { if ( ! apply_filters( 'pvc_user_can_see_stats', current_user_can( 'publish_posts' ) ) || ! check_ajax_referer( 'pvc-dashboard-widget', 'nonce' ) ) wp_die( __( 'You do not have permission to access this page.', 'post-views-counter' ) ); // get period $period = isset( $_POST['period'] ) ? preg_replace( '/[^a-z0-9_|]/', '', $_POST['period'] ) : 'this_month'; // get post types $post_types = Post_Views_Counter()->options['general']['post_types_count']; // get dashboard user options $user_options = $this->get_dashboard_user_options( get_current_user_id(), 'post_types' ); // get current date $now = getdate( current_time( 'timestamp', get_option( 'gmt_offset' ) ) ); // get colors $colors = Post_Views_Counter()->functions->get_colors(); // set chart labels switch ( $period ) { case 'this_week': //TODO $data = [ 'design' => [ 'fill' => true, 'backgroundColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 0.2)', 'borderColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 1)', 'borderWidth' => 1.2, 'borderDash' => [], 'pointBorderColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 1)', 'pointBackgroundColor' => 'rgba(255, 255, 255, 1)', 'pointBorderWidth' => 1.2 ] ]; $data['data']['datasets'][0]['label'] = __( 'Post Views', 'post-views-counter' ); $data['data']['datasets'][0]['post_type'] = '_pvc_total_views'; for ( $day = 0; $day <= 6; $day++ ) { $date = strtotime( $now['mday'] . '-' . $now['mon'] . '-' . $now['year'] . ' + ' . $day . ' days - ' . $now['wday'] . ' days' ); $query = new WP_Query( wp_parse_args( [ 'post_type' => $post_types, 'posts_per_page' => -1, 'paged' => false, 'orderby' => 'post_views', 'suppress_filters' => false, 'no_found_rows' => true ], [ 'views_query' => [ 'year' => date( 'Y', $date ), 'month' => date( 'n', $date ), 'day' => date( 'd', $date ) ] ] ) ); $data['data']['labels'][] = date_i18n( 'j', $date ); $data['data']['dates'][] = date_i18n( get_option( 'date_format' ), $date ); $data['data']['datasets'][0]['data'][] = $query->total_views; $data['data']['datasets'][$day]['data'][] = $query->total_views; } break; case 'this_year': $data = [ 'design' => [ 'fill' => true, 'backgroundColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 0.2)', 'borderColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 1)', 'borderWidth' => 1.2, 'borderDash' => [], 'pointBorderColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 1)', 'pointBackgroundColor' => 'rgba(255, 255, 255, 1)', 'pointBorderWidth' => 1.2 ] ]; $data['data']['datasets'][0]['label'] = __( 'Total Views', 'post-views-counter' ); $data['data']['datasets'][0]['post_type'] = '_pvc_total_views'; $data['data']['datasets'][0]['hidden'] = in_array( '_pvc_total_views', $user_options, true ); $data['data']['datasets'][0]['data'] = []; $sum = []; // any post types? if ( ! empty( $post_types ) ) { // reindex post types $post_types = array_combine( range( 1, count( $post_types ) ), array_values( $post_types ) ); $post_type_data = []; foreach ( $post_types as $id => $post_type ) { $post_type_obj = get_post_type_object( $post_type ); // unrecognized post type? (mainly from deactivated plugins) if ( empty( $post_type_obj ) ) $label = $post_type; else $label = $post_type_obj->labels->name; $data['data']['datasets'][$id]['label'] = $label; $data['data']['datasets'][$id]['post_type'] = $post_type; $data['data']['datasets'][$id]['hidden'] = in_array( $post_type, $user_options, true ); $data['data']['datasets'][$id]['data'] = []; // get month views $post_type_data[$id] = array_values( pvc_get_views( [ 'fields' => 'date=>views', 'post_type' => $post_type, 'views_query' => [ 'year' => date( 'Y' ), 'month' => '', 'week' => '', 'day' => '' ] ] ) ); } foreach ( $post_type_data as $post_type_id => $post_views ) { foreach ( $post_views as $id => $views ) { // generate chart data for specific post types $data['data']['datasets'][$post_type_id]['data'][] = $views; if ( ! array_key_exists( $id, $sum ) ) $sum[$id] = 0; $sum[$id] += $views; } } } // this month all days for ( $i = 1; $i <= 12; $i++ ) { // generate chart data $data['data']['labels'][] = $i; $data['data']['dates'][] = date_i18n( 'F Y', strtotime( date( 'Y' ) . '-' . str_pad( $i, 2, '0', STR_PAD_LEFT ) . '-01' ) ); $data['data']['datasets'][0]['data'][] = ( array_key_exists( $i - 1, $sum ) ? $sum[$i - 1] : 0 ); } break; case 'this_month': default: // convert period $time = $this->period2timestamp( $period ); // get date chunks $date = explode( ' ', date( "m Y t F", $time ) ); $data = [ 'months' => $this->generate_months( $time ), 'design' => [ 'fill' => true, 'backgroundColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 0.2)', 'borderColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 1)', 'borderWidth' => 1.2, 'borderDash' => [], 'pointBorderColor' => 'rgba(' . $colors['r'] . ',' . $colors['g'] . ',' . $colors['b'] . ', 1)', 'pointBackgroundColor' => 'rgba(255, 255, 255, 1)', 'pointBorderWidth' => 1.2 ] ]; $data['data']['datasets'][0]['label'] = __( 'Total Views', 'post-views-counter' ); $data['data']['datasets'][0]['post_type'] = '_pvc_total_views'; $data['data']['datasets'][0]['hidden'] = in_array( '_pvc_total_views', $user_options, true ); $data['data']['datasets'][0]['data'] = []; $sum = []; // any post types? if ( ! empty( $post_types ) ) { // reindex post types $post_types = array_combine( range( 1, count( $post_types ) ), array_values( $post_types ) ); $post_type_data = []; foreach ( $post_types as $id => $post_type ) { $post_type_obj = get_post_type_object( $post_type ); // unrecognized post type? (mainly from deactivated plugins) if ( empty( $post_type_obj ) ) $label = $post_type; else $label = $post_type_obj->labels->name; $data['data']['datasets'][$id]['label'] = $label; $data['data']['datasets'][$id]['post_type'] = $post_type; $data['data']['datasets'][$id]['hidden'] = in_array( $post_type, $user_options, true ); $data['data']['datasets'][$id]['data'] = []; // get month views $post_type_data[$id] = array_values( pvc_get_views( [ 'fields' => 'date=>views', 'post_type' => $post_type, 'views_query' => [ 'year' => $date[1], 'month' => $date[0], 'week' => '', 'day' => '', 'hide_empty' => true ] ] ) ); } foreach ( $post_type_data as $post_type_id => $post_views ) { foreach ( $post_views as $id => $views ) { // generate chart data for specific post types $data['data']['datasets'][$post_type_id]['data'][] = $views; if ( ! array_key_exists( $id, $sum ) ) $sum[$id] = 0; $sum[$id] += $views; } } } // this month all days for ( $i = 1; $i <= $date[2]; $i++ ) { // generate chart data $data['data']['labels'][] = ( $i % 2 === 0 ? '' : $i ); $data['data']['dates'][] = date_i18n( get_option( 'date_format' ), strtotime( $date[1] . '-' . $date[0] . '-' . str_pad( $i, 2, '0', STR_PAD_LEFT ) ) ); $data['data']['datasets'][0]['data'][] = ( array_key_exists( $i - 1, $sum ) ? $sum[$i - 1] : 0 ); } break; } echo wp_json_encode( $data ); exit; } /** * Render dashboard widget with most viewed posts. * * @return void */ public function dashboard_post_most_viewed() { if ( ! apply_filters( 'pvc_user_can_see_stats', current_user_can( 'publish_posts' ) ) || ! check_ajax_referer( 'pvc-dashboard-widget', 'nonce' ) ) wp_die( __( 'You do not have permission to access this page.', 'post-views-counter' ) ); // get main instance $pvc = Post_Views_Counter(); // get post types $post_types = $pvc->options['general']['post_types_count']; // get period $period = isset( $_POST['period'] ) ? preg_replace( '/[^a-z0-9_|]/', '', $_POST['period'] ) : 'this_month'; // convert period $time = $this->period2timestamp( $period ); // get date chunks $date = explode( ' ', date( "m Y t F", $time ) ); // get stats $query_args = [ 'post_type' => $post_types, 'posts_per_page' => 10, 'paged' => false, 'suppress_filters' => false, 'no_found_rows' => true, 'views_query' => [ 'year' => $date[1], 'month' => $date[0], 'hide_empty' => true ] ]; $posts = pvc_get_most_viewed_posts( $query_args ); $data = [ 'months' => $this->generate_months( $time ), 'html' => '' ]; $html = ' '; if ( $posts ) { // active post types $active_post_types = []; foreach ( $posts as $index => $post ) { setup_postdata( $post ); $html .= ' '; // check post type existence if ( array_key_exists( $post->post_type, $active_post_types ) ) $post_type_exists = $active_post_types[$post->post_type]; else $post_type_exists = $active_post_types[$post->post_type] = post_type_exists( $post->post_type ); $title = get_the_title( $post ); if ( $title === '' ) $title = __( '(no title)' ); // edit post link if ( $post_type_exists && current_user_can( 'edit_post', $post->ID ) ) { $html .= ' '; } else { $html .= ' '; } $html .= ' '; } } else { $html .= ' '; } $html .= '
# ' . esc_html__( 'Post', 'post-views-counter' ) . ' ' . esc_html__( 'Views', 'post-views-counter' ) . '
' . ( $index + 1 ) . '' . esc_html( $title ) . '' . esc_html( $title ). '' . number_format_i18n( $post->post_views ) . '
' . esc_html__( 'No most viewed posts found.', 'post-views-counter' ) . '
'; $data['html'] = $html; echo wp_json_encode( $data ); exit; } /** * Generate dashboard widget months HTML. * * @param int $timestamp * @return string */ public function generate_months( $timestamp ) { $dates = [ explode( ' ', date( "m F Y", strtotime( "-1 months", $timestamp ) ) ), explode( ' ', date( "m F Y", $timestamp ) ), explode( ' ', date( "m F Y", strtotime( "+1 months", $timestamp ) ) ) ]; $current = date( "Ym", current_time( 'timestamp', false ) ); if ( (int) $current <= (int) ( $dates[1][2] . $dates[1][0] ) ) $next = '' . $dates[2][1] . ' ' . $dates[2][2] . ' ›'; else $next = ''; $dates = [ 'prev' => '', 'current' => '' . $dates[1][1] . ' ' . $dates[1][2] . '', 'next' => $next ]; return $dates['prev'] . $dates['current'] . $dates['next']; } /** * Update dashboard widget user options. * * @return void */ public function update_dashboard_user_options() { if ( ! check_ajax_referer( 'pvc-dashboard-user-options', 'nonce' ) ) wp_die( __( 'You do not have permission to access this page.', 'post-views-counter' ) ); // valid data? if ( isset( $_POST['nonce'], $_POST['options'] ) && ! empty( $_POST['options'] ) ) { // get sanitized options $update = map_deep( $_POST['options'], 'sanitize_text_field' ); // get user ID $user_id = get_current_user_id(); // get user dashboard data $user_options = get_user_meta( $user_id, 'pvc_dashboard', true ); // empty userdata? if ( ! is_array( $user_options ) || empty( $user_options ) ) $user_options = []; // empty post types? if ( ! array_key_exists( 'post_types', $user_options ) || ! is_array( $user_options['post_types'] ) ) $user_options['post_types'] = []; // hide post type? if ( ! empty( $update['post_type'] ) ) { // get allowed post types $allowed_post_types = Post_Views_Counter()->options['general']['post_types_count']; // simulate total post views as post type $allowed_post_types[] = '_pvc_total_views'; if ( in_array( $update['post_type'], $allowed_post_types, true ) ) { if ( isset( $update['hidden'] ) && $update['hidden'] === 'true' ) { if ( ! in_array( $update['post_type'], $user_options['post_types'], true ) ) $user_options['post_types'][] = $update['post_type']; } else { if ( ( $key = array_search( $update['post_type'], $user_options['post_types'] ) ) !== false ) unset( $user_options['post_types'][$key] ); } } } // empty menu items? if ( ! array_key_exists( 'menu_items', $user_options ) || ! is_array( $user_options['menu_items'] ) ) $user_options['menu_items'] = []; if ( ! empty( $update['menu_items'] ) && is_array( $update['menu_items'] ) ) { $user_options['menu_items'] = []; // get allowed menu items $allowed_menu_items = array_column( $this->widget_items, 'id' ); foreach ( $update['menu_items'] as $menu_item => $hidden ) { if ( in_array( $menu_item, $allowed_menu_items, true ) && $hidden === 'true' ) $user_options['menu_items'][] = $menu_item; } } // filter user options $user_options = apply_filters( 'pvc_update_dashboard_user_options', $user_options, $update, $user_id ); // update userdata update_user_meta( $user_id, 'pvc_dashboard', $user_options ); } exit; } /** * Get user dashboard data. * * @param int $user_id * @param string $data_type * @return array */ public function get_dashboard_user_options( $user_id, $data_type ) { $user_options = get_user_meta( $user_id, 'pvc_dashboard', true ); if ( ! is_array( $user_options ) || empty( $user_options ) ) $user_options = []; if ( ! array_key_exists( $data_type, $user_options ) || ! is_array( $user_options[$data_type] ) ) $user_options[$data_type] = []; return $user_options[$data_type]; } /** * Convert period to timestamp. * * @param string $period * @return int */ public function period2timestamp( $period ) { // whitelisted period? if ( in_array( $period, [ 'this_week', 'this_year', 'this_month' ], true ) ) { $timestamp = current_time( 'timestamp', false ); } else { if ( preg_match( '/^([0-9]{2}\|[0-9]{4})$/', $period ) === 1 ) { // month|year $date = explode( '|', $period, 2 ); // get timestamp $timestamp = strtotime( (string) $date[1] . '-' . (string) $date[0] . '-13' ); } else $timestamp = current_time( 'timestamp', false ); } return $timestamp; } } includes/class-crawler-detect.php000064400000100522147206624130013100 0ustar00crawlers = $this->get_crawlers_list(); $this->exclusions = $this->get_exclusions_list(); add_action( 'after_setup_theme', [ $this, 'init' ] ); } /** * Initialize class. * * @return void */ public function init() { // break on admin side if ( is_admin() && ! wp_doing_ajax() ) return; $this->ua_http_headers = $this->get_headers_list(); $this->set_http_headers(); $this->set_user_agent(); } /** * Set HTTP headers. * * @param array $http_headers * @return void */ public function set_http_headers( $http_headers = null ) { // use global _SERVER if $http_headers aren't defined if ( ! is_array( $http_headers ) || ! count( $http_headers ) ) $http_headers = $_SERVER; // clear existing headers $this->http_headers = []; // only save HTTP headers - in PHP land, that means only _SERVER vars that start with HTTP_. foreach ( $http_headers as $key => $value ) { if ( substr( $key, 0, 5 ) === 'HTTP_' ) $this->http_headers[$key] = $value; } } /** * Return user agent headers. * * @return array */ public function get_ua_http_headers() { return $this->ua_http_headers; } /** * Return the user agent. * * @return string */ public function get_user_agent() { return $this->user_agent; } /** * Set the user agent. * * @param string $user_agent * @return string */ public function set_user_agent( $user_agent = null ) { if ( false === empty( $user_agent ) ) return $this->user_agent = $user_agent; else { $this->user_agent = null; foreach ( $this->get_ua_http_headers() as $alt_header ) { if ( false === empty( $this->http_headers[$alt_header] ) ) // @todo: should use get_http_header(), but it would be slow. $this->user_agent .= $this->http_headers[$alt_header] . ' '; } return $this->user_agent = ( ! empty( $this->user_agent ) ? trim( $this->user_agent ) : null ); } } /** * Build the user agent regex. * * @return string */ public function get_regex() { return '(' . implode( '|', $this->crawlers ) . ')'; } /** * Build the replacement regex. * * @return string */ public function get_exclusions() { return '(' . implode( '|', $this->exclusions ) . ')'; } /** * Check user agent string against the regex. * * @param string $user_agent * @return bool */ public function is_crawler( $user_agent = null ) { $agent = (string)( is_null( $user_agent ) ? $this->user_agent : $user_agent ); $agent = preg_replace( '/' . $this->get_exclusions() . '/i', '', $agent ); if ( strlen( trim( $agent ) ) === 0 ) return false; else $result = preg_match( '/' . $this->get_regex() . '/i', trim( $agent ), $matches ); if ( $matches ) $this->matches = $matches; return (bool) $result; } /** * Return the matches. * * @return string */ public function get_matches() { return isset( $this->matches[0] ) ? $this->matches[0] : null; } /** * Return the regular expressions to match against the user agent. * * @return array */ protected function get_crawlers_list() { return [ ' YLT', '^Aether', '^Amazon Simple Notification Service Agent$', '^Amazon-Route53-Health-Check-Service', '^Amazon CloudFront', '^b0t$', '^bluefish ', '^Calypso v\/', '^COMODO DCV', '^Corax', '^DangDang', '^DavClnt', '^DHSH', '^docker\/[0-9]', '^Expanse', '^FDM ', '^git\/', '^Goose\/', '^Grabber', '^Gradle\/', '^HTTPClient\/', '^HTTPing', '^Java\/', '^Jeode\/', '^Jetty\/', '^Mail\/', '^Mget', '^Microsoft URL Control', '^Mikrotik\/', '^Netlab360', '^NG\/[0-9\.]', '^NING\/', '^npm\/', '^Nuclei', '^PHP-AYMAPI\/', '^PHP\/', '^pip\/', '^pnpm\/', '^RMA\/', '^Ruby|Ruby\/[0-9]', '^Swurl ', '^TLS tester ', '^twine\/', '^ureq', '^VSE\/[0-9]', '^WordPress\.com', '^XRL\/[0-9]', '^ZmEu', '008\/', '13TABS', '192\.comAgent', '2GDPR\/', '2ip\.ru', '404enemy', '7Siters', '80legs', 'a3logics\.in', 'A6-Indexer', 'Abonti', 'Aboundex', 'aboutthedomain', 'Accoona-AI-Agent', 'acebookexternalhit\/', 'acoon', 'acrylicapps\.com\/pulp', 'Acunetix', 'AdAuth\/', 'adbeat', 'AddThis', 'ADmantX', 'AdminLabs', 'adressendeutschland', 'adreview\/', 'adscanner', 'adstxt-worker', 'Adstxtaggregator', 'adstxt\.com', 'Adyen HttpClient', 'AffiliateLabz\/', 'affilimate-puppeteer', 'agentslug', 'AHC', 'aihit', 'aiohttp\/', 'Airmail', 'akka-http\/', 'akula\/', 'alertra', 'alexa site audit', 'Alibaba\.Security\.Heimdall', 'Alligator', 'allloadin', 'AllSubmitter', 'alyze\.info', 'amagit', 'Anarchie', 'AndroidDownloadManager', 'Anemone', 'AngleSharp', 'annotate_google', 'Anthill', 'Anturis Agent', 'Ant\.com', 'AnyEvent-HTTP\/', 'Apache Ant\/', 'Apache Droid', 'Apache OpenOffice', 'Apache-HttpAsyncClient', 'Apache-HttpClient', 'ApacheBench', 'Apexoo', 'apimon\.de', 'APIs-Google', 'AportWorm\/', 'AppBeat\/', 'AppEngine-Google', 'AppleSyndication', 'Aprc\/[0-9]', 'Arachmo', 'arachnode', 'Arachnophilia', 'aria2', 'Arukereso', 'asafaweb', 'Asana\/', 'Ask Jeeves', 'AskQuickly', 'ASPSeek', 'Asterias', 'Astute', 'asynchttp', 'Attach', 'attohttpc', 'autocite', 'AutomaticWPTester', 'Autonomy', 'awin\.com', 'AWS Security Scanner', 'axios\/', 'a\.pr-cy\.ru', 'B-l-i-t-z-B-O-T', 'Backlink-Ceck', 'BacklinkHttpStatus', 'BackStreet', 'BackupLand', 'BackWeb', 'Bad-Neighborhood', 'Badass', 'baidu\.com', 'Bandit', 'basicstate', 'BatchFTP', 'Battleztar Bazinga', 'baypup\/', 'BazQux', 'BBBike', 'BCKLINKS', 'BDFetch', 'BegunAdvertising', 'Bewica-security-scan', 'Bidtellect', 'BigBozz', 'Bigfoot', 'biglotron', 'BingLocalSearch', 'BingPreview', 'binlar', 'biNu image cacher', 'Bitacle', 'Bitrix link preview', 'biz_Directory', 'BKCTwitterUnshortener\/', 'Black Hole', 'Blackboard Safeassign', 'BlackWidow', 'BlockNote\.Net', 'BlogBridge', 'Bloglines', 'Bloglovin', 'BlogPulseLive', 'BlogSearch', 'Blogtrottr', 'BlowFish', 'boitho\.com-dc', 'Boost\.Beast', 'BPImageWalker', 'Braintree-Webhooks', 'Branch Metrics API', 'Branch-Passthrough', 'Brandprotect', 'BrandVerity', 'Brandwatch', 'Brodie\/', 'Browsershots', 'BUbiNG', 'Buck\/', 'Buddy', 'BuiltWith', 'Bullseye', 'BunnySlippers', 'Burf Search', 'Butterfly\/', 'BuzzSumo', 'CAAM\/[0-9]', 'CakePHP', 'Calculon', 'Canary%20Mail', 'CaretNail', 'catexplorador', 'CC Metadata Scaper', 'Cegbfeieh', 'censys', 'centuryb.o.t9[at]gmail.com', 'Cerberian Drtrs', 'CERT\.at-Statistics-Survey', 'cf-facebook', 'cg-eye', 'changedetection', 'ChangesMeter', 'Charlotte', 'chatterino-api-cache', 'CheckHost', 'checkprivacy', 'CherryPicker', 'ChinaClaw', 'Chirp\/', 'chkme\.com', 'Chlooe', 'Chromaxa', 'CirrusExplorer', 'CISPA Vulnerability Notification', 'CISPA Web Analyser', 'Citoid', 'CJNetworkQuality', 'Clarsentia', 'clips\.ua\.ac\.be', 'Cloud mapping', 'CloudEndure', 'CloudFlare-AlwaysOnline', 'Cloudflare-Healthchecks', 'Cloudinary', 'cmcm\.com', 'coccoc', 'cognitiveseo', 'ColdFusion', 'colly -', 'CommaFeed', 'Commons-HttpClient', 'commonscan', 'contactbigdatafr', 'contentkingapp', 'Contextual Code Sites Explorer', 'convera', 'CookieReports', 'copyright sheriff', 'CopyRightCheck', 'Copyscape', 'cortex\/', 'Cosmos4j\.feedback', 'Covario-IDS', 'Craw\/', 'Crescent', 'Criteo', 'Crowsnest', 'CSHttp', 'CSSCheck', 'Cula\/', 'curb', 'Curious George', 'curl', 'cuwhois\/', 'cybo\.com', 'DAP\/NetHTTP', 'DareBoost', 'DatabaseDriverMysqli', 'DataCha0s', 'DatadogSynthetics', 'Datafeedwatch', 'Datanyze', 'DataparkSearch', 'dataprovider', 'DataXu', 'Daum(oa)?[ \/][0-9]', 'dBpoweramp', 'ddline', 'deeris', 'delve\.ai', 'Demon', 'DeuSu', 'developers\.google\.com\/\+\/web\/snippet\/', 'Devil', 'Digg', 'Digincore', 'DigitalPebble', 'Dirbuster', 'Discourse Forum Onebox', 'Dispatch\/', 'Disqus\/', 'DittoSpyder', 'dlvr', 'DMBrowser', 'DNSPod-reporting', 'docoloc', 'Dolphin http client', 'DomainAppender', 'DomainLabz', 'Domains Project\/', 'Donuts Content Explorer', 'dotMailer content retrieval', 'dotSemantic', 'downforeveryoneorjustme', 'Download Wonder', 'downnotifier', 'DowntimeDetector', 'Drip', 'drupact', 'Drupal \(\+http:\/\/drupal\.org\/\)', 'DTS Agent', 'dubaiindex', 'DuplexWeb-Google', 'DynatraceSynthetic', 'EARTHCOM', 'Easy-Thumb', 'EasyDL', 'Ebingbong', 'ec2linkfinder', 'eCairn-Grabber', 'eCatch', 'ECCP', 'eContext\/', 'Ecxi', 'EirGrabber', 'ElectricMonk', 'elefent', 'EMail Exractor', 'EMail Wolf', 'EmailWolf', 'Embarcadero', 'Embed PHP Library', 'Embedly', 'endo\/', 'europarchive\.org', 'evc-batch', 'EventMachine HttpClient', 'Everwall Link Expander', 'Evidon', 'Evrinid', 'ExactSearch', 'ExaleadCloudview', 'Excel\/', 'exif', 'ExoRank', 'Exploratodo', 'Express WebPictures', 'Extreme Picture Finder', 'EyeNetIE', 'ezooms', 'facebookexternalhit', 'facebookexternalua', 'facebookplatform', 'fairshare', 'Faraday v', 'fasthttp', 'Faveeo', 'Favicon downloader', 'faviconarchive', 'faviconkit', 'FavOrg', 'Feed Wrangler', 'Feedable\/', 'Feedbin', 'FeedBooster', 'FeedBucket', 'FeedBunch\/', 'FeedBurner', 'feeder', 'Feedly', 'FeedshowOnline', 'Feedshow\/', 'Feedspot', 'FeedViewer\/', 'Feedwind\/', 'FeedZcollector', 'feeltiptop', 'Fetch API', 'Fetch\/[0-9]', 'Fever\/[0-9]', 'FHscan', 'Fiery%20Feeds', 'Filestack', 'Fimap', 'findlink', 'findthatfile', 'FlashGet', 'FlipboardBrowserProxy', 'FlipboardProxy', 'FlipboardRSS', 'Flock\/', 'Florienzh\/', 'fluffy', 'Flunky', 'flynxapp', 'forensiq', 'ForusP', 'FoundSeoTool', 'fragFINN\.de', 'free thumbnails', 'Freeuploader', 'FreshRSS', 'frontman', 'Funnelback', 'Fuzz Faster U Fool', 'G-i-g-a-b-o-t', 'g00g1e\.net', 'ganarvisitas', 'gdnplus\.com', 'geek-tools', 'Genieo', 'GentleSource', 'GetCode', 'Getintent', 'GetLinkInfo', 'getprismatic', 'GetRight', 'getroot', 'GetURLInfo\/', 'GetWeb', 'Geziyor', 'Ghost Inspector', 'GigablastOpenSource', 'GIS-LABS', 'github-camo', 'GitHub-Hookshot', 'github\.com', 'Go http package', 'Go [\d\.]* package http', 'Go!Zilla', 'Go-Ahead-Got-It', 'Go-http-client', 'go-mtasts\/', 'gobuster', 'gobyus', 'Gofeed', 'gofetch', 'Goldfire Server', 'GomezAgent', 'gooblog', 'Goodzer\/', 'Google AppsViewer', 'Google Desktop', 'Google favicon', 'Google Keyword Suggestion', 'Google Keyword Tool', 'Google Page Speed Insights', 'Google PP Default', 'Google Search Console', 'Google Web Preview', 'Google-Ads', 'Google-Adwords', 'Google-Apps-Script', 'Google-Calendar-Importer', 'Google-HotelAdsVerifier', 'Google-HTTP-Java-Client', 'Google-InspectionTool', 'Google-Podcast', 'Google-Publisher-Plugin', 'Google-Read-Aloud', 'Google-SearchByImage', 'Google-Site-Verification', 'Google-SMTP-STS', 'Google-speakr', 'Google-Structured-Data-Testing-Tool', 'Google-Transparency-Report', 'google-xrawler', 'Google-Youtube-Links', 'GoogleDocs', 'GoogleHC\/', 'GoogleProber', 'GoogleProducer', 'GoogleSites', 'Gookey', 'GoSpotCheck', 'gosquared-thumbnailer', 'Gotit', 'GoZilla', 'grabify', 'GrabNet', 'Grafula', 'Grammarly', 'GrapeFX', 'GreatNews', 'Gregarius', 'GRequests', 'grokkit', 'grouphigh', 'grub-client', 'gSOAP\/', 'GT::WWW', 'GTmetrix', 'GuzzleHttp', 'gvfs\/', 'HAA(A)?RTLAND http client', 'Haansoft', 'hackney\/', 'Hadi Agent', 'HappyApps-WebCheck', 'Hardenize', 'Hatena', 'Havij', 'HaxerMen', 'HeadlessChrome', 'HEADMasterSEO', 'HeartRails_Capture', 'help@dataminr\.com', 'heritrix', 'Hexometer', 'historious', 'hkedcity', 'hledejLevne\.cz', 'Hloader', 'HMView', 'Holmes', 'HonesoSearchEngine', 'HootSuite Image proxy', 'Hootsuite-WebFeed', 'hosterstats', 'HostTracker', 'ht:\/\/check', 'htdig', 'HTMLparser', 'htmlyse', 'HTTP Banner Detection', 'http-get', 'HTTP-Header-Abfrage', 'http-kit', 'http-request\/', 'HTTP-Tiny', 'HTTP::Lite', 'http:\/\/www.neomo.de\/', //'Francis [Bot]' 'HttpComponents', 'httphr', 'HTTPie', 'HTTPMon', 'httpRequest', 'httpscheck', 'httpssites_power', 'httpunit', 'HttpUrlConnection', 'http\.rb\/', 'HTTP_Compression_Test', 'http_get', 'http_request2', 'http_requester', 'httrack', 'huaweisymantec', 'HubSpot ', 'HubSpot-Link-Resolver', 'Humanlinks', 'i2kconnect\/', 'Iblog', 'ichiro', 'Id-search', 'IdeelaborPlagiaat', 'IDG Twitter Links Resolver', 'IDwhois\/', 'Iframely', 'igdeSpyder', 'iGooglePortal', 'IlTrovatore', 'Image Fetch', 'Image Sucker', 'ImageEngine\/', 'ImageVisu\/', 'Imagga', 'imagineeasy', 'imgsizer', 'InAGist', 'inbound\.li parser', 'InDesign%20CC', 'Indy Library', 'InetURL', 'infegy', 'infohelfer', 'InfoTekies', 'InfoWizards Reciprocal Link', 'inpwrd\.com', 'instabid', 'Instapaper', 'Integrity', 'integromedb', 'Intelliseek', 'InterGET', 'Internet Ninja', 'InternetSeer', 'internetVista monitor', 'internetwache', 'internet_archive', 'intraVnews', 'IODC', 'IOI', 'Inboxb0t', 'iplabel', 'ips-agent', 'IPS\/[0-9]', 'IPWorks HTTP\/S Component', 'iqdb\/', 'Iria', 'Irokez', 'isitup\.org', 'iskanie', 'isUp\.li', 'iThemes Sync\/', 'IZaBEE', 'iZSearch', 'JAHHO', 'janforman', 'Jaunt\/', 'Java.*outbrain', 'javelin\.io', 'Jbrofuzz', 'Jersey\/', 'JetCar', 'Jigsaw', 'Jobboerse', 'JobFeed discovery', 'Jobg8 URL Monitor', 'jobo', 'Jobrapido', 'Jobsearch1\.5', 'JoinVision Generic', 'JolokiaPwn', 'Joomla', 'Jorgee', 'JS-Kit', 'JungleKeyThumbnail', 'JustView', 'Kaspersky Lab CFR link resolver', 'Kelny\/', 'Kerrigan\/', 'KeyCDN', 'Keyword Density', 'Keywords Research', 'khttp\/', 'KickFire', 'KimonoLabs\/', 'Kml-Google', 'knows\.is', 'KOCMOHABT', 'kouio', 'kube-probe', 'kubectl', 'kulturarw3', 'KumKie', 'Larbin', 'Lavf\/', 'leakix\.net', 'LeechFTP', 'LeechGet', 'letsencrypt', 'Lftp', 'LibVLC', 'LibWeb', 'Libwhisker', 'libwww', 'Licorne', 'Liferea\/', 'Lighthouse', 'Lightspeedsystems', 'Likse', 'limber\.io', 'Link Valet', 'LinkAlarm\/', 'LinkAnalyser', 'link-check', 'linkCheck', 'linkdex', 'LinkExaminer', 'linkfluence', 'linkpeek', 'LinkPreview', 'LinkScan', 'LinksManager', 'LinkTiger', 'LinkWalker', 'link_thumbnailer', 'Lipperhey', 'Litemage_walker', 'livedoor ScreenShot', 'LoadImpactRload', 'localsearch-web', 'LongURL API', 'longurl-r-package', 'looid\.com', 'looksystems\.net', 'ltx71', 'lua-resty-http', 'Lucee \(CFML Engine\)', 'Lush Http Client', 'lwp-request', 'lwp-trivial', 'LWP::Simple', 'lycos', 'LYT\.SR', 'L\.webis', 'mabontland', 'MacOutlook\/', 'Mag-Net', 'MagpieRSS', 'Mail::STS', 'MailChimp', 'Mail\.Ru', 'Majestic12', 'makecontact\/', 'Mandrill', 'MapperCmd', 'marketinggrader', 'MarkMonitor', 'MarkWatch', 'Mass Downloader', 'masscan\/', 'Mata Hari', 'mattermost', 'Mediametric', 'Mediapartners-Google', 'mediawords', 'MegaIndex\.ru', 'MeltwaterNews', 'Melvil Rawi', 'MemGator', 'Metaspinner', 'MetaURI', 'MFC_Tear_Sample', 'Microsearch', 'Microsoft Data Access', 'Microsoft Office', 'Microsoft Outlook', 'Microsoft Windows Network Diagnostics', 'Microsoft-WebDAV-MiniRedir', 'Microsoft\.Data\.Mashup', 'MIDown tool', 'MIIxpc', 'Mindjet', 'Miniature\.io', 'Miniflux', 'mio_httpc', 'Miro-HttpClient', 'Mister PiX', 'mixdata dot com', 'mixed-content-scan', 'mixnode', 'Mnogosearch', 'mogimogi', 'Mojeek', 'Mojolicious \(Perl\)', 'Mollie', 'monitis', 'Monitority\/', 'Monit\/', 'montastic', 'MonTools', 'Moreover', 'Morfeus Fucking Scanner', 'Morning Paper', 'MovableType', 'mowser', 'Mrcgiguy', 'Mr\.4x3 Powered', 'MS Web Services Client Protocol', 'MSFrontPage', 'mShots', 'MuckRack\/', 'muhstik-scan', 'MVAClient', 'MxToolbox\/', 'myseosnapshot', 'nagios', 'Najdi\.si', 'Name Intelligence', 'NameFo\.com', 'Nameprotect', 'nationalarchives', 'Navroad', 'nbertaupete95', 'NearSite', 'Needle', 'Nessus', 'Net Vampire', 'NetAnts', 'NETCRAFT', 'NetLyzer', 'NetMechanic', 'NetNewsWire', 'Netpursual', 'netresearch', 'NetShelter ContentScan', 'Netsparker', 'NetSystemsResearch', 'nettle', 'NetTrack', 'Netvibes', 'NetZIP', 'Neustar WPM', 'NeutrinoAPI', 'NewRelicPinger', 'NewsBlur .*Finder', 'NewsGator', 'newsme', 'newspaper\/', 'Nexgate Ruby Client', 'NG-Search', 'nghttp2', 'Nibbler', 'NICErsPRO', 'NihilScio', 'Nikto', 'nineconnections', 'NLNZ_IAHarvester', 'Nmap Scripting Engine', 'node-fetch', 'node-superagent', 'node-urllib', 'Nodemeter', 'NodePing', 'node\.io', 'nominet\.org\.uk', 'nominet\.uk', 'Norton-Safeweb', 'Notifixious', 'notifyninja', 'NotionEmbedder', 'nuhk', 'nutch', 'Nuzzel', 'nWormFeedFinder', 'nyawc\/', 'Nymesis', 'NYU', 'Observatory\/', 'Ocelli\/', 'Octopus', 'oegp', 'Offline Explorer', 'Offline Navigator', 'OgScrper', 'okhttp', 'omgili', 'OMSC', 'Online Domain Tools', 'Open Source RSS', 'OpenCalaisSemanticProxy', 'Openfind', 'OpenLinkProfiler', 'Openstat\/', 'OpenVAS', 'OPPO A33', 'Optimizer', 'Orbiter', 'OrgProbe\/', 'orion-semantics', 'Outlook-Express', 'Outlook-iOS', 'Owler', 'Owlin', 'ownCloud News', 'ow\.ly', 'OxfordCloudService', 'page scorer', 'Page Valet', 'page2rss', 'PageFreezer', 'PageGrabber', 'PagePeeker', 'PageScorer', 'Pagespeed\/', 'PageThing', 'page_verifier', 'Panopta', 'panscient', 'Papa Foto', 'parsijoo', 'Pavuk', 'PayPal IPN', 'pcBrowser', 'Pcore-HTTP', 'PDF24 URL To PDF', 'Pearltrees', 'PECL::HTTP', 'peerindex', 'Peew', 'PeoplePal', 'Perlu -', 'PhantomJS Screenshoter', 'PhantomJS\/', 'Photon\/', 'php-requests', 'phpservermon', 'Pi-Monster', 'Picscout', 'Picsearch', 'PictureFinder', 'Pimonster', 'Pingability', 'PingAdmin\.Ru', 'Pingdom', 'Pingoscope', 'PingSpot', 'ping\.blo\.gs', 'pinterest\.com', 'Pixray', 'Pizilla', 'Plagger\/', 'Pleroma ', 'Ploetz \+ Zeller', 'Plukkie', 'plumanalytics', 'PocketImageCache', 'PocketParser', 'Pockey', 'PodcastAddict\/', 'POE-Component-Client-HTTP', 'Polymail\/', 'Pompos', 'Porkbun', 'Port Monitor', 'postano', 'postfix-mta-sts-resolver', 'PostmanRuntime', 'postplanner\.com', 'PostPost', 'postrank', 'PowerPoint\/', 'Prebid', 'Prerender', 'Priceonomics Analysis Engine', 'PrintFriendly', 'PritTorrent', 'Prlog', 'probethenet', 'Project ?25499', 'Project-Resonance', 'prospectb2b', 'Protopage', 'ProWebWalker', 'proximic', 'PRTG Network Monitor', 'pshtt, https scanning', 'PTST ', 'PTST\/[0-9]+', 'Pump', 'Python-httplib2', 'python-httpx', 'python-requests', 'Python-urllib', 'Qirina Hurdler', 'QQDownload', 'QrafterPro', 'Qseero', 'Qualidator', 'QueryN Metasearch', 'queuedriver', 'quic-go-HTTP\/', 'QuiteRSS', 'Quora Link Preview', 'Qwantify', 'Radian6', 'RadioPublicImageResizer', 'Railgun\/', 'RankActive', 'RankFlex', 'RankSonicSiteAuditor', 'RapidLoad\/', 'Re-re Studio', 'ReactorNetty', 'Readability', 'RealDownload', 'RealPlayer%20Downloader', 'RebelMouse', 'Recorder', 'RecurPost\/', 'redback\/', 'ReederForMac', 'Reeder\/', 'ReGet', 'RepoMonkey', 'request\.js', 'reqwest\/', 'ResponseCodeTest', 'RestSharp', 'Riddler', 'Rival IQ', 'Robosourcer', 'Robozilla', 'ROI Hunter', 'RPT-HTTPClient', 'RSSMix\/', 'RSSOwl', 'RyowlEngine', 'safe-agent-scanner', 'SalesIntelligent', 'Saleslift', 'SAP NetWeaver Application Server', 'SauceNAO', 'SBIder', 'sc-downloader', 'scalaj-http', 'Scamadviser-Frontend', 'ScanAlert', 'scan\.lol', 'Scoop', 'scooter', 'ScopeContentAG-HTTP-Client', 'ScoutJet', 'ScoutURLMonitor', 'ScrapeBox Page Scanner', 'Scrapy', 'Screaming', 'ScreenShotService', 'Scrubby', 'Scrutiny\/', 'Search37', 'searchenginepromotionhelp', 'Searchestate', 'SearchExpress', 'SearchSight', 'SearchWP', 'search\.thunderstone', 'Seeker', 'semanticdiscovery', 'semanticjuice', 'Semiocast HTTP client', 'Semrush', 'Sendsay\.Ru', 'sentry\/', 'SEO Browser', 'Seo Servis', 'seo-nastroj\.cz', 'seo4ajax', 'Seobility', 'SEOCentro', 'SeoCheck', 'seocompany', 'SEOkicks', 'SEOlizer', 'Seomoz', 'SEOprofiler', 'seoscanners', 'SEOsearch', 'seositecheckup', 'SEOstats', 'servernfo', 'sexsearcher', 'Seznam', 'Shelob', 'Shodan', 'Shoppimon', 'ShopWiki', 'ShortLinkTranslate', 'shortURL lengthener', 'shrinktheweb', 'Sideqik', 'Siege', 'SimplePie', 'SimplyFast', 'Siphon', 'SISTRIX', 'Site Sucker', 'Site-Shot\/', 'Site24x7', 'SiteBar', 'Sitebeam', 'Sitebulb\/', 'SiteCondor', 'SiteExplorer', 'SiteGuardian', 'Siteimprove', 'SiteIndexed', 'Sitemap(s)? Generator', 'SitemapGenerator', 'SiteMonitor', 'Siteshooter B0t', 'SiteSnagger', 'SiteSucker', 'SiteTruth', 'Sitevigil', 'sitexy\.com', 'SkypeUriPreview', 'Slack\/', 'sli-systems\.com', 'slider\.com', 'slurp', 'SlySearch', 'SmartDownload', 'SMRF URL Expander', 'SMUrlExpander', 'Snake', 'Snappy', 'SnapSearch', 'Snarfer\/', 'SniffRSS', 'sniptracker', 'Snoopy', 'SnowHaze Search', 'sogou web', 'SortSite', 'Sottopop', 'sovereign\.ai', 'SpaceBison', 'SpamExperts', 'Spammen', 'Spanner', 'Spawning-AI', 'spaziodati', 'SPDYCheck', 'Specificfeeds', 'SpeedKit', 'speedy', 'SPEng', 'Spinn3r', 'spray-can', 'Sprinklr ', 'spyonweb', 'sqlmap', 'Sqlworm', 'Sqworm', 'SSL Labs', 'ssl-tools', 'StackRambler', 'Statastico\/', 'Statically-', 'StatusCake', 'Steeler', 'Stratagems Kumo', 'Stripe\/', 'Stroke\.cz', 'StudioFACA', 'StumbleUpon', 'suchen', 'Sucuri', 'summify', 'SuperHTTP', 'Surphace Scout', 'Suzuran', 'swcd ', 'Symfony BrowserKit', 'Symfony2 BrowserKit', 'Synapse\/', 'Syndirella\/', 'SynHttpClient-Built', 'Sysomos', 'sysscan', 'Szukacz', 'T0PHackTeam', 'tAkeOut', 'Tarantula\/', 'Taringa UGC', 'TarmotGezgin', 'tchelebi\.io', 'techiaith\.cymru', 'Teleport', 'Telesoft', 'Telesphoreo', 'Telesphorep', 'Tenon\.io', 'teoma', 'terrainformatica', 'Test Certificate Info', 'testuri', 'Tetrahedron', 'TextRazor Downloader', 'The Drop Reaper', 'The Expert HTML Source Viewer', 'The Intraformant', 'The Knowledge AI', 'theinternetrules', 'TheNomad', 'Thinklab', 'Thumbor', 'Thumbshots', 'ThumbSniper', 'timewe\.net', 'TinEye', 'Tiny Tiny RSS', 'TLSProbe\/', 'Toata', 'topster', 'touche\.com', 'Traackr\.com', 'tracemyfile', 'Trackuity', 'TrapitAgent', 'Trendiction', 'Trendsmap', 'trendspottr', 'truwoGPS', 'TryJsoup', 'TulipChain', 'Turingos', 'Turnitin', 'tweetedtimes', 'Tweetminster', 'Tweezler\/', 'twibble', 'Twice', 'Twikle', 'Twingly', 'Twisted PageGetter', 'Typhoeus', 'ubermetrics-technologies', 'uclassify', 'UdmSearch', 'ultimate_sitemap_parser', 'unchaos', 'unirest-java', 'UniversalFeedParser', 'unshortenit', 'Unshorten\.It', 'Untiny', 'UnwindFetchor', 'updated', 'updown\.io daemon', 'Upflow', 'Uptimia', 'URL Verifier', 'Urlcheckr', 'URLitor', 'urlresolver', 'Urlstat', 'URLTester', 'UrlTrends Ranking Updater', 'URLy Warning', 'URLy\.Warning', 'URL\/Emacs', 'Vacuum', 'Vagabondo', 'VB Project', 'vBSEO', 'VCI', 'via ggpht\.com GoogleImageProxy', 'Virusdie', 'visionutils', 'Visual Rights Group', 'vkShare', 'VoidEYE', 'Voil', 'voltron', 'voyager\/', 'VSAgent\/', 'VSB-TUO\/', 'Vulnbusters Meter', 'VYU2', 'w3af\.org', 'W3C-checklink', 'W3C-mobileOK', 'W3C_Unicorn', 'WAC-OFU', 'WakeletLinkExpander', 'WallpapersHD', 'Wallpapers\/[0-9]+', 'wangling', 'Wappalyzer', 'WatchMouse', 'WbSrch\/', 'WDT\.io', 'Web Auto', 'Web Collage', 'Web Enhancer', 'Web Fetch', 'Web Fuck', 'Web Pix', 'Web Sauger', 'Web spyder', 'Web Sucker', 'web-capture\.net', 'Web-sniffer', 'Webalta', 'Webauskunft', 'WebAuto', 'WebCapture', 'WebClient\/', 'webcollage', 'WebCookies', 'WebCopier', 'WebCorp', 'WebDataStats', 'WebDoc', 'WebEnhancer', 'WebFetch', 'WebFuck', 'WebGazer', 'WebGo IS', 'WebImageCollector', 'WebImages', 'WebIndex', 'webkit2png', 'WebLeacher', 'webmastercoffee', 'webmon ', 'WebPix', 'WebReaper', 'WebSauger', 'webscreenie', 'Webshag', 'Webshot', 'Website Quester', 'websitepulse agent', 'WebsiteQuester', 'Websnapr', 'WebSniffer', 'Webster', 'WebStripper', 'WebSucker', 'webtech\/', 'WebThumbnail', 'Webthumb\/', 'WebWhacker', 'WebZIP', 'WeLikeLinks', 'WEPA', 'WeSEE', 'wf84', 'Wfuzz\/', 'wget', 'WhatCMS', 'WhatsApp', 'WhatsMyIP', 'WhatWeb', 'WhereGoes\?', 'Whibse', 'WhoAPI\/', 'WhoRunsCoinHive', 'Whynder Magnet', 'Windows-RSS-Platform', 'WinHttp-Autoproxy-Service', 'WinHTTP\/', 'WinPodder', 'wkhtmlto', 'wmtips', 'Woko', 'Wolfram HTTPClient', 'woorankreview', 'WordPress\/', 'WordupinfoSearch', 'Word\/', 'worldping-api', 'wotbox', 'WP Engine Install Performance API', 'wpif', 'wprecon\.com survey', 'WPScan', 'wscheck', 'Wtrace', 'WWW-Collector-E', 'WWW-Mechanize', 'WWW::Document', 'WWW::Mechanize', 'WWWOFFLE', 'www\.monitor\.us', 'x09Mozilla', 'x22Mozilla', 'XaxisSemanticsClassifier', 'XenForo\/', 'Xenu Link Sleuth', 'XING-contenttabreceiver', 'xpymep([0-9]?)\.exe', 'Y!J-[A-Z][A-Z][A-Z]', 'Yaanb', 'yacy', 'Yahoo Link Preview', 'YahooCacheSystem', 'YahooMailProxy', 'YahooYSMcm', 'YandeG', 'Yandex(?!Search)', 'yanga', 'yeti', 'Yo-yo', 'Yoleo Consumer', 'yomins\.com', 'yoogliFetchAgent', 'YottaaMonitor', 'Your-Website-Sucks', 'yourls\.org', 'YoYs\.net', 'YP\.PL', 'Zabbix', 'Zade', 'Zao', 'Zauba', 'Zemanta Aggregator', 'Zend\\\\Http\\\\Client', 'Zend_Http_Client', 'Zermelo', 'Zeus ', 'zgrab', 'ZnajdzFoto', 'ZnHTTP', 'Zombie\.js', 'Zoom\.Mac', 'ZoteroTranslationServer', 'ZyBorg', '[a-z0-9\-_]*(bot|crawl|archiver|transcoder|spider|uptime|validator|fetcher|cron|checker|reader|extractor|monitoring|analyzer|scraper)' ]; } /** * Return the list of strings to remove from the user agent before running the crawler regex. * * @return array */ public function get_exclusions_list() { return [ 'Safari.[\d\.]*', 'Firefox.[\d\.]*', ' Chrome.[\d\.]*', 'Chromium.[\d\.]*', 'MSIE.[\d\.]', 'Opera\/[\d\.]*', 'Mozilla.[\d\.]*', 'AppleWebKit.[\d\.]*', 'Trident.[\d\.]*', 'Windows NT.[\d\.]*', 'Android [\d\.]*', 'Macintosh.', 'Ubuntu', 'Linux', '[ ]Intel', 'Mac OS X [\d_]*', '(like )?Gecko(.[\d\.]*)?', 'KHTML,', 'CriOS.[\d\.]*', 'CPU iPhone OS ([0-9_])* like Mac OS X', 'CPU OS ([0-9_])* like Mac OS X', 'iPod', 'compatible', 'x86_..', 'i686', 'x64', 'X11', 'rv:[\d\.]*', 'Version.[\d\.]*', 'WOW64', 'Win64', 'Dalvik.[\d\.]*', ' \.NET CLR [\d\.]*', 'Presto.[\d\.]*', 'Media Center PC', 'BlackBerry', 'Build', 'Opera Mini\/\d{1,2}\.\d{1,2}\.[\d\.]*\/\d{1,2}\.', 'Opera', ' \.NET[\d\.]*', 'cubot', '; M bot', '; CRONO', '; B bot', '; IDbot', '; ID bot', '; POWER BOT', 'OCTOPUS-CORE' ]; } /** * Return all possible HTTP headers that represent the User-Agent string. * * @return array */ public function get_headers_list() { return [ // The default User-Agent string. 'HTTP_USER_AGENT', // Header can occur on devices using Opera Mini. 'HTTP_X_OPERAMINI_PHONE_UA', // Vodafone specific header: http://www.seoprinciple.com/mobile-web-community-still-angry-at-vodafone/24/ 'HTTP_X_DEVICE_USER_AGENT', 'HTTP_X_ORIGINAL_USER_AGENT', 'HTTP_X_SKYFIRE_PHONE', 'HTTP_X_BOLT_PHONE_UA', 'HTTP_DEVICE_STOCK_UA', 'HTTP_X_UCBROWSER_DEVICE_UA', // Sometimes, bots (especially Google) use a genuine user agent, but fill this header in with their email address 'HTTP_FROM', 'HTTP_X_SCANNER' // Seen in use by Netsparker ]; } } includes/class-widgets.php000064400000026437147206624130011655 0ustar00 __( 'Displays a list of the most viewed posts', 'post-views-counter' ) ] ); // default settings $this->pvc_defaults = [ 'title' => __( 'Most Viewed Posts', 'post-views-counter' ), 'number_of_posts' => 5, 'thumbnail_size' => 'thumbnail', 'post_type' => [], 'order' => 'desc', 'list_type' => 'unordered', 'show_post_views' => true, 'show_post_thumbnail' => false, 'show_post_excerpt' => false, 'show_post_author' => false, 'no_posts_message' => __( 'No most viewed posts found', 'post-views-counter' ) ]; // order types $this->pvc_order_types = [ 'asc' => __( 'Ascending', 'post-views-counter' ), 'desc' => __( 'Descending', 'post-views-counter' ) ]; // list types $this->pvc_list_types = [ 'unordered' => __( 'Unordered list', 'post-views-counter' ), 'ordered' => __( 'Ordered list', 'post-views-counter' ) ]; // image sizes $this->pvc_image_sizes = array_merge( [ 'full' ], get_intermediate_image_sizes() ); // sort image sizes by name, ascending sort( $this->pvc_image_sizes, SORT_STRING ); } /** * Display widget. * * @param array $args * @param array $instance * @return void */ public function widget( $args, $instance ) { // empty title? if ( empty( $instance['title'] ) ) $instance['title'] = $this->pvc_defaults['title']; // filter title $instance['title'] = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ); $html = $args['before_widget'] . ( ! empty( $instance['title'] ) ? $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'] : '' ); $html .= pvc_most_viewed_posts( $instance, false ); $html .= $args['after_widget']; echo $html; } /** Render widget form. * * @param array $instance * @return void */ public function form( $instance ) { $html = '


'; // post types foreach ( Post_Views_Counter()->functions->get_post_types() as $post_type => $post_type_name ) { $html .= ' '; } $show_post_thumbnail = isset( $instance['show_post_thumbnail'] ) ? $instance['show_post_thumbnail'] : $this->pvc_defaults['show_post_thumbnail']; $html .= '

pvc_defaults['show_post_views'] ), false ) . ' />
pvc_defaults['show_post_excerpt'] ), false ) . ' />
pvc_defaults['show_post_author'] ), false ) . ' />

'; echo $html; } /** * Save widget form. * * @param array $new_instance * @param array $old_instance * @return array */ public function update( $new_instance, $old_instance ) { // number of posts $old_instance['number_of_posts'] = (int) (isset( $new_instance['number_of_posts'] ) ? $new_instance['number_of_posts'] : $this->pvc_defaults['number_of_posts']); // order $old_instance['order'] = isset( $new_instance['order'] ) && in_array( $new_instance['order'], array_keys( $this->pvc_order_types ), true ) ? $new_instance['order'] : $this->pvc_defaults['order']; // list type $old_instance['list_type'] = isset( $new_instance['list_type'] ) && in_array( $new_instance['list_type'], array_keys( $this->pvc_list_types ), true ) ? $new_instance['list_type'] : $this->pvc_defaults['list_type']; // thumbnail size $old_instance['thumbnail_size'] = isset( $new_instance['thumbnail_size'] ) && in_array( $new_instance['thumbnail_size'], $this->pvc_image_sizes, true ) ? $new_instance['thumbnail_size'] : $this->pvc_defaults['thumbnail_size']; // booleans $old_instance['show_post_views'] = ! empty( $new_instance['show_post_views'] ); $old_instance['show_post_thumbnail'] = ! empty( $new_instance['show_post_thumbnail'] ); $old_instance['show_post_excerpt'] = ! empty( $new_instance['show_post_excerpt'] ); $old_instance['show_post_author'] = ! empty( $new_instance['show_post_author'] ); // texts $old_instance['title'] = sanitize_text_field( isset( $new_instance['title'] ) ? $new_instance['title'] : $this->pvc_defaults['title'] ); $old_instance['no_posts_message'] = sanitize_text_field( isset( $new_instance['no_posts_message'] ) ? $new_instance['no_posts_message'] : $this->pvc_defaults['no_posts_message'] ); // post types if ( isset( $new_instance['post_type'] ) ) { $post_types = []; // get post types $_post_types = Post_Views_Counter()->functions->get_post_types(); foreach ( $new_instance['post_type'] as $post_type ) { if ( isset( $_post_types[$post_type] ) ) $post_types[] = $post_type; } $old_instance['post_type'] = array_unique( $post_types ); } else $old_instance['post_type'] = [ 'post' ]; return $old_instance; } } includes/functions.php000064400000057701147206624130011112 0ustar00 'type = 4' ]; // update where clause $where = apply_filters( 'pvc_get_post_views_period_where', $where, $period, $post_id ); // updated where clause $_where = []; // sanitize where clause foreach ( $where as $index => $value ) { if ( $index === 'type' || $index === 'content' ) $_where[$index] = preg_replace( '/[^0-9]/', '', $value ); elseif ( $index === 'period' ) { $values = preg_match_all( '/\d+/', $value, $matches ); // any values? if ( $values !== false && $values > 0 ) $_where['period'] = $matches[0]; } } // get current number of ids $ids_count = count( $numbers ); $where_clause = ''; // validate where clause foreach( $_where as $index => $value ) { if ( $index === 'type' ) { $where_clause .= ' AND type = %d'; $numbers[] = (int) $value; } elseif ( $index === 'content' ) { $where_clause .= ' AND content = %d'; $numbers[] = (int) $value; } elseif ( $index === 'period' ) { $nop = count( $_where['period'] ); if ( $nop === 1 ) { $where_clause .= ' AND CAST( period AS SIGNED ) = %d'; $numbers[] = (int) $_where['period'][0]; } elseif ( $nop === 2 ) { $where_clause .= ' AND CAST( period AS SIGNED ) <= %d AND CAST( period AS SIGNED ) >= %d'; $numbers[] = (int) $_where['period'][0]; $numbers[] = (int) $_where['period'][1]; } } } // prepare query $query = $wpdb->prepare( "SELECT SUM(count) AS views FROM " . $wpdb->prefix . "post_views WHERE id IN (" . implode( ',', array_fill( 0, $ids_count, '%d' ) ) . ")" . $where_clause, $numbers ); // calculate query hash $query_hash = md5( $query ); // get cached data $post_views = wp_cache_get( $query_hash, 'pvc-get_post_views' ); // cached data not found? if ( $post_views === false ) { // get post views $post_views = (int) $wpdb->get_var( $query ); // set the cache expiration, 5 minutes by default $expire = absint( apply_filters( 'pvc_object_cache_expire', 300 ) ); // add cached post views wp_cache_add( $query_hash, $post_views, 'pvc-get_post_views', $expire ); } return (int) apply_filters( 'pvc_get_post_views', $post_views, $post_id, $period ); } } /** * Get views query. * * @global object $wpdb * * @param array $args * @return int|array */ if ( ! function_exists( 'pvc_get_views' ) ) { function pvc_get_views( $args = [] ) { global $wpdb; $range = []; $defaults = [ 'fields' => 'views', 'post_id' => '', 'post_type' => '', 'views_query' => [ 'year' => '', 'month' => '', 'week' => '', 'day' => '', 'after' => '', // string or array 'before' => '', // string or array 'inclusive' => true ] ]; // merge default options with new arguments $args = array_merge( $defaults, $args ); // check views query if ( ! is_array( $args['views_query'] ) ) $args['views_query'] = $defaults['views_query']; // merge views query too $args['views_query'] = array_merge( $defaults['views_query'], $args['views_query'] ); // filter arguments $args = apply_filters( 'pvc_get_views_args', $args ); // check post types if ( is_string( $args['post_type'] ) ) $args['post_type'] = [ $args['post_type'] ]; elseif ( ! is_array( $args['post_type'] ) ) $args['post_type'] = []; // get number of post types $post_types_count = count( $args['post_type'] ); // check post ids if ( is_array( $args['post_id'] ) && ! empty( $args['post_id'] ) ) $args['post_id'] = array_filter( array_unique( array_map( 'intval', $args['post_id'] ) ) ); elseif ( is_string( $args['post_id'] ) || is_numeric( $args['post_id'] ) ) { $post_id = (int) $args['post_id']; if ( $post_id === 0 ) $args['post_id'] = []; else $args['post_id'] = [ $post_id ]; } else $args['post_id'] = []; // get number of post ids $post_ids_count = count( $args['post_id'] ); // placeholder for empty query data $query_data = [ 1 ]; // set query data if ( $post_ids_count === 0 && $post_types_count === 0 ) $query_data = [ 1 ]; elseif ( $post_ids_count === 0 ) $query_data = array_merge( $query_data, array_values( $args['post_type'] ) ); elseif ( $post_types_count === 0 ) $query_data = array_merge( $query_data, array_values( $args['post_id'] ) ); else $query_data = array_merge( $query_data, array_values( $args['post_id'] ), array_values( $args['post_type'] ) ); // check fields if ( ! in_array( $args['fields'], [ 'views', 'date=>views' ], true ) ) $args['fields'] = $defaults['fields']; $query_chunks = []; $views_query = ''; // views query after/before parameters work only when fields == views if ( $args['fields'] === 'views' ) { // check views query inclusive if ( ! isset( $args['views_query']['inclusive'] ) ) $args['views_query']['inclusive'] = $defaults['views_query']['inclusive']; else $args['views_query']['inclusive'] = (bool) $args['views_query']['inclusive']; // check after and before dates foreach ( [ 'after' => '>', 'before' => '<' ] as $date => $type ) { $year_ = null; $month_ = null; $week_ = null; $day_ = null; // check views query date if ( ! empty( $args['views_query'][$date] ) ) { // is it a date array? if ( is_array( $args['views_query'][$date] ) ) { // check views query $date date year if ( ! empty( $args['views_query'][$date]['year'] ) ) $year_ = str_pad( (int) $args['views_query'][$date]['year'], 4, 0, STR_PAD_LEFT ); // check views query date month if ( ! empty( $args['views_query'][$date]['month'] ) ) $month_ = str_pad( (int) $args['views_query'][$date]['month'], 2, 0, STR_PAD_LEFT ); // check views query date week if ( ! empty( $args['views_query'][$date]['week'] ) ) $week_ = str_pad( (int) $args['views_query'][$date]['week'], 2, 0, STR_PAD_LEFT ); // check views query date day if ( ! empty( $args['views_query'][$date]['day'] ) ) $day_ = str_pad( (int) $args['views_query'][$date]['day'], 2, 0, STR_PAD_LEFT ); // is it a date string? } elseif ( is_string( $args['views_query'][$date] ) ) { $time_ = strtotime( $args['views_query'][$date] ); // valid datetime? if ( $time_ !== false ) { // week does not exists here, string dates are always treated as year + month + day list( $day_, $month_, $year_ ) = explode( ' ', date( "d m Y", $time_ ) ); } } // valid date? if ( ! ( $year_ === null && $month_ === null && $week_ === null && $day_ === null ) ) { $query_chunks[] = [ 'year' => $year_, 'month' => $month_, 'day' => $day_, 'week' => $week_, 'type' => $type . ( $args['views_query']['inclusive'] ? '=' : '' ) ]; } } } if ( ! empty( $query_chunks ) ) { $valid_dates = true; // after and before? if ( count( $query_chunks ) === 2 ) { // before and after dates should be the same foreach ( [ 'year', 'month', 'day', 'week' ] as $date_type ) { if ( ! ( ( $query_chunks[0][$date_type] !== null && $query_chunks[1][$date_type] !== null ) || ( $query_chunks[0][$date_type] === null && $query_chunks[1][$date_type] === null ) ) ) $valid_dates = false; } } if ( $valid_dates ) { foreach ( $query_chunks as $chunk ) { // year if ( isset( $chunk['year'] ) ) { // year, week if ( isset( $chunk['week'] ) ) $views_query .= " AND pvc.type = 1 AND CAST( pvc.period AS SIGNED ) " . $chunk['type'] . " " . (int) ( $chunk['year'] . $chunk['week'] ); // year, month elseif ( isset( $chunk['month'] ) ) { // year, month, day if ( isset( $chunk['day'] ) ) $views_query .= " AND pvc.type = 0 AND CAST( pvc.period AS SIGNED ) " . $chunk['type'] . " " . (int) ( $chunk['year'] . $chunk['month'] . $chunk['day'] ); // year, month else $views_query .= " AND pvc.type = 2 AND CAST( pvc.period AS SIGNED ) " . $chunk['type'] . " " . (int) ( $chunk['year'] . $chunk['month'] ); // year } else $views_query .= " AND pvc.type = 3 AND CAST( pvc.period AS SIGNED ) " . $chunk['type'] . " " . (int) ( $chunk['year'] ); // month } elseif ( isset( $chunk['month'] ) ) { // month, day if ( isset( $chunk['day'] ) ) { $views_query .= " AND pvc.type = 0 AND CAST( RIGHT( pvc.period, 4 ) AS SIGNED ) " . $chunk['type'] . " " . (int) ( $chunk['month'] . $chunk['day'] ); // month } else $views_query .= " AND pvc.type = 2 AND CAST( RIGHT( pvc.period, 2 ) AS SIGNED ) " . $chunk['type'] . " " . (int) ( $chunk['month'] ); // week } elseif ( isset( $chunk['week'] ) ) $views_query .= " AND pvc.type = 1 AND CAST( RIGHT( pvc.period, 2 ) AS SIGNED ) " . $chunk['type'] . " " . (int) ( $chunk['week'] ); // day elseif ( isset( $chunk['day'] ) ) $views_query .= " AND pvc.type = 0 AND CAST( RIGHT( pvc.period, 2 ) AS SIGNED ) " . $chunk['type'] . " " . (int) ( $chunk['day'] ); } } } } $special_views_query = ( $views_query !== '' ); if ( $args['fields'] === 'date=>views' || $views_query === '' ) { // check views query year if ( ! empty( $args['views_query']['year'] ) ) $year = str_pad( (int) $args['views_query']['year'], 4, 0, STR_PAD_LEFT ); // check views query month if ( ! empty( $args['views_query']['month'] ) ) $month = str_pad( (int) $args['views_query']['month'], 2, 0, STR_PAD_LEFT ); // check views query week if ( ! empty( $args['views_query']['week'] ) ) $week = str_pad( (int) $args['views_query']['week'], 2, 0, STR_PAD_LEFT ); // check views query day if ( ! empty( $args['views_query']['day'] ) ) $day = str_pad( (int) $args['views_query']['day'], 2, 0, STR_PAD_LEFT ); // year if ( isset( $year ) ) { // year, week if ( isset( $week ) ) { if ( $args['fields'] === 'date=>views' ) { // create date based on week number $date = new DateTime( $year . 'W' . $week ); // get monday $monday = $date->format( 'd' ); // get month of monday $monday_month = $date->format( 'm' ); // prepare range for( $i = 1; $i <= 6; $i++ ) { $range[(string) ( $date->format( 'Y' ) . $date->format( 'm' ) . $date->format( 'd' ) )] = 0; $date->modify( '+1days' ); } $range[(string) ( $date->format( 'Y' ) . $date->format( 'm' ) . $date->format( 'd' ) )] = 0; // get month of sunday $sunday_month = $date->format( 'm' ); $views_query = " AND pvc.type = 0 AND CAST( pvc.period AS SIGNED ) >= " . (int) ( $year . $monday_month . $monday ) . " AND CAST( pvc.period AS SIGNED ) <= " . (int) ( $date->format( 'Y' ) . $sunday_month . $date->format( 'd' ) ); } else $views_query = " AND pvc.type = 1 AND CAST( pvc.period AS SIGNED ) = " . (int) ( $year . $week ); // year, month } elseif ( isset( $month ) ) { // year, month, day if ( isset( $day ) ) { if ( $args['fields'] === 'date=>views' ) // prepare range $range[(string) ( $year . $month . $day )] = 0; $views_query = " AND pvc.type = 0 AND CAST( pvc.period AS SIGNED ) = " . (int) ( $year . $month . $day ); // year, month } else { if ( $args['fields'] === 'date=>views' ) { // create date $date = new DateTime( $year . '-' . $month . '-01' ); // get last day $last = $date->format( 't' ); // prepare range for( $i = 1; $i <= $last; $i++ ) { $range[(string) ( $year . $month . str_pad( $i, 2, 0, STR_PAD_LEFT ) )] = 0; } $views_query = " AND pvc.type = 0 AND CAST( pvc.period AS SIGNED ) >= " . (int) ( $year . $month ) . "01 AND CAST( pvc.period AS SIGNED ) <= " . (int) ( $year . $month . $last ); } else $views_query = " AND pvc.type = 2 AND CAST( pvc.period AS SIGNED ) = " . (int) ( $year . $month ); } // year } else { if ( $args['fields'] === 'date=>views' ) { // prepare range for( $i = 1; $i <= 12; $i++ ) { $range[(string) ( $year . str_pad( $i, 2, 0, STR_PAD_LEFT ) )] = 0; } // create date $date = new DateTime( $year . '-12-01' ); $views_query = " AND pvc.type = 2 AND CAST( pvc.period AS SIGNED ) >= " . (int) ( $year ) . "01 AND CAST( pvc.period AS SIGNED ) <= " . (int) ( $year ) . "12"; } else $views_query = " AND pvc.type = 3 AND CAST( pvc.period AS SIGNED ) = " . (int) ( $year ); } // month } elseif ( isset( $month ) ) { // month, day if ( isset( $day ) ) { $views_query = " AND pvc.type = 0 AND CAST( RIGHT( pvc.period, 4 ) AS SIGNED ) = " . (int) ( $month . $day ); // month } else { $views_query = " AND pvc.type = 2 AND CAST( RIGHT( pvc.period, 2 ) AS SIGNED ) = " . (int) ( $month ); } // week } elseif ( isset( $week ) ) { $views_query = " AND pvc.type = 1 AND CAST( RIGHT( pvc.period, 2 ) AS SIGNED ) = " . (int) ( $week ); // day } elseif ( isset( $day ) ) { $views_query = " AND pvc.type = 0 AND CAST( RIGHT( pvc.period, 2 ) AS SIGNED ) = " . (int) ( $day ); } } $query = $wpdb->prepare( "SELECT " . ( $args['fields'] === 'date=>views' ? 'pvc.period, ' : '' ) . "SUM( COALESCE( pvc.count, 0 ) ) AS post_views FROM " . $wpdb->prefix . "posts wpp LEFT JOIN " . $wpdb->prefix . "post_views pvc ON pvc.id = wpp.ID AND 1 = %d" . ( $views_query !== '' ? ' ' . $views_query : ' AND pvc.type = 4' ) . ( ! empty( $args['post_id'] ) ? ' AND pvc.id IN (' . implode( ',', array_fill( 0, $post_ids_count, '%d' ) ) . ')' : '' ) . " " . ( ! empty( $args['post_type'] ) ? 'WHERE wpp.post_type IN (' . implode( ',', array_fill( 0, $post_types_count, '%s' ) ) . ')' : '' ) . " " . ( $views_query !== '' && $special_views_query === false ? 'GROUP BY pvc.period HAVING post_views > 0' : '' ), $query_data ); // get cached data $post_views = wp_cache_get( md5( $query ), 'pvc-get_views' ); // cached data not found? if ( $post_views === false ) { if ( $args['fields'] === 'date=>views' && ! empty( $range ) ) { $results = $wpdb->get_results( $query ); if ( ! empty( $results ) ) { foreach ( $results as $row ) { $range[$row->period] = (int) $row->post_views; } } $post_views = $range; } else $post_views = (int) $wpdb->get_var( $query ); // set the cache expiration, 5 minutes by default $expire = absint( apply_filters( 'pvc_object_cache_expire', 300 ) ); wp_cache_add( md5( $query ), $post_views, 'pvc-get_views', $expire ); } return apply_filters( 'pvc_get_views', $post_views ); } } /** * Display post views for a given post. * * @param int|array $post_id * @param bool $display * @return string|void */ if ( ! function_exists( 'pvc_post_views' ) ) { function pvc_post_views( $post_id = 0, $display = true ) { // get all data $post_id = (int) ( empty( $post_id ) ? get_the_ID() : $post_id ); // get display options $options = Post_Views_Counter()->options['display']; // get post views $views = pvc_get_post_views( $post_id, $options['display_period'] ); // use number format? $views = $options['use_format'] ? number_format_i18n( $views ) : $views; // container class $class = apply_filters( 'pvc_post_views_class', 'post-views content-post post-' . $post_id . ' entry-meta', $post_id ); // dynamic loading? $class .= $options['dynamic_loading'] === true ? ' load-dynamic' : ' load-static'; // prepare display $label = apply_filters( 'pvc_post_views_label', ( function_exists( 'icl_t' ) ? icl_t( 'Post Views Counter', 'Post Views Label', $options['label'] ) : $options['label'] ), $post_id ); // add dashicons class if needed $icon_class = strpos( $options['icon_class'], 'dashicons' ) === false ? $options['icon_class'] : 'dashicons ' . $options['icon_class']; // prepare icon output $icon = apply_filters( 'pvc_post_views_icon', ' ', $post_id ); // final views $views = apply_filters( 'pvc_post_views_number_format', $views, $post_id ); $html = apply_filters( 'pvc_post_views_html', '
' . ( $options['display_style']['icon'] ? $icon : '' ) . ( $options['display_style']['text'] ? '' . esc_html( $label ) . ' ' : '' ) . '' . $views . '
', $post_id, $views, $label, $icon ); if ( $display ) echo $html; else return $html; } } /** * Get most viewed posts. * * @param array $args * @return array */ if ( ! function_exists( 'pvc_get_most_viewed_posts' ) ) { function pvc_get_most_viewed_posts( $args = [] ) { $args = array_merge( [ 'posts_per_page' => 10, 'order' => 'desc', 'post_type' => 'post', 'fields' => '' ], $args ); $args = apply_filters( 'pvc_get_most_viewed_posts_args', $args ); // force to use filters $args['suppress_filters'] = false; // force to use post views as order $args['orderby'] = 'post_views'; return apply_filters( 'pvc_get_most_viewed_posts', get_posts( $args ), $args ); } } /** * Display a list of most viewed posts. * * @param array $post_id * @param bool $display * @return mixed */ if ( ! function_exists( 'pvc_most_viewed_posts' ) ) { function pvc_most_viewed_posts( $args = [], $display = true ) { $defaults = [ 'number_of_posts' => 5, 'post_type' => [ 'post' ], 'order' => 'desc', 'thumbnail_size' => 'thumbnail', 'list_type' => 'unordered', 'show_post_views' => true, 'show_post_thumbnail' => false, 'show_post_author' => false, 'show_post_excerpt' => false, 'no_posts_message' => __( 'No Posts', 'post-views-counter' ), 'item_before' => '', 'item_after' => '' ]; $args = apply_filters( 'pvc_most_viewed_posts_args', wp_parse_args( $args, $defaults ) ); $args['show_post_views'] = (bool) $args['show_post_views']; $args['show_post_thumbnail'] = (bool) $args['show_post_thumbnail']; $args['show_post_author'] = (bool) $args['show_post_author']; $args['show_post_excerpt'] = (bool) $args['show_post_excerpt']; $posts = pvc_get_most_viewed_posts( [ 'posts_per_page' => ( isset( $args['number_of_posts'] ) ? (int) $args['number_of_posts'] : $defaults['number_of_posts'] ), 'order' => ( isset( $args['order'] ) ? $args['order'] : $defaults['order'] ), 'post_type' => ( isset( $args['post_type'] ) ? $args['post_type'] : $defaults['post_type'] ) ] ); if ( ! empty( $posts ) ) { $html = ( $args['list_type'] === 'unordered' ? '
    ' : '
      ' ); foreach ( $posts as $post ) { setup_postdata( $post ); $html .= '
    1. '; $html .= apply_filters( 'pvc_most_viewed_posts_item_before', $args['item_before'], $post ); if ( $args['show_post_thumbnail'] && has_post_thumbnail( $post->ID ) ) { $html .= ' ' . get_the_post_thumbnail( $post->ID, $args['thumbnail_size'] ) . ' '; } $html .= ' ' . get_the_title( $post->ID ) . '' . ( $args['show_post_author'] ? ' (' . get_the_author_meta( 'display_name', $post->post_author ) . ') ' : '' ) . ( $args['show_post_views'] ? ' (' . number_format_i18n( pvc_get_post_views( $post->ID ) ) . ')' : '' ); $excerpt = ''; if ( $args['show_post_excerpt'] ) { if ( empty( $post->post_excerpt ) ) $text = $post->post_content; else $text = $post->post_excerpt; if ( ! empty( $text ) ) $excerpt = wp_trim_words( str_replace( ']]>', ']]>', strip_shortcodes( $text ) ), apply_filters( 'excerpt_length', 55 ), apply_filters( 'excerpt_more', ' ' . '[…]' ) ); } if ( ! empty( $excerpt ) ) $html .= '
      ' . esc_html( $excerpt ) . '
      '; $html .= apply_filters( 'pvc_most_viewed_posts_item_after', $args['item_after'], $post ); $html .= '
    2. '; } wp_reset_postdata(); $html .= ( $args['list_type'] === 'unordered' ? '
' : '' ); } else $html = $args['no_posts_message']; $html = apply_filters( 'pvc_most_viewed_posts_html', $html, $args ); if ( $display ) echo $html; else return $html; } } /** * Update total number of post views for a post. * * @global object $wpdb * * @param int $post_id Post ID * @param int $post_views Number of post views * @return bool|int */ function pvc_update_post_views( $post_id = 0, $post_views = 0 ) { global $wpdb; // cast post ID $post_id = (int) $post_id; // get post $post = get_post( $post_id ); // check if post exists if ( empty( $post ) ) return false; // cast number of views $post_views = (int) $post_views; $post_views = $post_views < 0 ? 0 : $post_views; // change post views? $post_views = apply_filters( 'pvc_update_post_views_count', $post_views, $post_id ); // insert or update database post views count $wpdb->query( $wpdb->prepare( "INSERT INTO " . $wpdb->prefix . "post_views (id, type, period, count) VALUES (%d, %d, %s, %d) ON DUPLICATE KEY UPDATE count = %d", $post_id, 4, 'total', $post_views, $post_views ) ); // query fails only if it returns false return apply_filters( 'pvc_update_post_views', $post_id ); } /** * View post manually function. * * By default this function has limitations. It works properly only between * wp_loaded (minimum priority 10) and wp_head (maximum priority 6) actions and * it can handle only one function execution per site request. * * To bypass these limitations there is a $bypass_content argument. It requires * JavaScript or REST API as counter mode but it extends the ability to use * pvc_view_post up to wp_print_footer_scripts (maximum priority 10) action. It * also bypass one function execution limitation to allow multiple function * calls during one site request. This also includes the correct saving of * cookies. * * @since 1.2.0 * * @param int $post_id * @param bool $bypass_content * @return bool */ function pvc_view_post( $post_id = 0, $bypass_content = false ) { // no post id? if ( empty( $post_id ) ) { // get current id $post_id = get_the_ID(); } else { // cast post id $post_id = (int) $post_id; } // get post $post = get_post( $post_id ); // invalid post? if ( ! is_a( $post, 'WP_Post' ) ) return false; // get main instance $pvc = Post_Views_Counter(); if ( $bypass_content ) $pvc->counter->add_to_queue( $post_id ); else $pvc->counter->check_post( $post_id ); return true; }index.php000064400000000704147206624130006372 0ustar00 Loading languages/post-views-counter.pot000064400000046630147206624130013051 0ustar00#, fuzzy msgid "" msgstr "" "Project-Id-Version: Post Views Counter\n" "POT-Creation-Date: 2024-06-04 19:48+0200\n" "PO-Revision-Date: 2015-04-08 18:59+0100\n" "Last-Translator: Bartosz Arendt \n" "Language-Team: dFactory \n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.4.4\n" "X-Poedit-KeywordsList: gettext;gettext_noop;__;_e;esc_attr__;esc_attr_e;" "esc_html__;esc_html_e\n" "X-Poedit-Basepath: .\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SearchPath-0: ..\n" #: ../includes/class-admin.php:75 ../includes/class-admin.php:79 #: ../includes/class-admin.php:112 ../includes/class-admin.php:116 msgid "You are not allowed to edit this item." msgstr "" #: ../includes/class-admin.php:108 ../includes/class-counter.php:560 msgid "Invalid post ID." msgstr "" #: ../includes/class-admin.php:157 ../includes/class-columns.php:60 #: ../includes/class-columns.php:232 ../includes/class-columns.php:295 #: ../includes/class-dashboard.php:100 ../includes/class-dashboard.php:267 #: ../includes/class-settings.php:571 msgid "Post Views" msgstr "" #: ../includes/class-admin.php:158 ../includes/class-columns.php:73 msgid "Adjust the views count for this post." msgstr "" #: ../includes/class-admin.php:159 ../includes/class-columns.php:78 msgid "Cancel" msgstr "" #: ../includes/class-columns.php:69 msgid "Edit" msgstr "" #: ../includes/class-columns.php:77 msgid "OK" msgstr "" #: ../includes/class-counter.php:553 msgid "REST API method is disabled." msgstr "" #: ../includes/class-counter.php:567 msgid "Post type excluded." msgstr "" #: ../includes/class-counter.php:574 msgid "Invalid storage type." msgstr "" #: ../includes/class-cron.php:69 msgid "Post Views Counter reset daily counts interval" msgstr "" #: ../includes/class-dashboard.php:61 ../includes/class-settings.php:571 msgid "Post Views Counter" msgstr "" #: ../includes/class-dashboard.php:101 msgid "Displays a chart of most viewed post types." msgstr "" #: ../includes/class-dashboard.php:107 msgid "Top Posts" msgstr "" #: ../includes/class-dashboard.php:108 msgid "Displays a list of most viewed single posts or pages." msgstr "" #: ../includes/class-dashboard.php:182 msgid "Powered by" msgstr "" #: ../includes/class-dashboard.php:233 ../includes/class-dashboard.php:483 #: ../includes/class-dashboard.php:621 msgid "You do not have permission to access this page." msgstr "" #: ../includes/class-dashboard.php:313 ../includes/class-dashboard.php:402 #: ../includes/class-settings.php:382 msgid "Total Views" msgstr "" #: ../includes/class-dashboard.php:526 msgid "Post" msgstr "" #: ../includes/class-dashboard.php:527 msgid "Views" msgstr "" #: ../includes/class-dashboard.php:552 msgid "(no title)" msgstr "" #: ../includes/class-dashboard.php:570 msgid "No most viewed posts found." msgstr "" #: ../includes/class-settings-api.php:328 msgid "Reset to defaults" msgstr "" #: ../includes/class-settings-api.php:697 msgid "Settings saved." msgstr "" #: ../includes/class-settings-api.php:723 msgid "Settings restored to defaults." msgstr "" #: ../includes/class-settings.php:75 msgid "You're using" msgstr "" #: ../includes/class-settings.php:80 msgid "" "Get more accurate information about the number of views of your site, " "regardless of what the user is visiting." msgstr "" #: ../includes/class-settings.php:81 msgid "Unlock optimization features and speed up view count tracking." msgstr "" #: ../includes/class-settings.php:82 msgid "" "Take your insights to the next level with dedicated, customizable " "reporting." msgstr "" #: ../includes/class-settings.php:85 ../includes/class-settings.php:1278 msgid "Upgrade to Pro" msgstr "" #: ../includes/class-settings.php:125 msgid "PHP" msgstr "" #: ../includes/class-settings.php:126 msgid "JavaScript" msgstr "" #: ../includes/class-settings.php:127 msgid "REST API" msgstr "" #: ../includes/class-settings.php:128 msgid "Fast AJAX" msgstr "" #: ../includes/class-settings.php:146 ../includes/class-settings.php:1018 msgid "minutes" msgstr "" #: ../includes/class-settings.php:147 msgid "hours" msgstr "" #: ../includes/class-settings.php:148 msgid "days" msgstr "" #: ../includes/class-settings.php:149 msgid "weeks" msgstr "" #: ../includes/class-settings.php:150 msgid "months" msgstr "" #: ../includes/class-settings.php:151 msgid "years" msgstr "" #: ../includes/class-settings.php:156 msgid "robots" msgstr "" #: ../includes/class-settings.php:157 msgid "logged in users" msgstr "" #: ../includes/class-settings.php:158 msgid "guests" msgstr "" #: ../includes/class-settings.php:159 msgid "selected user roles" msgstr "" #: ../includes/class-settings.php:170 ../includes/class-settings.php:570 msgid "Post Views Counter Settings" msgstr "" #: ../includes/class-settings.php:201 msgid "Post Types Count" msgstr "" #: ../includes/class-settings.php:205 msgid "Select post types for which post views will be counted." msgstr "" #: ../includes/class-settings.php:210 msgid "Taxonomies Count" msgstr "" #: ../includes/class-settings.php:213 msgid "Enable to count taxonomy terms visits." msgstr "" #: ../includes/class-settings.php:220 msgid "Authors Count" msgstr "" #: ../includes/class-settings.php:223 msgid "Enable to count authors archive visits." msgstr "" #: ../includes/class-settings.php:230 msgid "Other Count" msgstr "" #: ../includes/class-settings.php:233 msgid "" "Enable to count visits of front page, post type and date archives, 404 and " "search pages." msgstr "" #: ../includes/class-settings.php:241 msgid "Counter Mode" msgstr "" #: ../includes/class-settings.php:244 msgid "" "Select the method of collecting post views data. If you are using any of the " "caching plugins select JavaScript, REST API or Fast AJAX (up to 10+ times faster!)." msgstr "" #: ../includes/class-settings.php:251 msgid "Post Views Column" msgstr "" #: ../includes/class-settings.php:255 msgid "" "Enable to display post views count column for each of the selected post " "types." msgstr "" #: ../includes/class-settings.php:259 msgid "Data Storage" msgstr "" #: ../includes/class-settings.php:264 msgid "" "Choose how to store the content views data in the user's browser - with or " "without cookies." msgstr "" #: ../includes/class-settings.php:266 msgid "Cookies" msgstr "" #: ../includes/class-settings.php:267 msgid "Cookieless" msgstr "" #: ../includes/class-settings.php:274 msgid "AMP Support" msgstr "" #: ../includes/class-settings.php:281 msgid "Enable to support Google AMP." msgstr "" #: ../includes/class-settings.php:282 msgid "" "This feature requires official WordPress Google AMP plugin to be installed " "and activated." msgstr "" #: ../includes/class-settings.php:286 msgid "Restrict Edit" msgstr "" #: ../includes/class-settings.php:290 msgid "Enable to restrict post views editing to admins only." msgstr "" #: ../includes/class-settings.php:294 msgid "Count Interval" msgstr "" #: ../includes/class-settings.php:306 msgid "Cleanup Interval" msgstr "" #: ../includes/class-settings.php:309 #, php-format msgid "" "Delete single day post views data older than specified above. Enter %s if " "you want to preserve your daily views data regardless of its age." msgstr "" #: ../includes/class-settings.php:318 msgid "Object Cache Support" msgstr "" #: ../includes/class-settings.php:325 msgid "Enable to use object cache optimization." msgstr "" #: ../includes/class-settings.php:326 #, php-format msgid "" "This feature requires a persistent object cache like %s or %s to be " "installed and activated." msgstr "" #: ../includes/class-settings.php:330 msgid "Exclude Visitors" msgstr "" #: ../includes/class-settings.php:343 msgid "Exclude IPs" msgstr "" #: ../includes/class-settings.php:352 msgid "Strict counts" msgstr "" #: ../includes/class-settings.php:360 msgid "" "Enable to prevent bypassing the counts interval (for e.g. using incognito " "browser window or by clearing cookies)." msgstr "" #: ../includes/class-settings.php:364 msgid "Views Label" msgstr "" #: ../includes/class-settings.php:367 msgid "Enter the label for the post views counter field." msgstr "" #: ../includes/class-settings.php:374 msgid "Views Period" msgstr "" #: ../includes/class-settings.php:380 msgid "" "Select the time period to be included when displaying the number of views. " "The default display is the total number of views of the post." msgstr "" #: ../includes/class-settings.php:387 ../includes/class-widgets.php:162 msgid "Display Style" msgstr "" #: ../includes/class-settings.php:390 msgid "Choose how to display the post views counter." msgstr "" #: ../includes/class-settings.php:394 msgid "icon" msgstr "" #: ../includes/class-settings.php:395 msgid "label" msgstr "" #: ../includes/class-settings.php:400 msgid "Icon Class" msgstr "" #: ../includes/class-settings.php:404 #, php-format msgid "" "Enter the post views icon class. Any of the Dashicons classes are available." msgstr "" #: ../includes/class-settings.php:409 msgid "Position" msgstr "" #: ../includes/class-settings.php:412 #, php-format msgid "" "Select where would you like to display the post views counter. Use %s " "shortcode for manual display." msgstr "" #: ../includes/class-settings.php:414 msgid "before the content" msgstr "" #: ../includes/class-settings.php:415 msgid "after the content" msgstr "" #: ../includes/class-settings.php:416 msgid "manual" msgstr "" #: ../includes/class-settings.php:421 msgid "Dynamic Loading" msgstr "" #: ../includes/class-settings.php:425 msgid "" "Enable dynamic loading and prevent caching of the displayed views count." msgstr "" #: ../includes/class-settings.php:429 msgid "Format Number" msgstr "" #: ../includes/class-settings.php:432 msgid "" "Enable to display the views number formatted based on the locale (using the " "WP number_format_i18n function)." msgstr "" #: ../includes/class-settings.php:436 msgid "Taxonomies" msgstr "" #: ../includes/class-settings.php:446 msgid "Authors" msgstr "" #: ../includes/class-settings.php:453 msgid "Display number of views on authors archive pages." msgstr "" #: ../includes/class-settings.php:457 msgid "Post Type" msgstr "" #: ../includes/class-settings.php:461 msgid "Select post types for which the views count will be displayed." msgstr "" #: ../includes/class-settings.php:466 msgid "Page Type" msgstr "" #: ../includes/class-settings.php:470 msgid "Select page types where the views count will be displayed." msgstr "" #: ../includes/class-settings.php:474 msgid "Home" msgstr "" #: ../includes/class-settings.php:475 msgid "Archives" msgstr "" #: ../includes/class-settings.php:476 msgid "Single pages" msgstr "" #: ../includes/class-settings.php:477 msgid "Search results" msgstr "" #: ../includes/class-settings.php:483 msgid "User Type" msgstr "" #: ../includes/class-settings.php:496 msgid "Toolbar Chart" msgstr "" #: ../includes/class-settings.php:499 msgid "" "The post views chart will be displayed for the post types that are being " "counted." msgstr "" #: ../includes/class-settings.php:500 msgid "Enable to display the post views chart at the toolbar." msgstr "" #: ../includes/class-settings.php:504 msgid "License" msgstr "" #: ../includes/class-settings.php:509 msgid "" "Enter your Post Views Counter Pro license key (requires Pro version to be " "installed and active)." msgstr "" #: ../includes/class-settings.php:516 msgid "Menu Position" msgstr "" #: ../includes/class-settings.php:520 msgid "Top menu" msgstr "" #: ../includes/class-settings.php:521 msgid "Settings submenu" msgstr "" #: ../includes/class-settings.php:523 msgid "Choose where to display the plugin's menu." msgstr "" #: ../includes/class-settings.php:527 ../includes/class-settings.php:1168 msgid "Import Views" msgstr "" #: ../includes/class-settings.php:536 ../includes/class-settings.php:1182 msgid "Delete Views" msgstr "" #: ../includes/class-settings.php:545 msgid "Deactivation" msgstr "" #: ../includes/class-settings.php:548 msgid "" "If you deactivate the plugin with this option enabled all plugin data will " "be deleted along with the number of post views." msgstr "" #: ../includes/class-settings.php:549 msgid "Enable to delete all plugin data on deactivation." msgstr "" #: ../includes/class-settings.php:576 ../includes/class-settings.php:612 #: ../includes/class-settings.php:613 msgid "General" msgstr "" #: ../includes/class-settings.php:580 ../includes/class-settings.php:622 #: ../includes/class-settings.php:623 msgid "Display" msgstr "" #: ../includes/class-settings.php:584 ../includes/class-settings.php:632 #: ../includes/class-settings.php:633 msgid "Reports" msgstr "" #: ../includes/class-settings.php:588 ../includes/class-settings.php:642 #: ../includes/class-settings.php:643 msgid "Other" msgstr "" #: ../includes/class-settings.php:734 msgid "Post views data imported successfully." msgstr "" #: ../includes/class-settings.php:736 msgid "There was no post views data to import." msgstr "" #: ../includes/class-settings.php:743 msgid "All existing data deleted successfully." msgstr "" #: ../includes/class-settings.php:745 msgid "Error occurred. All existing data were not deleted." msgstr "" #: ../includes/class-settings.php:808 msgid "Select taxonomies for which the views count will be displayed." msgstr "" #: ../includes/class-settings.php:922 #, php-format msgid "" "Enter the time between single user visit count. Enter %s if you want to " "count every page view." msgstr "" #: ../includes/class-settings.php:1019 msgid "Persistent Object Cache" msgstr "" #: ../includes/class-settings.php:1019 msgid "available" msgstr "" #: ../includes/class-settings.php:1019 msgid "unavailable" msgstr "" #: ../includes/class-settings.php:1020 #, php-format msgid "" "How often to flush cached view counts from the object cache into the " "database. This feature is used only if a persistent object cache like %s or " "%s is detected and the interval is greater than %s. When used, view counts " "will be collected and stored in the object cache instead of the database and " "will then be asynchronously flushed to the database according to the " "specified interval. The maximum value is %s which means 24 hours.%sNotice:%s " "Potential data loss may occur if the object cache is cleared/unavailable for " "the duration of the interval." msgstr "" #: ../includes/class-settings.php:1043 msgid "Use it exclude specific user groups from post views count." msgstr "" #: ../includes/class-settings.php:1052 msgid "Use it to exclude specific user roles from post views count." msgstr "" #: ../includes/class-settings.php:1111 ../includes/class-settings.php:1117 msgid "Remove" msgstr "" #: ../includes/class-settings.php:1122 msgid "Add new" msgstr "" #: ../includes/class-settings.php:1122 msgid "Add my current IP" msgstr "" #: ../includes/class-settings.php:1123 msgid "Enter the IP addresses to be excluded from post views count." msgstr "" #: ../includes/class-settings.php:1165 msgid "" "Enter the meta key from which the views data is to be retrieved during " "import." msgstr "" #: ../includes/class-settings.php:1168 msgid "Override existing views data during import." msgstr "" #: ../includes/class-settings.php:1169 msgid "Click Import Views to start importing the views data." msgstr "" #: ../includes/class-settings.php:1183 msgid "" "Delete ALL the existing post views data. Note that this is an irreversible " "process!" msgstr "" #: ../includes/class-settings.php:1209 msgid "Use it to hide the post views counter from selected type of visitors." msgstr "" #: ../includes/class-settings.php:1218 msgid "Use it to hide the post views counter from selected user roles." msgstr "" #: ../includes/class-settings.php:1274 msgid "Display Reports and Export Views to CSV/XML" msgstr "" #: ../includes/class-settings.php:1275 msgid "View detailed stats about the popularity of your content." msgstr "" #: ../includes/class-settings.php:1276 msgid "Generate views reports in any date range you need." msgstr "" #: ../includes/class-settings.php:1277 msgid "Export, download and share your website views data." msgstr "" #: ../includes/class-update.php:140 msgid "" "Post Views Counter - this version requires a database " "update. Make sure to back up your database first." msgstr "" #: ../includes/class-update.php:141 msgid "Run the Update" msgstr "" #: ../includes/class-update.php:179 msgid "Thank you! Datebase was successfully updated." msgstr "" #: ../includes/class-widgets.php:54 ../includes/class-widgets.php:62 msgid "Most Viewed Posts" msgstr "" #: ../includes/class-widgets.php:56 msgid "Displays a list of the most viewed posts" msgstr "" #: ../includes/class-widgets.php:72 msgid "No most viewed posts found" msgstr "" #: ../includes/class-widgets.php:77 msgid "Ascending" msgstr "" #: ../includes/class-widgets.php:78 msgid "Descending" msgstr "" #: ../includes/class-widgets.php:83 msgid "Unordered list" msgstr "" #: ../includes/class-widgets.php:84 msgid "Ordered list" msgstr "" #: ../includes/class-widgets.php:124 msgid "Title" msgstr "" #: ../includes/class-widgets.php:128 msgid "Post Types" msgstr "" #: ../includes/class-widgets.php:141 msgid "Number of posts to show" msgstr "" #: ../includes/class-widgets.php:145 msgid "No posts message" msgstr "" #: ../includes/class-widgets.php:149 msgid "Order" msgstr "" #: ../includes/class-widgets.php:175 msgid "Display post views?" msgstr "" #: ../includes/class-widgets.php:177 msgid "Display post excerpt?" msgstr "" #: ../includes/class-widgets.php:179 msgid "Display post author?" msgstr "" #: ../includes/class-widgets.php:181 msgid "Display post thumbnail?" msgstr "" #: ../includes/class-widgets.php:184 msgid "Thumbnail size" msgstr "" #: ../includes/functions.php:505 msgid "No Posts" msgstr "" #: ../post-views-counter.php:318 #, php-format msgid "" "Hey, you've been using Post Views Counter for more than %s." msgstr "" #: ../post-views-counter.php:318 msgid "" "Could you please do me a BIG favor and give it a 5-star rating on WordPress " "to help us spread the word and boost our motivation." msgstr "" #: ../post-views-counter.php:318 msgid "Your help is much appreciated. Thank you very much" msgstr "" #: ../post-views-counter.php:318 msgid "founder of" msgstr "" #: ../post-views-counter.php:318 msgid "Ok, you deserve it" msgstr "" #: ../post-views-counter.php:318 msgid "Nope, maybe later" msgstr "" #: ../post-views-counter.php:318 msgid "I already did" msgstr "" #: ../post-views-counter.php:659 msgid "Are you sure you want to reset these settings to defaults?" msgstr "" #: ../post-views-counter.php:660 msgid "Are you sure you want to delete all existing data?" msgstr "" #: ../post-views-counter.php:740 msgid "Settings" msgstr "" assets/chartjs/README.md000064400000005123147206624130010771 0ustar00

https://www.chartjs.org/
Simple yet flexible JavaScript charting for designers & developers

Downloads GitHub Workflow Status Coverage Awesome Discord

## Documentation All the links point to the new version 4 of the lib. * [Introduction](https://www.chartjs.org/docs/latest/) * [Getting Started](https://www.chartjs.org/docs/latest/getting-started/index) * [General](https://www.chartjs.org/docs/latest/general/data-structures) * [Configuration](https://www.chartjs.org/docs/latest/configuration/index) * [Charts](https://www.chartjs.org/docs/latest/charts/line) * [Axes](https://www.chartjs.org/docs/latest/axes/index) * [Developers](https://www.chartjs.org/docs/latest/developers/index) * [Popular Extensions](https://github.com/chartjs/awesome) * [Samples](https://www.chartjs.org/samples/) In case you are looking for an older version of the docs, you will have to specify the specific version in the url like this: [https://www.chartjs.org/docs/2.9.4/](https://www.chartjs.org/docs/2.9.4/) ## Contributing Instructions on building and testing Chart.js can be found in [the documentation](https://www.chartjs.org/docs/master/developers/contributing.html#building-and-testing). Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://www.chartjs.org/docs/master/developers/contributing) first. For support, please post questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/chart.js) with the `chart.js` tag. ## License Chart.js is available under the [MIT license](LICENSE.md). assets/chartjs/chart.min.js000064400000620654147206624130011747 0ustar00/*! * Chart.js v4.4.2 * https://www.chartjs.org * (c) 2024 Chart.js Contributors * Released under the MIT License */ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,l,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class bt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var xt=new bt; /*! * @kurkle/color v0.3.2 * https://github.com/kurkle/color#readme * (c) 2023 Jukka Kurkela * Released under the MIT License */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function be(t,e){return me(t).getPropertyValue(e)}const xe=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=xe[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=be(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Je(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Je(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e))return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Ze(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Je(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Ze(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const bi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,xi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(bi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(xi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hx||l(n,b,p)&&0!==r(n,b),v=()=>!x||0===r(o,p)||l(o,b,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==b&&(x=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,b=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Xi={evaluateInteractionItems:Hi,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tji(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Yi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Ui(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>Ui(t,ve(e,t),"y",i.intersect,s)}};const qi=["left","top","right","bottom"];function Ki(t,e){return t.filter((t=>t.pos===e))}function Gi(t,e){return t.filter((t=>-1===qi.indexOf(t.pos)&&t.box.axis===e))}function Zi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Ji(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!qi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function ss(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Zi(Ki(e,"left"),!0),n=Zi(Ki(e,"right")),o=Zi(Ki(e,"top"),!0),a=Zi(Ki(e,"bottom")),r=Gi(e,"x"),l=Gi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ki(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);ts(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Ji(l.concat(h),d);ss(r.fullSize,g,d,p),ss(l,g,d,p),ss(h,g,d,p)&&ss(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),os(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,os(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class rs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class ls extends rs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const hs="$chartjs",cs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ds=t=>null===t||""===t;const us=!!Se&&{passive:!0};function fs(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,us)}function gs(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ps(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.addedNodes,s),e=e&&!gs(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function ms(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.removedNodes,s),e=e&&!gs(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const bs=new Map;let xs=0;function _s(){const t=window.devicePixelRatio;t!==xs&&(xs=t,bs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ys(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){bs.size||window.addEventListener("resize",_s),bs.set(t,e)}(t,o),a}function vs(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){bs.delete(t),bs.size||window.removeEventListener("resize",_s)}(t)}function Ms(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=cs[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,us)}(s,e,n),n}class ws extends rs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[hs]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ds(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(ds(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[hs])return!1;const i=e[hs].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[hs],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:ps,detach:ms,resize:ys}[e]||Ms;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:vs,detach:vs,resize:vs}[e]||fs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=ge(t);return!(!e||!e.isConnected)}}function ks(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?ls:ws}var Ss=Object.freeze({__proto__:null,BasePlatform:rs,BasicPlatform:ls,DomPlatform:ws,_detectPlatform:ks});const Ps="transparent",Ds={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Ps),n=s.valid&&Qt(e||Ps);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Cs{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Ds[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Cs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(xt.add(this._chart,i),!0):void 0}}function As(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Ts(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function zs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Vs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Bs=t=>"reset"===t||"none"===t,Ws=(t,e)=>e?t:Object.assign({},t);class Ns{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Es(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Vs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Fs(t,"x")),o=e.yAxisID=l(i.yAxisID,Fs(t,"y")),a=e.rAxisID=l(i.rAxisID,Fs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Vs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e))this._data=function(t){const e=Object.keys(t),i=new Array(e.length);let s,n,o;for(s=0,n=e.length;s0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Ts(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ws(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Os(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Bs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Bs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Bs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function js(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for($s(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,Us=(t,e)=>Math.min(e||t,t);function Xs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ks(t){return t.drawTicks?t.tickLength:0}function Gs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Zs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class Js extends Hs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ks(t.grid)-e.padding-Gs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Gs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ks(n)+o):(t.height=this.maxHeight,t.width=Ks(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ks(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(t){return Ae(i,t,p)};let x,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)x=b(this.bottom),w=this.bottom-u,S=x-m,D=b(t.top)+m,O=t.bottom;else if("bottom"===a)x=b(this.top),D=t.top,O=b(t.bottom)-m,w=x+m,S=this.top+u;else if("left"===a)x=b(this.right),M=this.right-u,k=x-m,P=b(t.left)+m,C=t.right;else if("right"===a)x=b(this.left),P=t.left,C=b(t.right)-m,M=x+m,k=this.left+u;else if("x"===e){if("center"===a)x=b((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=x+m,S=w+u}else if("y"===e){if("center"===a)x=b((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}M=x-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}b.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return b}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class tn{constructor(){this.controllers=new Qs(Ns,"datasets",!0),this.elements=new Qs(Hs,"elements"),this.plugins=new Qs(Object,"plugins"),this.scales=new Qs(Js,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function nn(t,e){return e||!1!==t?!0===t?{}:t:null}function on(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function an(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function rn(t){if("x"===t||"y"===t||"r"===t)return t}function ln(t,...e){if(rn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&rn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function hn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function cn(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=an(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=ln(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return hn(t,"x",i[0])||hn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=x(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||an(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),x(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];x(e,[ue.scales[e.type],ue.scale])})),a}function dn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=cn(t,e)}function un(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const fn=new Map,gn=new Set;function pn(t,e){let i=fn.get(t);return i||(i=e(),fn.set(t,i),gn.add(i)),i}const mn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class bn{constructor(t){this._config=function(t){return(t=t||{}).data=un(t.data),dn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=un(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),dn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return pn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return pn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return pn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>mn(r,t,e)))),e.forEach((t=>mn(r,s,t))),e.forEach((t=>mn(r,re[n]||{},t))),e.forEach((t=>mn(r,ue,t))),e.forEach((t=>mn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),gn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=xn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||_n(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=xn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function xn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const _n=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const yn=["top","bottom","left","right","chartArea"];function vn(t,e){return"top"===t||"bottom"===t||-1===yn.indexOf(t)&&"x"===e}function Mn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function wn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function kn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Sn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Pn={},Dn=t=>{const e=Sn(t);return Object.values(Pn).filter((t=>t.canvas===e)).pop()};function Cn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}function On(t,e,i){return t.options.clip?t[i]:e[i]}class An{static defaults=ue;static instances=Pn;static overrides=re;static registry=en;static version="4.4.2";static getChart=Dn;static register(...t){en.add(...t),Tn()}static unregister(...t){en.remove(...t),Tn()}constructor(t,e){const s=this.config=new bn(e),n=Sn(t),o=Dn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ks(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new sn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Pn[this.id]=this,r&&l?(xt.listen(this,"complete",wn),xt.listen(this,"progress",kn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return en}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return xt.stop(this),this}resize(t,e){xt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=ln(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=ln(o,n),r=l(n.type,e.dtype);void 0!==n.position&&vn(n.position,a)===vn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(en.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{as.configure(this,t,t.options),as.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Mn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{as.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){Cn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;as.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t,e){const{xScale:i,yScale:s}=t;return i&&s?{left:On(i,e,"left"),right:On(i,e,"right"),top:On(s,e,"top"),bottom:On(s,e,"bottom")}:e}(t,this.chartArea),o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Ie(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&ze(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Xi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),xt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Tn(){return u(An.instances,(t=>t._plugins.invalidate()))}function Ln(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class En{static override(t){Object.assign(En.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ln()}parse(){return Ln()}format(){return Ln()}add(){return Ln()}diff(){return Ln()}startOf(){return Ln()}endOf(){return Ln()}}var Rn={_date:En};function In(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Fn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nZ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),b=g(C,h,d),x=g(C+E,c,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),b=(i.width-o)/f,x=(i.height-o)/g,_=Math.max(Math.min(b,x)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Yn=Object.freeze({__proto__:null,BarController:class extends Ns{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Fn(t,e,i,s)}parseArrayData(t,e,i,s){return Fn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=t=>{const i=t.controller.getParsed(e),n=i&&i[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!r(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(b-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);b=Math.max(Math.min(b,h),o),d=b+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(b))}if(b===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;b+=t,u-=t}return{size:u,base:b,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=b?g:{};if(i=x){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),b||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends jn{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:$n,RadarController:class extends Ns{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>b,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),x||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Un(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Xn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function qn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,b=n-p-f,{outerStart:x,outerEnd:_,innerStart:y,innerEnd:v}=Un(e,u,d,b-m),M=d-x,w=d-_,k=m+x/M,S=b-_/w,P=u+y,D=u+v,O=m+y/P,A=b-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Xn(w,S,a,r);t.arc(e.x,e.y,_,S,b+E)}const i=Xn(D,b,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Xn(D,A,a,r);t.arc(e.x,e.y,v,b+E,A+Math.PI)}const s=(b-v/u+(m+y/u))/2;if(t.arc(a,r,u,b-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Xn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Xn(M,m,a,r);if(t.lineTo(n.x,n.y),x>0){const e=Xn(M,k,a,r);t.arc(e.x,e.y,x,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Kn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u}=l,f="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,f?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let g=e.endAngle;if(o){qn(t,e,i,s,g,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,g),o||(qn(t,e,i,s,g,n),t.stroke())}function Gn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Jn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[x(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[x(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(b*m+e)/++b):(_(),t.lineTo(e,i),u=s,b=0,f=g=i),p=i}_()}function eo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?to:Qn}const io="function"==typeof Path2D;function so(t,e,i,s){io&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Gn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=eo(e);for(const r of n)Gn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class no extends Hs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a)>=O||Z(n,a,r),g=tt(o,h+u,c+u);return f&&g}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){qn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function po(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,b=!s(a),x=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!b&&!x)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),b&&x&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=b?a:M,w=x?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(b&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return x&&u&&w!==r?i.length&&V(i[i.length-1].value,r,mo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):x&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class xo extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const _o=t=>Math.floor(z(t)),yo=(t,e)=>Math.pow(10,_o(t)+e);function vo(t){return 1===t/Math.pow(10,_o(t))}function Mo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function wo(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=_o(e);let o=function(t,e){let i=_o(e-t);for(;Mo(t,e,i)>10;)i++;for(;Mo(t,e,i)<10;)i--;return Math.min(i,_o(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:vo(g),significand:u}),s}class ko extends Js{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===yo(this.min,0)?yo(this.min,-1):yo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(yo(i,-1)),o(yo(s,1)))),i<=0&&n(yo(s,-1)),s<=0&&o(yo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=wo({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function So(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Po(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Oo(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function Ao(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function To(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Lo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));To(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash),o.lineDashOffset=n.dashOffset,o.beginPath(),Lo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Ro={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Io=Object.keys(Ro);function zo(t,e){return t-e}function Fo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Vo(t,e,i,s){const n=Io.length;for(let o=Io.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function Wo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class No extends Js{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Rn._date(t.adapters.date);s.init(e),x(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Fo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Vo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Io.length-1;o>=Io.indexOf(i);o--){const i=Io[o];if(Ro[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Io[i?Io.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Io.indexOf(t)+1,i=Io.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Vo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var jo=Object.freeze({__proto__:null,CategoryScale:class extends Js{static id="category";static defaults={ticks:{callback:po}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:go(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return po.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:xo,LogarithmicScale:ko,RadialLinearScale:Eo,TimeScale:No,TimeSeriesScale:class extends No{static id="timeseries";static defaults=No.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ho(e,this.min),this._tableRange=Ho(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(Ho(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return Ho(this._table,i*this._tableRange+this._minPos,!0)}}});const $o=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Yo=$o.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Uo(t){return $o[t%$o.length]}function Xo(t){return Yo[t%Yo.length]}function qo(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof jn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Uo(e++))),e}(i,e):n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Uo(e),t.backgroundColor=Xo(e),++e}(i,e))}}function Ko(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Go={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n;if(!i.forceOverride&&(Ko(s)||(a=n)&&(a.borderColor||a.backgroundColor)||o&&Ko(o)))return;var a;const r=qo(t);s.forEach(r)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Jo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var Qo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Jo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const b=[],x=e+i-1,_=t[e].x,y=t[x].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&b.push({...t[e],x:p}),s!==u&&s!==i&&b.push({...t[s],x:p})}o>0&&i!==u&&b.push(t[i]),b.push(a),h=e,m=0,f=g=l,c=d=u=o}}return b}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Jo(t)}};function ta(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ea(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function ia(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function sa(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ea(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new no({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function na(t){return t&&!1!==t.fill}function oa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function aa(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function ra(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&da(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;na(i)&&da(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;na(s)&&"beforeDatasetDraw"===i.drawTime&&da(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const ba=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class xa extends Hs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=ba(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=_a(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=ba(o,d),b=this.isHorizontal(),x=this._computeTitleHeight();f=b?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+x,line:0}:{x:this.left+c,y:ft(n,this.top+x+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),b?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+x+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,b?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),b)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=_a(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class va extends Hs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var Ma={id:"title",_element:va,start(t,e,i){!function(t,e){const i=new va({ctx:t.ctx,options:e,chart:t});as.configure(t,i,e),as.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;as.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const wa=new WeakMap;var ka={id:"subtitle",start(t,e,i){const s=new va({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s),wa.set(t,s)},stop(t){as.removeBox(t,wa.get(t)),wa.delete(t)},beforeUpdate(t,e,i){const s=wa.get(t);as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,"beforeLabel",this,t))),Pa(e.lines,Fa(n,"label",this,t)),Pa(e.after,Da(Fa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,"beforeFooter",this,t),n=Fa(i,"footer",this,t),o=Fa(i,"afterFooter",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Ia(t.callbacks,e);s.push(Fa(i,"labelColor",this,e)),n.push(Fa(i,"labelPointStyle",this,e)),o.push(Fa(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Sa[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Oa(this,i),a=Object.assign({},t,e),r=Ta(this.chart,i,a),l=La(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,b,x,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,x=_+o,y=_-o):(p=d+f,m=p+o,x=_-o,y=_+o),b=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(x=u,_=x-o,p=m-o,b=m+o):(x=u+g,_=x+o,p=m+o,b=m-o),y=x),{x1:p,x2:m,x3:b,y1:x,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let b,x,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ea(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Sa[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Oa(this,t),a=Object.assign({},i,this._size),r=Ta(e,t,a),l=La(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Sa[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ba={id:"tooltip",_element:Va,positioners:Sa,afterInit(t,e,i){i&&(t.tooltip=new Va({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:za},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return An.register(Yn,jo,fo,t),An.helpers={...Wi},An._adapters=Rn,An.Animation=Cs,An.Animations=Os,An.animator=xt,An.controllers=en.controllers.items,An.DatasetController=Ns,An.Element=Hs,An.elements=fo,An.Interaction=Xi,An.layouts=as,An.platforms=Ss,An.Scale=Js,An.Ticks=ae,Object.assign(An,Yn,jo,fo,t,Ss),An.Chart=An,"undefined"!=typeof window&&(window.Chart=An),An})); //# sourceMappingURL=chart.umd.js.map assets/chartjs/LICENSE000064400000002116147206624130010516 0ustar00The MIT License (MIT) Copyright (c) 2014-2022 Chart.js Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. assets/microtip/microtip.min.css000064400000015033147206624130013025 0ustar00/* ------------------------------------------------------------------- Microtip Modern, lightweight css-only tooltips Just 1kb minified and gzipped @author Ghosh @package Microtip ---------------------------------------------------------------------- 1. Base Styles 2. Direction Modifiers 3. Position Modifiers --------------------------------------------------------------------*/ [aria-label][role~="tooltip"]{position:relative}[aria-label][role~="tooltip"]::before,[aria-label][role~="tooltip"]::after{transform:translate3d(0,0,0);-webkit-backface-visibility:hidden;backface-visibility:hidden;will-change:transform;opacity:0;pointer-events:none;transition:all var(--microtip-transition-duration,.18s) var(--microtip-transition-easing,ease-in-out) var(--microtip-transition-delay,0s);position:absolute;box-sizing:border-box;z-index:10;transform-origin:top}[aria-label][role~="tooltip"]::before{background-size:100% auto!important;content:""}[aria-label][role~="tooltip"]::after{background:rgba(17,17,17,.9);border-radius:4px;color:#fff;content:attr(aria-label);font-size:var(--microtip-font-size,13px);font-weight:var(--microtip-font-weight,normal);text-transform:var(--microtip-text-transform,none);padding:.5em 1em;white-space:nowrap;box-sizing:content-box}[aria-label][role~="tooltip"]:hover::before,[aria-label][role~="tooltip"]:hover::after,[aria-label][role~="tooltip"]:focus::before,[aria-label][role~="tooltip"]:focus::after{opacity:1;pointer-events:auto}[role~="tooltip"][data-microtip-position|="top"]::before{background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2817,%2017,%2017,%200.9%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E) no-repeat;height:6px;width:18px;margin-bottom:5px}[role~="tooltip"][data-microtip-position|="top"]::after{margin-bottom:11px}[role~="tooltip"][data-microtip-position|="top"]::before{transform:translate3d(-50%,0,0);bottom:100%;left:50%}[role~="tooltip"][data-microtip-position|="top"]:hover::before{transform:translate3d(-50%,-5px,0)}[role~="tooltip"][data-microtip-position|="top"]::after{transform:translate3d(-50%,0,0);bottom:100%;left:50%}[role~="tooltip"][data-microtip-position="top"]:hover::after{transform:translate3d(-50%,-5px,0)}[role~="tooltip"][data-microtip-position="top-left"]::after{transform:translate3d(calc(-100% + 16px),0,0);bottom:100%}[role~="tooltip"][data-microtip-position="top-left"]:hover::after{transform:translate3d(calc(-100% + 16px),-5px,0)}[role~="tooltip"][data-microtip-position="top-right"]::after{transform:translate3d(calc(0% + -16px),0,0);bottom:100%}[role~="tooltip"][data-microtip-position="top-right"]:hover::after{transform:translate3d(calc(0% + -16px),-5px,0)}[role~="tooltip"][data-microtip-position|="bottom"]::before{background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2817,%2017,%2017,%200.9%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E) no-repeat;height:6px;width:18px;margin-top:5px;margin-bottom:0}[role~="tooltip"][data-microtip-position|="bottom"]::after{margin-top:11px}[role~="tooltip"][data-microtip-position|="bottom"]::before{transform:translate3d(-50%,-10px,0);bottom:auto;left:50%;top:100%}[role~="tooltip"][data-microtip-position|="bottom"]:hover::before{transform:translate3d(-50%,0,0)}[role~="tooltip"][data-microtip-position|="bottom"]::after{transform:translate3d(-50%,-10px,0);top:100%;left:50%}[role~="tooltip"][data-microtip-position="bottom"]:hover::after{transform:translate3d(-50%,0,0)}[role~="tooltip"][data-microtip-position="bottom-left"]::after{transform:translate3d(calc(-100% + 16px),-10px,0);top:100%}[role~="tooltip"][data-microtip-position="bottom-left"]:hover::after{transform:translate3d(calc(-100% + 16px),0,0)}[role~="tooltip"][data-microtip-position="bottom-right"]::after{transform:translate3d(calc(0% + -16px),-10px,0);top:100%}[role~="tooltip"][data-microtip-position="bottom-right"]:hover::after{transform:translate3d(calc(0% + -16px),0,0)}[role~="tooltip"][data-microtip-position="left"]::before,[role~="tooltip"][data-microtip-position="left"]::after{bottom:auto;left:auto;right:100%;top:50%;transform:translate3d(10px,-50%,0)}[role~="tooltip"][data-microtip-position="left"]::before{background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2817,%2017,%2017,%200.9%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E) no-repeat;height:18px;width:6px;margin-right:5px;margin-bottom:0}[role~="tooltip"][data-microtip-position="left"]::after{margin-right:11px}[role~="tooltip"][data-microtip-position="left"]:hover::before,[role~="tooltip"][data-microtip-position="left"]:hover::after{transform:translate3d(0,-50%,0)}[role~="tooltip"][data-microtip-position="right"]::before,[role~="tooltip"][data-microtip-position="right"]::after{bottom:auto;left:100%;top:50%;transform:translate3d(-10px,-50%,0)}[role~="tooltip"][data-microtip-position="right"]::before{background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2817,%2017,%2017,%200.9%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E) no-repeat;height:18px;width:6px;margin-bottom:0;margin-left:5px}[role~="tooltip"][data-microtip-position="right"]::after{margin-left:11px}[role~="tooltip"][data-microtip-position="right"]:hover::before,[role~="tooltip"][data-microtip-position="right"]:hover::after{transform:translate3d(0,-50%,0)}[role~="tooltip"][data-microtip-size="small"]::after{white-space:initial;width:80px}[role~="tooltip"][data-microtip-size="medium"]::after{white-space:initial;width:150px}[role~="tooltip"][data-microtip-size="large"]::after{white-space:initial;width:260px}readme.txt000064400000022445147206624130006556 0ustar00=== Post Views Counter === Contributors: dfactory Tags: counter, postviews, statistics, stats, analytics, pageviews, tracking Requires at least: 5.1 Requires PHP: 7.3.0 Tested up to: 6.5.4 Stable tag: 1.4.7 License: MIT License License URI: http://opensource.org/licenses/MIT Post Views Counter allows you to display how many times a post, page or custom post type had been viewed in a simple, fast and reliable way. == Description == [Post Views Counter](https://postviewscounter.com/) allows you to display how many times a post, page or custom post type had been viewed with this simple, fast and easy to use plugin. = Features include: = * Option to select post types for which post views will be counted and displayed. * 3 methods of collecting post views data: PHP, Javascript and REST API for greater flexibility * Compatible with data privacy regulations * Possibility to manually set views count for each post * Dashboard post views stats widget * Full Privacy regulations compliance * Capability to query posts according to its views count * Custom REST API endpoints * Option to set counts interval * Excluding counts from visitors: bots, logged in users, selected user roles * Excluding users by IPs * Restricting display by user roles * Restricting post views editing to admins * One-click data import from WP-PostViews * Sortable admin column * Post views display position, automatic or manual via shortcode * Multisite compatibile * WPML and Polylang compatible * .pot file for translations included == Installation == 1. Install Post Views Counter either via the WordPress.org plugin directory, or by uploading the files to your server 2. Activate the plugin through the 'Plugins' menu in WordPress 3. Go to the Post Views Counter settings and set your options. == Frequently Asked Questions == No questions yet. == Screenshots == 1. screenshot-1.png 2. screenshot-2.png == Changelog == = 1.4.7 = * New: Dynamic views loading option (Pro) * Fix: Multi-sorting queries with post_views orderby parameter = 1.4.6 = * Fix: Bulk posts selection * Fix: Additional SQL queries escaping * Tweak: Call to undefined function is_favicon() * Tweak: Enqueue main script in header instead of footer * Tweak: Better JS error handling * Tweak: Updated Chart.js to 4.4.2 = 1.4.5 = * Fix: Post views bulk saving security * Tweak: Removed WP Rocket as bot in crawler detection = 1.4.4 = * New: Option to enter meta_key for importing the views * New: Revamped Reports for Views by Date, Views by Post and Views by Author (Pro) * New: REST API support for post, site, term and user views (Pro) * New: Views Period option to display views from a selected time period instead of total (Pro) * New: [site-views] shortcode for total site views display (Pro) * Tweak: Improved icon handling * Tweak: Updated crawler detection = 1.4.3 = * Tweak: Update languages file = 1.4.2 = * New: Option to select position of the plugin menu = 1.4.1 = * Fix: Frontpage views not recorded properly = 1.4 = * New: Introducing Post Views Counter Pro * New: Fast Ajax views counting mode (Pro) * New: Google AMP support (Pro) * New: Taxonomy term views (Pro) * New: Author archive views (Pro) * New: Cookies/Cookieless data storage option (Pro) * New: Dedicated Reports page (Pro) * New: Exporting views to CSV or XML files (Pro) * Tweak: Improved validation and sanitization * Tweak: Chart.js updated to 4.3.0 = 1.3.13 = * New: Compatibility with WP 6.2 and PHP 8.2 * Fix: Invalid year in seconds * Fix: Possible invalid cookie data in views storage * Fix: Default database prefix * Tweak: Switch from wp_localize_script to wp_add_inline_script * Tweak: Updated bot detection = 1.3.12 = * Fix: Frontend Javascript rewritten from jQuery to Vanilla JS * Fix: Admin Bar Style loading on every page * Fix: Network initialization process for new sites * Fix: IP address encryption * Fix: REST API endpoints * Fix: Removed couple of deprecated functions * Tweak: Updated chart.js script to version 3.9.1 * Tweak: Added SameSite attribute to cookie = 1.3.11 = * Fix: Potentailly incorrect counting of post views in edge case db queries * Fix: Possible empty chart in dashboard * Fix: Incorrect saving of dashboard widget user options * Tweak: Updated Chart.js to version 3.7.0 = 1.3.10 = * Fix: Post views column not working properly * Tweak: Switched to openssl_encrypt method for IP encryption * Tweak: Improved user input escaping = 1.3.9 = * Tweak: Remove unnecessary plugin files = 1.3.8 = * Tweak: Improved user input escaping = 1.3.7 = * Tweak: Implemented internal settings API = 1.3.6 = * Fix: Option to hide admin bar chart = 1.3.5 = * New: Option to hide admin bar chart * Fix: Small security bug with views label * Tweak: Remove unnecessary CSS on every page = 1.3.4 = * New: Post Views stats preview in the admin bar * New: Top Posts data available in the dashboard widget * Tweak: Improved privacy using IP encrypting * Tweak: PHP 8.x compatibility = 1.3.3 = * Fix: PHP Notice: Trying to get property 'colors' of non-object * Fix: PHP Notice: register_rest_route was called incorrectly = 1.3.2 = * New: Introducing dashboard widget navigation * New: Counter support for Media (attachments) * Tweak: Extended views query for handling complex date/time requests = 1.3.1 = * Fix: Gutenberg CSS file missing * Tweak: POT translation file update = 1.3 = * New: Gutenberg compatibility * New: Additional options in widgets: post author and display style * Fix: Undefined variables when IP saving enabled * Fix: Check cookie not being triggered in Fast Ajax mode * Fix: Invalid arguments in implode function causing warning * Fix: Thumbnail size option did not show up after thumbnail checkbox was checked * Fix: Saving post (in quick edit mode too) did not update post views = 1.2.14 = * Fix: Bulk edit post views count reset issue = 1.2.13 = * New: Experimental Fast AJAX counter method (10+ times faster) = 1.2.12 = * New: GDPR compatibility with Cookie Notice plugin = 1.2.11 = * Tweak: Additional IP expiration checks added as an option = 1.2.10 = * New: Additional transient based IP expiration checks * Tweak: Chart.js script update to 2.7.1 = 1.2.9 = * Fix: WooCommerce products list table broken = 1.2.8 = * New: Multisite compatibility * Fix: Undefined index post_views_column on post_views_counter/includes/settings.php * Tweak: Improved user IP handling = 1.2.7 = * Fix: Chart data not updating for object cached installs due to missing expire parameter * Fix: Bug preventing hiding the counter based on user role. * Fix: Undefined notice in the admin dashboard request = 1.2.6 = * Fix: Hardcoded post_views database table prefix = 1.2.5 = * New: REST API counter mode * New: Adjust dashboard chart colors to admin color scheme * Tweak: Dashboard chart query optimization * Tweak: post_views database table optimization * Tweak: Added plugin documentation link = 1.2.4 = * New: Advanced crawler detection * Tweak: Chart.js script update to 2.4.0 = 1.2.3 = * New: IP wildcard support * Tweak: Delete post_views database table on deactivation = 1.2.2 = * Fix: Notice undefined variable: post_ids, thanks to [zytzagoo](https://github.com/zytzagoo) * Tweak: Switched translation files storage, from local to WP repository = 1.2.1 = * New: Option to display post views on select page types * Tweak: Dashboard widget query optimization = 1.2.0 = * New: Dashboard post views stats widget * Fix: A couple of typos in method names = 1.1.4 = * Fix: Dashicons link broken. * Tweak: Confirmed WordPress 4.4 compatibility = 1.1.3 = * Fix: Duplicated views count in custom post types * Fix: Exclude visitors checkboxes not working = 1.1.2 = * Fix: Most viewed posts widget broken = 1.1.1 = * Tweak: Enable edit views on new post. * Tweak: Extend WP_Query post data with post_views = 1.1.0 = * New: Quick post views edit * New: Bulk post views edit * Tweak: Admin UI improvements = 1.0.12 = * New: Italian translation, thanks to [Rene Querin](http://www.q-design.it) = 1.0.11 = * New: French translation, thanks to [Theophil Bethel](http://reseau-chretien-gironde.fr/) = 1.0.10 = * New: Option to limit post views editing to admins only = 1.0.9 = * New: Spanish translation, thanks to [Carlos Rodriguez](http://cglevel.com/) = 1.0.8 = * New: Croation translation, thanks to [Tomas Trkulja](http://zytzagoo.net/blog/) = 1.0.7 = * New: Possibility to manually set views count for each post * New: Plugin development moved to [dFactory GitHub Repository](https://github.com/dfactoryplugins) = 1.0.6 = * New: Object cache support, thanks to [Tomas Trkulja](http://zytzagoo.net/blog/) * New: Hebrew translation, thanks to [Ahrale Shrem](http://atar4u.com/) = 1.0.5 = * Tweak: Added number_format_i18n for displayed views count * Tweak: Additional action hook for developers = 1.0.4 = * Fix: Possible issue with remove_post_views_count function = 1.0.3 = * New: Russian translation, thanks to moonkir * Fix: Remove [post-views] shortcode from post excerpts if excerpt is empty = 1.0.2 = * Fix: Pluggable functions initialized too lately = 1.0.0 = Initial release == Upgrade Notice == = 1.4.7 = New: Dynamic views loading option (Pro), Fix: Multi-sorting queries with post_viewsjs/counter.js000064400000003114147206624130007201 0ustar00PostViewsCounterManual = { /** * Initialize counter. * * @param {object} args * * @return {void} */ init: function( args ) { let params = { action: 'pvc-view-posts', pvc_nonce: args.nonce, ids: args.ids }; let newParams = Object.keys( params ).map( function( el ) { // add extra "data" array return encodeURIComponent( el ) + '=' + encodeURIComponent( params[el] ); } ).join( '&' ).replace( /%20/g, '+' ); const _this = this; return fetch( args.url, { method: 'POST', mode: 'cors', cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' }, body: newParams } ).then( function( response ) { // invalid response? if ( ! response.ok ) throw Error( response.statusText ); return response.json(); } ).then( function( response ) { try { if ( typeof response === 'object' && response !== null ) _this.triggerEvent( 'pvcCheckPost', response ); } catch( error ) { console.log( 'Invalid JSON data' ); console.log( error ); } } ).catch( function( error ) { console.log( 'Invalid response' ); console.log( error ); } ); }, /** * Prepare the data to be sent with the request. * * @param {string} eventName * @param {object} data * * @return {void} */ triggerEvent: function( eventName, data ) { const newEvent = new CustomEvent( eventName, { bubbles: true, detail: data } ); // trigger event document.dispatchEvent( newEvent ); } }js/frontend.js000064400000016221147206624130007344 0ustar00var initPostViewsCounter = function() { PostViewsCounter = { promise: null, args: {}, /** * Initialize counter. * * @param {object} args * * @return {void} */ init: function( args ) { this.args = args; // default parameters let params = {}; // set cookie/storage name let name = 'pvc_visits' + ( args.multisite !== false ? '_' + parseInt( args.multisite ) : '' ); // cookieless data storage? if ( args.dataStorage === 'cookieless' && this.isLocalStorageAvailable() ) { params.storage_type = 'cookieless'; params.storage_data = this.readStorageData( name ); /* COUNT_POST_AS_AUTHOR_VIEW | removed parameter from request if ( 'countAuthor' in args && args.countAuthor === true ) { params.storage_data_author = this.readStorageData( 'pvc_visits_user' + ( args.multisite !== false ? '_' + parseInt( args.multisite ) : '' ) ); } */ } else { params.storage_type = 'cookies'; params.storage_data = this.readCookieData( name ); /* COUNT_POST_AS_AUTHOR_VIEW | removed parameter from request if ( 'countAuthor' in args && args.countAuthor === true ) { params.storage_data_author = this.readCookieData( 'pvc_visits_user' + ( args.multisite !== false ? '_' + parseInt( args.multisite ) : '' ) ); } */ } // rest api request if ( args.mode === 'rest_api' ) { this.promise = this.request( args.requestURL, params, 'POST', { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'X-WP-Nonce': args.nonce }, name ); // admin ajax request } else { params.action = 'pvc-check-post'; params.pvc_nonce = args.nonce; params.id = args.postID; this.promise = this.request( args.requestURL, params, 'POST', { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' }, name ); } }, /** * Handle fetch request. * * @param {string} url * @param {object} params * @param {string} method * @param {object} headers * @param {string} name * * @return {object} */ request: function( url, params, method, headers, name = '' ) { let options = { method: method, mode: 'cors', cache: 'no-cache', credentials: 'same-origin', headers: headers, body: this.prepareRequestData( params ) }; const _this = this; return fetch( url, options ).then( function( response ) { // invalid response? if ( ! response.ok ) throw Error( response.statusText ); return response.json(); } ).then( function( response ) { try { if ( typeof response === 'object' && response !== null ) { if ( 'success' in response && response.success === false ) { console.log( 'Request error' ); console.log( response.data ); } else { if ( _this.args.dataStorage === 'cookieless' ) _this.saveStorageData.call( _this, name, response.storage, response.type ); else _this.saveCookieData( name, response.storage ); _this.triggerEvent( 'pvcCheckPost', response ); } } else { console.log( 'Invalid object' ); console.log( response ); } } catch( error ) { console.log( 'Invalid JSON data' ); console.log( error ); } } ).catch( function( error ) { console.log( 'Invalid response' ); console.log( error ); } ); }, /** * Prepare the data to be sent with the request. * * @param {object} data * * @return {string} */ prepareRequestData: function( data ) { return Object.keys( data ).map( function( el ) { // add extra "data" array return encodeURIComponent( el ) + '=' + encodeURIComponent( data[el] ); } ).join( '&' ).replace( /%20/g, '+' ); }, /** * Prepare the data to be sent with the request. * * @param {string} eventName * @param {object} data * * @return {void} */ triggerEvent: function( eventName, data ) { const newEvent = new CustomEvent( eventName, { bubbles: true, detail: data } ); // trigger event document.dispatchEvent( newEvent ); }, /** * Save localStorage data. * * @param {string} name * @param {object} data * @param {string} type * * @return {void} */ saveStorageData: function( name, data, type ) { /* COUNT_POST_AS_AUTHOR_VIEW | removed setting localStorage user data if ( 'countAuthor' in this.args && this.args.countAuthor === true && 'user' in data ) window.localStorage.setItem( 'pvc_visits_user' + ( this.args.multisite !== false ? '_' + parseInt( this.args.multisite ) : '' ), JSON.stringify( data['user'] ) ); */ window.localStorage.setItem( name, JSON.stringify( data[type] ) ); }, /** * Read localStorage data. * * @param {string} name * * @return {string} */ readStorageData: function( name ) { let data = null; // get data data = window.localStorage.getItem( name ); // no data? if ( data === null ) data = ''; return data; }, /** * Save cookies. * * @param {string} name * @param {object} data * * @return {void} */ saveCookieData: function( name, data ) { var cookieSecure = ''; // ssl? if ( document.location.protocol === 'https:' ) cookieSecure = ';secure'; for ( let i = 0; i < data.name.length; i++ ) { var cookieDate = new Date(); var expiration = parseInt( data.expiry[i] ); // valid expiration timestamp? if ( expiration ) expiration = expiration * 1000; // add default 24 hours else expiration = cookieDate.getTime() + 86400000; // set cookie date expiry cookieDate.setTime( expiration ); // set cookie document.cookie = data.name[i] + '=' + data.value[i] + ';expires=' + cookieDate.toUTCString() + ';path=/' + ( this.args.path === '/' ? '' : this.args.path ) + ';domain=' + this.args.domain + cookieSecure + ';SameSite=Lax'; } }, /** * Read cookies. * * @param {string} name * * @return {string} */ readCookieData: function( name ) { var cookies = []; document.cookie.split( ';' ).forEach( function( el ) { var [key, value] = el.split( '=' ); var trimmedKey = key.trim(); var regex = new RegExp( name + '\\[\\d+\\]' ); // look all cookies starts with name parameter if ( regex.test( trimmedKey ) ) cookies.push( value ); } ); return cookies.join( 'a' ); }, /** * Check whether localStorage is available. * * @return {bool} */ isLocalStorageAvailable: function() { var storage; try { storage = window['localStorage']; storage.setItem( '__pvcStorageTest', 0 ); storage.removeItem( '__pvcStorageTest' ); return true; } catch( e ) { return e instanceof DOMException && ( e.code === 22 || e.code === 1014 || e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED' ) && ( storage && storage.length !== 0 ); } } } PostViewsCounter.init( pvcArgsFrontend ); } document.addEventListener( 'DOMContentLoaded', initPostViewsCounter );js/admin-dashboard.js000064400000017321147206624130010544 0ustar00( function( $ ) { /** * Load initial data. */ window.addEventListener( 'load', function() { updatePostViewsWidget( 'this_month' ); updatePostMostViewedWidget( 'this_month' ); } ); /** * Ready event. */ $( function() { // toggle collapse items $( '.pvc-accordion-header' ).on( 'click', function( e ) { $( this ).closest( '.pvc-accordion-item' ).toggleClass( 'pvc-collapsed' ); var items = $( '#pvc-dashboard-accordion' ).find( '.pvc-accordion-item' ); var menuItems = {}; if ( items.length > 0 ) { $( items ).each( function( index, item ) { var itemName = $( item ).attr( 'id' ); itemName = itemName.replace( 'pvc-', '' ); menuItems[itemName] = $( item ).hasClass( 'pvc-collapsed' ); } ); } // update user options pvcUpdateUserOptions( { menu_items: menuItems } ); } ); } ); /** * Update user options. */ pvcUpdateUserOptions = function( options ) { $.ajax( { url: pvcArgs.ajaxURL, type: 'POST', dataType: 'json', data: { action: 'pvc_dashboard_user_options', nonce: pvcArgs.nonceUser, options: options }, success: function() {} } ); } /** * Update configuration. */ pvcUpdateConfig = function( config, args ) { // update datasets config.data = args.data; // update tooltips with new dates config.options.plugins.tooltip = { callbacks: { title: function( tooltip ) { return args.data.dates[tooltip[0].dataIndex]; } } }; // update colors $.each( config.data.datasets, function( i, dataset ) { dataset.fill = args.design.fill; dataset.tension = 0.4; dataset.borderColor = args.design.borderColor; dataset.backgroundColor = args.design.backgroundColor; dataset.borderWidth = args.design.borderWidth; dataset.borderDash = args.design.borderDash; dataset.pointBorderColor = args.design.pointBorderColor; dataset.pointBackgroundColor = args.design.pointBackgroundColor; dataset.pointBorderWidth = args.design.pointBorderWidth; } ); return config; } /** * Get post most viewed data. */ function getPostMostViewedData( init, period, container ) { $( container ).addClass( 'loading' ).find( '.spinner' ).addClass( 'is-active' ); $.ajax( { url: pvcArgs.ajaxURL, type: 'POST', dataType: 'json', data: { action: 'pvc_dashboard_post_most_viewed', nonce: pvcArgs.nonce, period: period }, success: function( response ) { // remove loader $( container ).removeClass( 'loading' ); $( container ).find( '.spinner' ).removeClass( 'is-active' ); // next call? if ( ! init ) bindMonthEvents( response.months, container ); $( container ).find( '#pvc-post-most-viewed-content' ).html( response.html ); } } ); } /** * Get post views data. */ function getPostViewsData( init, period, container ) { $( container ).addClass( 'loading' ).find( '.spinner' ).addClass( 'is-active' ); $.ajax( { url: pvcArgs.ajaxURL, type: 'POST', dataType: 'json', data: { action: 'pvc_dashboard_post_views_chart', nonce: pvcArgs.nonce, period: period }, success: function( response ) { // remove loader $( container ).removeClass( 'loading' ); $( container ).find( '.spinner' ).removeClass( 'is-active' ); // first call? if ( init ) { var config = { type: 'line', options: { maintainAspectRatio: false, responsive: true, plugins: { legend: { display: true, position: 'bottom', align: 'center', fullSize: true, onHover: function( e ) { e.native.target.style.cursor = 'pointer'; }, onLeave: function( e ) { e.native.target.style.cursor = 'default'; }, onClick: function( e, element, legend ) { var index = element.datasetIndex; var ci = legend.chart; var meta = ci.getDatasetMeta( index ); // set new hidden value if ( ci.isDatasetVisible( index ) ) meta.hidden = true; else meta.hidden = false; // rerender the chart ci.update(); // update user options pvcUpdateUserOptions( { post_type: ci.data.datasets[index].post_type, hidden: meta.hidden } ); }, labels: { boxWidth: 8, boxHeight: 8, font: { size: 13, weight: 'normal', family: "'-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen-Sans', 'Ubuntu', 'Cantarell', 'Helvetica Neue', 'sans-serif'" }, padding: 10, usePointStyle: false, textAlign: 'center' } } }, scales: { x: { display: true, title: { display: false } }, y: { display: true, grace: 0, beginAtZero: true, title: { display: false }, ticks: { precision: 0, maxTicksLimit: 12 } } }, hover: { mode: 'label' } } }; config = pvcUpdateConfig( config, response ); window.pvcPostViewsChart = new Chart( document.getElementById( 'pvc-post-views-chart' ).getContext( '2d' ), config ); } else { bindMonthEvents( response.months, container ); window.pvcPostViewsChart.config = pvcUpdateConfig( window.pvcPostViewsChart.config, response ); window.pvcPostViewsChart.update(); } } } ); } /** * Update post views widget. */ function updatePostViewsWidget( period ) { var container = $( '#pvc-post-views' ).find( '.pvc-dashboard-container' ); if ( $( container ).length > 0 ) { bindMonthEvents( false, container ); getPostViewsData( true, period, container ); } } /** * Update post most viewed widget. */ function updatePostMostViewedWidget( period ) { var container = $( '#pvc-post-most-viewed' ).find( '.pvc-dashboard-container' ); if ( $( container ).length > 0 ) { bindMonthEvents( false, container ); getPostMostViewedData( true, period, container ); } } /** * Bind month events. */ function bindMonthEvents( newMonths, container ) { var months = $( container ).find( '.pvc-months' ); // replace months? if ( newMonths !== false ) months[0].innerHTML = newMonths; var prev = months[0].getElementsByClassName( 'prev' )[0]; var next = months[0].getElementsByClassName( 'next' )[0]; var id = $( container ).closest( '.pvc-accordion-item' ).attr( 'id' ); if ( id === 'pvc-post-most-viewed' ) prev.addEventListener( 'click', loadPostMostViewedData ); else if ( id === 'pvc-post-views' ) prev.addEventListener( 'click', loadPostViewsData ); // skip span if ( next.tagName === 'A' ) { if ( id === 'pvc-post-most-viewed' ) next.addEventListener( 'click', loadPostMostViewedData ); else if ( id === 'pvc-post-views' ) next.addEventListener( 'click', loadPostViewsData ); } } /** * Load post views data. */ function loadPostViewsData( e ) { e.preventDefault(); var container = $( '#pvc-post-views' ).find( '.pvc-dashboard-container' ); getPostViewsData( false, e.target.dataset.date, container ); } /** * Load post most viewed data. */ function loadPostMostViewedData( e ) { e.preventDefault(); var container = $( '#pvc-post-most-viewed' ).find( '.pvc-dashboard-container' ); getPostMostViewedData( false, e.target.dataset.date, container ); } } )( jQuery );js/admin-widgets.js000064400000000367147206624130010265 0ustar00( function( $ ) { // ready event $( function() { $( document ).on( 'change', '.pvc-show-post-thumbnail', function() { $( this ).closest( '.widget-content' ).find( '.pvc-post-thumbnail-size' ).fadeToggle( 300 ); } ); } ); } )( jQuery );js/frontend.min.js000064400000005354147206624130010133 0ustar00var initPostViewsCounter=function(){(PostViewsCounter={promise:null,args:{},init:function(e){this.args=e;let t={},a="pvc_visits"+(!1!==e.multisite?"_"+parseInt(e.multisite):"");"cookieless"===e.dataStorage&&this.isLocalStorageAvailable()?(t.storage_type="cookieless",t.storage_data=this.readStorageData(a)):(t.storage_type="cookies",t.storage_data=this.readCookieData(a)),"rest_api"===e.mode?this.promise=this.request(e.requestURL,t,"POST",{"Content-Type":"application/x-www-form-urlencoded; charset=utf-8","X-WP-Nonce":e.nonce},a):(t.action="pvc-check-post",t.pvc_nonce=e.nonce,t.id=e.postID,this.promise=this.request(e.requestURL,t,"POST",{"Content-Type":"application/x-www-form-urlencoded; charset=utf-8"},a))},request:function(e,t,a,o,r=""){let n={method:a,mode:"cors",cache:"no-cache",credentials:"same-origin",headers:o,body:this.prepareRequestData(t)},i=this;return fetch(e,n).then(function(e){if(!e.ok)throw Error(e.statusText);return e.json()}).then(function(e){try{"object"==typeof e&&null!==e?"success"in e&&!1===e.success?(console.log("Request error"),console.log(e.data)):("cookieless"===i.args.dataStorage?i.saveStorageData.call(i,r,e.storage,e.type):i.saveCookieData(r,e.storage),i.triggerEvent("pvcCheckPost",e)):(console.log("Invalid object"),console.log(e))}catch(t){console.log("Invalid JSON data"),console.log(t)}}).catch(function(e){console.log("Invalid response"),console.log(e)})},prepareRequestData:function(e){return Object.keys(e).map(function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])}).join("&").replace(/%20/g,"+")},triggerEvent:function(e,t){let a=new CustomEvent(e,{bubbles:!0,detail:t});document.dispatchEvent(a)},saveStorageData:function(e,t,a){window.localStorage.setItem(e,JSON.stringify(t[a]))},readStorageData:function(e){let t=null;return null===(t=window.localStorage.getItem(e))&&(t=""),t},saveCookieData:function(e,t){var a="";"https:"===document.location.protocol&&(a=";secure");for(let o=0;o 1 ) new_ip_box.find( '.remove-exclude-ip' ).show(); // add and display new ip box parent.find( '.ip-box:last' ).after( new_ip_box ).next().slideDown( 'fast' ); } ); // add current ip $( document ).on( 'click', '.add-current-ip', function() { // fill input with user's current ip $( this ).parents( '#post_views_counter_general_exclude_ips_setting' ).find( '.ip-box' ).last().find( 'input' ).val( $( this ).attr( 'data-rel' ) ); } ); // toggle user roles $( '#pvc_exclude-roles, #pvc_restrict_display-roles' ).on( 'change', function() { if ( $( this ).is( ':checked' ) ) $( '.pvc_user_roles' ).slideDown( 'fast' ); else $( '.pvc_user_roles' ).slideUp( 'fast' ); } ); // menu position referer update $( 'input[name="post_views_counter_settings_other[menu_position]"]' ).on( 'change', function() { if ( $( this ).val() === 'top' ) $( 'input[data-pvc-menu="submenu"]' ).after( $( 'input[data-pvc-menu="topmenu"]' ) ); else $( 'input[data-pvc-menu="submenu"]' ).before( $( 'input[data-pvc-menu="topmenu"]' ) ); } ); } ); } )( jQuery );js/admin-post.js000064400000002306147206624130007577 0ustar00( function( $ ) { // ready event $( function() { // post views input $( '#post-views .edit-post-views' ).on( 'click', function() { if ( $( '#post-views-input-container' ).is( ":hidden" ) ) { $( '#post-views-input-container' ).slideDown( 'fast' ); $( this ).hide(); } return false; } ); // save post views $( '#post-views .save-post-views' ).on( 'click', function() { var views = ( $( '#post-views-display b' ).text() ).trim(); $( '#post-views-input-container' ).slideUp( 'fast' ); $( '#post-views .edit-post-views' ).show(); views = parseInt( $( '#post-views-input' ).val() ); // reassign value as integer $( '#post-views-input' ).val( views ); $( '#post-views-display b' ).text( views ); return false; } ); // cancel post views $( '#post-views .cancel-post-views' ).on( 'click', function() { var views = ( $( '#post-views-display b' ).text() ).trim(); $( '#post-views-input-container' ).slideUp( 'fast' ); $( '#post-views .edit-post-views' ).show(); views = parseInt( $( '#post-views-current' ).val() ); $( '#post-views-display b' ).text( views ); $( '#post-views-input' ).val( views ); return false; } ); } ); } )( jQuery );js/admin-quick-edit.js000064400000004217147206624130010654 0ustar00( function( $ ) { // ready event $( function() { // we create a copy of the WP inline edit post function var wpInlineEdit = inlineEditPost.edit; // and then we overwrite the function with our own code inlineEditPost.edit = function( id ) { // call the original WP edit function, we don't want to leave WordPress hanging wpInlineEdit.apply( this, arguments ); // get the post ID var postId = 0; if ( typeof ( id ) == 'object' ) postId = parseInt( this.getId( id ) ); if ( postId > 0 ) { // define the edit row var editRow = $( '#edit-' + postId ); var postRow = $( '#post-' + postId ); // get the data var postViews = $( '.column-post_views', postRow ).text(); // populate the data $( ':input[name="post_views"]', editRow ).val( postViews ); $( ':input[name="current_post_views"]', editRow ).val( postViews ); } return false; }; $( document ).on( 'click', '#bulk_edit', function() { // define the bulk edit row var bulkRow = $( '#bulk-edit' ); // get the selected post ids that are being edited var postIds = []; // at least wp 5.9? if ( pvcArgsQuickEdit.wpVersion59 ) { bulkRow.find( '#bulk-titles-list' ).children( '.ntdelitem' ).each( function() { postIds.push( $( this ).find( 'button' ).attr( 'id' ).replace( /[^0-9]/i, '' ) ); } ); } else { bulkRow.find( '#bulk-titles' ).children().each( function() { postIds.push( $( this ).attr( 'id' ).replace( /^(ttle)/i, '' ) ); } ); } // get the data var postViews = bulkRow.find( 'input[name="post_views"]' ).val(); // save the data $.ajax( { url: ajaxurl, // this is a variable that WordPress has already defined for us type: 'post', async: false, cache: false, data: { action: 'save_bulk_post_views', // this is the name of our WP AJAX function that we'll set up next post_ids: postIds, // and these are the 2 parameters we're passing to our function post_views: postViews, current_post_views: postViews, nonce: pvcArgsQuickEdit.nonce } } ); } ); } ); } )( jQuery );post-views-counter.php000064400000061305147206624130011064 0ustar00 [ 'post_types_count' => [ 'post' ], 'taxonomies_count' => false, 'users_count' => false, 'other_count' => false, 'data_storage' => 'cookies', 'amp_support' => false, 'counter_mode' => 'php', 'post_views_column' => true, 'restrict_edit_views' => false, 'time_between_counts' => [ 'number' => 24, 'type' => 'hours' ], 'reset_counts' => [ 'number' => 0, 'type' => 'days' ], 'object_cache' => false, 'flush_interval' => [ 'number' => 0, 'type' => 'minutes' ], 'exclude' => [ 'groups' => [], 'roles' => [] ], 'exclude_ips' => [], 'strict_counts' => false, 'cron_run' => true, 'cron_update' => true, 'update_version' => 1, 'update_notice' => true, 'update_delay_date' => 0 ], 'display' => [ 'label' => 'Post Views:', 'display_period' => 'total', 'taxonomies_display' => [], 'user_display' => false, 'post_types_display' => [ 'post' ], 'page_types_display' => [ 'singular' ], 'restrict_display' => [ 'groups' => [], 'roles' => [] ], 'position' => 'after', 'dynamic_loading' => false, 'use_format' => true, 'display_style' => [ 'icon' => true, 'text' => true ], 'icon_class' => 'dashicons-chart-bar', 'toolbar_statistics' => true ], 'other' => [ 'menu_position' => 'top', 'import_meta_key' => 'views', 'deactivation_delete' => false, 'license' => '' ], 'version' => '1.4.7' ]; // instances public $counter; public $crawler; public $cron; public $dashboard; public $frontend; public $functions; public $settings_api; /** * Disable object cloning. * * @return void */ public function __clone() {} /** * Disable unserializing of the class. * * @return void */ public function __wakeup() {} /** * Main plugin instance, insures that only one instance of the class exists in memory at one time. * * @return object */ public static function instance() { if ( ! isset( self::$instance ) && ! ( self::$instance instanceof Post_Views_Counter ) ) { self::$instance = new Post_Views_Counter(); // short init? if ( defined( 'SHORTINIT' ) && SHORTINIT ) { include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-counter.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-crawler-detect.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/functions.php' ); self::$instance->counter = new Post_Views_Counter_Counter(); self::$instance->crawler = new Post_Views_Counter_Crawler_Detect(); // regular setup } else { add_action( 'init', [ self::$instance, 'load_textdomain' ] ); self::$instance->includes(); // create settings API self::$instance->settings_api = new Post_Views_Counter_Settings_API( [ 'object' => self::$instance, 'prefix' => 'post_views_counter', 'slug' => 'post-views-counter', 'domain' => 'post-views-counter', 'plugin' => 'Post Views Counter', 'plugin_url' => POST_VIEWS_COUNTER_URL ] ); // initialize other classes self::$instance->functions = new Post_Views_Counter_Functions(); new Post_Views_Counter_Update(); new Post_Views_Counter_Settings(); new Post_Views_Counter_Admin(); new Post_Views_Counter_Query(); self::$instance->cron = new Post_Views_Counter_Cron(); self::$instance->counter = new Post_Views_Counter_Counter(); new Post_Views_Counter_Columns(); self::$instance->crawler = new Post_Views_Counter_Crawler_Detect(); self::$instance->frontend = new Post_Views_Counter_Frontend(); self::$instance->dashboard = new Post_Views_Counter_Dashboard(); new Post_Views_Counter_Widgets(); } } return self::$instance; } /** * Setup plugin constants. * * @return void */ private function define_constants() { // fix plugin_basename empty $wp_plugin_paths var if ( ! ( defined( 'SHORTINIT' ) && SHORTINIT ) ) { define( 'POST_VIEWS_COUNTER_URL', plugins_url( '', __FILE__ ) ); define( 'POST_VIEWS_COUNTER_BASENAME', plugin_basename( __FILE__ ) ); define( 'POST_VIEWS_COUNTER_REL_PATH', dirname( POST_VIEWS_COUNTER_BASENAME ) ); } define( 'POST_VIEWS_COUNTER_PATH', plugin_dir_path( __FILE__ ) ); } /** * Include required files. * * @return void */ private function includes() { include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-functions.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-update.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings-api.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-settings.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-admin.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-columns.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-query.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-cron.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-counter.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-crawler-detect.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-frontend.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-dashboard.php' ); include_once( POST_VIEWS_COUNTER_PATH . 'includes/class-widgets.php' ); } /** * Class constructor. * * @return void */ private function __construct() { // define plugin constants $this->define_constants(); // short init? if ( defined( 'SHORTINIT' ) && SHORTINIT ) { $this->options = [ 'general' => array_merge( $this->defaults['general'], get_option( 'post_views_counter_settings_general', $this->defaults['general'] ) ), 'display' => array_merge( $this->defaults['display'], get_option( 'post_views_counter_settings_display', $this->defaults['display'] ) ), 'other' => array_merge( $this->defaults['other'], get_option( 'post_views_counter_settings_other', $this->defaults['other'] ) ) ]; return; } // activation hooks register_activation_hook( __FILE__, [ $this, 'activation' ] ); register_deactivation_hook( __FILE__, [ $this, 'deactivation' ] ); // settings $this->options = [ 'general' => array_merge( $this->defaults['general'], get_option( 'post_views_counter_settings_general', $this->defaults['general'] ) ), 'display' => array_merge( $this->defaults['display'], get_option( 'post_views_counter_settings_display', $this->defaults['display'] ) ), 'other' => array_merge( $this->defaults['other'], get_option( 'post_views_counter_settings_other', $this->defaults['other'] ) ) ]; // actions add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); add_action( 'wp_loaded', [ $this, 'load_pluggable_functions' ] ); add_action( 'admin_init', [ $this, 'update_notice' ] ); add_action( 'wp_initialize_site', [ $this, 'initialize_new_network_site' ] ); add_action( 'wp_ajax_pvc_dismiss_notice', [ $this, 'dismiss_notice' ] ); // filters add_filter( 'plugin_action_links_' . POST_VIEWS_COUNTER_BASENAME, [ $this, 'plugin_settings_link' ] ); } /** * Update notice. * * @return void */ public function update_notice() { if ( ! current_user_can( 'install_plugins' ) ) return; $current_update = 2; // get current time $current_time = time(); if ( $this->options['general']['update_version'] < $current_update ) { // check version, if update version is lower than plugin version, set update notice to true $this->options['general'] = array_merge( $this->options['general'], [ 'update_version' => $current_update, 'update_notice' => true ] ); update_option( 'post_views_counter_settings_general', $this->options['general'] ); // set activation date $activation_date = get_option( 'post_views_counter_activation_date' ); if ( $activation_date === false ) update_option( 'post_views_counter_activation_date', $current_time ); } // display current version notice if ( $this->options['general']['update_notice'] === true ) { // include notice js, only if needed add_action( 'admin_print_scripts', [ $this, 'admin_inline_js' ], 999 ); // get activation date $activation_date = get_option( 'post_views_counter_activation_date' ); if ( (int) $this->options['general']['update_delay_date'] === 0 ) { if ( $activation_date + 2 * WEEK_IN_SECONDS > $current_time ) $this->options['general']['update_delay_date'] = $activation_date + 2 * WEEK_IN_SECONDS; else $this->options['general']['update_delay_date'] = $current_time; update_option( 'post_views_counter_settings_general', $this->options['general'] ); } if ( ( ! empty( $this->options['general']['update_delay_date'] ) ? (int) $this->options['general']['update_delay_date'] : $current_time ) <= $current_time ) $this->add_notice( sprintf( __( "Hey, you've been using Post Views Counter for more than %s.", 'post-views-counter' ), human_time_diff( $activation_date, $current_time ) ) . '
' . __( 'Could you please do me a BIG favor and give it a 5-star rating on WordPress to help us spread the word and boost our motivation.', 'post-views-counter' ) . '

' . __( 'Your help is much appreciated. Thank you very much', 'post-views-counter' ) . ' ~ Bartosz Arendt, ' . __( 'founder of', 'post-views-counter' ) . ' Post Views Counter.' . '

' . '' . __( 'Ok, you deserve it', 'post-views-counter' ) . '
' . __( 'Nope, maybe later', 'post-views-counter' ) . '
' . __( 'I already did', 'post-views-counter' ) . '', 'notice notice-info is-dismissible pvc-notice' ); } } /** * Add admin notices. * * @param string $html Notice HTML * @param string $status Notice status * @param bool $paragraph Whether to use paragraph * @param bool $network * @return void */ public function add_notice( $html = '', $status = 'error', $paragraph = true, $network = false ) { $this->notices[] = [ 'html' => $html, 'status' => $status, 'paragraph' => $paragraph ]; add_action( 'admin_notices', [ $this, 'display_notice' ] ); if ( $network ) add_action( 'network_admin_notices', [ $this, 'display_notice' ] ); } /** * Print admin notices. * * @return void */ public function display_notice() { $allowed_html = array_merge( wp_kses_allowed_html( 'post' ), [ 'input' => [ 'type' => true, 'name' => true, 'class' => true, 'value' => true ], 'form' => [ 'action' => true, 'method' => true ] ] ); foreach ( $this->notices as $notice ) { echo '
' . ( $notice['paragraph'] ? '

' : '' ) . ' ' . wp_kses( $notice['html'], $allowed_html ) . ' ' . ( $notice['paragraph'] ? '

' : '' ) . '
'; } } /** * Print admin scripts. * * @return void */ public function admin_inline_js() { if ( ! current_user_can( 'install_plugins' ) ) return; // register and enqueue styles wp_register_script( 'pvc-notices', false ); wp_enqueue_script( 'pvc-notices' ); // add styles wp_add_inline_script( 'pvc-notices', " ( function( $ ) { // ready event $( function() { // save dismiss state $( '.pvc-notice.is-dismissible' ).on( 'click', '.notice-dismiss, .pvc-dismissible-notice', function( e ) { if ( e.currentTarget.target !== '_blank' ) e.preventDefault(); var notice_action = 'hide'; if ( e.currentTarget.classList.contains( 'pvc-delay-notice' ) ) notice_action = 'delay'; $.post( ajaxurl, { action: 'pvc_dismiss_notice', notice_action: notice_action, url: '" . esc_url_raw( admin_url( 'admin-ajax.php' ) ) . "', nonce: '" . esc_attr( wp_create_nonce( 'pvc_dismiss_notice' ) ) . "' } ); $( e.delegateTarget ).slideUp( 'fast' ); } ); } ); } )( jQuery ); ", 'after' ); } /** * Dismiss notice. * * @return void */ public function dismiss_notice() { if ( ! current_user_can( 'install_plugins' ) ) return; if ( isset( $_REQUEST['nonce'] ) && wp_verify_nonce( $_REQUEST['nonce'], 'pvc_dismiss_notice' ) ) { $notice_action = empty( $_REQUEST['notice_action'] ) || $_REQUEST['notice_action'] === 'hide' ? 'hide' : sanitize_text_field( $_REQUEST['notice_action'] ); switch ( $notice_action ) { // delay notice case 'delay': // set delay period to 1 week from now $this->options['general'] = array_merge( $this->options['general'], [ 'update_delay_date' => time() + 2 * WEEK_IN_SECONDS ] ); update_option( 'post_views_counter_settings_general', $this->options['general'] ); break; // hide notice default: $this->options['general'] = array_merge( $this->options['general'], [ 'update_notice' => false ] ); $this->options['general'] = array_merge( $this->options['general'], [ 'update_delay_date' => 0 ] ); update_option( 'post_views_counter_settings_general', $this->options['general'] ); } } exit; } /** * Plugin activation. * * @global object $wpdb * * @param bool $network * @return void */ public function activation( $network ) { // network activation? if ( is_multisite() && $network ) { global $wpdb; // get all available sites $blogs_ids = $wpdb->get_col( 'SELECT blog_id FROM ' . $wpdb->blogs ); foreach ( $blogs_ids as $blog_id ) { // change to another site switch_to_blog( (int) $blog_id ); // run current site activation process $this->activate_site(); restore_current_blog(); } } else $this->activate_site(); } /** * Single site activation. * * @global object $wpdb * @global string $charset_collate * * @return void */ public function activate_site() { global $wpdb, $charset_collate; // required for dbdelta require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); // create post views table dbDelta( ' CREATE TABLE IF NOT EXISTS ' . $wpdb->prefix . 'post_views ( `id` bigint unsigned NOT NULL, `type` tinyint(1) unsigned NOT NULL, `period` varchar(8) NOT NULL, `count` bigint unsigned NOT NULL, PRIMARY KEY (type, period, id), UNIQUE INDEX id_type_period_count (id, type, period, count) USING BTREE, INDEX type_period_count (type, period, count) USING BTREE ) ' . $charset_collate . ';' ); // add default options add_option( 'post_views_counter_settings_general', $this->defaults['general'], null, false ); add_option( 'post_views_counter_settings_display', $this->defaults['display'], null, false ); add_option( 'post_views_counter_settings_other', $this->defaults['other'], null, false ); add_option( 'post_views_counter_version', $this->defaults['version'], null, false ); } /** * Plugin deactivation. * * @global object $wpdb * * @param bool $network * @return void */ public function deactivation( $network ) { // network deactivation? if ( is_multisite() && $network ) { global $wpdb; // get all available sites $blogs_ids = $wpdb->get_col( 'SELECT blog_id FROM ' . $wpdb->blogs ); foreach ( $blogs_ids as $blog_id ) { // change to another site switch_to_blog( (int) $blog_id ); // run current site deactivation process $this->deactivate_site( true ); restore_current_blog(); } } else $this->deactivate_site(); } /** * Single site deactivation. * * @global object $wpdb * * @param bool $multi * @return void */ public function deactivate_site( $multi = false ) { if ( $multi === true ) { $options = get_option( 'post_views_counter_settings_other' ); $check = $options['deactivation_delete']; } else $check = $this->options['other']['deactivation_delete']; // delete options if needed if ( $check ) { // delete options delete_option( 'post_views_counter_settings_general' ); delete_option( 'post_views_counter_settings_display' ); delete_option( 'post_views_counter_settings_other' ); delete_option( 'post_views_counter_activation_date' ); delete_option( 'post_views_counter_version' ); // delete transients delete_transient( 'post_views_counter_ip_cache' ); global $wpdb; // delete table from database $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'post_views' ); } // remove schedule wp_clear_scheduled_hook( 'pvc_reset_counts' ); remove_action( 'pvc_reset_counts', [ $this->cron, 'reset_counts' ] ); } /** * Initialize new network site. * * @param object $site * @return void */ public function initialize_new_network_site( $site ) { if ( is_multisite() ) { // change to another site switch_to_blog( $site->blog_id ); // run current site activation process $this->activate_site(); restore_current_blog(); } } /** * Load text domain. * * @return void */ public function load_textdomain() { load_plugin_textdomain( 'post-views-counter', false, POST_VIEWS_COUNTER_REL_PATH . '/languages/' ); } /** * Load pluggable template functions. * * @return void */ public function load_pluggable_functions() { include_once( POST_VIEWS_COUNTER_PATH . 'includes/functions.php' ); } /** * Enqueue admin scripts and styles. * * @global string $post_type * @global string $wp_version * * @param string $page * @return void */ public function admin_enqueue_scripts( $page ) { global $post_type; global $wp_version; // register styles wp_register_style( 'pvc-admin', POST_VIEWS_COUNTER_URL . '/css/admin.min.css', [], $this->defaults['version'] ); // register scripts wp_register_script( 'pvc-admin-settings', POST_VIEWS_COUNTER_URL . '/js/admin-settings.js', [ 'jquery' ], $this->defaults['version'] ); wp_register_script( 'pvc-admin-post', POST_VIEWS_COUNTER_URL . '/js/admin-post.js', [ 'jquery' ], $this->defaults['version'] ); wp_register_script( 'pvc-admin-quick-edit', POST_VIEWS_COUNTER_URL . '/js/admin-quick-edit.js', [ 'jquery' ], $this->defaults['version'] ); // load on pvc settings page if ( in_array( $page, [ 'toplevel_page_post-views-counter', 'settings_page_post-views-counter' ], true ) ) { wp_enqueue_script( 'pvc-admin-settings' ); // prepare script data $script_data = [ 'resetToDefaults' => esc_html__( 'Are you sure you want to reset these settings to defaults?', 'post-views-counter' ), 'resetViews' => esc_html__( 'Are you sure you want to delete all existing data?', 'post-views-counter' ) ]; wp_add_inline_script( 'pvc-admin-settings', 'var pvcArgsSettings = ' . wp_json_encode( $script_data ) . ";\n", 'before' ); wp_enqueue_style( 'pvc-admin' ); // load on single post page } elseif ( $page === 'post.php' || $page === 'post-new.php' ) { $post_types = Post_Views_Counter()->options['general']['post_types_count']; if ( ! in_array( $post_type, (array) $post_types ) ) return; wp_enqueue_style( 'pvc-admin' ); wp_enqueue_script( 'pvc-admin-post' ); // edit post } elseif ( $page === 'edit.php' ) { $post_types = Post_Views_Counter()->options['general']['post_types_count']; if ( ! in_array( $post_type, (array) $post_types ) ) return; wp_enqueue_style( 'pvc-admin' ); // woocommerce if ( get_post_type() !== 'product' ) { wp_enqueue_script( 'pvc-admin-quick-edit' ); // prepare script data $script_data = [ 'nonce' => wp_create_nonce( 'pvc_save_bulk_post_views' ), 'wpVersion59' => version_compare( $wp_version, '5.9', '>=' ) ]; wp_add_inline_script( 'pvc-admin-quick-edit', 'var pvcArgsQuickEdit = ' . wp_json_encode( $script_data ) . ";\n", 'before' ); } // widgets } elseif ( $page === 'widgets.php' ) wp_enqueue_script( 'pvc-admin-widgets', POST_VIEWS_COUNTER_URL . '/js/admin-widgets.js', [ 'jquery' ], $this->defaults['version'] ); // media elseif ( $page === 'upload.php' ) wp_enqueue_style( 'pvc-admin' ); // register and enqueue styles wp_register_style( 'pvc-pro-style', false ); wp_enqueue_style( 'pvc-pro-style' ); // add styles wp_add_inline_style( 'pvc-pro-style', ' .post-views-counter-settings tr.pvc-pro th:after, .nav-tab-wrapper a.nav-tab.nav-tab-disabled.pvc-pro:after, .post-views-counter-settings tr.pvc-pro-extended label[for="post_views_counter_general_counter_mode_ajax"]:after { content: \'PRO\'; display: inline; background-color: #ffc107; color: white; padding: 2px 4px; text-align: center; border-radius: 4px; margin-left: 4px; font-weight: bold; font-size: 11px; }' ); } /** * Add link to Settings page. * * @param array $links * @return array */ public function plugin_settings_link( $links ) { if ( ! current_user_can( 'manage_options' ) ) return $links; // submenu? if ( $this->options['other']['menu_position'] === 'sub' ) $url = admin_url( 'options-general.php?page=post-views-counter' ); // topmenu? else $url = admin_url( 'admin.php?page=post-views-counter' ); array_unshift( $links, sprintf( '%s', esc_url_raw( $url ), esc_html__( 'Settings', 'post-views-counter' ) ) ); return $links; } } } /** * Initialize Post Views Counter. * * @return object */ function Post_Views_Counter() { static $instance; // first call to instance() initializes the plugin if ( $instance === null || ! ( $instance instanceof Post_Views_Counter ) ) $instance = Post_Views_Counter::instance(); return $instance; } Post_Views_Counter();