From d77231ae1a8381165cc82e8cc1c52a9638cc75b7 Mon Sep 17 00:00:00 2001 From: chicpro Date: Mon, 2 Nov 2015 13:41:31 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=A0=91?= =?UTF-8?q?=EC=86=8D=EC=9E=90=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adm/admin.menu100.php | 1 + adm/browscap.php | 42 + adm/browscap_update.php | 21 + adm/config_form.php | 8 + adm/css/admin.css | 14 +- adm/img/ajax_loader.gif | Bin 0 -> 45592 bytes adm/img/check.png | Bin 0 -> 7401 bytes adm/visit.sub.php | 1 + adm/visit_browser.php | 6 +- adm/visit_device.php | 101 +++ adm/visit_list.php | 23 +- adm/visit_os.php | 6 +- adm/visit_search.php | 24 +- bbs/visit_insert.inc.php | 9 +- lib/common.lib.php | 18 + lib/thumbnail.lib.php | 2 +- lib/visit.lib.php | 47 +- plugin/browscap/Browscap.php | 1459 ++++++++++++++++++++++++++++++++++ 18 files changed, 1722 insertions(+), 60 deletions(-) create mode 100644 adm/browscap.php create mode 100644 adm/browscap_update.php create mode 100644 adm/img/ajax_loader.gif create mode 100644 adm/img/check.png create mode 100644 adm/visit_device.php create mode 100644 plugin/browscap/Browscap.php diff --git a/adm/admin.menu100.php b/adm/admin.menu100.php index 44f93712a..6ac1e32ba 100644 --- a/adm/admin.menu100.php +++ b/adm/admin.menu100.php @@ -12,6 +12,7 @@ $menu['menu100'] = array ( array('100910', '캡챠파일 일괄삭제',G5_ADMIN_URL.'/captcha_file_delete.php', 'cf_captcha', 1), array('100920', '썸네일파일 일괄삭제',G5_ADMIN_URL.'/thumbnail_file_delete.php', 'cf_thumbnail', 1), array('100500', 'phpinfo()', G5_ADMIN_URL.'/phpinfo.php', 'cf_phpinfo'), + array('100510', 'Browscap 업데이트', G5_ADMIN_URL.'/browscap.php', 'cf_browscap'), array('100400', '부가서비스', G5_ADMIN_URL.'/service.php', 'cf_service') ); ?> \ No newline at end of file diff --git a/adm/browscap.php b/adm/browscap.php new file mode 100644 index 000000000..6db2aa589 --- /dev/null +++ b/adm/browscap.php @@ -0,0 +1,42 @@ + + +
+

Browscap 정보를 업데이트하시려면 아래 업데이트 버튼을 클릭해 주세요.

+
+
+ + + + \ No newline at end of file diff --git a/adm/browscap_update.php b/adm/browscap_update.php new file mode 100644 index 000000000..2b27c237f --- /dev/null +++ b/adm/browscap_update.php @@ -0,0 +1,21 @@ +updateMethod = 'cURL'; +$browscap->cacheFilename = 'browscap_cache.php'; +$browscap->updateCache(); + +die(''); +?> \ No newline at end of file diff --git a/adm/config_form.php b/adm/config_form.php index 410bbc503..2619ad458 100644 --- a/adm/config_form.php +++ b/adm/config_form.php @@ -186,6 +186,14 @@ if(!isset($config['cf_sms_type'])) { ADD `cf_sms_type` varchar(10) NOT NULL DEFAULT '' AFTER `cf_sms_use` ", true); } +// 접속자 정보 필드 추가 +if(!sql_query(" select vi_browser from {$g5['visit_table']} limit 1 ")) { + sql_query(" ALTER TABLE `{$g5['visit_table']}` + ADD `vi_browser` varchar(255) NOT NULL DEFAULT '' AFTER `vi_agent`, + ADD `vi_os` varchar(255) NOT NULL DEFAULT '' AFTER `vi_browser`, + ADD `vi_device` varchar(255) NOT NULL DEFAULT '' AFTER `vi_os` ", true); +} + if(!$config['cf_faq_skin']) $config['cf_faq_skin'] = "basic"; if(!$config['cf_mobile_faq_skin']) $config['cf_mobile_faq_skin'] = "basic"; diff --git a/adm/css/admin.css b/adm/css/admin.css index feecc59d8..4028c3b63 100644 --- a/adm/css/admin.css +++ b/adm/css/admin.css @@ -395,6 +395,9 @@ td.td_grpset {width:160px;border-left:1px solid #e9ecee;text-align:center} .td_tdiv {border-bottom:1px solid #c9c9c9 !important} .td_tel {width:80px;text-align:center} .td_test {width:50px;text-align:center} +.td_category1{width:130px;text-align:center} +.td_category2{width:100px;text-align:center} +.td_category3{width:80px;text-align:center} .txt_true {color:#e8180c} .txt_false {color:#ccc} @@ -730,7 +733,7 @@ strong.sodr_nonpay {display:block;padding:5px 0;text-align:right} #theme_list li .tmli_if:hover button.tmli_dt{display:block} #theme_list li .theme_sl{float:left;border:none;margin-top:5px;padding:0 5px;height:26px;background:#999;color:#fff} #theme_list li .theme_sl:hover{background:#ff3061} -#theme_list li .theme_deactive{margin-left:4px} +#theme_list li .theme_deactive{margin-left:4px} #theme_list li .theme_sl_use{background:#ff3061;line-height:26px} #theme_list li .theme_pr{float:right;margin-top:5px;padding:0 5px;height:24px;line-height:24px; border: 1px solid #ccc; background: #fafafa; } #theme_list li .theme_preview{ float: right; margin-top: 5px; padding:0 5px;height:26px; border: 1px solid #ccc; background: #fafafa; margin-right:3px} @@ -763,4 +766,11 @@ strong.sodr_nonpay {display:block;padding:5px 0;text-align:right} /*전송실패 문자 재전송 내역*/ .sms_table{padding:0 20px 40px;} .sms_table table th{border:1px solid #ddd;padding:9px 0} -.sms_table table td{border:1px solid #ddd;text-align:center;width:16%;padding:9px 0} \ No newline at end of file +.sms_table table td{border:1px solid #ddd;text-align:center;width:16%;padding:9px 0} + +/* Browscap */ +.update_processing{width:300px;height:300px;margin:0 auto;background:url(../img/ajax_loader.gif) no-repeat 0 0} +#processing{margin: 0 auto;padding: 70px 0;max-width: 800px;border: 1px solid #eee;background: #f9f9f9;text-align:center;} +#processing p{font-size:1.2em} +.check_processing {width:300px;height:300px;margin:0 auto;background:url(../img/check.png) no-repeat 50% 50% } +#processing button{background:#ff3061;border:none;color:#fff;padding: 15px;width:150px;margin-top:15px} \ No newline at end of file diff --git a/adm/img/ajax_loader.gif b/adm/img/ajax_loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..5169619bedb26c4f74dc2738b449b0c72c6927ce GIT binary patch literal 45592 zcmdqHXFwC*xA!}fLPBT>C4?Rj5D+00r3jkPq^Y5ZU=2;G5D=wV5_&iE4gu*Rh9=FD zAR-`Y5DOq`P(;86s5A>Vzf=C_+;Z-7-#&NVWXjrY_MV;fS>Lta&c@ipQybI<*Vcef zpFaIuT~$+4o1C1uaf4-NXCEIQ|M&0TU%!6+`Sa)d_wT=d|K8ZxSX)~g7#R5S<;#Ny z54N_pUcGwt^XJdu;o;ApKQApUef#!peSLjpWo3MPd~_B_t$FOia9a^Cl@NX>@dSePd&K z`qkLj*woY{|G0bi?#AZk%na}K>(^6LQwI(lc>46|qeqW!+_;gMnfc@i_x0=Pw{PG4 z`u*F~)RaP@ba(fB`m|V9RyH{~rKP21Z*PC-;6d`CL$6*MAKIDJUo?Dk|#i?5wS=_4W1r`t{4g!Xl6N z_T|e7UvKZ8o}S2a=NudyZr{F>o16Ra;i$Klm!pHj*J z+xO_vV|{)7+qZ95mY1)t{`B+pjib|}BO}k8Ia6L({T}ym|A=l`BI-!)|VFPGqNxv9Y&$d#_%- zs-dA#T3VWsk=_6QzuTkI6F5rHU7AZ_ebmFFzrX+B!Gnf|hWG9bgoTDyS6A~F z1&hVH(b8gNW!2Hqc_lM5Bslokv16AnU+(O@>F(~%;c$9;d%HS26XN6V+__64k!q@| z>+9?N{ryiIJ6=>!IC%elU*9bo8yhn-Gq=Ns3v%;9Lqq%f@0|?`)78~Ie(YFAdivu> zk8a(%_3G7hcXxMebTo(4Pp8ut7Ur9pnt}rZ=jT7x*3{g)caOhff&bx9-V!=!?doDl zB3tY05TGF6jDO4h02y!P8R5zQHQN_O_4E$%jlg^P`UQlTt9kF;_XV>j2>Z z)za5Lz&0k_*Cpnlt9MM0w~3F+zP)&}DATAj)HA*jp7^LU!69c&qs&!2eL}r_P5I~l z+}2gW|C>ofkhu!!p9lDV+O!M}_r>qhA?)-f?AF8YHq_BGB$ybQXyf$>dIViPeO-dl zPCbI@E+bQd9{xWL6@F{sKBrBct!)0IE&i3ciho1|)l^rPKbbm_20EePe!6-lCMLQB zeO-P1oqUU(XQM+RJfn7ooZa@HHCXwc^$ri9Mg)Y0;Qy)7(<}5`gt-d8r~f{LGt_@o z8**0X--e^(9ePGL%9ENdIYg;K2X8>6tVCvOXK(?E7E+{eP_ZtZOvYSJ&D1 zZ0NagZ(qLowtt$Unp%eYdPanXyM~4a|L0pI`-et^p7jr<;w@cv>FD8)1cdm6MxOmQ z`vV6|?Ly8*c!qfU+F6;a@N3Ws2=Fnru(CGPGqEwTG&C^K)3YWR>k+I-#+JrL239uK zR=W)Tv#eF9_qj8^Arb#s*5|*S?MqpZ*W+!y2jv8*n?GrIpw?0=i+|GdRtJ^!5kYuWNI{a0PFf zCU4hPGn}j8(sX>dt@dFd!F#04uD$M2sqxvx>EZVJCzZr_l!ARn!`M~ZT&)u$9gXAl zkk_VNbhUYdf#&F0B=|M8|14{u(7)kS?jQtr^z^13hT`{L_| zT{qs|r9p)h9lKdG_fjNwoE+_LegA;5&7s1vr|rYTB7>keqdjNs;ksq+`ot1Mfw#^1 zD1qsI39+Xv@7yHVEIaVFLfO!pouvmdUPY43W)7IGD!gKUz-~C*G6^^|l-qaW>#g40 z`KQmTDJO4l)_)G#1KV0WxL0t!7#^GoZucr&{~7+Lyv>YCQ>Mo!N(;B&BW>&-o-oM z-e)F?XVcPUV9)%oW(Lg@IMz)W?h0Z;DEjf+va9!6mOLgq3M^lkPv`|-Y;wXnEBap;B@Kz z!)T1mS?p1w!FkaR&aT+qN4F8g4-0C9$q@5Ws*=9u_(MdE7Xwr=xAinbOF!3>cO3K5 z+Adu*u|Th{c(FwVt%w=Da#-|aRa4dD1Um~6R!q}=h}a{_wcr#p*O8W@z-k{8PCshV zk|8hj3MqEGzqu-ty-w_4*?R?AXu1c{)WlA-U9Y-iMyXQf^*7a7;vBuM3yWQBZKS_1AEr!)x{u1TI;I zIv%WDuLBJP+OD=+L|6`QyCj`G5`JEfr=)9Snrvn8E9!xjab%;xA|$0w4zJawa|T_L7{(sUP&OVs>iOC#BAzA3N9L-hgex4WBWaz&0ApJa=F6>UI#*d4Q! z+n?13^~N$^4si~(7~e^3$9fUBefR5(>>0S%n6Og|FEjb{W2zZ+Pg^Mtl6P%r#q0 zbbV7N>(3pcNP_qo5k;i9%QHG+Uvru;bVA9(jVZAIIZbRcXastJB(P_;8o1hEp+s>? zo6#gF%Qsj;%Ljy!J|v$=8Hmw)>bO|FcI5P$W3tg;7!k+i0&%2ox>1Nc2K0;Q4RhqC&8 G1LH$2w0A`)Y@4 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 literal 0 HcmV?d00001 diff --git a/adm/img/check.png b/adm/img/check.png new file mode 100644 index 0000000000000000000000000000000000000000..c3c6abd9df806916b5279a72627984e579c2d71d GIT binary patch literal 7401 zcmX9@byO7H-=|~gk`!2wkZur=TvD2qZVBlSq@=qPq(Qp7yFo-iDe3N3y6e5q@BL$U z&e=0NXJ+pAlQ%+5MGgl-0YN}Oz)_HgX@FzPe;*7a@O?Lmi3JYit};5VnvRyP?xrpl z2$JTGW)`#x_NG=A8WyJJp3Z|7q6i37Q3^0gxX1jV;f*$2d%6J45)~yuS0+|clGq6W z8bODqkPk&v&V7TZp!L;3#NMn``w4leV zH}u+`pP$p})i*Ri;S$Uk)E5^Q9x_4!LiG=OYF{cU*aLrmd-Z|p`Ew{~@xaIkZooiq zZ=g-cyH%qjZKNC|!j=y^D&RpqJzp&!ubpp?W*zP9?EL%pSr8wm_e%+(5_X=RDTzF* z_`|uTy83v7?M&?ZSV zmdeStQ9}hIS6l`qO(9E|YpqJm$$`%5eE+(+X`UboN8In7nGxjX<`xnn3)NUT~VzOp1u^Nlh?xFXxI$G0u1T zv^9ED)|VwlMn)ziB;@Ai%6*|$H)=p9A|iszVq#)OJ!2=#*TR3Jp{aRxcJ?y)kd}{+ zPfX0n+d-REPm(xp&^*ic*4n~?p&(ZI`u6tUV#}Z5VXQb=GCfJQo6UIDwDfdSyO(}; zNSD)tgL|WXkGy8l2=y-O-y`bl>-YCghgVAhvAypwDD(Odkn3coRK8HV>KrVBdyUd~woY$Y&dA{h7+5R=~z zNfs0oM5~>*U&U!7keX~}J>4_Su0?6Fu&{iMkFPTBQVFxTLE}O)>c$mXH(~`)cI}SY zK@eHRP;xFL32p|IQ`Pm@nw{-`+VjDmo(r|J(2S68CQ>R#$ax*bB`32So8kTc|2|Pu znxjQK%Bo#SCGs))BReLl3FOnm`5#p~+pYNq+kr1Mjt&lmR2Uemth~HLi`JJrHedRo zxnNYeD`L<0v2aa(Pft(x_4WPwrL;6?ZH80-_3Kv+0|TY1HVJhdoo4qVBOIklFFnum z-SHbRKN%u&_K{AA-s$o2$kbEz5}K;4JgIh1j}I|1G3Sw36aY)B>Pu^W{Ph!4Q*I~g z{Q#rFv7D~{lE(nM3*+NCBo&)mTR9R%qO^_3nsv!HnZ_`&?h&yPc5?&W>}! zy9-`2ZGBci3DpBKGBU1f;vY@S&Fh^vel#~X>(*H+ydeC^@bcwLHn!;B9}s5x`}aLs+Wo>!YKim6VjII{YK6s*diCy1HzpSOnZZAZ&nh)72{{8Ql}=cfq%M3$b-gl%+0QX>+aRK8=Y?V z{#2Rx^=i<`$G;)|VvpAw;o%q`RnVe?u1+k;zvLtq7BVKN;?aoZlf*XGeEd5f9E{@a zeTP~49vwaG`r1Q>yaaxLB^l^^jC(ooDO)I1E1RuZZ_VNAB{4501;y=%$dQMam&)z1 zgM-6DqkU9FL`_Z2_+>l{fLZ!SK*^Q$X4msD}T&Pcz& z-P(5ppO24^0g1f75I{RJp`q}2v|g{%E2*r^bS+Odi^RajUTpbLl$V#M=VxJQdAaO; z1Z`c7X&lApDXyr&j%BoeKvYxZ*r2bk)PR@2}UkY=q?|sXCY2xFf?~80;A%@BX{PWMo zLE-5J~ z)~a_OQA0XMczaJlA?GJbh4a^s9v2S}Z#JsBnoFbMAK)fjDj}!cF=zz`4K{-W-qO-i z@q{0d_HhYNT~|eP-SB=hQc}{Cx#MFe4XM5TePNf)pN|iBx;j=AUd3f)lsDPN^%LXc z&Oj1}ICN*{<~klO7jklP_LByP5LjM$9MjqwulGal;$h{=gr@zDQAwUE6Fs>U`%( z$@rb5*bJ6o7W8zKl21j1Sc)dgj1sn1S68R0q0u~FWt(5-qQlD0&d%J&{96;9XXN(c zV8vqtCy>6h*elj+Qtv*HEgOe!mSbCn1 zah&P?0x3-58r$fyLd2Q`ia_@1ASE~~G-97T_9hrSh({dXy$^bY{!$ZVT)nx(WWL@S z3WWl>;^5%Ohr|X31_I-73|*F{RC>wxEojr_mEoP~jLqJVUO+%x)AIBNa%)ue%v?>9-n`)@6v!WV?D_!WY z&+#f^Rb?lP4gf{0qzAJibLS0bN4@9SHY(P$*w!Fu0vL&;goI-o?B|dGmG6Kf^Sp_L z!r&|yCns~AK6k^MU7|q4msOa!xVcZx&b+Ud+!gGzUUPA&sjJsGu82{OK*OZ;^z{oc zINNzxGcz+CyB;oUErz>;V`6?^t@w$zh6IWOXkH)p{Won2&#OyIA=DvRQKynnr)G`s zB$a3=S?F{_N$mPboW>63=H_;GR76BUT=7XsKbq!h!F+c;JskP?_<+9_7NQvwVTNbP zpshm`p@S0zZ_3Kb)PH~Qm1>rQCh*%%M=Pz!ut#zQ@u5h%yStxl_Ci;Id$HfQucIGP zXN-ql++S~eX|IrVUcu#D^>_R{AH6V!6u-N_pO%oor^k$aOz&W}dAhy54Up(?z>m`M z>cjT^$rnFtOiZYY1ljET{Cr;+-V=D1PYxK|e0;$)_` z&>y3-tLr9g#;j&`WaQt)jPaM4n71`k$fAIA`mCV%EX0s$LQxI8ArFkSNyMY2HuRvt zY`O$F)-gzK3Cd$qJR-J7!S3$+W_-5=^`*r{Hg@*7=vnJM3HD1sTos0GNpL^@a{%8O zt5IrGH^Gf%>d&l)w*8BX_YE^f@4Nyhm)%s^WVZvqFC>gLWu&qPwo|SWa?YK;elyqyf{=;alpdsaXeY_S& zFK40|JUbt)4S0Wb2C42#9~dwjd_(*!I(m9R$K_}& zDVDWoq@+(9G*4qOuL%42c`G@YLa`7LaQlXbqu{RGYARc4BJLc)E*n2^+_0oz#QD_P z+S(fP9j<#5m4dGB-vLeGS2DoJHMJgJAHzLAdO5eFK5DS**B4< z*GSafs0#l>8opeQ;&hm+wOH#7fvF&9p91UJ=6*C?z#a+IYt5EnIEh`_wAU01>l2N< zo4b2cef^6IVp4(kgI^i&s6~rQOVhH4`UcnyTZsw!M@PTFt6^+6;F3Vi-K;5a{xO49 zS9t%Pxg5F4$JwQY>g7A&?@;ov98h(*E#dC%D<>BhK06uiCN(FvFfn4n0J)OlVw0Pi zh(=Qf2V5p5&zr5}HOl7as<1@|TU+S|B;=3}bX$h_hE9UC2GCZ~r}LGSl_xL^G7v`o zNMkLnM!=twV#18X-Mzg?)`o3SBG=)$;m}!QN?Q%QcPxWoWmnDQN+@+GC^lSljY) zwv3g(Nb~+u+3{;odzht>ZV~~XpkPsczCT{)@9e$9Ls7SV;y}n#(Iq}N>o2xW<9eQ2 zG4T}#?sKV?e`**KOVb*2?U z%spqkvfW`?s~=nRNXVvNh>QEpR2n)ppxp)&cw*xfMD?!&8-MbY@2sB%+!3gkRFsrn zOm!3ra~QUs15>oS)@eV6_Gg-&`o_}(KUuBl%Oyf-EGkHZKi((X%mMX5$LC&k4)_UW}NxQ=P z=I7J)F7dy+lTlFt6Sg^H>=!d6U&i|rnowI?J1Tak#28P$OOuVRsyb@b5ZH`_in(k1 z--&)tcPmU;pK_;mk#bh))6&wg&q>t!f4b*uNA^0mON-rcP{`JQbnBR`-e;FFOFwKge;wo16Tx|;h%N6Ei1 zna>-^d@a)7x%;1aznIdNmvD`zVh%`66b#iOYw51V;Kq6Fb4whzD*kjcTB2FR_m2pW z$bUV}@NnK8TpC%emv_T{6t@!<1toUAInYOfV{mYA2h?VJd+KHS{?@+!sShUuG&s!I z*x888YpH&pv{->-Gcq#jefSEpFkmk+m(6fJ-poyx8!!t-a5(I3F9L->A>%kC(t0vW z9^SsxY&C%~J~82PzN?NxMxw7oTV>exk>uI4ed+jxe1!jH9bbH7(ZWJCHq6$IvWkko zvo-I~@uKhK9qMowPj_j2{JYX?<>c^JI=_6x#!uweE?n^L^0;N z5;a+bA3b*-nnV2?iAtb1d-m*^6kbs{M@vhKvG2_muYQrcn_G@(sG6{_@Gf;N{r^&^ zWPDA7nWZH-SJuYL#$kME$>-nVE1jjdke*nt&otthsi`I64^u2i(LwIjRiRobl|$Lt zw2zLT${=WHXeyba5fXvBNItaRx(l7AnuhiLZMnoIxNk%m^;(hEfKVDXJ4p&w$^g=; zyJi@A2dxaRvPVHd@jt=zHKnWgmHB#MX69aTlTHYD9W*kER5)#SV(3l(5dD@FqCFZlTxO1!NMU7Um~ zFiCI@&{EtEu~uPMahLSE(j+o|S(B*hye;kO67OghMsuPdAt_jI=X%RT0g6F%baX6R z4UGQ$IrML(%lu_O@+Zf})XeF;BHYkG#zHPCed?splXSqs!qRVc%AqZMt*P|A)g_l_ z7CszQI?&qGbXBOFj^ThJ--Fpr>tU`%Jd>oA^484QV+}PhU0A(LKm#db!wzIlA9VE zCq6 z@9sdqa|3b#TuV#iaW=TBiu2cDASzaUjbNp`Hx>toUita?#KgpEkU2?td3h-*DHD_I zq|cmF%OxB61cU_|*3Qn{hDh)-E&%~t2qf~$7voy&peP6Rpjx1U>CNfZ`1m;RQtzzNvJ&`jrR^+pj?f8h5c5K*oc6MyFcuh=w9a@=UK9G2Qb@jN+%+F-fniISEK{~xk z{ecBr?O&9>$B(;{8Np?_xWNw22q1bX$yVJahYYXxlal)0NlBMZ~#qP zTr_y@efyKm4<+JPD(}FLAE7&}iQ&P7%W=`ArKPlK^YePCN?Mwl^ac^lZEd^@=Mr?) zjPw5(#zZQj5kg6~uYh9)9kdGrin?VULgQOA(1L@VjOt2IxF8Lu^ftu&R0g#pDk^Gm z3Q%gmHEWiDa6Kl}8JrcOx* zc3@;g1=K3!)V2A@# zFTvcKKA^bTwm~u&)iM*;l_`!}rwlP8)4)VAH8Ts`G_$jd7BVgW4)&VZ*w|>XL_|bD zO@zWSKKO=ZA|BT}C8e^ml92)5h4%SwXdJ<2uWRW?7H${GY-C#o>;%I;e25+TI?6IF qoHlrsL|pjQt39R76tX9#ZUifLuc>j`Aa}5LiJ%~(0;`lV3H(2&=QphY literal 0 HcmV?d00001 diff --git a/adm/visit.sub.php b/adm/visit.sub.php index b153152b1..a9b3d7468 100644 --- a/adm/visit.sub.php +++ b/adm/visit.sub.php @@ -29,6 +29,7 @@ $query_string = $qstr ? '?'.$qstr : '';
  • 도메인
  • 브라우저
  • 운영체제
  • +
  • 접속기기
  • 시간
  • 요일
  • diff --git a/adm/visit_browser.php b/adm/visit_browser.php index 49b2e77d6..aca685012 100644 --- a/adm/visit_browser.php +++ b/adm/visit_browser.php @@ -15,7 +15,9 @@ $sql = " select * from {$g5['visit_table']} where vi_date between '{$fr_date}' and '{$to_date}' "; $result = sql_query($sql); while ($row=sql_fetch_array($result)) { - $s = get_brow($row['vi_agent']); + $s = $row['vi_browser']; + if(!$s) + $s = get_brow($row['vi_agent']); $arr[$s]++; @@ -70,7 +72,7 @@ while ($row=sql_fetch_array($result)) { - +
    diff --git a/adm/visit_device.php b/adm/visit_device.php new file mode 100644 index 000000000..629a87d18 --- /dev/null +++ b/adm/visit_device.php @@ -0,0 +1,101 @@ + $max) $max = $arr[$s]; + + $sum_count++; +} +?> + +
    + + + + + + + + + + + + + + + + + + + + $value) { + $count = $arr[$key]; + if ($save_count != $count) { + $i++; + $no = $i; + $save_count = $count; + } else { + $no = ''; + } + + if (!$key) { + $key = 'Unknown'; + } + + $rate = ($count / $sum_count * 100); + $s_rate = number_format($rate, 1); + + $bg = 'bg'.($i%2); + ?> + + + + + + + + + + '; + } + ?> + +
    목록
    순위접속기기그래프접속자수비율(%)
    합계100%
    +
    + +
    +
    자료가 없습니다.
    +
    + + diff --git a/adm/visit_list.php b/adm/visit_list.php index afc950164..4dd2cdc1c 100644 --- a/adm/visit_list.php +++ b/adm/visit_list.php @@ -7,7 +7,7 @@ auth_check($auth[$sub_menu], 'r'); $g5['title'] = '접속자집계'; include_once('./visit.sub.php'); -$colspan = 5; +$colspan = 6; $sql_common = " from {$g5['visit_table']} "; $sql_search = " where vi_date between '{$fr_date}' and '{$to_date}' "; @@ -41,15 +41,25 @@ $result = sql_query($sql); IP 접속 경로 브라우저 - 운영체제 + OS + 접속기기 일시 "> - - + + + diff --git a/adm/visit_os.php b/adm/visit_os.php index c0e043683..5e6c86022 100644 --- a/adm/visit_os.php +++ b/adm/visit_os.php @@ -15,7 +15,9 @@ $sql = " select * from {$g5['visit_table']} where vi_date between '$fr_date' and '$to_date' "; $result = sql_query($sql); while ($row=sql_fetch_array($result)) { - $s = get_os($row['vi_agent']); + $s = $row['vi_os']; + if(!$s) + $s = get_os($row['vi_agent']); $arr[$s]++; @@ -63,7 +65,7 @@ while ($row=sql_fetch_array($result)) { } if (!$key) { - $key = '직접'; + $key = 'Unknown'; } $rate = ($count / $sum_count * 100); diff --git a/adm/visit_search.php b/adm/visit_search.php index 23f1af471..269c20b52 100644 --- a/adm/visit_search.php +++ b/adm/visit_search.php @@ -9,7 +9,7 @@ $g5['title'] = '접속자검색'; include_once('./admin.head.php'); include_once(G5_PLUGIN_PATH.'/jquery-ui/datepicker.php'); -$colspan = 5; +$colspan = 6; $listall = '처음'; //페이지 처음으로 (초기화용도) ?> @@ -36,6 +36,7 @@ $listall = '처음'; //페이지 처 접속 경로 브라우저 OS + 접속기기 일시 @@ -68,8 +69,17 @@ $listall = '처음'; //페이지 처 $result = sql_query($sql); for ($i=0; $row=sql_fetch_array($result); $i++) { - $brow = get_brow($row['vi_agent']); - $os = get_os($row['vi_agent']); + $brow = $row['vi_browser']; + if(!$brow) + $brow = get_brow($row['vi_agent']); + + $os = $row['vi_os']; + if(!$os) + $os = get_os($row['vi_agent']); + + $device = $row['vi_device']; + if(!$device) + $device = get_device($row['vi_agent']); $link = ""; $referer = ""; @@ -92,16 +102,14 @@ $listall = '처음'; //페이지 처 else $ip = preg_replace("/([0-9]+).([0-9]+).([0-9]+).([0-9]+)/", G5_IP_DISPLAY, $row['vi_ip']); - if ($brow == '기타') $brow = ''.$brow.''; - if ($os == '기타') $os = ''.$os.''; - $bg = 'bg'.($i%2); ?> - - + + + diff --git a/bbs/visit_insert.inc.php b/bbs/visit_insert.inc.php index 538201b53..994dcb1e7 100644 --- a/bbs/visit_insert.inc.php +++ b/bbs/visit_insert.inc.php @@ -15,7 +15,14 @@ if (get_cookie('ck_visit_ip') != $_SERVER['REMOTE_ADDR']) if (isset($_SERVER['HTTP_REFERER'])) $referer = escape_trim(clean_xss_tags($_SERVER['HTTP_REFERER'])); $user_agent = escape_trim(clean_xss_tags($_SERVER['HTTP_USER_AGENT'])); - $sql = " insert {$g5['visit_table']} ( vi_id, vi_ip, vi_date, vi_time, vi_referer, vi_agent ) values ( '{$vi_id}', '{$remote_addr}', '".G5_TIME_YMD."', '".G5_TIME_HIS."', '{$referer}', '{$user_agent}' ) "; + // Browscap 캐시 파일이 있으면 실행 + if(is_file(G5_DATA_PATH.'/cache/browscap_cache.php')) { + $browscap = get_browscap_info($_SERVER['HTTP_USER_AGENT']); + $vi_browser = $browscap->Comment; + $vi_os = $browscap->Platform; + $vi_device = $browscap->Device_Type; + } + $sql = " insert {$g5['visit_table']} ( vi_id, vi_ip, vi_date, vi_time, vi_referer, vi_agent, vi_browser, vi_os, vi_device ) values ( '{$vi_id}', '{$remote_addr}', '".G5_TIME_YMD."', '".G5_TIME_HIS."', '{$referer}', '{$user_agent}', '{$vi_browser}', '{$vi_os}', '{$vi_device}' ) "; $result = sql_query($sql, FALSE); // 정상으로 INSERT 되었다면 방문자 합계에 반영 diff --git a/lib/common.lib.php b/lib/common.lib.php index 8313f6e18..deca88652 100644 --- a/lib/common.lib.php +++ b/lib/common.lib.php @@ -3162,4 +3162,22 @@ function check_vaild_callback($callback){ return true; } } + +// Browscap 정보 얻기 +function get_browscap_info($agent) +{ + if(!$agent) + return false; + + include_once(G5_PLUGIN_PATH.'/browscap/Browscap.php'); + + $browscap = new phpbrowscap\Browscap(G5_DATA_PATH.'/cache'); + $browscap->updateMethod = 'cURL'; + $browscap->doAutoUpdate = false; + $browscap->cacheFilename = 'browscap_cache.php'; + + $info = $browscap->getBrowser($agent); + + return $info; +} ?> \ No newline at end of file diff --git a/lib/thumbnail.lib.php b/lib/thumbnail.lib.php index 0033c2aed..c3dac55a2 100644 --- a/lib/thumbnail.lib.php +++ b/lib/thumbnail.lib.php @@ -1,7 +1,7 @@ "; - - if (preg_match("/msie ([1-9][0-9]\.[0-9]+)/", $agent, $m)) { $s = 'MSIE '.$m[1]; } - else if(preg_match("/firefox/", $agent)) { $s = "FireFox"; } - else if(preg_match("/chrome/", $agent)) { $s = "Chrome"; } - else if(preg_match("/x11/", $agent)) { $s = "Netscape"; } - else if(preg_match("/opera/", $agent)) { $s = "Opera"; } - else if(preg_match("/gec/", $agent)) { $s = "Gecko"; } - else if(preg_match("/bot|slurp/", $agent)) { $s = "Robot"; } - else if(preg_match("/internet explorer/", $agent)) { $s = "IE"; } - else if(preg_match("/mozilla/", $agent)) { $s = "Mozilla"; } - else { $s = "기타"; } - - return $s; + return $info->Comment; } function get_os($agent) { - $agent = strtolower($agent); + $info = get_browscap_info($agent); - //echo $agent; echo "
    "; + return $info->Platform; +} - if (preg_match("/windows 98/", $agent)) { $s = "98"; } - else if(preg_match("/windows 95/", $agent)) { $s = "95"; } - else if(preg_match("/windows nt 4\.[0-9]*/", $agent)) { $s = "NT"; } - else if(preg_match("/windows nt 5\.0/", $agent)) { $s = "2000"; } - else if(preg_match("/windows nt 5\.1/", $agent)) { $s = "XP"; } - else if(preg_match("/windows nt 5\.2/", $agent)) { $s = "2003"; } - else if(preg_match("/windows nt 6\.0/", $agent)) { $s = "Vista"; } - else if(preg_match("/windows nt 6\.1/", $agent)) { $s = "Windows7"; } - else if(preg_match("/windows nt 6\.2/", $agent)) { $s = "Windows8"; } - else if(preg_match("/windows 9x/", $agent)) { $s = "ME"; } - else if(preg_match("/windows ce/", $agent)) { $s = "CE"; } - else if(preg_match("/mac/", $agent)) { $s = "MAC"; } - else if(preg_match("/linux/", $agent)) { $s = "Linux"; } - else if(preg_match("/sunos/", $agent)) { $s = "sunOS"; } - else if(preg_match("/irix/", $agent)) { $s = "IRIX"; } - else if(preg_match("/phone/", $agent)) { $s = "Phone"; } - else if(preg_match("/bot|slurp/", $agent)) { $s = "Robot"; } - else if(preg_match("/internet explorer/", $agent)) { $s = "IE"; } - else if(preg_match("/mozilla/", $agent)) { $s = "Mozilla"; } - else { $s = "기타"; } +function get_device($agent) +{ + $info = get_browscap_info($agent); - return $s; + return $info->Device_Type; } ?> \ No newline at end of file diff --git a/plugin/browscap/Browscap.php b/plugin/browscap/Browscap.php new file mode 100644 index 000000000..921cc9449 --- /dev/null +++ b/plugin/browscap/Browscap.php @@ -0,0 +1,1459 @@ + + * @author Vítor Brandão + * @author Mikołaj Misiurewicz + * @copyright Copyright (c) 2006-2012 Jonathan Stoppani + * @version 1.0 + * @license http://www.opensource.org/licenses/MIT MIT License + * @link https://github.com/GaretJax/phpbrowscap/ + */ +class Browscap +{ + /** + * Current version of the class. + */ + const VERSION = '2.0.5'; + + const CACHE_FILE_VERSION = '2.0.5'; + + /** + * Different ways to access remote and local files. + * + * UPDATE_FOPEN: Uses the fopen url wrapper (use file_get_contents). + * UPDATE_FSOCKOPEN: Uses the socket functions (fsockopen). + * UPDATE_CURL: Uses the cURL extension. + * UPDATE_LOCAL: Updates from a local file (file_get_contents). + */ + const UPDATE_FOPEN = 'URL-wrapper'; + const UPDATE_FSOCKOPEN = 'socket'; + const UPDATE_CURL = 'cURL'; + const UPDATE_LOCAL = 'local'; + + /** + * Options for regex patterns. + * + * REGEX_DELIMITER: Delimiter of all the regex patterns in the whole class. + * REGEX_MODIFIERS: Regex modifiers. + */ + const REGEX_DELIMITER = '@'; + const REGEX_MODIFIERS = 'i'; + const COMPRESSION_PATTERN_START = '@'; + const COMPRESSION_PATTERN_DELIMITER = '|'; + + /** + * The values to quote in the ini file + */ + const VALUES_TO_QUOTE = 'Browser|Parent'; + + const BROWSCAP_VERSION_KEY = 'GJK_Browscap_Version'; + + /** + * The headers to be sent for checking the version and requesting the file. + */ + const REQUEST_HEADERS = "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: Close\r\n\r\n"; + + /** + * how many pattern should be checked at once in the first step + */ + const COUNT_PATTERN = 100; + + /** + * Options for auto update capabilities + * + * $remoteVerUrl: The location to use to check out if a new version of the + * browscap.ini file is available. + * $remoteIniUrl: The location from which download the ini file. + * The placeholder for the file should be represented by a %s. + * $timeout: The timeout for the requests. + * $updateInterval: The update interval in seconds. + * $errorInterval: The next update interval in seconds in case of an error. + * $doAutoUpdate: Flag to disable the automatic interval based update. + * $updateMethod: The method to use to update the file, has to be a value of + * an UPDATE_* constant, null or false. + * + * The default source file type is changed from normal to full. The performance difference + * is MINIMAL, so there is no reason to use the standard file whatsoever. Either go for light, + * which is blazing fast, or get the full one. (note: light version doesn't work, a fix is on its way) + */ + public $remoteIniUrl = 'http://browscap.org/stream?q=PHP_BrowscapINI'; + public $remoteVerUrl = 'http://browscap.org/version'; + public $timeout = 5; + public $updateInterval = 432000; // 5 days + public $errorInterval = 7200; // 2 hours + public $doAutoUpdate = true; + public $updateMethod = null; + + /** + * The path of the local version of the browscap.ini file from which to + * update (to be set only if used). + * + * @var string + */ + public $localFile = null; + + /** + * The useragent to include in the requests made by the class during the + * update process. + * + * @var string + */ + public $userAgent = 'http://browscap.org/ - PHP Browscap/%v %m'; + + /** + * Flag to enable only lowercase indexes in the result. + * The cache has to be rebuilt in order to apply this option. + * + * @var bool + */ + public $lowercase = false; + + /** + * Flag to enable/disable silent error management. + * In case of an error during the update process the class returns an empty + * array/object if the update process can't take place and the browscap.ini + * file does not exist. + * + * @var bool + */ + public $silent = false; + + /** + * Where to store the cached PHP arrays. + * + * @var string + */ + public $cacheFilename = 'cache.php'; + + /** + * Where to store the downloaded ini file. + * + * @var string + */ + public $iniFilename = 'browscap.ini'; + + /** + * Path to the cache directory + * + * @var string + */ + public $cacheDir = null; + + /** + * Flag to be set to true after loading the cache + * + * @var bool + */ + protected $_cacheLoaded = false; + + /** + * Where to store the value of the included PHP cache file + * + * @var array + */ + protected $_userAgents = array(); + protected $_browsers = array(); + protected $_patterns = array(); + protected $_properties = array(); + protected $_source_version; + + /** + * An associative array of associative arrays in the format + * `$arr['wrapper']['option'] = $value` passed to stream_context_create() + * when building a stream resource. + * + * Proxy settings are stored in this variable. + * + * @see http://www.php.net/manual/en/function.stream-context-create.php + * @var array + */ + protected $_streamContextOptions = array(); + + /** + * A valid context resource created with stream_context_create(). + * + * @see http://www.php.net/manual/en/function.stream-context-create.php + * @var resource + */ + protected $_streamContext = null; + + /** + * Constructor class, checks for the existence of (and loads) the cache and + * if needed updated the definitions + * + * @param string $cache_dir + * + * @throws Exception + */ + public function __construct($cache_dir = null) + { + // has to be set to reach E_STRICT compatibility, does not affect system/app settings + date_default_timezone_set(date_default_timezone_get()); + + if (!isset($cache_dir)) { + throw new Exception('You have to provide a path to read/store the browscap cache file'); + } + + $old_cache_dir = $cache_dir; + $cache_dir = realpath($cache_dir); + + if (false === $cache_dir) { + throw new Exception( + sprintf( + 'The cache path %s is invalid. Are you sure that it exists and that you have permission to access it?', + $old_cache_dir + ) + ); + } + + // Is the cache dir really the directory or is it directly the file? + if (substr($cache_dir, -4) === '.php') { + $this->cacheFilename = basename($cache_dir); + $this->cacheDir = dirname($cache_dir); + } else { + $this->cacheDir = $cache_dir; + } + + $this->cacheDir .= DIRECTORY_SEPARATOR; + } + + /** + * @return mixed + */ + public function getSourceVersion() + { + return $this->_source_version; + } + + /** + * @return bool + */ + public function shouldCacheBeUpdated() + { + // Load the cache at the first request + if ($this->_cacheLoaded) { + return false; + } + + $cache_file = $this->cacheDir . $this->cacheFilename; + $ini_file = $this->cacheDir . $this->iniFilename; + + // Set the interval only if needed + if ($this->doAutoUpdate && file_exists($ini_file)) { + $interval = time() - filemtime($ini_file); + } else { + $interval = 0; + } + + $shouldBeUpdated = true; + + if (file_exists($cache_file) && file_exists($ini_file) && ($interval <= $this->updateInterval)) { + if ($this->_loadCache($cache_file)) { + $shouldBeUpdated = false; + } + } + + return $shouldBeUpdated; + } + + /** + * Gets the information about the browser by User Agent + * + * @param string $user_agent the user agent string + * @param bool $return_array whether return an array or an object + * + * @throws Exception + * @return \stdClass|array the object containing the browsers details. Array if + * $return_array is set to true. + */ + public function getBrowser($user_agent = null, $return_array = false) + { + if ($this->shouldCacheBeUpdated()) { + try { + $this->updateCache(); + } catch (Exception $e) { + $ini_file = $this->cacheDir . $this->iniFilename; + + if (file_exists($ini_file)) { + // Adjust the filemtime to the $errorInterval + touch($ini_file, time() - $this->updateInterval + $this->errorInterval); + } elseif ($this->silent) { + // Return an array if silent mode is active and the ini db doesn't exsist + return array(); + } + + if (!$this->silent) { + throw $e; + } + } + } + + $cache_file = $this->cacheDir . $this->cacheFilename; + if (!$this->_cacheLoaded && !$this->_loadCache($cache_file)) { + throw new Exception('Cannot load cache file - the cache format is not compatible.'); + } + + // Automatically detect the useragent + if (!isset($user_agent)) { + if (isset($_SERVER['HTTP_USER_AGENT'])) { + $user_agent = $_SERVER['HTTP_USER_AGENT']; + } else { + $user_agent = ''; + } + } + + $browser = array(); + + $patterns = array_keys($this->_patterns); + $chunks = array_chunk($patterns, self::COUNT_PATTERN); + + foreach ($chunks as $chunk) { + $longPattern = self::REGEX_DELIMITER + . '^(?:' . implode(')|(?:', $chunk) . ')$' + . self::REGEX_DELIMITER . 'i'; + + if (!preg_match($longPattern, $user_agent)) { + continue; + } + + foreach ($chunk as $pattern) { + $patternToMatch = self::REGEX_DELIMITER . '^' . $pattern . '$' . self::REGEX_DELIMITER . 'i'; + $matches = array(); + + if (!preg_match($patternToMatch, $user_agent, $matches)) { + continue; + } + + $patternData = $this->_patterns[$pattern]; + + if (1 === count($matches)) { + // standard match + $key = $patternData; + $simpleMatch = true; + } else { + $patternData = unserialize($patternData); + + // match with numeric replacements + array_shift($matches); + + $matchString = self::COMPRESSION_PATTERN_START + . implode(self::COMPRESSION_PATTERN_DELIMITER, $matches); + + if (!isset($patternData[$matchString])) { + // partial match - numbers are not present, but everything else is ok + continue; + } + + $key = $patternData[$matchString]; + + $simpleMatch = false; + } + + $browser = array( + $user_agent, // Original useragent + trim(strtolower($pattern), self::REGEX_DELIMITER), + $this->_pregUnQuote($pattern, $simpleMatch ? false : $matches) + ); + + $browser = $value = $browser + unserialize($this->_browsers[$key]); + + while (array_key_exists(3, $value)) { + $value = unserialize($this->_browsers[$value[3]]); + $browser += $value; + } + + if (!empty($browser[3]) && array_key_exists($browser[3], $this->_userAgents)) { + $browser[3] = $this->_userAgents[$browser[3]]; + } + + break 2; + } + } + + // Add the keys for each property + $array = array(); + foreach ($browser as $key => $value) { + if ($value === 'true') { + $value = true; + } elseif ($value === 'false') { + $value = false; + } + + $propertyName = $this->_properties[$key]; + + if ($this->lowercase) { + $propertyName = strtolower($propertyName); + } + + $array[$propertyName] = $value; + } + + return $return_array ? $array : (object) $array; + } + + /** + * Load (auto-set) proxy settings from environment variables. + */ + public function autodetectProxySettings() + { + $wrappers = array('http', 'https', 'ftp'); + + foreach ($wrappers as $wrapper) { + $url = getenv($wrapper . '_proxy'); + if (!empty($url)) { + $params = array_merge( + array( + 'port' => null, + 'user' => null, + 'pass' => null, + ), + parse_url($url) + ); + $this->addProxySettings($params['host'], $params['port'], $wrapper, $params['user'], $params['pass']); + } + } + } + + /** + * Add proxy settings to the stream context array. + * + * @param string $server Proxy server/host + * @param int $port Port + * @param string $wrapper Wrapper: "http", "https", "ftp", others... + * @param string $username Username (when requiring authentication) + * @param string $password Password (when requiring authentication) + * + * @return Browscap + */ + public function addProxySettings($server, $port = 3128, $wrapper = 'http', $username = null, $password = null) + { + $settings = array( + $wrapper => array( + 'proxy' => sprintf('tcp://%s:%d', $server, $port), + 'request_fulluri' => true, + 'timeout' => $this->timeout, + ) + ); + + // Proxy authentication (optional) + if (isset($username) && isset($password)) { + $settings[$wrapper]['header'] = 'Proxy-Authorization: Basic ' . base64_encode($username . ':' . $password); + } + + // Add these new settings to the stream context options array + $this->_streamContextOptions = array_merge( + $this->_streamContextOptions, + $settings + ); + + /* Return $this so we can chain addProxySettings() calls like this: + * $browscap-> + * addProxySettings('http')-> + * addProxySettings('https')-> + * addProxySettings('ftp'); + */ + return $this; + } + + /** + * Clear proxy settings from the stream context options array. + * + * @param string $wrapper Remove settings from this wrapper only + * + * @return array Wrappers cleared + */ + public function clearProxySettings($wrapper = null) + { + $wrappers = isset($wrapper) ? array($wrapper) : array_keys($this->_streamContextOptions); + + $clearedWrappers = array(); + $options = array('proxy', 'request_fulluri', 'header'); + foreach ($wrappers as $wrapper) { + + // remove wrapper options related to proxy settings + if (isset($this->_streamContextOptions[$wrapper]['proxy'])) { + foreach ($options as $option) { + unset($this->_streamContextOptions[$wrapper][$option]); + } + + // remove wrapper entry if there are no other options left + if (empty($this->_streamContextOptions[$wrapper])) { + unset($this->_streamContextOptions[$wrapper]); + } + + $clearedWrappers[] = $wrapper; + } + } + + return $clearedWrappers; + } + + /** + * Returns the array of stream context options. + * + * @return array + */ + public function getStreamContextOptions() + { + $streamContextOptions = $this->_streamContextOptions; + + if (empty($streamContextOptions)) { + // set default context, including timeout + $streamContextOptions = array( + 'http' => array( + 'timeout' => $this->timeout, + ) + ); + } + + return $streamContextOptions; + } + + /** + * Parses the ini file and updates the cache files + * + * @throws Exception + * @return bool whether the file was correctly written to the disk + */ + public function updateCache() + { + $lockfile = $this->cacheDir . 'cache.lock'; + + $lockRes = fopen($lockfile, 'w+'); + if (false === $lockRes) { + throw new Exception(sprintf('error opening lockfile %s', $lockfile)); + } + if (false === flock($lockRes, LOCK_EX | LOCK_NB)) { + throw new Exception(sprintf('error locking lockfile %s', $lockfile)); + } + + $ini_path = $this->cacheDir . $this->iniFilename; + $cache_path = $this->cacheDir . $this->cacheFilename; + + // Choose the right url + if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) { + $url = realpath($this->localFile); + } else { + $url = $this->remoteIniUrl; + } + + $this->_getRemoteIniFile($url, $ini_path); + + $this->_properties = array(); + $this->_browsers = array(); + $this->_userAgents = array(); + $this->_patterns = array(); + + $iniContent = file_get_contents($ini_path); + + //$this->createCacheOldWay($iniContent); + $this->createCacheNewWay($iniContent); + + // Write out new cache file + $dir = dirname($cache_path); + + // "tempnam" did not work with VFSStream for tests + $tmpFile = $dir . '/temp_' . md5(time() . basename($cache_path)); + + // asume that all will be ok + if (false === ($fileRes = fopen($tmpFile, 'w+'))) { + // opening the temparary file failed + throw new Exception('opening temporary file failed'); + } + + if (false === fwrite($fileRes, $this->_buildCache())) { + // writing to the temparary file failed + throw new Exception('writing to temporary file failed'); + } + + fclose($fileRes); + + if (false === rename($tmpFile, $cache_path)) { + // renaming file failed, remove temp file + @unlink($tmpFile); + + throw new Exception('could not rename temporary file to the cache file'); + } + + @flock($lockRes, LOCK_UN); + @fclose($lockRes); + @unlink($lockfile); + $this->_cacheLoaded = false; + + return true; + } + + /** + * creates the cache content + * + * @param string $iniContent The content of the downloaded ini file + * @param bool $actLikeNewVersion + */ + protected function createCacheOldWay($iniContent, $actLikeNewVersion = false) + { + $browsers = parse_ini_string($iniContent, true, INI_SCANNER_RAW); + + if ($actLikeNewVersion) { + $this->_source_version = (int) $browsers[self::BROWSCAP_VERSION_KEY]['Version']; + } else { + $this->_source_version = $browsers[self::BROWSCAP_VERSION_KEY]['Version']; + } + + unset($browsers[self::BROWSCAP_VERSION_KEY]); + + if (!$actLikeNewVersion) { + unset($browsers['DefaultProperties']['RenderingEngine_Description']); + } + + $this->_properties = array_keys($browsers['DefaultProperties']); + + array_unshift( + $this->_properties, + 'browser_name', + 'browser_name_regex', + 'browser_name_pattern', + 'Parent' + ); + + $tmpUserAgents = array_keys($browsers); + + usort($tmpUserAgents, array($this, 'compareBcStrings')); + + $userAgentsKeys = array_flip($tmpUserAgents); + $propertiesKeys = array_flip($this->_properties); + $tmpPatterns = array(); + + foreach ($tmpUserAgents as $i => $userAgent) { + $properties = $browsers[$userAgent]; + + if (empty($properties['Comment']) + || false !== strpos($userAgent, '*') + || false !== strpos($userAgent, '?') + ) { + $pattern = $this->_pregQuote($userAgent); + + $countMatches = preg_match_all( + self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER, + $pattern, + $matches + ); + + if (!$countMatches) { + $tmpPatterns[$pattern] = $i; + } else { + $compressedPattern = preg_replace( + self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER, + '(\d)', + $pattern + ); + + if (!isset($tmpPatterns[$compressedPattern])) { + $tmpPatterns[$compressedPattern] = array('first' => $pattern); + } + + $tmpPatterns[$compressedPattern][$i] = $matches[0]; + } + } + + if (!empty($properties['Parent'])) { + $parent = $properties['Parent']; + + $parentKey = $userAgentsKeys[$parent]; + + $properties['Parent'] = $parentKey; + $this->_userAgents[$parentKey . '.0'] = $tmpUserAgents[$parentKey]; + }; + + $this->_browsers[] = $this->resortProperties($properties, $propertiesKeys); + } + + // reducing memory usage by unsetting $tmp_user_agents + unset($tmpUserAgents); + + $this->_patterns = $this->deduplicatePattern($tmpPatterns); + } + + /** + * creates the cache content + * + * @param string $iniContent The content of the downloaded ini file + * + * @throws \phpbrowscap\Exception + */ + protected function createCacheNewWay($iniContent) + { + $patternPositions = array(); + + // get all patterns from the ini file in the correct order, + // so that we can calculate with index number of the resulting array, + // which part to use when the ini file is split into its sections. + preg_match_all('/(?<=\[)(?:[^\r\n]+)(?=\])/m', $iniContent, $patternPositions); + + if (!isset($patternPositions[0])) { + throw new Exception('could not extract patterns from ini file'); + } + + $patternPositions = $patternPositions[0]; + + if (!count($patternPositions)) { + throw new Exception('no patterns were found inside the ini file'); + } + + // split the ini file into sections and save the data in one line with a hash of the belonging + // pattern (filtered in the previous step) + $iniParts = preg_split('/\[[^\r\n]+\]/', $iniContent); + $tmpPatterns = array(); + $propertiesKeys = array(); + $matches = array(); + + if (preg_match('/.*\[DefaultProperties\]([^[]*).*/', $iniContent, $matches)) { + $properties = parse_ini_string($matches[1], true, INI_SCANNER_RAW); + + $this->_properties = array_keys($properties); + + array_unshift( + $this->_properties, + 'browser_name', + 'browser_name_regex', + 'browser_name_pattern', + 'Parent' + ); + + $propertiesKeys = array_flip($this->_properties); + } + + $key = $this->_pregQuote(self::BROWSCAP_VERSION_KEY); + $this->_source_version = 0; + $matches = array(); + + if (preg_match("/\\.*[" . $key . "\\][^[]*Version=(\\d+)\\D.*/", $iniContent, $matches)) { + if (isset($matches[1])) { + $this->_source_version = (int)$matches[1]; + } + } + + $userAgentsKeys = array_flip($patternPositions); + foreach ($patternPositions as $position => $userAgent) { + if (self::BROWSCAP_VERSION_KEY === $userAgent) { + continue; + } + + $properties = parse_ini_string($iniParts[($position + 1)], true, INI_SCANNER_RAW); + + if (empty($properties['Comment']) + || false !== strpos($userAgent, '*') + || false !== strpos($userAgent, '?') + ) { + $pattern = $this->_pregQuote(strtolower($userAgent)); + $matches = array(); + $i = $position - 1; + $countMatches = preg_match_all( + self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER, + $pattern, + $matches + ); + + if (!$countMatches) { + $tmpPatterns[$pattern] = $i; + } else { + $compressedPattern = preg_replace( + self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER, + '(\d)', + $pattern + ); + + if (!isset($tmpPatterns[$compressedPattern])) { + $tmpPatterns[$compressedPattern] = array('first' => $pattern); + } + + $tmpPatterns[$compressedPattern][$i] = $matches[0]; + } + } + + if (!empty($properties['Parent'])) { + $parent = $properties['Parent']; + $parentKey = $userAgentsKeys[$parent]; + + $properties['Parent'] = $parentKey - 1; + $this->_userAgents[($parentKey - 1) . '.0'] = $patternPositions[$parentKey]; + }; + + $this->_browsers[] = $this->resortProperties($properties, $propertiesKeys); + } + + $patternList = $this->deduplicatePattern($tmpPatterns); + + $positionIndex = array(); + $lengthIndex = array(); + $shortLength = array(); + $patternArray = array(); + $counter = 0; + + foreach (array_keys($patternList) as $pattern) { + $decodedPattern = str_replace('(\d)', 0, $this->_pregUnQuote($pattern, false)); + + // force "defaultproperties" (if available) to first position, and "*" to last position + if ($decodedPattern === 'defaultproperties') { + $positionIndex[$pattern] = 0; + } elseif ($decodedPattern === '*') { + $positionIndex[$pattern] = 2; + } else { + $positionIndex[$pattern] = 1; + } + + // sort by length + $lengthIndex[$pattern] = strlen($decodedPattern); + $shortLength[$pattern] = strlen(str_replace(array('*', '?'), '', $decodedPattern)); + + // sort by original order + $patternArray[$pattern] = $counter; + + $counter++; + } + + array_multisort( + $positionIndex, + SORT_ASC, + SORT_NUMERIC, + $lengthIndex, + SORT_DESC, + SORT_NUMERIC, + $shortLength, + SORT_DESC, + SORT_NUMERIC, + $patternArray, + SORT_ASC, + SORT_NUMERIC, + $patternList + ); + + $this->_patterns = $patternList; + } + + /** + * @param array $properties + * @param array $propertiesKeys + * + * @return array + */ + protected function resortProperties(array $properties, array $propertiesKeys) + { + $browser = array(); + + foreach ($properties as $propertyName => $propertyValue) { + if (!isset($propertiesKeys[$propertyName])) { + continue; + } + + $browser[$propertiesKeys[$propertyName]] = $propertyValue; + } + + return $browser; + } + + /** + * @param array $tmpPatterns + * + * @return array + */ + protected function deduplicatePattern(array $tmpPatterns) + { + $patternList = array(); + + foreach ($tmpPatterns as $pattern => $patternData) { + if (is_int($patternData)) { + $data = $patternData; + } elseif (2 == count($patternData)) { + end($patternData); + + $pattern = $patternData['first']; + $data = key($patternData); + } else { + unset($patternData['first']); + + $data = $this->deduplicateCompressionPattern($patternData, $pattern); + } + + $patternList[$pattern] = $data; + } + + return $patternList; + } + + /** + * @param string $a + * @param string $b + * + * @return int + */ + protected function compareBcStrings($a, $b) + { + $a_len = strlen($a); + $b_len = strlen($b); + + if ($a_len > $b_len) { + return -1; + } + + if ($a_len < $b_len) { + return 1; + } + + $a_len = strlen(str_replace(array('*', '?'), '', $a)); + $b_len = strlen(str_replace(array('*', '?'), '', $b)); + + if ($a_len > $b_len) { + return -1; + } + + if ($a_len < $b_len) { + return 1; + } + + return 0; + } + + /** + * That looks complicated... + * + * All numbers are taken out into $matches, so we check if any of those numbers are identical + * in all the $matches and if they are we restore them to the $pattern, removing from the $matches. + * This gives us patterns with "(\d)" only in places that differ for some matches. + * + * @param array $matches + * @param string $pattern + * + * @return array of $matches + */ + protected function deduplicateCompressionPattern($matches, &$pattern) + { + $tmp_matches = $matches; + $first_match = array_shift($tmp_matches); + $differences = array(); + + foreach ($tmp_matches as $some_match) { + $differences += array_diff_assoc($first_match, $some_match); + } + + $identical = array_diff_key($first_match, $differences); + + $prepared_matches = array(); + + foreach ($matches as $i => $some_match) { + $key = self::COMPRESSION_PATTERN_START + . implode(self::COMPRESSION_PATTERN_DELIMITER, array_diff_assoc($some_match, $identical)); + + $prepared_matches[$key] = $i; + } + + $pattern_parts = explode('(\d)', $pattern); + + foreach ($identical as $position => $value) { + $pattern_parts[$position + 1] = $pattern_parts[$position] . $value . $pattern_parts[$position + 1]; + unset($pattern_parts[$position]); + } + + $pattern = implode('(\d)', $pattern_parts); + + return $prepared_matches; + } + + /** + * Converts browscap match patterns into preg match patterns. + * + * @param string $user_agent + * + * @return string + */ + protected function _pregQuote($user_agent) + { + $pattern = preg_quote($user_agent, self::REGEX_DELIMITER); + + // the \\x replacement is a fix for "Der gro\xdfe BilderSauger 2.00u" user agent match + + return str_replace( + array('\*', '\?', '\\x'), + array('.*', '.', '\\\\x'), + $pattern + ); + } + + /** + * Converts preg match patterns back to browscap match patterns. + * + * @param string $pattern + * @param array|boolean $matches + * + * @return string + */ + protected function _pregUnQuote($pattern, $matches) + { + // list of escaped characters: http://www.php.net/manual/en/function.preg-quote.php + // to properly unescape '?' which was changed to '.', I replace '\.' (real dot) with '\?', + // then change '.' to '?' and then '\?' to '.'. + $search = array( + '\\' . self::REGEX_DELIMITER, '\\.', '\\\\', '\\+', '\\[', '\\^', '\\]', '\\$', '\\(', '\\)', '\\{', '\\}', + '\\=', '\\!', '\\<', '\\>', '\\|', '\\:', '\\-', '.*', '.', '\\?' + ); + $replace = array( + self::REGEX_DELIMITER, '\\?', '\\', '+', '[', '^', ']', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|', + ':', '-', '*', '?', '.' + ); + + $result = substr(str_replace($search, $replace, $pattern), 2, -2); + + if ($matches) { + foreach ($matches as $oneMatch) { + $position = strpos($result, '(\d)'); + $result = substr_replace($result, $oneMatch, $position, 4); + } + } + + return $result; + } + + /** + * Loads the cache into object's properties + * + * @param string $cache_file + * + * @return boolean + */ + protected function _loadCache($cache_file) + { + $cache_version = null; + $source_version = null; + $browsers = array(); + $userAgents = array(); + $patterns = array(); + $properties = array(); + + $this->_cacheLoaded = false; + + require $cache_file; + + if (!isset($cache_version) || $cache_version != self::CACHE_FILE_VERSION) { + return false; + } + + $this->_source_version = $source_version; + $this->_browsers = $browsers; + $this->_userAgents = $userAgents; + $this->_patterns = $patterns; + $this->_properties = $properties; + + $this->_cacheLoaded = true; + + return true; + } + + /** + * Parses the array to cache and writes the resulting PHP string to disk + * + * @return boolean False on write error, true otherwise + */ + protected function _buildCache() + { + $content = sprintf( + "_source_version . "'", + "'" . self::CACHE_FILE_VERSION . "'" + ); + + $content .= ";\n\$properties="; + $content .= $this->_array2string($this->_properties); + + $content .= ";\n\$browsers="; + $content .= $this->_array2string($this->_browsers); + + $content .= ";\n\$userAgents="; + $content .= $this->_array2string($this->_userAgents); + + $content .= ";\n\$patterns="; + $content .= $this->_array2string($this->_patterns) . ";\n"; + + return $content; + } + + /** + * Lazy getter for the stream context resource. + * + * @param bool $recreate + * + * @return resource + */ + protected function _getStreamContext($recreate = false) + { + if (!isset($this->_streamContext) || true === $recreate) { + $this->_streamContext = stream_context_create($this->getStreamContextOptions()); + } + + return $this->_streamContext; + } + + /** + * Updates the local copy of the ini file (by version checking) and adapts + * his syntax to the PHP ini parser + * + * @param string $url the url of the remote server + * @param string $path the path of the ini file to update + * + * @throws Exception + * @return bool if the ini file was updated + */ + protected function _getRemoteIniFile($url, $path) + { + // local and remote file are the same, no update possible + if ($url == $path) { + return false; + } + + // Check version + if (file_exists($path) && filesize($path)) { + $local_tmstp = filemtime($path); + + if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) { + $remote_tmstp = $this->_getLocalMTime(); + } else { + $remote_tmstp = $this->_getRemoteMTime(); + } + + if ($remote_tmstp <= $local_tmstp) { + // No update needed, return + touch($path); + + return false; + } + } + + // Check if it's possible to write to the .ini file. + if (is_file($path)) { + if (!is_writable($path)) { + throw new Exception( + 'Could not write to "' . $path . '" (check the permissions of the current/old ini file).' + ); + } + } else { + // Test writability by creating a file only if one already doesn't exist, so we can safely delete it after + // the test. + $test_file = fopen($path, 'a'); + if ($test_file) { + fclose($test_file); + unlink($path); + } else { + throw new Exception( + 'Could not write to "' . $path . '" (check the permissions of the cache directory).' + ); + } + } + + // Get updated .ini file + $content = $this->_getRemoteData($url); + + if (!is_string($content) || strlen($content) < 1) { + throw new Exception('Could not load .ini content from "' . $url . '"'); + } + + if (false !== strpos('rate limit', $content)) { + throw new Exception( + 'Could not load .ini content from "' . $url . '" because the rate limit is exeeded for your IP' + ); + } + + // replace opening and closing php and asp tags + $content = $this->sanitizeContent($content); + + if (!file_put_contents($path, $content)) { + throw new Exception('Could not write .ini content to "' . $path . '"'); + } + + return true; + } + + /** + * @param string $content + * + * @return mixed + */ + protected function sanitizeContent($content) + { + // replace everything between opening and closing php and asp tags + $content = preg_replace('/<[?%].*[?%]>/', '', $content); + + // replace opening and closing php and asp tags + return str_replace(array('', '%>'), '', $content); + } + + /** + * Gets the remote ini file update timestamp + * + * @throws Exception + * @return int the remote modification timestamp + */ + protected function _getRemoteMTime() + { + $remote_datetime = $this->_getRemoteData($this->remoteVerUrl); + $remote_tmstp = strtotime($remote_datetime); + + if (!$remote_tmstp) { + throw new Exception("Bad datetime format from {$this->remoteVerUrl}"); + } + + return $remote_tmstp; + } + + /** + * Gets the local ini file update timestamp + * + * @throws Exception + * @return int the local modification timestamp + */ + protected function _getLocalMTime() + { + if (!is_readable($this->localFile) || !is_file($this->localFile)) { + throw new Exception('Local file is not readable'); + } + + return filemtime($this->localFile); + } + + /** + * Converts the given array to the PHP string which represent it. + * This method optimizes the PHP code and the output differs form the + * var_export one as the internal PHP function does not strip whitespace or + * convert strings to numbers. + * + * @param array $array The array to parse and convert + * + * @return boolean False on write error, true otherwise + */ + protected function _array2string($array) + { + $content = "array(\n"; + + foreach ($array as $key => $value) { + if (is_int($key)) { + $key = ''; + } elseif (ctype_digit((string) $key)) { + $key = intval($key) . ' => '; + } elseif ('.0' === substr($key, -2) && !preg_match('/[^\d\.]/', $key)) { + $key = intval($key) . ' => '; + } else { + $key = "'" . str_replace("'", "\'", $key) . "' => "; + } + + if (is_array($value)) { + $value = "'" . addcslashes(serialize($value), "'") . "'"; + } elseif (ctype_digit((string) $value)) { + $value = intval($value); + } else { + $value = "'" . str_replace("'", "\'", $value) . "'"; + } + + $content .= $key . $value . ",\n"; + } + + $content .= "\n)"; + + return $content; + } + + /** + * Checks for the various possibilities offered by the current configuration + * of PHP to retrieve external HTTP data + * + * @return string|false the name of function to use to retrieve the file or false if no methods are available + */ + protected function _getUpdateMethod() + { + // Caches the result + if ($this->updateMethod === null) { + if ($this->localFile !== null) { + $this->updateMethod = self::UPDATE_LOCAL; + } elseif (ini_get('allow_url_fopen') && function_exists('file_get_contents')) { + $this->updateMethod = self::UPDATE_FOPEN; + } elseif (function_exists('fsockopen')) { + $this->updateMethod = self::UPDATE_FSOCKOPEN; + } elseif (extension_loaded('curl')) { + $this->updateMethod = self::UPDATE_CURL; + } else { + $this->updateMethod = false; + } + } + + return $this->updateMethod; + } + + /** + * Retrieve the data identified by the URL + * + * @param string $url the url of the data + * + * @throws Exception + * @return string the retrieved data + */ + protected function _getRemoteData($url) + { + ini_set('user_agent', $this->_getUserAgent()); + + switch ($this->_getUpdateMethod()) { + case self::UPDATE_LOCAL: + $file = file_get_contents($url); + + if ($file !== false) { + return $file; + } else { + throw new Exception('Cannot open the local file'); + } + case self::UPDATE_FOPEN: + if (ini_get('allow_url_fopen') && function_exists('file_get_contents')) { + // include proxy settings in the file_get_contents() call + $context = $this->_getStreamContext(); + $file = file_get_contents($url, false, $context); + + if ($file !== false) { + return $file; + } + }// else try with the next possibility (break omitted) + case self::UPDATE_FSOCKOPEN: + if (function_exists('fsockopen')) { + $remote_url = parse_url($url); + $contextOptions = $this->getStreamContextOptions(); + + $errno = 0; + $errstr = ''; + + if (empty($contextOptions)) { + $port = (empty($remote_url['port']) ? 80 : $remote_url['port']); + $remote_handler = fsockopen($remote_url['host'], $port, $errno, $errstr, $this->timeout); + } else { + $context = $this->_getStreamContext(); + + $remote_handler = stream_socket_client( + $url, + $errno, + $errstr, + $this->timeout, + STREAM_CLIENT_CONNECT, + $context + ); + } + + if ($remote_handler) { + stream_set_timeout($remote_handler, $this->timeout); + + if (isset($remote_url['query'])) { + $remote_url['path'] .= '?' . $remote_url['query']; + } + + $out = sprintf( + self::REQUEST_HEADERS, + $remote_url['path'], + $remote_url['host'], + $this->_getUserAgent() + ); + + fwrite($remote_handler, $out); + + $response = fgets($remote_handler); + if (strpos($response, '200 OK') !== false) { + $file = ''; + while (!feof($remote_handler)) { + $file .= fgets($remote_handler); + } + + $file = str_replace("\r\n", "\n", $file); + $file = explode("\n\n", $file); + array_shift($file); + + $file = implode("\n\n", $file); + + fclose($remote_handler); + + return $file; + } + } + }// else try with the next possibility + case self::UPDATE_CURL: + if (extension_loaded('curl')) { // make sure curl is loaded + $ch = curl_init($url); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_USERAGENT, $this->_getUserAgent()); + + $file = curl_exec($ch); + + curl_close($ch); + + if ($file !== false) { + return $file; + } + }// else try with the next possibility + case false: + throw new Exception( + 'Your server can\'t connect to external resources. Please update the file manually.' + ); + } + + return ''; + } + + /** + * Format the useragent string to be used in the remote requests made by the + * class during the update process. + * + * @return string the formatted user agent + */ + protected function _getUserAgent() + { + $ua = str_replace('%v', self::VERSION, $this->userAgent); + $ua = str_replace('%m', $this->_getUpdateMethod(), $ua); + + return $ua; + } +} + +/** + * Browscap.ini parsing class exception + * + * @package Browscap + * @author Jonathan Stoppani + * @copyright Copyright (c) 2006-2012 Jonathan Stoppani + * @version 1.0 + * @license http://www.opensource.org/licenses/MIT MIT License + * @link https://github.com/GaretJax/phpbrowscap/ + */ +class Exception extends \Exception +{ + // nothing to do here +} +?> \ No newline at end of file