From 420f89af99dab5eedda84f9d5df5e5d63d835420 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 8 Feb 2023 23:06:42 +0000 Subject: [PATCH 1/6] Built custom favicon.ico file creator Followed wikipedia-defined ICO file format info, and used with Intervention's good bmp support, to create a working proof-of-concept. --- app/Settings/AppSettingsStore.php | 12 +++--- app/Uploads/FaviconHandler.php | 69 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 app/Uploads/FaviconHandler.php diff --git a/app/Settings/AppSettingsStore.php b/app/Settings/AppSettingsStore.php index 8d7b73c1c..d830df639 100644 --- a/app/Settings/AppSettingsStore.php +++ b/app/Settings/AppSettingsStore.php @@ -2,16 +2,16 @@ namespace BookStack\Settings; +use BookStack\Uploads\FaviconHandler; use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; class AppSettingsStore { - protected ImageRepo $imageRepo; - - public function __construct(ImageRepo $imageRepo) - { - $this->imageRepo = $imageRepo; + public function __construct( + protected ImageRepo $imageRepo, + protected FaviconHandler $faviconHandler, + ) { } public function storeFromUpdateRequest(Request $request, string $category) @@ -39,6 +39,8 @@ class AppSettingsStore $icon = $this->imageRepo->saveNew($iconFile, 'system', 0, $size, $size); setting()->put('app-icon-' . $size, $icon->url); } + + $this->faviconHandler->saveForUploadedImage($iconFile); } // Clear icon image if requested diff --git a/app/Uploads/FaviconHandler.php b/app/Uploads/FaviconHandler.php new file mode 100644 index 000000000..78c9a899b --- /dev/null +++ b/app/Uploads/FaviconHandler.php @@ -0,0 +1,69 @@ +getRealPath()); + $image = $this->imageTool->make($imageData); + $image->resize(32, 32); + $bmpData = $image->encode('bmp'); + $icoData = $this->bmpToIco($bmpData, 32, 32); + + // TODO - Below are test paths + file_put_contents(public_path('uploads/test.ico'), $icoData); + file_put_contents(public_path('uploads/test.bmp'), $bmpData); + + // TODO - Permission check for icon overwrite + // TODO - Write to correct location + // TODO - Handle deletion and restore of original icon on user icon clear + } + + /** + * Convert BMP image data to ICO file format. + * Built following the file format info from Wikipedia: + * https://en.wikipedia.org/wiki/ICO_(file_format) + */ + protected function bmpToIco(string $bmpData, int $width, int $height): string + { + // Trim off the header of the bitmap file + $rawBmpData = substr($bmpData, 14); + + // ICO header + $header = pack('v', 0x00); // Reserved. Must always be 0 + $header .= pack('v', 0x01); // Specifies ico image + $header .= pack('v', 0x01); // Specifies number of images + + // ICO Image Directory + $entry = hex2bin(dechex($width)); // Image width + $entry .= hex2bin(dechex($height)); // Image height + $entry .= "\0"; // Color palette, typically 0 + $entry .= "\0"; // Reserved + + // Color planes, Appears to remain 1 for bmp image data + $entry .= pack('v', 0x01); + // Bits per pixel, can range from 1 to 32. From testing conversion + // via intervention from png typically provides this as 32. + $entry .= pack('v', 0x20); + // Size of the image data in bytes + $entry .= pack('V', strlen($rawBmpData)); + // Offset of the bmp data from file start + $entry .= pack('V', strlen($header) + strlen($entry) + 4); + + // Join & return the combined parts of the ICO image data + return $header . $entry . $rawBmpData; + } +} From 1a189640f10c91eb3c519837e4228d5eb955517a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 9 Feb 2023 13:24:43 +0000 Subject: [PATCH 2/6] Integrated favicon handler with correct files & actions Format does not look 100% correct though, won't show in Firefox/gimp. --- app/Settings/AppSettingsStore.php | 2 ++ app/Uploads/FaviconHandler.php | 25 +++++++++++++++++++------ public/favicon.ico | Bin 10933 -> 3134 bytes public/icon.ico | Bin 0 -> 10933 bytes tests/Settings/SettingsTest.php | 14 ++++++++++++++ 5 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 public/icon.ico diff --git a/app/Settings/AppSettingsStore.php b/app/Settings/AppSettingsStore.php index d830df639..e6fc466ba 100644 --- a/app/Settings/AppSettingsStore.php +++ b/app/Settings/AppSettingsStore.php @@ -51,6 +51,8 @@ class AppSettingsStore $this->destroyExistingSettingImage('app-icon-' . $size); setting()->remove('app-icon-' . $size); } + + $this->faviconHandler->restoreOriginal(); } } diff --git a/app/Uploads/FaviconHandler.php b/app/Uploads/FaviconHandler.php index 78c9a899b..f61e7ae64 100644 --- a/app/Uploads/FaviconHandler.php +++ b/app/Uploads/FaviconHandler.php @@ -17,19 +17,32 @@ class FaviconHandler */ public function saveForUploadedImage(UploadedFile $file): void { + $targetPath = public_path('favicon.ico'); + if (!is_writeable($targetPath)) { + return; + } + $imageData = file_get_contents($file->getRealPath()); $image = $this->imageTool->make($imageData); $image->resize(32, 32); $bmpData = $image->encode('bmp'); $icoData = $this->bmpToIco($bmpData, 32, 32); - // TODO - Below are test paths - file_put_contents(public_path('uploads/test.ico'), $icoData); - file_put_contents(public_path('uploads/test.bmp'), $bmpData); + file_put_contents($targetPath, $icoData); + } - // TODO - Permission check for icon overwrite - // TODO - Write to correct location - // TODO - Handle deletion and restore of original icon on user icon clear + /** + * Restore the original favicon image. + */ + public function restoreOriginal(): void + { + $targetPath = public_path('favicon.ico'); + $original = public_path('icon.ico'); + if (!is_writeable($targetPath)) { + return; + } + + copy($original, $targetPath); } /** diff --git a/public/favicon.ico b/public/favicon.ico index 41655ccba55f8dd5f250471eb2e8928b21a85992..e047657ccad8c38f318f2bda36f0809a9b0c878f 100644 GIT binary patch literal 3134 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 literal 10933 zcmV;mDoWLfP)j2^ZpQX*P6L$X71eay!XC;zEx{=Rqg$KyUwYqKD*%m zGDs<d&6Qc5WwY9eiO9OvbMX1mRw+55}LUy_WitPh=GnGdbfIaN;k(rT~bd?fb< zML0S+r^>x5uQ&Ve`P1?K*--q^g8pL=vRtmG8;%7Ub9K9~v*wPIaU#!9VehkWL&l?S z1$X_DGTtpKwXU3Cu=I8;kKd(@qJlk?-W3CVO zP!}oM(*CV83A2kXTSTModyhPOUb3-z=hJmwwltJ*Jb$t2g3XQSqr(#SmL=9m>oqWB zw1=YF?U8|zUwF^<_X+9ec}nd+ytxcxFuRX?SDxqS^2+nPt}BEPLj2L${{ce3;pj@% zeChmFwsD>wc&~DMYoFvkMJ1L~36}MVvE2&ttn~&oJTyM_GbwA#_^E&VR4HNkyN{bn z318VBrAetA^3~nP&7>Tbe56ZSij+?c1+B~i@TfsDBx6|2#p}H(-J5ZhoiGzz4SS|wT&Q)>?C`}5rh=t%4-WxQ(Ky=fv6ulkV=heL877a>Q zg;Q*RhXZr96;(hClZB)}hXb`PxJNp_X!&)3n_G5*i+-7=qay#r2bFPL8(hh!4^*;K z|3AG#{;(nF&jnX6t>oajvWU$jbqzOV+LPtXu8H@DWLr%1y?$-Nt8`;cUGF2?lG>E| z5cTv@-c0k4V6CvnI6ncgOwnF=gI_C;a&-W7jH%UFw;H+~2(G1@1tHzNN?fi$O3RXJ zmi}=1^~<*n>2Ot>-m1JYSY|rr$t1YeJM*^Na>bJVtHPG+-Rjfst(uYU&XB9k4_903 zwz*?255%YRs@YEQDc?}-c;)0TbN0S5WXj)a$Un`1KNnowahA~){KhNvT!QAaO*ifR zWmNP#NqNPYmZ_sAJ&eySsc7d>Yu^{~UM0Il+zr21kIRpPak2HP^97M06v7L}>Kr{AX?)DuZVmj7n;cY2~Bi>(xZYkO6UA{J_l@X)Khpz~Wn z!`v6cpn2ib^>v}jE7$I-dvN^Q`X8m&%iQDW->xcTmc?va39YLe*4J zF0O!noo1o(Q)o84S~u6(FnU4c9PcyGFs^F4))TI$XXY5Su%+eWSw*q%WJZ2~ISXVp zxkf$%`8hYp1#+>V0=EgOnTq}3SMEMaRRO(xLcZ}xhv9DZJ-wcTdo`CX3J2^fdR9=p z8+P+-T1hV`jj=3nY(fU^*4x#t+?}+xk*DuIdvtVWco&ZiLIm#9FLrtFa;#gdTBAta z(p_~@j3@lcYO+wWqxf;9F4m9!WXRubz+Y7OAcFXjOywGDn8S;nZcS7>luOvq@q+2R zZM<{J`y2C$jZXW7meGeJ6Yi8MwPn97d$O!(w#%sGw@b!ZUlVITC_^%%5>lF*|q(;-rWbeQ~%fQ~o4${N5<1>T2yUy3Y7GYK?EM zIko%i;3>LbQ!B4z?S7^6Vy?SZ^{wKapI@6^+RTpBTesX+s{1S3TF?E}2K%BeWP*P6ScoFRlZ`^tREo-NboPPKG$9G9`p&K#MMxV3a`K%Jf2pKdFU~_K{ z{#qp(8iZEuuUQ1Y_j%RXm<%x&;~SI|L0s?rP39lL^o8xdb1XQH$am!!;9Z(g1%x|l z6d-dkVF!+rL@B6!6q7^&=!d#!m2=tEZfmV*xBJ|k6xb-uM{B;GG5F?=(O;-jN~CvN`Oi{Fu>Pn-;}9naZ9X5hrRVmz1>%4&1%v*_Ps} z?#2ghIUW@^#OJy5jp?=Mb1R`o%}VsN*shbamnVrG>Un*GqVN{~h84P{swIKPGOzP)oK|2|Bq)_Al} zTj-h|yh7Irp8lNQ#=I8}HGlC#$##f+JLgq}0doC~dV3mJTpU;BaS-do0|tW_N?p7F zM`e8fsXj|9hdB8_PUJqgUsYFEn4ngFeZIN_baD^;UN_IN`<=HU4tV`kdZYc6S}olN zrG6*=I>G;h8o8DH_Lt@Z4j~r}S`gbMr+0;^jw0q{{C6cfKi}&;vRW853T;Bu!k(4` zm`nvWH)mbkguA%FP!K8>(GW0T#qnPxRE!Zj$gwhqb-*#{!%hI^;|XG;;sd^cDvjJO zs$yW;7fo9DY=!wfTfXf#4Ia+TU0u-)4EwB}&c$$kTdK3H9Io%loMGw%3Hyq^v!p@A z99t`AD-dYgzuw$XMAtsa(*mdvmRJxpDMRO!Zyi6+^PKD4Gq9m9i^WwHZu?aG@X9%F z{KdA9-^8B_?siCClWSy`HakQI@t(Nc?ZT_cWjAfldM&wq`}KCy!Kg!LTc*@6Tgd(2 z8m4wEkAti&WgDz5KrRuZ#ZnM;#VPuLXoVlHfbGA&bpLL+R*uC^qrAvhP_0Pks@e(R zC%lJfHb8Q_;#f;B$Z2lKw*`VcAZCiyAOgf;x`Jp0+AxV zWRkFLv7dClZoO4CN!d>Xw3?P)UKU_29MUT}uxN|5U5gjje#$&;T~S*qkjFAUZr`ca_wJd@tx(w?ID69S z%#D|y8;-E_$fN{KX&|KBD!FTY1ie#3Jw1klkU6r4j6EICyZFjp(kFudOjUqoJP&qirlsp zl94jn8N&(CE>S-hanK_v4Y!tgd+f)6S7y(Th_|JA-2b11ul;=|uy7&k6(Dj4YZicD0A@J#~YnGfT z=+B)>Id5gJlONwzev56=%0`(!dze2UG<=uGE|lGE!RvM1;qc*v zPf9<y@0?B<1=7Mp3PEW8G;RHh;mw3Ab}^6;P-ZE1iLDS8 z4;|=R(5->aQQUG`z^`tw|F4eVt`7enp76u49s11tLid;!6;b=uq=- zPrmYsU>Q$L64{!Lf-xsV2v1v7bq5qpV~Tp2wdrz zr~U?R_kFD~Re^JzQX&mHxO*l$&D0Ar>XdD_%>Ye~>ZzCky?53Q@~8=-bg4z}h%7cG zbS&Ir>FVWrqkYyy;|Cl%*z5VF(z;Z+6Z3k7aW|bEHJl}~7my~Fi*+ETvWh$4IZ3Ni zmO#h7weGr4hn^Ec2707G=g$If=oUkIMCk(SRJibU`Y+`#;M~TS`DHU9erfJn^9W$E z>SHki6km&0d<@9{=DK(@$;(sV}GMDBoi>J1zlQ@tHUPn4LMYA$b0vaZx5i zm(C&C9zf4sAun|!pmSK@Oxot9QU8GbvEy4f%hQfd@yET4k?hO>f> zK!Mm!dvHlsz7mb0hb|<=BOb)w3-NoZ(_ffu=jCQBR~wI%g0T@81(Wi2*chm13&Syq4GqN1Bf01+-W(iub$9;g8~ z+);z`7rC7Uz+h*LoC3jmZ?k4G^tc$ZRkshio(p=V^M*#7{2sc#3lApdbTiF?WykJHe7ct|6U zY*0*KEc@Y_TjpcqDfrbtVNmIJ@OVs4(~8?*p6l4*+zm_>E5#;YlZ=pkz^zPOM`4C$ z0|QiUaWHy9WFK!I5broogunXO5qtA;=6l8&il$wM1>Mpa(GC1Ke5L15qJtiLE>nL= z%(G+p3(W&y?ixzNsT>97L_2A5&lyQa;aIa~0rfEHtFR{8hzXEE}3x5FT zLOBJhchmpiG7=)@RK1}|hG<9Vdi{Rr@?lV4-6_a_Wg2R40Vk7^XBd8h{T0tei34)$ zmw#_-4^qKWnuBalD~I!$Z^7eA#@_(D(v&r+91&`}14yN`wF zE@9R5rO>W_%~JQ*ASSdrH6mlWW8AO0gbzy#6gx^UR`j>mgTTXHKf5%6;cX%|dWFK< zvYyTB6}UcEHB_vH-)^RP8vJ43@9~#QUO}RzsL;|7N>^L#j^03DUXl;8w%o=`P&5{? zbcbf`s`YaF9L6tb*vBt{$tSWP{s)roDWT{XBZ2+Rz7W9d<>r4$a`cy4HPSSumspH z6Da{^vX@g(@!{fnt!TEtZ0R1NR@5IncPm?~dzm7&^CZq~{oDzqw{# z_bA9tG48W}1VuM0mfE|4s_J~V?^HHtFU=LzXwh8(7ZnY}1<24Cj@Wv`s(p7u3Yx(= z&y>0b6HE$j)WG+17%(lYp1v(~Q3S2j#lV`0_i_uO;F>jah4BZ-EG-*qO9FFS$8_gT zkaY+ExYr=Nn`tz>cwUIv4xQ$tU~qvMu1yd4wtyuk+E?+J^@#jh&!meshzbS4EuP&B-XSIRY zNGl3K>=K7)2O^DW)Bt^VZC&N(5R|Py=Mo4b;v%GH0z@AP-JqWWs{4vn!U+3szGzQyZN+T-TY+=tqapVlszWY&Q6l|PaQ@(LN4K`~bB z72UvHul81yK$8xBbF>R#_KoHfs~mt9qXHt`(jnt(gU&h*mVbFumA49RJF>nt#Y1_0 z+Y09?uzbyh#QQfHmJud^x_8iv~>MeH6Dg`s2k;3 z2PQ3Pr1XgepX;tCl+)ozzr?`OR#Bu|}f@J~zR)g*HW%35Q6yUV0t2o1e@s>Iu)=7VNMb z1D8bQQ{e?Y8iZWZ{RpalX`wCjInH>TGDF6}diU4|g)8CS;hbp|7r>HjKjh2<=VDnX zPeb`~YkP-3-0uFmrs+N;=KbcCH5xj6SMy`{$1q#pj4J(L{pp@wH6+}foNX}$!un5Q zj}}&eYhfAfj4mMJ#dJ{*98t2FJPHM4D&DfUh6CZxT}lqa<)i7}lplnEb~=MY#U(VFR;3ij5{C^;2GJoEStL?+#-3HoW8!O8{D zZA9=B-F28Vs9AE=?htg&bD;V?xHa~*#pHku&tmo2<#4}u&J)uIV7cs2$Rl9C>{QDZ zpkAxIB`$)(k6ofGQvCi_y0!d(b*-P({fw%K`d!64iZP-cT)3DPWatVzHpj&jeFpb_ z&JM0P3gWitNn@ZYI#hsf<=aDdS9Mc`32OY}u}obXddv!O(KUck5fLulK~Qyy>v)wJ z&goOWHw=Sqou0T9wSd^Kvok7Ifmo?{msP+6vZ(-($|UN7YKWq$xD56iPAR*CH1h>< zAWx7;8L&c@kO(ctRgZF85ASzs-mXd`Xtt@^G}q4|M`c`SSHk9BANdwdhg+#xbB!Ay zYgqYvwlJ_Rb!5xiz$BXD0Wwv7#68f)t1gNUp!t%3O>U=PdQg+2zO|vHwYqeh1^J^) ztLy>r{ee5%@|wZJ{M<|C^^lWqTxPommVx$>as`O_;wZgAu85PU`%U`ZA%!fVSJ?mFI z5mJ1LcUs1RC{g?(egSz`mSP2ZGK)Cy^>yv8d=Kg$@$IFZ1pV)X57PI8uHHd2byGo1 zN6k$*8U1op*<<*5QS6Pv#*o*fTxXvTv{Q89bHIa8JV4szw>$xkIx^*5XwoI%p4%-L zF(vYMuMeQx#vq+84X8sIK5%S*Qbg%UIQwIYuDmlO>@A#Neh%ddYn1aCkR@v23bKh@ z!hPT~TnUGU^Q!8#9br^vWQ_MP=%cIM-=hIIGvp6khMk6|KNls!t@T-+rri*y$e&p; z6Dr=eg~0%{gyEIti-3jj|!T!fxZ{!!K^1>@V zw~xgC*5S-y#&AeJR}^491~-*i$BZ#BK!iu@ePC4Eh}&L6Au7FEtn0UM(w0owRrvMm zgfpczpk$G`hT}3&i&u<X7!b%w%l~4`XuIR}@13n6O}dBN#TJ z?isI@5aHolrnSJ8k*_Y7?}uMzKG{|LE*P6yW;j|y%}1WzF3(`__`1tI6JSuI@Y9}7 z@EPDbPWc$_Hh4YWlncjaB@Zmifd@Tu3QXN0cZ}f|yB6dT;ldW+UG`E0q&Ox^3j|O1 zzMvJL67`jHE4EnB`wo2e;Ox9H6YdAIc~N%3y1RE_M*SS$Wv+cL_VW-fG2-`Q z`C`uf3!Ad+wpx4ApS>y>qIy*@w7i#X7J&FcOoWKTRhwvg!+^%&W_<>9c)wPH`(3CN zrN5~@4_9WT-!gWElbU2r*#Nk9D#O?K0xI6I{p!pEN<>1#=VfzugKT(0Z>~JDP92SoZ~g% zki+C|h@4S%vg-iob+`6lkM7X*W#B5^9%IiYr~J*@4Y8MNwDpKmw>g!n(SY`@+u@z> zBddGoLF;qX^==7J)Y`noaSqO1N*Qlx0~bx{i_7oAi}L(C=8jM?-~NTOKk$y&CN=|m zx1N@Axd*S}2oDWQ^;iN}Wd6_%hzWjQz=@Ddx8m`;Q z!TP}AB42_C5HENOf7bJ;X##bAJaIf zxN6Ulg8`{{o38*N8xR97`;~n~M+lwaHP&S?ba+uKO&1I!>PNKong~8Q#+{OIm z6`Nqg<%ji)_Q0J5Sz6Dl$e6RyemWjUMb6F4I|tkgMf@mUE(F$a=JVGA0 zPFMRW(qZO}re}QPVc@Lr_WH%(e?ZqqaG1@!1Pa(pI}kg>IKn|5k>mIs)I*ft2q#pDbX~661&vL9 z!P>>})_e6jcs7I{eS*j9nn7a6;uY3M@KeP7qQXz$;=S}1hPhBa(OS*92}EyE$QU4w z1gzltS{0$#1%Vgz8`S%tV~fB(y3d>uQw}xG_nB4rS)-eNb+p+TuUrZh&qbTv`*;W_ z_s)@S0J#%H{2PJ)yVcPZ*KBD{vtpCBy(&m(ng*K(I09siIVX*;Y{ky{U2@N~dU@WE z?6YNCK-`(4a5vqzH!8?ISS0u%2+5Kt{2TbRgi( z{|AS7K?x}O3V$<uW8W#7?Mp-#1P>7)Gs%sP5GdhODdmg=ZP$KQuY)_2Abp5;m0X zg!S8FM;1naA;6mFI0lU5IB6i;vYV%%b|@x@eDE5r{Z#1!EjI;Jcgv6s$A91OSD!WJ zzR{P$7I;j4JwS%aTJ%2GQSGjLBCf8vscbGzvn1uDZ7YCnPsQf|r0O&NN(264$zg4K z7uil;b8ASj^Q<`E^|pO0jb-7q$Jt)x7MowEJ4f;p9+w?dDmJxz98%a{)8N&)vi7!z zXsz8H_vI!0WTyN?K13k~i4CGHsJkmZ7gng}<9l9Xg$e5$R`>1(LxStB_8bcNJ55s@ zdtt}zC+`(?gnfSTk4nx!=|hXBV-Yw$m17wL$~EEyhoSm7orih`jGPjo^6Ce}``6F# zN(GNLE+vZLaO(ccZie&lO`BUEJ0EiG#pjk&N@jduYH?_0PcHZx{)p*?2 zN%ontE!W?)-rlOEI;PI^)Djm(?A9ue;tL0g-wJM2eCk+28%LpZmq&m|wvq^{bBgBT zA-LzMt0)Sf#pr+wZZ}|Z@5ViSK8DtFs<(052T5NQZ?iaIb?f`~g1vCRG<#A-L+H07 zY_r}9<5o8;@k)c>=labqv*C2&%b2pCV9U_QzQuY-IbPDoDxkQVS=f_77V`rsAa*m5 zKoF&3j%YwdZGW#Im&UkG8rA5U&sbYYbbQ!1`m0V+KKKUmL+fQVr*Z(k?q$1v_7R? z`SLQ`fT>x@MaAjXQU0s8{Tf%L$V)ZojPYeq*=gP?J!ih?>@9OA*9Aplc zt9~xq%6B>1ytn}+hyL(>#n znT!B#iphKpwB;S{gL|S&hGGOX53ZK%+5_4o)oAbD8Jd>)FLeC`E?-OETz(&}Z_Ugz zWltcob@@D)V5#$*?4S0=zZ0-dCPeD-4SN9Nc=Z010758SPQ^_~~toGD@P=khYN+pLUS8|78C7)T^I_~sc z7nhkAX1?BW$ki`00*pqVrN_4>MV0Mj)q{sQ@0%tv*ZIJ?jWxhUA(#&0v2RB2g$FG6z;MnQ>sSp8P-KZKz;QW4js^D~>Y)m6Xx^*(5cd`^w7jmx z(~q#05dn5LO8lDm`X_6b9PxE&sQf_mIA2p{uph2`uZUteThnc`@;BKheA& zKy_HU}@zJvTh< z`YiYtUC6CEnlBaQqQ}J@e9tZXj#md-Lb7LNnD&l;bR+gZsYy?J)bT`Kg)U}gL(vfi zt1=E5y4p(GUq18jx&qd-pitxdwHLYrlBdi#au-+uJX&Ev+LJB zZzZ!k4ct>Fd9Re;kGZ`uFV(UqIix7oawu(Laz$IdNp~*jKrc70ar1evpxKvSPS?N6(WX2Xx`Z zSs}ij4P2@xyp*p=q{Gho8l`fjm~`Y><dh XHrNJO5L3!r00000NkvXXu0mjfR=v%1 diff --git a/public/icon.ico b/public/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..41655ccba55f8dd5f250471eb2e8928b21a85992 GIT binary patch literal 10933 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 XHrNJO5L3!r00000NkvXXu0mjfR=v%1 literal 0 HcmV?d00001 diff --git a/tests/Settings/SettingsTest.php b/tests/Settings/SettingsTest.php index 30bb50f7c..fb952585a 100644 --- a/tests/Settings/SettingsTest.php +++ b/tests/Settings/SettingsTest.php @@ -52,6 +52,10 @@ class SettingsTest extends TestCase $this->assertFalse(setting()->get('app-icon-128')); $this->assertFalse(setting()->get('app-icon-64')); $this->assertFalse(setting()->get('app-icon-32')); + $this->assertEquals( + file_get_contents(public_path('icon.ico')), + file_get_contents(public_path('favicon.ico')), + ); $prevFileCount = count(glob(dirname($expectedPath) . DIRECTORY_SEPARATOR . '*.png')); @@ -71,6 +75,11 @@ class SettingsTest extends TestCase $resp = $this->get('/'); $this->withHtml($resp)->assertElementCount('link[sizes][href*="my-app-icon"]', 6); + $this->assertNotEquals( + file_get_contents(public_path('icon.ico')), + file_get_contents(public_path('favicon.ico')), + ); + $reset = $this->post('/settings/customization', ['app_icon_reset' => 'true']); $reset->assertRedirect('/settings/customization'); @@ -81,5 +90,10 @@ class SettingsTest extends TestCase $this->assertFalse(setting()->get('app-icon-128')); $this->assertFalse(setting()->get('app-icon-64')); $this->assertFalse(setting()->get('app-icon-32')); + + $this->assertEquals( + file_get_contents(public_path('icon.ico')), + file_get_contents(public_path('favicon.ico')), + ); } } From 2845e0003ed3a7960257ef19b139627ee69523dc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 9 Feb 2023 15:14:41 +0000 Subject: [PATCH 3/6] Got favicons better supported, can't get transparency right Digging deeper, I don't think PHPGD supports 32bit bmp output which complicates matters. --- app/Uploads/FaviconHandler.php | 19 +++++++++++++++---- public/favicon.ico | Bin 3134 -> 3134 bytes 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/Uploads/FaviconHandler.php b/app/Uploads/FaviconHandler.php index f61e7ae64..39f8b12ca 100644 --- a/app/Uploads/FaviconHandler.php +++ b/app/Uploads/FaviconHandler.php @@ -28,6 +28,8 @@ class FaviconHandler $bmpData = $image->encode('bmp'); $icoData = $this->bmpToIco($bmpData, 32, 32); +// file_put_contents(public_path('icon.bmp'), $bmpData); +// file_put_contents(public_path('icon-test.png'), $image->encode('png')); file_put_contents($targetPath, $icoData); } @@ -54,6 +56,9 @@ class FaviconHandler { // Trim off the header of the bitmap file $rawBmpData = substr($bmpData, 14); + // Double the height in the "BITMAPINFOHEADER" since, when in an ICO file, half + // of the image data is expected to be a mask. + $rawBmpData[8] = hex2bin(dechex($height * 2)); // ICO header $header = pack('v', 0x00); // Reserved. Must always be 0 @@ -66,17 +71,23 @@ class FaviconHandler $entry .= "\0"; // Color palette, typically 0 $entry .= "\0"; // Reserved + // AND mask +// $pxCount = $width * $height; +// $pxMask = hex2bin('00000000'); +// $mask = str_repeat($pxMask, $pxCount); + $mask = ''; + // Color planes, Appears to remain 1 for bmp image data $entry .= pack('v', 0x01); // Bits per pixel, can range from 1 to 32. From testing conversion - // via intervention from png typically provides this as 32. - $entry .= pack('v', 0x20); + // via intervention from png typically provides this as 24. + $entry .= pack('v', 0x18); // Size of the image data in bytes - $entry .= pack('V', strlen($rawBmpData)); + $entry .= pack('V', strlen($rawBmpData) + strlen($mask)); // Offset of the bmp data from file start $entry .= pack('V', strlen($header) + strlen($entry) + 4); // Join & return the combined parts of the ICO image data - return $header . $entry . $rawBmpData; + return $header . $entry . $rawBmpData . $mask; } } diff --git a/public/favicon.ico b/public/favicon.ico index e047657ccad8c38f318f2bda36f0809a9b0c878f..e114831177f415bd8dfb65b3f6f9d5988796a7b5 100644 GIT binary patch literal 3134 zcmd5;X-pht7+(JiODmVqhEjgGJG=Sjo%eg*?|t5D zM4}t;U9(1n|C>b_H;P1QB9SNqKZrz|@MGoi51#$+KC#PFxWYkKY$k@q@}3WXk+5%~ zTO!TBMP0i!U{w%v*KdWznTa~24Z{Z>q_UI9MdzlDw`GO{_UmzkoX}^wsH%E}M;R-9p@YJ)C=-z**b{SDrtvivEJ=1lDJ zVuJ$kNHk#XEtKuMjeyX(t0y|M7^mKN>N`3ob@cE{UgLSEqII}r17}67X?UM;YRJK^ z78LgBOf0_f;n|Ii+XFVG^+LIZiGK4Bc1$=;$l$LPq4ggn5RZvsNUMLy5* zZnwHi@ynAuvaDp~?oCEU?8?x3>8oP51wq}e+@C`^OV%ICd?0hSQXm!QuAC^~nQu{|^B z)R{W=(79;@0SuVW6)+B*^HVUR{B@&S-RaSEE6SfH3iwwYdEGxTWS2G0rm-YN*Yr`) zZ6sDgL6@d(8z{039g!7hBsPNQ=Fhj=l#6t%DVCmMGIXD-Eq*F-~2) z4AY4a98R3cjmCno;x+UuD>o;Qairf~_Kja~jx>*ytmlN{vFa_d{kJCobJI*6KL(Lk zZQrBfh)}lw4om+*#y+M!{vjnvaLBj}(dn?f@E)QweBc3i3V&7ATR7w-MAoA{i_ByL z+Omt1tR#=*Mfcib?sYxuoR__xm0aleV?-OIj8|Y=$ph-XKlygU;~5hn&Mr@w$F>)_Ub;kG>a}iqc0pVKUpPEOPr$HG&%sxpgOY z2JC9fKnZ*#I&7b5{{fXXetI`T&uwJ8kgDo6-^3-S{5Z^UaxpmJ5I7EVQjsvxCW(?q>)#1jM{Jj>WJPy#2#NkiZfVI1!=JklP;*b7G@lD7%YXzK$TGlj2!2qxd}=Am zo^Xx*?a`f6Ry?~PvQUU{Pxk%9IMlimAM*j_B1n-40zVREsGL=@f;%n$lrnae_Sgq} z1qsovwpNrr;!^(QG4zifdV$wL2BPkefg!&wIu#yo-_2%k*Iqg=;ViGYxJ9zM=1u>k z+)mZXi|IeU*g0Y&di-VdI2;mkqSmUX0krJlPEWT5LA9TF9i4gb!OTsNn zSj!66TF#dTY@^zS?ckZW$CuS!mtEw`XsPfj}@pSOZ>ImwSpNs6KQGng><^O?oyla9$v#6e*%v}WLdZq}1jMk)z6l5f1gTb00a@HoK*8-j zXtn66)&;HBeXY3FqSd-kMPJ>~y0k99`@qxp$Gelc=gT)|?!Di=zx%rr6!ZzW*zBMQ z9VGlmP!J_3C`bryur+yq5<~+lAmG{mOK@;7i@^jM$ke|DtZ6iw7{`?wt-}+Ol$A4Y z$@1+`d>o?pE4>eAg; zm+rm1aOaP6LnBlQ72}`^sT3Hb(`X0`Aqb1d<2j>a+W+0R@AT!fH}Bqk{`&rl_rJd! zeem+b?Wb>!T^{P_{VsE6Swec2$?i;;kyGEAqc57RLJLfW8>^mPMK8dbh%3c)pg6-2W~%p_-^dkhq1TgWA|TRxH7p42Tj!`FLoA%|H|(I^s*)Jvj~0DYI~y9OZC8 zdMAxfWg&=EPAXI?t-+vF>mr@*)$6}LJ~-Ug-Lrkq{w>@4mn`qFI-*I1k^{oxaxpH( z;c)H#%8bSX>CMMyw4O?7Jf6JpNLK5K+@%Kst9F+)txqeQm6V+ypPJ=ON{x=6rni_Q zoKCGd!W3m6yz}7pi?@fa-k-a;IlHhtt2p5DBpSm_0v=y17Gpd#q$L7@)bzaJB zyO7m+IkW3(Zu`aj_MeJ6FVys2Ub$$n!i1D`Uvf%vR!&O6Okm&a@>pH5`RmS+Mz>UHu(@NB z(ledW9)meTCYMWaLaI>cOjeEFLaOv4F@Yd#CIb;kb=K6H*zy(5%xbGQWqRq{xZKhi z#d8*~-Rx& zk0-+JCe>P*OpUU+EQn1g3<{G=APAL6HR4b?PO1zMPMaeky{f_G&(Ve3bf!pxRLX!G zqczf->dvc=FRb?@7e=}hg+eit$)r*!3_2a;TqqIaQn|(9R_hD|A&2NNi^ah(kqB3D zIYORDN+`_|sWwy{9_I2J-AQIok^~1Hl?t6cRG}m_2E8>tx}YJmvMFvxsov^FSQ9V< zFs4(fECvH;7)JC)vlL*7%Y$eP3WWl2&SJ4~wIe#P$>7OWn`6RbGNfv&*5Z|^A|!-r zk_PAqiXm)1%2zDjIasyqtH}6lrOpB|VPJtqrP3%=2Dm5`48w4#R3w&gIa~;WCz>cY z1V*s<>5IPt_nG7Zu;vPJ5E_lb6p~hd`=yMnPaU&&DC~J*(`HYr?2OK; zv!_*sxl;v%hQr4ZjzC13WyZJ-xBm*PKc;i0Lol1e6LE#oFmw#89c9bY}qS5~=b*D0+dF*rB`Fc=3D;_@(_02hc9$}k&W zBtuZ4F)7DcToaK~5j$&9V%6%XtcAGF#U~9Iq2f!mgx;mJB=F^ydHWvE+3{OsUbCm9 z&7MBDrhBu^8>@Hu0{=Qu()FV&uTg9BvrukaVI6=9jm7}g2SwI+64RG;O|SgIosyQk zXsN|tl;3gCnzw}X429OBVSZH*{M|uRBG)2Xg1*8fUL6T`r%PraEF{Q1^%{yXid!s8i z*a{ns$u*=US*&&~*!w7NW{4#AbkSZod|5PHhy>tiz{aHvEvccF6oJwq)_KGlSNVp4 zrbEw)H(W079c(=Qy0(9$cWA8k`&Y3|gH6YuIRcx7D$8e|ea3{@R4Nl>29$%wU`{}o zLW3ZLgLnOh>&V^w_QNTSJ5pQrR_}aJw(UvR zPp{_hejc~zZ2hrU&P6Ab?u<`A{glNOL#U923b^7>2!`o&FiU|8&~7w{34tjCx`V+4 zxEF{uQ4+mPq_SWV6;Bra^!2;Nn+`bBs+_5_{eh0mmB(wgT$$f@JGJFx>&4eetFK39 z&H?c=V3dQa1cXj*_CPSOhD;m*oCTDe!I<2E!5ZP=VwH&_CLsg`6eA#wzZ z7!(Sq3I&`6!C*EY%uGjZg!QevsP{OwVcw$ z%ml*}2+EVN`Jjw;rOrsFLty74oF*SmDDW591R4l%3629~Y<&EWccTfVoid$|jp3oH zNLyTnH?uM#ccmeBLqgkGS-9_?lM+IqGT9i&utH-%IoSWq0yO<^4NeLKc$q8^P;?Og zz_s6f`AhhC7?a7CDXk8FsUxjMGp$yW(QPdI)?Uy8UI!jPR3HOXI+MfYt5h10d*Bh| z280S8eRKg96KSE*Su7S{c)<3OKlUmbp$U9(1Q_K653 zjEW)?ijxD&`D7HIw4!{uEW)q#7QrYFyi21oX>^D$6w_E(biraxLIqE(hY$`ZEC_yr z*a2bz$AUfqpIKlQvAHn9-E--|?A3>T1&w4_ELS8av{sH##ln;VxkDKf5QV1^#zZzh zlmW8-EB|E@}w8*Jh@@f znG#*J#b1hw6d_=C&;j*Lz?I5iASjO~Btc(GNaOaA(YCWM9r+8np^?&Xe`S5w>Mck1 zU-<3PZ%=-C@qS=rbl31-y|@1A9eUHS?{4?`KU({*tUol^_|2J$j{SLU2hy5$MJ89U zFd--e=qU=FiC{uZAQj?jp;Rl9>9#x`D_*@%W}8{QboA%ufy From 48f1934387b7a0fb1481182374285a161d96896f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 9 Feb 2023 17:47:33 +0000 Subject: [PATCH 4/6] Updated favicon gen to use png-based ICO From testing, worked on Firefox, Chrome, Gnome Web --- .gitignore | 1 + app/Uploads/FaviconHandler.php | 28 +++++++--------------------- public/favicon.ico | Bin 3134 -> 10933 bytes 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 0a858681c..90b80e7b8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ yarn-error.log /public/js /public/bower /public/build/ +/public/favicon.ico /storage/images _ide_helper.php /storage/debugbar diff --git a/app/Uploads/FaviconHandler.php b/app/Uploads/FaviconHandler.php index 39f8b12ca..2e756c587 100644 --- a/app/Uploads/FaviconHandler.php +++ b/app/Uploads/FaviconHandler.php @@ -25,11 +25,9 @@ class FaviconHandler $imageData = file_get_contents($file->getRealPath()); $image = $this->imageTool->make($imageData); $image->resize(32, 32); - $bmpData = $image->encode('bmp'); - $icoData = $this->bmpToIco($bmpData, 32, 32); + $bmpData = $image->encode('png'); + $icoData = $this->pngToIco($bmpData, 32, 32); -// file_put_contents(public_path('icon.bmp'), $bmpData); -// file_put_contents(public_path('icon-test.png'), $image->encode('png')); file_put_contents($targetPath, $icoData); } @@ -48,18 +46,12 @@ class FaviconHandler } /** - * Convert BMP image data to ICO file format. + * Convert PNG image data to ICO file format. * Built following the file format info from Wikipedia: * https://en.wikipedia.org/wiki/ICO_(file_format) */ - protected function bmpToIco(string $bmpData, int $width, int $height): string + protected function pngToIco(string $bmpData, int $width, int $height): string { - // Trim off the header of the bitmap file - $rawBmpData = substr($bmpData, 14); - // Double the height in the "BITMAPINFOHEADER" since, when in an ICO file, half - // of the image data is expected to be a mask. - $rawBmpData[8] = hex2bin(dechex($height * 2)); - // ICO header $header = pack('v', 0x00); // Reserved. Must always be 0 $header .= pack('v', 0x01); // Specifies ico image @@ -71,23 +63,17 @@ class FaviconHandler $entry .= "\0"; // Color palette, typically 0 $entry .= "\0"; // Reserved - // AND mask -// $pxCount = $width * $height; -// $pxMask = hex2bin('00000000'); -// $mask = str_repeat($pxMask, $pxCount); - $mask = ''; - // Color planes, Appears to remain 1 for bmp image data $entry .= pack('v', 0x01); // Bits per pixel, can range from 1 to 32. From testing conversion // via intervention from png typically provides this as 24. - $entry .= pack('v', 0x18); + $entry .= pack('v', 0x00); // Size of the image data in bytes - $entry .= pack('V', strlen($rawBmpData) + strlen($mask)); + $entry .= pack('V', strlen($bmpData)); // Offset of the bmp data from file start $entry .= pack('V', strlen($header) + strlen($entry) + 4); // Join & return the combined parts of the ICO image data - return $header . $entry . $rawBmpData . $mask; + return $header . $entry . $bmpData; } } diff --git a/public/favicon.ico b/public/favicon.ico index e114831177f415bd8dfb65b3f6f9d5988796a7b5..41655ccba55f8dd5f250471eb2e8928b21a85992 100644 GIT binary patch literal 10933 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 XHrNJO5L3!r00000NkvXXu0mjfR=v%1 literal 3134 zcmd5;X-pht7+(JiODmVqhEjgGJG=Sjo%eg*?|t5D zM4}t;U9(1n|C>b_H;P1QB9SNqKZrz|@MGoi51#$+KC#PFxWYkKY$k@q@}3WXk+5%~ zTO!TBMP0i!U{w%v*KdWznTa~24Z{Z>q_UI9MdzlDw`GO{_UmzkoX}^wsH%E}M;R-9p@YJ)C=-z**b{SDrtvivEJ=1lDJ zVuJ$kNHk#XEtKuMjeyX(t0y|M7^mKN>N`3ob@cE{UgLSEqII}r17}67X?UM;YRJK^ z78LgBOf0_f;n|Ii+XFVG^+LIZiGK4Bc1$=;$l$LPq4ggn5RZvsNUMLy5* zZnwHi@ynAuvaDp~?oCEU?8?x3>8oP51wq}e+@C`^OV%ICd?0hSQXm!QuAC^~nQu{|^B z)R{W=(79;@0SuVW6)+B*^HVUR{B@&S-RaSEE6SfH3iwwYdEGxTWS2G0rm-YN*Yr`) zZ6sDgL6@d(8z{039g!7hBsPNQ=Fhj=l#6t%DVCmMGIXD-Eq*F-~2) z4AY4a98R3cjmCno;x+UuD>o;Qairf~_Kja~jx>*ytmlN{vFa_d{kJCobJI*6KL(Lk zZQrBfh)}lw4om+*#y+M!{vjnvaLBj}(dn?f@E)QweBc3i3V&7ATR7w-MAoA{i_ByL z+Omt1tR#=*Mfcib?sYxuoR__xm0aleV?-OIj8|Y=$ph-XKlygU;~5hn&Mr@w$F>)_Ub;kG>a}iqc0pVKUpPEOPr$HG&%sxpgOY z2JC9fKnZ*#I&7b5{{fXXetI`T&uwJ8kgDo6-^3-S{5Z^UaxpmJ5I7EVQjsvxCW(?q>)#1jM{Jj>WJPy#2#NkiZfVI1!=JklP;*b7G@lD7%YXzK$TGlj2!2qxd}=Am zo^Xx*?a`f6Ry?~PvQUU{Pxk%9IMlimAM*j_B1n-40zVREsGL=@f;%n$lrnae_Sgq} z1qsovwpNrr;!^(QG4zifdV$wL2BPkefg!&wIu#yo-_2%k*Iqg=;ViGYxJ9zM=1u>k z+)mZXi|IeU*g0Y&di-VdI2;mkqSmUX0krJlPEWT5LA9TF9i4gb!OTsNn zSj!66TF#dTY@^zS?ckZW$CuS!mtEw`XsPfj}@pSOZ>ImwSpNs6KQGng>< Date: Thu, 9 Feb 2023 20:57:35 +0000 Subject: [PATCH 5/6] Added default favicon creation upon access. --- app/Http/Controllers/HomeController.php | 12 +++++++++ app/Uploads/FaviconHandler.php | 31 +++++++++++++++++++----- public/favicon.ico | Bin 10933 -> 0 bytes routes/web.php | 1 + 4 files changed, 38 insertions(+), 6 deletions(-) delete mode 100644 public/favicon.ico diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index c3c8d1066..84651f653 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -10,6 +10,7 @@ use BookStack\Entities\Queries\TopFavourites; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Entities\Tools\PageContent; +use BookStack\Uploads\FaviconHandler; use BookStack\Util\SimpleListOptions; use Illuminate\Http\Request; @@ -127,4 +128,15 @@ class HomeController extends Controller { return response()->view('errors.404', [], 404); } + + /** + * Serve the application favicon. + * Ensures a 'favicon.ico' file exists at the web root location (if writable) to be served + * directly by the webserver in the future. + */ + public function favicon(FaviconHandler $favicons) + { + $favicons->restoreOriginalIfNotExists(); + return response()->file($favicons->getPath()); + } } diff --git a/app/Uploads/FaviconHandler.php b/app/Uploads/FaviconHandler.php index 2e756c587..3dc702ea6 100644 --- a/app/Uploads/FaviconHandler.php +++ b/app/Uploads/FaviconHandler.php @@ -7,9 +7,12 @@ use Intervention\Image\ImageManager; class FaviconHandler { + protected string $path; + public function __construct( protected ImageManager $imageTool ) { + $this->path = public_path('favicon.ico'); } /** @@ -17,8 +20,7 @@ class FaviconHandler */ public function saveForUploadedImage(UploadedFile $file): void { - $targetPath = public_path('favicon.ico'); - if (!is_writeable($targetPath)) { + if (!is_writeable($this->path)) { return; } @@ -28,7 +30,7 @@ class FaviconHandler $bmpData = $image->encode('png'); $icoData = $this->pngToIco($bmpData, 32, 32); - file_put_contents($targetPath, $icoData); + file_put_contents($this->path, $icoData); } /** @@ -36,13 +38,30 @@ class FaviconHandler */ public function restoreOriginal(): void { - $targetPath = public_path('favicon.ico'); $original = public_path('icon.ico'); - if (!is_writeable($targetPath)) { + if (!is_writeable($this->path)) { return; } - copy($original, $targetPath); + copy($original, $this->path); + } + + /** + * Restore the original favicon image if no favicon image is already in use. + */ + public function restoreOriginalIfNotExists(): void + { + if (!file_exists($this->path)) { + $this->restoreOriginal(); + } + } + + /** + * Get the path to the favicon file. + */ + public function getPath(): string + { + return $this->path; } /** diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 41655ccba55f8dd5f250471eb2e8928b21a85992..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10933 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 XHrNJO5L3!r00000NkvXXu0mjfR=v%1 diff --git a/routes/web.php b/routes/web.php index 95b4ae535..937dc0c6c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -40,6 +40,7 @@ use Illuminate\View\Middleware\ShareErrorsFromSession; Route::get('/status', [StatusController::class, 'show']); Route::get('/robots.txt', [HomeController::class, 'robots']); +Route::get('/favicon.ico', [HomeController::class, 'favicon']); // Authenticated routes... Route::middleware('auth')->group(function () { From f333db8e4f7bd164479f7cf437696345f1e52acc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 9 Feb 2023 21:16:27 +0000 Subject: [PATCH 6/6] Added control-upon-access of the default favicon.ico file --- app/Http/Controllers/HomeController.php | 4 ++-- app/Uploads/FaviconHandler.php | 28 ++++++++++++++++++------- tests/PublicActionTest.php | 12 +++++++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 84651f653..a82710523 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -136,7 +136,7 @@ class HomeController extends Controller */ public function favicon(FaviconHandler $favicons) { - $favicons->restoreOriginalIfNotExists(); - return response()->file($favicons->getPath()); + $exists = $favicons->restoreOriginalIfNotExists(); + return response()->file($exists ? $favicons->getPath() : $favicons->getOriginalPath()); } } diff --git a/app/Uploads/FaviconHandler.php b/app/Uploads/FaviconHandler.php index 3dc702ea6..c637356e0 100644 --- a/app/Uploads/FaviconHandler.php +++ b/app/Uploads/FaviconHandler.php @@ -35,25 +35,29 @@ class FaviconHandler /** * Restore the original favicon image. + * Returned boolean indicates if the copy occurred. */ - public function restoreOriginal(): void + public function restoreOriginal(): bool { - $original = public_path('icon.ico'); - if (!is_writeable($this->path)) { - return; + $permissionItem = file_exists($this->path) ? $this->path : dirname($this->path); + if (!is_writeable($permissionItem)) { + return false; } - copy($original, $this->path); + return copy($this->getOriginalPath(), $this->path); } /** * Restore the original favicon image if no favicon image is already in use. + * Returns a boolean to indicate if the file exists. */ - public function restoreOriginalIfNotExists(): void + public function restoreOriginalIfNotExists(): bool { - if (!file_exists($this->path)) { - $this->restoreOriginal(); + if (file_exists($this->path)) { + return true; } + + return $this->restoreOriginal(); } /** @@ -64,6 +68,14 @@ class FaviconHandler return $this->path; } + /** + * Get the path of the original favicon copy. + */ + public function getOriginalPath(): string + { + return public_path('icon.ico'); + } + /** * Convert PNG image data to ICO file format. * Built following the file format info from Wikipedia: diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index afc7fcef3..e21afdf33 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -155,6 +155,18 @@ class PublicActionTest extends TestCase $this->get('/robots.txt')->assertSee("User-agent: *\nDisallow: /"); } + public function test_default_favicon_file_created_upon_access() + { + $faviconPath = public_path('favicon.ico'); + if (file_exists($faviconPath)) { + unlink($faviconPath); + } + + $this->assertFileDoesNotExist($faviconPath); + $this->get('/favicon.ico'); + $this->assertFileExists($faviconPath); + } + public function test_public_view_then_login_redirects_to_previous_content() { $this->setSettings(['app-public' => 'true']);