Subs.php 150 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411
  1. <?php
  2. /**
  3. * This file has all the main functions in it that relate to, well, everything.
  4. *
  5. * Simple Machines Forum (SMF)
  6. *
  7. * @package SMF
  8. * @author Simple Machines http://www.simplemachines.org
  9. * @copyright 2014 Simple Machines and individual contributors
  10. * @license http://www.simplemachines.org/about/smf/license.php BSD
  11. *
  12. * @version 2.1 Alpha 1
  13. */
  14. if (!defined('SMF'))
  15. die('No direct access...');
  16. /**
  17. * Update some basic statistics.
  18. *
  19. * 'member' statistic updates the latest member, the total member
  20. * count, and the number of unapproved members.
  21. * 'member' also only counts approved members when approval is on, but
  22. * is much more efficient with it off.
  23. *
  24. * 'message' changes the total number of messages, and the
  25. * highest message id by id_msg - which can be parameters 1 and 2,
  26. * respectively.
  27. *
  28. * 'topic' updates the total number of topics, or if parameter1 is true
  29. * simply increments them.
  30. *
  31. * 'subject' updateds the log_search_subjects in the event of a topic being
  32. * moved, removed or split. parameter1 is the topicid, parameter2 is the new subject
  33. *
  34. * 'postgroups' case updates those members who match condition's
  35. * post-based membergroups in the database (restricted by parameter1).
  36. *
  37. * @param string $type Stat type - can be 'member', 'message', 'topic', 'subject' or 'postgroups'
  38. * @param mixed $parameter1 = null
  39. * @param mixed $parameter2 = null
  40. */
  41. function updateStats($type, $parameter1 = null, $parameter2 = null)
  42. {
  43. global $sourcedir, $modSettings, $smcFunc;
  44. switch ($type)
  45. {
  46. case 'member':
  47. $changes = array(
  48. 'memberlist_updated' => time(),
  49. );
  50. // #1 latest member ID, #2 the real name for a new registration.
  51. if (is_numeric($parameter1))
  52. {
  53. $changes['latestMember'] = $parameter1;
  54. $changes['latestRealName'] = $parameter2;
  55. updateSettings(array('totalMembers' => true), true);
  56. }
  57. // We need to calculate the totals.
  58. else
  59. {
  60. // Update the latest activated member (highest id_member) and count.
  61. $result = $smcFunc['db_query']('', '
  62. SELECT COUNT(*), MAX(id_member)
  63. FROM {db_prefix}members
  64. WHERE is_activated = {int:is_activated}',
  65. array(
  66. 'is_activated' => 1,
  67. )
  68. );
  69. list ($changes['totalMembers'], $changes['latestMember']) = $smcFunc['db_fetch_row']($result);
  70. $smcFunc['db_free_result']($result);
  71. // Get the latest activated member's display name.
  72. $result = $smcFunc['db_query']('', '
  73. SELECT real_name
  74. FROM {db_prefix}members
  75. WHERE id_member = {int:id_member}
  76. LIMIT 1',
  77. array(
  78. 'id_member' => (int) $changes['latestMember'],
  79. )
  80. );
  81. list ($changes['latestRealName']) = $smcFunc['db_fetch_row']($result);
  82. $smcFunc['db_free_result']($result);
  83. if (!empty($modSettings['registration_method']))
  84. {
  85. // Are we using registration approval?
  86. if ($modSettings['registration_method'] == 2 || !empty($modSettings['approveAccountDeletion']))
  87. {
  88. // Update the amount of members awaiting approval
  89. $result = $smcFunc['db_query']('', '
  90. SELECT COUNT(*)
  91. FROM {db_prefix}members
  92. WHERE is_activated IN ({array_int:activation_status})',
  93. array(
  94. 'activation_status' => array(3, 4),
  95. )
  96. );
  97. list ($changes['unapprovedMembers']) = $smcFunc['db_fetch_row']($result);
  98. $smcFunc['db_free_result']($result);
  99. }
  100. // What about unapproved COPPA registrations?
  101. if (!empty($modSettings['coppaType']) && $modSettings['coppaType'] != 1)
  102. {
  103. $result = $smcFunc['db_query']('', '
  104. SELECT COUNT(*)
  105. FROM {db_prefix}members
  106. WHERE is_activated = {int:coppa_approval}',
  107. array(
  108. 'coppa_approval' => 5,
  109. )
  110. );
  111. list ($coppa_approvals) = $smcFunc['db_fetch_row']($result);
  112. $smcFunc['db_free_result']($result);
  113. // Add this to the number of unapproved members
  114. if (!empty($changes['unapprovedMembers']))
  115. $changes['unapprovedMembers'] += $coppa_approvals;
  116. else
  117. $changes['unapprovedMembers'] = $coppa_approvals;
  118. }
  119. }
  120. }
  121. updateSettings($changes);
  122. break;
  123. case 'message':
  124. if ($parameter1 === true && $parameter2 !== null)
  125. updateSettings(array('totalMessages' => true, 'maxMsgID' => $parameter2), true);
  126. else
  127. {
  128. // SUM and MAX on a smaller table is better for InnoDB tables.
  129. $result = $smcFunc['db_query']('', '
  130. SELECT SUM(num_posts + unapproved_posts) AS total_messages, MAX(id_last_msg) AS max_msg_id
  131. FROM {db_prefix}boards
  132. WHERE redirect = {string:blank_redirect}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
  133. AND id_board != {int:recycle_board}' : ''),
  134. array(
  135. 'recycle_board' => isset($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
  136. 'blank_redirect' => '',
  137. )
  138. );
  139. $row = $smcFunc['db_fetch_assoc']($result);
  140. $smcFunc['db_free_result']($result);
  141. updateSettings(array(
  142. 'totalMessages' => $row['total_messages'] === null ? 0 : $row['total_messages'],
  143. 'maxMsgID' => $row['max_msg_id'] === null ? 0 : $row['max_msg_id']
  144. ));
  145. }
  146. break;
  147. case 'subject':
  148. // Remove the previous subject (if any).
  149. $smcFunc['db_query']('', '
  150. DELETE FROM {db_prefix}log_search_subjects
  151. WHERE id_topic = {int:id_topic}',
  152. array(
  153. 'id_topic' => (int) $parameter1,
  154. )
  155. );
  156. // Insert the new subject.
  157. if ($parameter2 !== null)
  158. {
  159. $parameter1 = (int) $parameter1;
  160. $parameter2 = text2words($parameter2);
  161. $inserts = array();
  162. foreach ($parameter2 as $word)
  163. $inserts[] = array($word, $parameter1);
  164. if (!empty($inserts))
  165. $smcFunc['db_insert']('ignore',
  166. '{db_prefix}log_search_subjects',
  167. array('word' => 'string', 'id_topic' => 'int'),
  168. $inserts,
  169. array('word', 'id_topic')
  170. );
  171. }
  172. break;
  173. case 'topic':
  174. if ($parameter1 === true)
  175. updateSettings(array('totalTopics' => true), true);
  176. else
  177. {
  178. // Get the number of topics - a SUM is better for InnoDB tables.
  179. // We also ignore the recycle bin here because there will probably be a bunch of one-post topics there.
  180. $result = $smcFunc['db_query']('', '
  181. SELECT SUM(num_topics + unapproved_topics) AS total_topics
  182. FROM {db_prefix}boards' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
  183. WHERE id_board != {int:recycle_board}' : ''),
  184. array(
  185. 'recycle_board' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
  186. )
  187. );
  188. $row = $smcFunc['db_fetch_assoc']($result);
  189. $smcFunc['db_free_result']($result);
  190. updateSettings(array('totalTopics' => $row['total_topics'] === null ? 0 : $row['total_topics']));
  191. }
  192. break;
  193. case 'postgroups':
  194. // Parameter two is the updated columns: we should check to see if we base groups off any of these.
  195. if ($parameter2 !== null && !in_array('posts', $parameter2))
  196. return;
  197. $postgroups = cache_get_data('updateStats:postgroups', 360);
  198. if ($postgroups == null || $parameter1 == null)
  199. {
  200. // Fetch the postgroups!
  201. $request = $smcFunc['db_query']('', '
  202. SELECT id_group, min_posts
  203. FROM {db_prefix}membergroups
  204. WHERE min_posts != {int:min_posts}',
  205. array(
  206. 'min_posts' => -1,
  207. )
  208. );
  209. $postgroups = array();
  210. while ($row = $smcFunc['db_fetch_assoc']($request))
  211. $postgroups[$row['id_group']] = $row['min_posts'];
  212. $smcFunc['db_free_result']($request);
  213. // Sort them this way because if it's done with MySQL it causes a filesort :(.
  214. arsort($postgroups);
  215. cache_put_data('updateStats:postgroups', $postgroups, 360);
  216. }
  217. // Oh great, they've screwed their post groups.
  218. if (empty($postgroups))
  219. return;
  220. // Set all membergroups from most posts to least posts.
  221. $conditions = '';
  222. $lastMin = 0;
  223. foreach ($postgroups as $id => $min_posts)
  224. {
  225. $conditions .= '
  226. WHEN posts >= ' . $min_posts . (!empty($lastMin) ? ' AND posts <= ' . $lastMin : '') . ' THEN ' . $id;
  227. $lastMin = $min_posts;
  228. }
  229. // A big fat CASE WHEN... END is faster than a zillion UPDATE's ;).
  230. $smcFunc['db_query']('', '
  231. UPDATE {db_prefix}members
  232. SET id_post_group = CASE ' . $conditions . '
  233. ELSE 0
  234. END' . ($parameter1 != null ? '
  235. WHERE ' . (is_array($parameter1) ? 'id_member IN ({array_int:members})' : 'id_member = {int:members}') : ''),
  236. array(
  237. 'members' => $parameter1,
  238. )
  239. );
  240. break;
  241. default:
  242. trigger_error('updateStats(): Invalid statistic type \'' . $type . '\'', E_USER_NOTICE);
  243. }
  244. }
  245. /**
  246. * Updates the columns in the members table.
  247. * Assumes the data has been htmlspecialchar'd.
  248. * this function should be used whenever member data needs to be
  249. * updated in place of an UPDATE query.
  250. *
  251. * id_member is either an int or an array of ints to be updated.
  252. *
  253. * data is an associative array of the columns to be updated and their respective values.
  254. * any string values updated should be quoted and slashed.
  255. *
  256. * the value of any column can be '+' or '-', which mean 'increment'
  257. * and decrement, respectively.
  258. *
  259. * if the member's post number is updated, updates their post groups.
  260. *
  261. * @param mixed $members An array of integers
  262. * @param array $data
  263. */
  264. function updateMemberData($members, $data)
  265. {
  266. global $modSettings, $user_info, $smcFunc;
  267. $parameters = array();
  268. if (is_array($members))
  269. {
  270. $condition = 'id_member IN ({array_int:members})';
  271. $parameters['members'] = $members;
  272. }
  273. elseif ($members === null)
  274. $condition = '1=1';
  275. else
  276. {
  277. $condition = 'id_member = {int:member}';
  278. $parameters['member'] = $members;
  279. }
  280. // Everything is assumed to be a string unless it's in the below.
  281. $knownInts = array(
  282. 'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages',
  283. 'new_pm', 'pm_prefs', 'gender', 'show_online', 'pm_email_notify', 'pm_receive_from', 'karma_good', 'karma_bad',
  284. 'notify_announcements', 'notify_send_body', 'notify_regularity', 'notify_types', 'alerts',
  285. 'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
  286. );
  287. $knownFloats = array(
  288. 'time_offset',
  289. );
  290. if (!empty($modSettings['integrate_change_member_data']))
  291. {
  292. // Only a few member variables are really interesting for integration.
  293. $integration_vars = array(
  294. 'member_name',
  295. 'real_name',
  296. 'email_address',
  297. 'id_group',
  298. 'gender',
  299. 'birthdate',
  300. 'website_title',
  301. 'website_url',
  302. 'location',
  303. 'time_format',
  304. 'time_offset',
  305. 'avatar',
  306. 'lngfile',
  307. );
  308. $vars_to_integrate = array_intersect($integration_vars, array_keys($data));
  309. // Only proceed if there are any variables left to call the integration function.
  310. if (count($vars_to_integrate) != 0)
  311. {
  312. // Fetch a list of member_names if necessary
  313. if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members)))
  314. $member_names = array($user_info['username']);
  315. else
  316. {
  317. $member_names = array();
  318. $request = $smcFunc['db_query']('', '
  319. SELECT member_name
  320. FROM {db_prefix}members
  321. WHERE ' . $condition,
  322. $parameters
  323. );
  324. while ($row = $smcFunc['db_fetch_assoc']($request))
  325. $member_names[] = $row['member_name'];
  326. $smcFunc['db_free_result']($request);
  327. }
  328. if (!empty($member_names))
  329. foreach ($vars_to_integrate as $var)
  330. call_integration_hook('integrate_change_member_data', array($member_names, $var, &$data[$var], &$knownInts, &$knownFloats));
  331. }
  332. }
  333. $setString = '';
  334. foreach ($data as $var => $val)
  335. {
  336. $type = 'string';
  337. if (in_array($var, $knownInts))
  338. $type = 'int';
  339. elseif (in_array($var, $knownFloats))
  340. $type = 'float';
  341. elseif ($var == 'birthdate')
  342. $type = 'date';
  343. // Doing an increment?
  344. if ($type == 'int' && ($val === '+' || $val === '-'))
  345. {
  346. $val = $var . ' ' . $val . ' 1';
  347. $type = 'raw';
  348. }
  349. // Ensure posts, instant_messages, and unread_messages don't overflow or underflow.
  350. if (in_array($var, array('posts', 'instant_messages', 'unread_messages')))
  351. {
  352. if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
  353. {
  354. if ($match[1] != '+ ')
  355. $val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
  356. $type = 'raw';
  357. }
  358. }
  359. $setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
  360. $parameters['p_' . $var] = $val;
  361. }
  362. $smcFunc['db_query']('', '
  363. UPDATE {db_prefix}members
  364. SET' . substr($setString, 0, -1) . '
  365. WHERE ' . $condition,
  366. $parameters
  367. );
  368. updateStats('postgroups', $members, array_keys($data));
  369. // Clear any caching?
  370. if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2 && !empty($members))
  371. {
  372. if (!is_array($members))
  373. $members = array($members);
  374. foreach ($members as $member)
  375. {
  376. if ($modSettings['cache_enable'] >= 3)
  377. {
  378. cache_put_data('member_data-profile-' . $member, null, 120);
  379. cache_put_data('member_data-normal-' . $member, null, 120);
  380. cache_put_data('member_data-minimal-' . $member, null, 120);
  381. }
  382. cache_put_data('user_settings-' . $member, null, 60);
  383. }
  384. }
  385. }
  386. /**
  387. * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
  388. *
  389. * - updates both the settings table and $modSettings array.
  390. * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')!
  391. * - if a variable is already set to what you want to change it to, that
  392. * variable will be skipped over; it would be unnecessary to reset.
  393. * - When use_update is true, UPDATEs will be used instead of REPLACE.
  394. * - when use_update is true, the value can be true or false to increment
  395. * or decrement it, respectively.
  396. *
  397. * @param array $changeArray
  398. * @param bool $update = false
  399. */
  400. function updateSettings($changeArray, $update = false)
  401. {
  402. global $modSettings, $smcFunc;
  403. if (empty($changeArray) || !is_array($changeArray))
  404. return;
  405. // In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
  406. if ($update)
  407. {
  408. foreach ($changeArray as $variable => $value)
  409. {
  410. $smcFunc['db_query']('', '
  411. UPDATE {db_prefix}settings
  412. SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
  413. WHERE variable = {string:variable}',
  414. array(
  415. 'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
  416. 'variable' => $variable,
  417. )
  418. );
  419. $modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
  420. }
  421. // Clean out the cache and make sure the cobwebs are gone too.
  422. cache_put_data('modSettings', null, 90);
  423. return;
  424. }
  425. $replaceArray = array();
  426. foreach ($changeArray as $variable => $value)
  427. {
  428. // Don't bother if it's already like that ;).
  429. if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
  430. continue;
  431. // If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
  432. elseif (!isset($modSettings[$variable]) && empty($value))
  433. continue;
  434. $replaceArray[] = array($variable, $value);
  435. $modSettings[$variable] = $value;
  436. }
  437. if (empty($replaceArray))
  438. return;
  439. $smcFunc['db_insert']('replace',
  440. '{db_prefix}settings',
  441. array('variable' => 'string-255', 'value' => 'string-65534'),
  442. $replaceArray,
  443. array('variable')
  444. );
  445. // Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
  446. cache_put_data('modSettings', null, 90);
  447. }
  448. /**
  449. * Constructs a page list.
  450. *
  451. * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
  452. * - flexible_start causes it to use "url.page" instead of "url;start=page".
  453. * - handles any wireless settings (adding special things to URLs.)
  454. * - very importantly, cleans up the start value passed, and forces it to
  455. * be a multiple of num_per_page.
  456. * - checks that start is not more than max_value.
  457. * - base_url should be the URL without any start parameter on it.
  458. * - uses the compactTopicPagesEnable and compactTopicPagesContiguous
  459. * settings to decide how to display the menu.
  460. *
  461. * an example is available near the function definition.
  462. * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
  463. *
  464. * @param string $base_url
  465. * @param int $start
  466. * @param int $max_value
  467. * @param int $num_per_page
  468. * @param bool $flexible_start = false
  469. * @param bool $show_prevnext = true
  470. */
  471. function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true)
  472. {
  473. global $modSettings, $context, $txt, $smcFunc;
  474. // Save whether $start was less than 0 or not.
  475. $start = (int) $start;
  476. $start_invalid = $start < 0;
  477. // Make sure $start is a proper variable - not less than 0.
  478. if ($start_invalid)
  479. $start = 0;
  480. // Not greater than the upper bound.
  481. elseif ($start >= $max_value)
  482. $start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
  483. // And it has to be a multiple of $num_per_page!
  484. else
  485. $start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
  486. $context['current_page'] = $start / $num_per_page;
  487. // Wireless will need the protocol on the URL somewhere.
  488. if (WIRELESS)
  489. $base_url .= ';' . WIRELESS_PROTOCOL;
  490. $base_link = '<a class="navPages" href="' . ($flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d') . '">%2$s</a> ';
  491. // Compact pages is off or on?
  492. if (empty($modSettings['compactTopicPagesEnable']))
  493. {
  494. // Show the left arrow.
  495. $pageindex = $start == 0 ? ' ' : sprintf($base_link, $start - $num_per_page, '<span class="previous_page">' . $txt['prev'] . '</span>');
  496. // Show all the pages.
  497. $display_page = 1;
  498. for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
  499. $pageindex .= $start == $counter && !$start_invalid ? '<span class="current_page"><strong>' . $display_page++ . '</strong></span> ' : sprintf($base_link, $counter, $display_page++);
  500. // Show the right arrow.
  501. $display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
  502. if ($start != $counter - $max_value && !$start_invalid)
  503. $pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, '<span class="next_page">' . $txt['next'] . '</span>');
  504. }
  505. else
  506. {
  507. // If they didn't enter an odd value, pretend they did.
  508. $PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
  509. // Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
  510. if (!empty($start) && $show_prevnext)
  511. $pageindex = sprintf($base_link, $start - $num_per_page, '<span class="previous_page">' . $txt['prev'] . '</span>');
  512. else
  513. $pageindex = '';
  514. // Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
  515. if ($start > $num_per_page * $PageContiguous)
  516. $pageindex .= sprintf($base_link, 0, '1');
  517. // Show the ... after the first page. (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
  518. if ($start > $num_per_page * ($PageContiguous + 1))
  519. $pageindex .= '<span class="expand_pages" onclick="' . $smcFunc['htmlspecialchars']('expandPages(this, ' . JavaScriptEscape(($flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d')) . ', ' . $num_per_page . ', ' . ($start - $num_per_page * $PageContiguous) . ', ' . $num_per_page . ');') . '"><strong> ... </strong></span>';
  520. // Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
  521. for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
  522. if ($start >= $num_per_page * $nCont)
  523. {
  524. $tmpStart = $start - $num_per_page * $nCont;
  525. $pageindex.= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
  526. }
  527. // Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
  528. if (!$start_invalid)
  529. $pageindex .= '<span class="current_page"><strong>' . ($start / $num_per_page + 1) . '</strong></span>';
  530. else
  531. $pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
  532. // Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
  533. $tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
  534. for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
  535. if ($start + $num_per_page * $nCont <= $tmpMaxPages)
  536. {
  537. $tmpStart = $start + $num_per_page * $nCont;
  538. $pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
  539. }
  540. // Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
  541. if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
  542. $pageindex .= '<span class="expand_pages" onclick="' . $smcFunc['htmlspecialchars']('expandPages(this, ' . JavaScriptEscape(($flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d')) . ', ' . ($start + $num_per_page * ($PageContiguous + 1)) . ', ' . $tmpMaxPages . ', ' . $num_per_page . ');') . '" onmouseover="this.style.cursor=\'pointer\';"> ... </span>';
  543. // Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15< next page)
  544. if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
  545. $pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
  546. // Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
  547. if ($start != $tmpMaxPages && $show_prevnext)
  548. $pageindex .= sprintf($base_link, $start + $num_per_page, '<span class="next_page">' . $txt['next'] . '</span>');
  549. }
  550. return $pageindex;
  551. }
  552. /**
  553. * - Formats a number.
  554. * - uses the format of number_format to decide how to format the number.
  555. * for example, it might display "1 234,50".
  556. * - caches the formatting data from the setting for optimization.
  557. *
  558. * @param float $number
  559. * @param bool $override_decimal_count = false
  560. */
  561. function comma_format($number, $override_decimal_count = false)
  562. {
  563. global $txt;
  564. static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
  565. // Cache these values...
  566. if ($decimal_separator === null)
  567. {
  568. // Not set for whatever reason?
  569. if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
  570. return $number;
  571. // Cache these each load...
  572. $thousands_separator = $matches[1];
  573. $decimal_separator = $matches[2];
  574. $decimal_count = strlen($matches[3]);
  575. }
  576. // Format the string with our friend, number_format.
  577. return number_format($number, (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
  578. }
  579. /**
  580. * Format a time to make it look purdy.
  581. *
  582. * - returns a pretty formated version of time based on the user's format in $user_info['time_format'].
  583. * - applies all necessary time offsets to the timestamp, unless offset_type is set.
  584. * - if todayMod is set and show_today was not not specified or true, an
  585. * alternate format string is used to show the date with something to show it is "today" or "yesterday".
  586. * - performs localization (more than just strftime would do alone.)
  587. *
  588. * @param int $log_time
  589. * @param bool $show_today = true
  590. * @param string $offset_type = false
  591. */
  592. function timeformat($log_time, $show_today = true, $offset_type = false)
  593. {
  594. global $context, $user_info, $txt, $modSettings;
  595. static $non_twelve_hour;
  596. // Offset the time.
  597. if (!$offset_type)
  598. $time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
  599. // Just the forum offset?
  600. elseif ($offset_type == 'forum')
  601. $time = $log_time + $modSettings['time_offset'] * 3600;
  602. else
  603. $time = $log_time;
  604. // We can't have a negative date (on Windows, at least.)
  605. if ($log_time < 0)
  606. $log_time = 0;
  607. // Today and Yesterday?
  608. if ($modSettings['todayMod'] >= 1 && $show_today === true)
  609. {
  610. // Get the current time.
  611. $nowtime = forum_time();
  612. $then = @getdate($time);
  613. $now = @getdate($nowtime);
  614. // Try to make something of a time format string...
  615. $s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
  616. if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
  617. {
  618. $h = strpos($user_info['time_format'], '%l') === false ? '%I' : '%l';
  619. $today_fmt = $h . ':%M' . $s . ' %p';
  620. }
  621. else
  622. $today_fmt = '%H:%M' . $s;
  623. // Same day of the year, same year.... Today!
  624. if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
  625. return $txt['today'] . timeformat($log_time, $today_fmt, $offset_type);
  626. // Day-of-year is one less and same year, or it's the first of the year and that's the last of the year...
  627. if ($modSettings['todayMod'] == '2' && (($then['yday'] == $now['yday'] - 1 && $then['year'] == $now['year']) || ($now['yday'] == 0 && $then['year'] == $now['year'] - 1) && $then['mon'] == 12 && $then['mday'] == 31))
  628. return $txt['yesterday'] . timeformat($log_time, $today_fmt, $offset_type);
  629. }
  630. $str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
  631. if (setlocale(LC_TIME, $txt['lang_locale']))
  632. {
  633. if (!isset($non_twelve_hour))
  634. $non_twelve_hour = trim(strftime('%p')) === '';
  635. if ($non_twelve_hour && strpos($str, '%p') !== false)
  636. $str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
  637. foreach (array('%a', '%A', '%b', '%B') as $token)
  638. if (strpos($str, $token) !== false)
  639. $str = str_replace($token, strftime($token, $time), $str);
  640. }
  641. else
  642. {
  643. // Do-it-yourself time localization. Fun.
  644. foreach (array('%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months') as $token => $text_label)
  645. if (strpos($str, $token) !== false)
  646. $str = str_replace($token, $txt[$text_label][(int) strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
  647. if (strpos($str, '%p') !== false)
  648. $str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
  649. }
  650. // Windows doesn't support %e; on some versions, strftime fails altogether if used, so let's prevent that.
  651. if ($context['server']['is_windows'] && strpos($str, '%e') !== false)
  652. $str = str_replace('%e', ltrim(strftime('%d', $time), '0'), $str);
  653. // Format any other characters..
  654. return strftime($str, $time);
  655. }
  656. /**
  657. * Removes special entities from strings. Compatibility...
  658. * Should be used instead of html_entity_decode for PHP version compatibility reasons.
  659. *
  660. * - removes the base entities (&lt;, &quot;, etc.) from text.
  661. * - additionally converts &nbsp; and &#039;.
  662. *
  663. * @param string $string
  664. * @return the string without entities
  665. */
  666. function un_htmlspecialchars($string)
  667. {
  668. static $translation = array();
  669. if (empty($translation))
  670. $translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES)) + array('&#039;' => '\'', '&nbsp;' => ' ');
  671. return strtr($string, $translation);
  672. }
  673. /**
  674. * Shorten a subject + internationalization concerns.
  675. *
  676. * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
  677. * - respects internationalization characters and entities as one character.
  678. * - avoids trailing entities.
  679. * - returns the shortened string.
  680. *
  681. * @param string $subject
  682. * @param int $len
  683. */
  684. function shorten_subject($subject, $len)
  685. {
  686. global $smcFunc;
  687. // It was already short enough!
  688. if ($smcFunc['strlen']($subject) <= $len)
  689. return $subject;
  690. // Shorten it by the length it was too long, and strip off junk from the end.
  691. return $smcFunc['substr']($subject, 0, $len) . '...';
  692. }
  693. /**
  694. * Gets the current time with offset.
  695. *
  696. * - always applies the offset in the time_offset setting.
  697. *
  698. * @param bool $use_user_offset = true if use_user_offset is true, applies the user's offset as well
  699. * @param int $timestamp = null
  700. * @return int seconds since the unix epoch
  701. */
  702. function forum_time($use_user_offset = true, $timestamp = null)
  703. {
  704. global $user_info, $modSettings;
  705. if ($timestamp === null)
  706. $timestamp = time();
  707. elseif ($timestamp == 0)
  708. return 0;
  709. return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
  710. }
  711. /**
  712. * Calculates all the possible permutations (orders) of array.
  713. * should not be called on huge arrays (bigger than like 10 elements.)
  714. * returns an array containing each permutation.
  715. *
  716. * @param array $array
  717. * @return array
  718. */
  719. function permute($array)
  720. {
  721. $orders = array($array);
  722. $n = count($array);
  723. $p = range(0, $n);
  724. for ($i = 1; $i < $n; null)
  725. {
  726. $p[$i]--;
  727. $j = $i % 2 != 0 ? $p[$i] : 0;
  728. $temp = $array[$i];
  729. $array[$i] = $array[$j];
  730. $array[$j] = $temp;
  731. for ($i = 1; $p[$i] == 0; $i++)
  732. $p[$i] = 1;
  733. $orders[] = $array;
  734. }
  735. return $orders;
  736. }
  737. /**
  738. * Parse bulletin board code in a string, as well as smileys optionally.
  739. *
  740. * - only parses bbc tags which are not disabled in disabledBBC.
  741. * - handles basic HTML, if enablePostHTML is on.
  742. * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
  743. * - only parses smileys if smileys is true.
  744. * - does nothing if the enableBBC setting is off.
  745. * - uses the cache_id as a unique identifier to facilitate any caching it may do.
  746. * -returns the modified message.
  747. *
  748. * @param string $message
  749. * @param bool $smileys = true
  750. * @param string $cache_id = ''
  751. * @param array $parse_tags = null
  752. * @return string
  753. */
  754. function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
  755. {
  756. global $txt, $scripturl, $context, $modSettings, $user_info;
  757. static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array();
  758. static $disabled;
  759. // Don't waste cycles
  760. if ($message === '')
  761. return '';
  762. // Just in case it wasn't determined yet whether UTF-8 is enabled.
  763. if (!isset($context['utf8']))
  764. $context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
  765. // Clean up any cut/paste issues we may have
  766. $message = sanitizeMSCutPaste($message);
  767. // If the load average is too high, don't parse the BBC.
  768. if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
  769. {
  770. $context['disabled_parse_bbc'] = true;
  771. return $message;
  772. }
  773. // Never show smileys for wireless clients. More bytes, can't see it anyway :P.
  774. if (WIRELESS)
  775. $smileys = false;
  776. elseif ($smileys !== null && ($smileys == '1' || $smileys == '0'))
  777. $smileys = (bool) $smileys;
  778. if (empty($modSettings['enableBBC']) && $message !== false)
  779. {
  780. if ($smileys === true)
  781. parsesmileys($message);
  782. return $message;
  783. }
  784. // If we are not doing every tag then we don't cache this run.
  785. if (!empty($parse_tags) && !empty($bbc_codes))
  786. {
  787. $temp_bbc = $bbc_codes;
  788. $bbc_codes = array();
  789. }
  790. // Allow mods access before entering the main parse_bbc loop
  791. call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
  792. // Sift out the bbc for a performance improvement.
  793. if (empty($bbc_codes) || $message === false || !empty($parse_tags))
  794. {
  795. if (!empty($modSettings['disabledBBC']))
  796. {
  797. $temp = explode(',', strtolower($modSettings['disabledBBC']));
  798. foreach ($temp as $tag)
  799. $disabled[trim($tag)] = true;
  800. }
  801. if (empty($modSettings['enableEmbeddedFlash']))
  802. $disabled['flash'] = true;
  803. /* The following bbc are formatted as an array, with keys as follows:
  804. tag: the tag's name - should be lowercase!
  805. type: one of...
  806. - (missing): [tag]parsed content[/tag]
  807. - unparsed_equals: [tag=xyz]parsed content[/tag]
  808. - parsed_equals: [tag=parsed data]parsed content[/tag]
  809. - unparsed_content: [tag]unparsed content[/tag]
  810. - closed: [tag], [tag/], [tag /]
  811. - unparsed_commas: [tag=1,2,3]parsed content[/tag]
  812. - unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
  813. - unparsed_equals_content: [tag=...]unparsed content[/tag]
  814. parameters: an optional array of parameters, for the form
  815. [tag abc=123]content[/tag]. The array is an associative array
  816. where the keys are the parameter names, and the values are an
  817. array which may contain the following:
  818. - match: a regular expression to validate and match the value.
  819. - quoted: true if the value should be quoted.
  820. - validate: callback to evaluate on the data, which is $data.
  821. - value: a string in which to replace $1 with the data.
  822. either it or validate may be used, not both.
  823. - optional: true if the parameter is optional.
  824. test: a regular expression to test immediately after the tag's
  825. '=', ' ' or ']'. Typically, should have a \] at the end.
  826. Optional.
  827. content: only available for unparsed_content, closed,
  828. unparsed_commas_content, and unparsed_equals_content.
  829. $1 is replaced with the content of the tag. Parameters
  830. are replaced in the form {param}. For unparsed_commas_content,
  831. $2, $3, ..., $n are replaced.
  832. before: only when content is not used, to go before any
  833. content. For unparsed_equals, $1 is replaced with the value.
  834. For unparsed_commas, $1, $2, ..., $n are replaced.
  835. after: similar to before in every way, except that it is used
  836. when the tag is closed.
  837. disabled_content: used in place of content when the tag is
  838. disabled. For closed, default is '', otherwise it is '$1' if
  839. block_level is false, '<div>$1</div>' elsewise.
  840. disabled_before: used in place of before when disabled. Defaults
  841. to '<div>' if block_level, '' if not.
  842. disabled_after: used in place of after when disabled. Defaults
  843. to '</div>' if block_level, '' if not.
  844. block_level: set to true the tag is a "block level" tag, similar
  845. to HTML. Block level tags cannot be nested inside tags that are
  846. not block level, and will not be implicitly closed as easily.
  847. One break following a block level tag may also be removed.
  848. trim: if set, and 'inside' whitespace after the begin tag will be
  849. removed. If set to 'outside', whitespace after the end tag will
  850. meet the same fate.
  851. validate: except when type is missing or 'closed', a callback to
  852. validate the data as $data. Depending on the tag's type, $data
  853. may be a string or an array of strings (corresponding to the
  854. replacement.)
  855. quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
  856. may be not set, 'optional', or 'required' corresponding to if
  857. the content may be quoted. This allows the parser to read
  858. [tag="abc]def[esdf]"] properly.
  859. require_parents: an array of tag names, or not set. If set, the
  860. enclosing tag *must* be one of the listed tags, or parsing won't
  861. occur.
  862. require_children: similar to require_parents, if set children
  863. won't be parsed if they are not in the list.
  864. disallow_children: similar to, but very different from,
  865. require_children, if it is set the listed tags will not be
  866. parsed inside the tag.
  867. parsed_tags_allowed: an array restricting what BBC can be in the
  868. parsed_equals parameter, if desired.
  869. */
  870. $codes = array(
  871. array(
  872. 'tag' => 'abbr',
  873. 'type' => 'unparsed_equals',
  874. 'before' => '<abbr title="$1">',
  875. 'after' => '</abbr>',
  876. 'quoted' => 'optional',
  877. 'disabled_after' => ' ($1)',
  878. ),
  879. array(
  880. 'tag' => 'acronym',
  881. 'type' => 'unparsed_equals',
  882. 'before' => '<abbr title="$1">',
  883. 'after' => '</abbr>',
  884. 'quoted' => 'optional',
  885. 'disabled_after' => ' ($1)',
  886. ),
  887. array(
  888. 'tag' => 'anchor',
  889. 'type' => 'unparsed_equals',
  890. 'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
  891. 'before' => '<span id="post_$1">',
  892. 'after' => '</span>',
  893. ),
  894. array(
  895. 'tag' => 'b',
  896. 'before' => '<span class="bbc_bold">',
  897. 'after' => '</span>',
  898. ),
  899. array(
  900. 'tag' => 'bdo',
  901. 'type' => 'unparsed_equals',
  902. 'before' => '<bdo dir="$1">',
  903. 'after' => '</bdo>',
  904. 'test' => '(rtl|ltr)\]',
  905. 'block_level' => true,
  906. ),
  907. array(
  908. 'tag' => 'black',
  909. 'before' => '<span style="color: black;" class="bbc_color">',
  910. 'after' => '</span>',
  911. ),
  912. array(
  913. 'tag' => 'blue',
  914. 'before' => '<span style="color: blue;" class="bbc_color">',
  915. 'after' => '</span>',
  916. ),
  917. array(
  918. 'tag' => 'br',
  919. 'type' => 'closed',
  920. 'content' => '<br>',
  921. ),
  922. array(
  923. 'tag' => 'center',
  924. 'before' => '<div class="centertext">',
  925. 'after' => '</div>',
  926. 'block_level' => true,
  927. ),
  928. array(
  929. 'tag' => 'code',
  930. 'type' => 'unparsed_content',
  931. 'content' => '<div class="codeheader">' . $txt['code'] . ': <a href="javascript:void(0);" onclick="return smfSelectText(this);" class="codeoperation">' . $txt['code_select'] . '</a></div>' . (isBrowser('gecko') || isBrowser('opera') ? '<pre style="margin: 0; padding: 0;">' : '') . '<code class="bbc_code">$1</code>' . (isBrowser('gecko') || isBrowser('opera') ? '</pre>' : ''),
  932. // @todo Maybe this can be simplified?
  933. 'validate' => isset($disabled['code']) ? null : create_function('&$tag, &$data, $disabled', '
  934. global $context;
  935. if (!isset($disabled[\'code\']))
  936. {
  937. $php_parts = preg_split(\'~(&lt;\?php|\?&gt;)~\', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
  938. for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
  939. {
  940. // Do PHP code coloring?
  941. if ($php_parts[$php_i] != \'&lt;?php\')
  942. continue;
  943. $php_string = \'\';
  944. while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != \'?&gt;\')
  945. {
  946. $php_string .= $php_parts[$php_i];
  947. $php_parts[$php_i++] = \'\';
  948. }
  949. $php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
  950. }
  951. // Fix the PHP code stuff...
  952. $data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode(\'\', $php_parts));
  953. $data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
  954. // Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
  955. if ($context[\'browser\'][\'is_opera\'])
  956. $data .= \'&nbsp;\';
  957. }'),
  958. 'block_level' => true,
  959. ),
  960. array(
  961. 'tag' => 'code',
  962. 'type' => 'unparsed_equals_content',
  963. 'content' => '<div class="codeheader">' . $txt['code'] . ': ($2) <a href="#" onclick="return smfSelectText(this);" class="codeoperation">' . $txt['code_select'] . '</a></div>' . (isBrowser('gecko') || isBrowser('opera') ? '<pre style="margin: 0; padding: 0;">' : '') . '<code class="bbc_code">$1</code>' . (isBrowser('gecko') || isBrowser('opera') ? '</pre>' : ''),
  964. // @todo Maybe this can be simplified?
  965. 'validate' => isset($disabled['code']) ? null : create_function('&$tag, &$data, $disabled', '
  966. global $context;
  967. if (!isset($disabled[\'code\']))
  968. {
  969. $php_parts = preg_split(\'~(&lt;\?php|\?&gt;)~\', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
  970. for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
  971. {
  972. // Do PHP code coloring?
  973. if ($php_parts[$php_i] != \'&lt;?php\')
  974. continue;
  975. $php_string = \'\';
  976. while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != \'?&gt;\')
  977. {
  978. $php_string .= $php_parts[$php_i];
  979. $php_parts[$php_i++] = \'\';
  980. }
  981. $php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
  982. }
  983. // Fix the PHP code stuff...
  984. $data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode(\'\', $php_parts));
  985. $data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
  986. // Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
  987. if ($context[\'browser\'][\'is_opera\'])
  988. $data[0] .= \'&nbsp;\';
  989. }'),
  990. 'block_level' => true,
  991. ),
  992. array(
  993. 'tag' => 'color',
  994. 'type' => 'unparsed_equals',
  995. 'test' => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s?,\s?){2}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\))\]',
  996. 'before' => '<span style="color: $1;" class="bbc_color">',
  997. 'after' => '</span>',
  998. ),
  999. array(
  1000. 'tag' => 'email',
  1001. 'type' => 'unparsed_content',
  1002. 'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
  1003. // @todo Should this respect guest_hideContacts?
  1004. 'validate' => create_function('&$tag, &$data, $disabled', '$data = strtr($data, array(\'<br>\' => \'\'));'),
  1005. ),
  1006. array(
  1007. 'tag' => 'email',
  1008. 'type' => 'unparsed_equals',
  1009. 'before' => '<a href="mailto:$1" class="bbc_email">',
  1010. 'after' => '</a>',
  1011. // @todo Should this respect guest_hideContacts?
  1012. 'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
  1013. 'disabled_after' => ' ($1)',
  1014. ),
  1015. array(
  1016. 'tag' => 'flash',
  1017. 'type' => 'unparsed_commas_content',
  1018. 'test' => '\d+,\d+\]',
  1019. 'content' => (isBrowser('ie') ? '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="$2" height="$3"><param name="movie" value="$1"><param name="play" value="true"><param name="loop" value="true"><param name="quality" value="high"><param name="AllowScriptAccess" value="never"><embed src="$1" width="$2" height="$3" play="true" loop="true" quality="high" AllowScriptAccess="never"><noembed><a href="$1" target="_blank" class="new_win">$1</a></noembed></object>' : '<embed type="application/x-shockwave-flash" src="$1" width="$2" height="$3" play="true" loop="true" quality="high" AllowScriptAccess="never"><noembed><a href="$1" target="_blank" class="new_win">$1</a></noembed>'),
  1020. 'validate' => create_function('&$tag, &$data, $disabled', '
  1021. if (isset($disabled[\'url\']))
  1022. $tag[\'content\'] = \'$1\';
  1023. elseif (strpos($data[0], \'http://\') !== 0 && strpos($data[0], \'https://\') !== 0)
  1024. $data[0] = \'http://\' . $data[0];
  1025. '),
  1026. 'disabled_content' => '<a href="$1" target="_blank" class="new_win">$1</a>',
  1027. ),
  1028. array(
  1029. 'tag' => 'font',
  1030. 'type' => 'unparsed_equals',
  1031. 'test' => '[A-Za-z0-9_,\-\s]+?\]',
  1032. 'before' => '<span style="font-family: $1;" class="bbc_font">',
  1033. 'after' => '</span>',
  1034. ),
  1035. array(
  1036. 'tag' => 'ftp',
  1037. 'type' => 'unparsed_content',
  1038. 'content' => '<a href="$1" class="bbc_ftp new_win" target="_blank">$1</a>',
  1039. 'validate' => create_function('&$tag, &$data, $disabled', '
  1040. $data = strtr($data, array(\'<br>\' => \'\'));
  1041. if (strpos($data, \'ftp://\') !== 0 && strpos($data, \'ftps://\') !== 0)
  1042. $data = \'ftp://\' . $data;
  1043. '),
  1044. ),
  1045. array(
  1046. 'tag' => 'ftp',
  1047. 'type' => 'unparsed_equals',
  1048. 'before' => '<a href="$1" class="bbc_ftp new_win" target="_blank">',
  1049. 'after' => '</a>',
  1050. 'validate' => create_function('&$tag, &$data, $disabled', '
  1051. if (strpos($data, \'ftp://\') !== 0 && strpos($data, \'ftps://\') !== 0)
  1052. $data = \'ftp://\' . $data;
  1053. '),
  1054. 'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
  1055. 'disabled_after' => ' ($1)',
  1056. ),
  1057. array(
  1058. 'tag' => 'glow',
  1059. 'type' => 'unparsed_commas',
  1060. 'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
  1061. 'before' => isBrowser('ie') ? '<table style="border: 0; border-spacing: 0; padding: 0; display: inline; vertical-align: middle; font: inherit;"><tr><td style="filter: Glow(color=$1, strength=$2); font: inherit;">' : '<span style="text-shadow: $1 1px 1px 1px">',
  1062. 'after' => isBrowser('ie') ? '</td></tr></table> ' : '</span>',
  1063. ),
  1064. array(
  1065. 'tag' => 'green',
  1066. 'before' => '<span style="color: green;" class="bbc_color">',
  1067. 'after' => '</span>',
  1068. ),
  1069. array(
  1070. 'tag' => 'html',
  1071. 'type' => 'unparsed_content',
  1072. 'content' => '$1',
  1073. 'block_level' => true,
  1074. 'disabled_content' => '$1',
  1075. ),
  1076. array(
  1077. 'tag' => 'hr',
  1078. 'type' => 'closed',
  1079. 'content' => '<hr>',
  1080. 'block_level' => true,
  1081. ),
  1082. array(
  1083. 'tag' => 'i',
  1084. 'before' => '<em>',
  1085. 'after' => '</em>',
  1086. ),
  1087. array(
  1088. 'tag' => 'img',
  1089. 'type' => 'unparsed_content',
  1090. 'parameters' => array(
  1091. 'alt' => array('optional' => true),
  1092. 'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
  1093. 'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
  1094. ),
  1095. 'content' => '<img src="$1" alt="{alt}"{width}{height} class="bbc_img resized">',
  1096. 'validate' => create_function('&$tag, &$data, $disabled', '
  1097. $data = strtr($data, array(\'<br>\' => \'\'));
  1098. if (strpos($data, \'http://\') !== 0 && strpos($data, \'https://\') !== 0)
  1099. $data = \'http://\' . $data;
  1100. '),
  1101. 'disabled_content' => '($1)',
  1102. ),
  1103. array(
  1104. 'tag' => 'img',
  1105. 'type' => 'unparsed_content',
  1106. 'content' => '<img src="$1" alt="" class="bbc_img">',
  1107. 'validate' => create_function('&$tag, &$data, $disabled', '
  1108. $data = strtr($data, array(\'<br>\' => \'\'));
  1109. if (strpos($data, \'http://\') !== 0 && strpos($data, \'https://\') !== 0)
  1110. $data = \'http://\' . $data;
  1111. '),
  1112. 'disabled_content' => '($1)',
  1113. ),
  1114. array(
  1115. 'tag' => 'iurl',
  1116. 'type' => 'unparsed_content',
  1117. 'content' => '<a href="$1" class="bbc_link">$1</a>',
  1118. 'validate' => create_function('&$tag, &$data, $disabled', '
  1119. $data = strtr($data, array(\'<br>\' => \'\'));
  1120. if (strpos($data, \'http://\') !== 0 && strpos($data, \'https://\') !== 0)
  1121. $data = \'http://\' . $data;
  1122. '),
  1123. ),
  1124. array(
  1125. 'tag' => 'iurl',
  1126. 'type' => 'unparsed_equals',
  1127. 'before' => '<a href="$1" class="bbc_link">',
  1128. 'after' => '</a>',
  1129. 'validate' => create_function('&$tag, &$data, $disabled', '
  1130. if (substr($data, 0, 1) == \'#\')
  1131. $data = \'#post_\' . substr($data, 1);
  1132. elseif (strpos($data, \'http://\') !== 0 && strpos($data, \'https://\') !== 0)
  1133. $data = \'http://\' . $data;
  1134. '),
  1135. 'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
  1136. 'disabled_after' => ' ($1)',
  1137. ),
  1138. array(
  1139. 'tag' => 'left',
  1140. 'before' => '<div style="text-align: left;">',
  1141. 'after' => '</div>',
  1142. 'block_level' => true,
  1143. ),
  1144. array(
  1145. 'tag' => 'li',
  1146. 'before' => '<li>',
  1147. 'after' => '</li>',
  1148. 'trim' => 'outside',
  1149. 'require_parents' => array('list'),
  1150. 'block_level' => true,
  1151. 'disabled_before' => '',
  1152. 'disabled_after' => '<br>',
  1153. ),
  1154. array(
  1155. 'tag' => 'list',
  1156. 'before' => '<ul class="bbc_list">',
  1157. 'after' => '</ul>',
  1158. 'trim' => 'inside',
  1159. 'require_children' => array('li', 'list'),
  1160. 'block_level' => true,
  1161. ),
  1162. array(
  1163. 'tag' => 'list',
  1164. 'parameters' => array(
  1165. 'type' => array('match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'),
  1166. ),
  1167. 'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
  1168. 'after' => '</ul>',
  1169. 'trim' => 'inside',
  1170. 'require_children' => array('li'),
  1171. 'block_level' => true,
  1172. ),
  1173. array(
  1174. 'tag' => 'ltr',
  1175. 'before' => '<div dir="ltr">',
  1176. 'after' => '</div>',
  1177. 'block_level' => true,
  1178. ),
  1179. array(
  1180. 'tag' => 'me',
  1181. 'type' => 'unparsed_equals',
  1182. 'before' => '<div class="meaction">* $1 ',
  1183. 'after' => '</div>',
  1184. 'quoted' => 'optional',
  1185. 'block_level' => true,
  1186. 'disabled_before' => '/me ',
  1187. 'disabled_after' => '<br>',
  1188. ),
  1189. array(
  1190. 'tag' => 'move',
  1191. 'before' => '<marquee>',
  1192. 'after' => '</marquee>',
  1193. 'block_level' => true,
  1194. 'disallow_children' => array('move'),
  1195. ),
  1196. array(
  1197. 'tag' => 'nobbc',
  1198. 'type' => 'unparsed_content',
  1199. 'content' => '$1',
  1200. ),
  1201. array(
  1202. 'tag' => 'php',
  1203. 'type' => 'unparsed_content',
  1204. 'content' => '<span class="phpcode">$1</span>',
  1205. 'validate' => isset($disabled['php']) ? null : create_function('&$tag, &$data, $disabled', '
  1206. if (!isset($disabled[\'php\']))
  1207. {
  1208. $add_begin = substr(trim($data), 0, 5) != \'&lt;?\';
  1209. $data = highlight_php_code($add_begin ? \'&lt;?php \' . $data . \'?&gt;\' : $data);
  1210. if ($add_begin)
  1211. $data = preg_replace(array(\'~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~\', \'~\?&gt;((?:</(font|span)>)*)$~\'), \'$1\', $data, 2);
  1212. }'),
  1213. 'block_level' => false,
  1214. 'disabled_content' => '$1',
  1215. ),
  1216. array(
  1217. 'tag' => 'pre',
  1218. 'before' => '<pre>',
  1219. 'after' => '</pre>',
  1220. ),
  1221. array(
  1222. 'tag' => 'quote',
  1223. 'before' => '<div class="quoteheader"><div class="topslice_quote">' . $txt['quote'] . '</div></div><blockquote>',
  1224. 'after' => '</blockquote><div class="quotefooter"><div class="botslice_quote"></div></div>',
  1225. 'block_level' => true,
  1226. ),
  1227. array(
  1228. 'tag' => 'quote',
  1229. 'parameters' => array(
  1230. 'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
  1231. ),
  1232. 'before' => '<div class="quoteheader"><div class="topslice_quote">' . $txt['quote_from'] . ': {author}</div></div><blockquote>',
  1233. 'after' => '</blockquote><div class="quotefooter"><div class="botslice_quote"></div></div>',
  1234. 'block_level' => true,
  1235. ),
  1236. array(
  1237. 'tag' => 'quote',
  1238. 'type' => 'parsed_equals',
  1239. 'before' => '<div class="quoteheader"><div class="topslice_quote">' . $txt['quote_from'] . ': $1</div></div><blockquote>',
  1240. 'after' => '</blockquote><div class="quotefooter"><div class="botslice_quote"></div></div>',
  1241. 'quoted' => 'optional',
  1242. // Don't allow everything to be embedded with the author name.
  1243. 'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
  1244. 'block_level' => true,
  1245. ),
  1246. array(
  1247. 'tag' => 'quote',
  1248. 'parameters' => array(
  1249. 'author' => array('match' => '([^<>]{1,192}?)'),
  1250. 'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
  1251. 'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
  1252. ),
  1253. 'before' => '<div class="quoteheader"><div class="topslice_quote"><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></div></div><blockquote>',
  1254. 'after' => '</blockquote><div class="quotefooter"><div class="botslice_quote"></div></div>',
  1255. 'block_level' => true,
  1256. ),
  1257. array(
  1258. 'tag' => 'quote',
  1259. 'parameters' => array(
  1260. 'author' => array('match' => '(.{1,192}?)'),
  1261. ),
  1262. 'before' => '<div class="quoteheader"><div class="topslice_quote">' . $txt['quote_from'] . ': {author}</div></div><blockquote>',
  1263. 'after' => '</blockquote><div class="quotefooter"><div class="botslice_quote"></div></div>',
  1264. 'block_level' => true,
  1265. ),
  1266. array(
  1267. 'tag' => 'red',
  1268. 'before' => '<span style="color: red;" class="bbc_color">',
  1269. 'after' => '</span>',
  1270. ),
  1271. array(
  1272. 'tag' => 'right',
  1273. 'before' => '<div style="text-align: right;">',
  1274. 'after' => '</div>',
  1275. 'block_level' => true,
  1276. ),
  1277. array(
  1278. 'tag' => 'rtl',
  1279. 'before' => '<div dir="rtl">',
  1280. 'after' => '</div>',
  1281. 'block_level' => true,
  1282. ),
  1283. array(
  1284. 'tag' => 's',
  1285. 'before' => '<del>',
  1286. 'after' => '</del>',
  1287. ),
  1288. array(
  1289. 'tag' => 'shadow',
  1290. 'type' => 'unparsed_commas',
  1291. 'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
  1292. 'before' => isBrowser('ie') ? '<span style="display: inline-block; filter: Shadow(color=$1, direction=$2); height: 1.2em;">' : '<span style="text-shadow: $1 $2">',
  1293. 'after' => '</span>',
  1294. 'validate' => isBrowser('ie') ? create_function('&$tag, &$data, $disabled', '
  1295. if ($data[1] == \'left\')
  1296. $data[1] = 270;
  1297. elseif ($data[1] == \'right\')
  1298. $data[1] = 90;
  1299. elseif ($data[1] == \'top\')
  1300. $data[1] = 0;
  1301. elseif ($data[1] == \'bottom\')
  1302. $data[1] = 180;
  1303. else
  1304. $data[1] = (int) $data[1];') : create_function('&$tag, &$data, $disabled', '
  1305. if ($data[1] == \'top\' || (is_numeric($data[1]) && $data[1] < 50))
  1306. $data[1] = \'0 -2px 1px\';
  1307. elseif ($data[1] == \'right\' || (is_numeric($data[1]) && $data[1] < 100))
  1308. $data[1] = \'2px 0 1px\';
  1309. elseif ($data[1] == \'bottom\' || (is_numeric($data[1]) && $data[1] < 190))
  1310. $data[1] = \'0 2px 1px\';
  1311. elseif ($data[1] == \'left\' || (is_numeric($data[1]) && $data[1] < 280))
  1312. $data[1] = \'-2px 0 1px\';
  1313. else
  1314. $data[1] = \'1px 1px 1px\';'),
  1315. ),
  1316. array(
  1317. 'tag' => 'size',
  1318. 'type' => 'unparsed_equals',
  1319. 'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
  1320. 'before' => '<span style="font-size: $1;" class="bbc_size">',
  1321. 'after' => '</span>',
  1322. ),
  1323. array(
  1324. 'tag' => 'size',
  1325. 'type' => 'unparsed_equals',
  1326. 'test' => '[1-7]\]',
  1327. 'before' => '<span style="font-size: $1;" class="bbc_size">',
  1328. 'after' => '</span>',
  1329. 'validate' => create_function('&$tag, &$data, $disabled', '
  1330. $sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
  1331. $data = $sizes[$data] . \'em\';'
  1332. ),
  1333. ),
  1334. array(
  1335. 'tag' => 'sub',
  1336. 'before' => '<sub>',
  1337. 'after' => '</sub>',
  1338. ),
  1339. array(
  1340. 'tag' => 'sup',
  1341. 'before' => '<sup>',
  1342. 'after' => '</sup>',
  1343. ),
  1344. array(
  1345. 'tag' => 'table',
  1346. 'before' => '<table class="bbc_table">',
  1347. 'after' => '</table>',
  1348. 'trim' => 'inside',
  1349. 'require_children' => array('tr'),
  1350. 'block_level' => true,
  1351. ),
  1352. array(
  1353. 'tag' => 'td',
  1354. 'before' => '<td>',
  1355. 'after' => '</td>',
  1356. 'require_parents' => array('tr'),
  1357. 'trim' => 'outside',
  1358. 'block_level' => true,
  1359. 'disabled_before' => '',
  1360. 'disabled_after' => '',
  1361. ),
  1362. array(
  1363. 'tag' => 'time',
  1364. 'type' => 'unparsed_content',
  1365. 'content' => '$1',
  1366. 'validate' => create_function('&$tag, &$data, $disabled', '
  1367. if (is_numeric($data))
  1368. $data = timeformat($data);
  1369. else
  1370. $tag[\'content\'] = \'[time]$1[/time]\';'),
  1371. ),
  1372. array(
  1373. 'tag' => 'tr',
  1374. 'before' => '<tr>',
  1375. 'after' => '</tr>',
  1376. 'require_parents' => array('table'),
  1377. 'require_children' => array('td'),
  1378. 'trim' => 'both',
  1379. 'block_level' => true,
  1380. 'disabled_before' => '',
  1381. 'disabled_after' => '',
  1382. ),
  1383. array(
  1384. 'tag' => 'tt',
  1385. 'before' => '<span class="bbc_tt">',
  1386. 'after' => '</span>',
  1387. ),
  1388. array(
  1389. 'tag' => 'u',
  1390. 'before' => '<span class="bbc_u">',
  1391. 'after' => '</span>',
  1392. ),
  1393. array(
  1394. 'tag' => 'url',
  1395. 'type' => 'unparsed_content',
  1396. 'content' => '<a href="$1" class="bbc_link" target="_blank">$1</a>',
  1397. 'validate' => create_function('&$tag, &$data, $disabled', '
  1398. $data = strtr($data, array(\'<br>\' => \'\'));
  1399. if (strpos($data, \'http://\') !== 0 && strpos($data, \'https://\') !== 0)
  1400. $data = \'http://\' . $data;
  1401. '),
  1402. ),
  1403. array(
  1404. 'tag' => 'url',
  1405. 'type' => 'unparsed_equals',
  1406. 'before' => '<a href="$1" class="bbc_link" target="_blank">',
  1407. 'after' => '</a>',
  1408. 'validate' => create_function('&$tag, &$data, $disabled', '
  1409. if (strpos($data, \'http://\') !== 0 && strpos($data, \'https://\') !== 0)
  1410. $data = \'http://\' . $data;
  1411. '),
  1412. 'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
  1413. 'disabled_after' => ' ($1)',
  1414. ),
  1415. array(
  1416. 'tag' => 'white',
  1417. 'before' => '<span style="color: white;" class="bbc_color">',
  1418. 'after' => '</span>',
  1419. ),
  1420. );
  1421. // Let mods add new BBC without hassle.
  1422. call_integration_hook('integrate_bbc_codes', array(&$codes));
  1423. // This is mainly for the bbc manager, so it's easy to add tags above. Custom BBC should be added above this line.
  1424. if ($message === false)
  1425. {
  1426. if (isset($temp_bbc))
  1427. $bbc_codes = $temp_bbc;
  1428. return $codes;
  1429. }
  1430. // So the parser won't skip them.
  1431. $itemcodes = array(
  1432. '*' => 'disc',
  1433. '@' => 'disc',
  1434. '+' => 'square',
  1435. 'x' => 'square',
  1436. '#' => 'square',
  1437. 'o' => 'circle',
  1438. 'O' => 'circle',
  1439. '0' => 'circle',
  1440. );
  1441. if (!isset($disabled['li']) && !isset($disabled['list']))
  1442. {
  1443. foreach ($itemcodes as $c => $dummy)
  1444. $bbc_codes[$c] = array();
  1445. }
  1446. // Inside these tags autolink is not recommendable.
  1447. $no_autolink_tags = array(
  1448. 'url',
  1449. 'iurl',
  1450. 'ftp',
  1451. 'email',
  1452. );
  1453. // Shhhh!
  1454. if (!isset($disabled['color']))
  1455. {
  1456. $codes[] = array(
  1457. 'tag' => 'chrissy',
  1458. 'before' => '<span style="color: #cc0099;">',
  1459. 'after' => ' :-*</span>',
  1460. );
  1461. $codes[] = array(
  1462. 'tag' => 'kissy',
  1463. 'before' => '<span style="color: #cc0099;">',
  1464. 'after' => ' :-*</span>',
  1465. );
  1466. }
  1467. foreach ($codes as $code)
  1468. {
  1469. // If we are not doing every tag only do ones we are interested in.
  1470. if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
  1471. $bbc_codes[substr($code['tag'], 0, 1)][] = $code;
  1472. }
  1473. $codes = null;
  1474. }
  1475. // Shall we take the time to cache this?
  1476. if ($cache_id != '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
  1477. {
  1478. // It's likely this will change if the message is modified.
  1479. $cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . serialize($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']);
  1480. if (($temp = cache_get_data($cache_key, 240)) != null)
  1481. return $temp;
  1482. $cache_t = microtime();
  1483. }
  1484. if ($smileys === 'print')
  1485. {
  1486. // [glow], [shadow], and [move] can't really be printed.
  1487. $disabled['glow'] = true;
  1488. $disabled['shadow'] = true;
  1489. $disabled['move'] = true;
  1490. // Colors can't well be displayed... supposed to be black and white.
  1491. $disabled['color'] = true;
  1492. $disabled['black'] = true;
  1493. $disabled['blue'] = true;
  1494. $disabled['white'] = true;
  1495. $disabled['red'] = true;
  1496. $disabled['green'] = true;
  1497. $disabled['me'] = true;
  1498. // Color coding doesn't make sense.
  1499. $disabled['php'] = true;
  1500. // Links are useless on paper... just show the link.
  1501. $disabled['ftp'] = true;
  1502. $disabled['url'] = true;
  1503. $disabled['iurl'] = true;
  1504. $disabled['email'] = true;
  1505. $disabled['flash'] = true;
  1506. // @todo Change maybe?
  1507. if (!isset($_GET['images']))
  1508. $disabled['img'] = true;
  1509. // @todo Interface/setting to add more?
  1510. }
  1511. $open_tags = array();
  1512. $message = strtr($message, array("\n" => '<br>'));
  1513. // The non-breaking-space looks a bit different each time.
  1514. $non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
  1515. $pos = -1;
  1516. while ($pos !== false)
  1517. {
  1518. $last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
  1519. $pos = strpos($message, '[', $pos + 1);
  1520. // Failsafe.
  1521. if ($pos === false || $last_pos > $pos)
  1522. $pos = strlen($message) + 1;
  1523. // Can't have a one letter smiley, URL, or email! (sorry.)
  1524. if ($last_pos < $pos - 1)
  1525. {
  1526. // Make sure the $last_pos is not negative.
  1527. $last_pos = max($last_pos, 0);
  1528. // Pick a block of data to do some raw fixing on.
  1529. $data = substr($message, $last_pos, $pos - $last_pos);
  1530. // Take care of some HTML!
  1531. if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
  1532. {
  1533. $data = preg_replace('~&lt;a\s+href=((?:&quot;)?)((?:https?://|ftps?://|mailto:)\S+?)\\1&gt;~i', '[url=$2]', $data);
  1534. $data = preg_replace('~&lt;/a&gt;~i', '[/url]', $data);
  1535. // <br> should be empty.
  1536. $empty_tags = array('br', 'hr');
  1537. foreach ($empty_tags as $tag)
  1538. $data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '[' . $tag . ' /]', $data);
  1539. // b, u, i, s, pre... basic tags.
  1540. $closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote');
  1541. foreach ($closable_tags as $tag)
  1542. {
  1543. $diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
  1544. $data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
  1545. if ($diff > 0)
  1546. $data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
  1547. }
  1548. // Do <img ...> - with security... action= -> action-.
  1549. preg_match_all('~&lt;img\s+src=((?:&quot;)?)((?:https?://|ftps?://)\S+?)\\1(?:\s+alt=(&quot;.*?&quot;|\S*?))?(?:\s?/)?&gt;~i', $data, $matches, PREG_PATTERN_ORDER);
  1550. if (!empty($matches[0]))
  1551. {
  1552. $replaces = array();
  1553. foreach ($matches[2] as $match => $imgtag)
  1554. {
  1555. $alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
  1556. // Remove action= from the URL - no funny business, now.
  1557. if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
  1558. $imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
  1559. // Check if the image is larger than allowed.
  1560. if (!empty($modSettings['max_image_width']) && !empty($modSettings['max_image_height']))
  1561. {
  1562. list ($width, $height) = url_image_size($imgtag);
  1563. if (!empty($modSettings['max_image_width']) && $width > $modSettings['max_image_width'])
  1564. {
  1565. $height = (int) (($modSettings['max_image_width'] * $height) / $width);
  1566. $width = $modSettings['max_image_width'];
  1567. }
  1568. if (!empty($modSettings['max_image_height']) && $height > $modSettings['max_image_height'])
  1569. {
  1570. $width = (int) (($modSettings['max_image_height'] * $width) / $height);
  1571. $height = $modSettings['max_image_height'];
  1572. }
  1573. // Set the new image tag.
  1574. $replaces[$matches[0][$match]] = '[img width=' . $width . ' height=' . $height . $alt . ']' . $imgtag . '[/img]';
  1575. }
  1576. else
  1577. $replaces[$matches[0][$match]] = '[img' . $alt . ']' . $imgtag . '[/img]';
  1578. }
  1579. $data = strtr($data, $replaces);
  1580. }
  1581. }
  1582. if (!empty($modSettings['autoLinkUrls']))
  1583. {
  1584. // Are we inside tags that should be auto linked?
  1585. $no_autolink_area = false;
  1586. if (!empty($open_tags))
  1587. {
  1588. foreach ($open_tags as $open_tag)
  1589. if (in_array($open_tag['tag'], $no_autolink_tags))
  1590. $no_autolink_area = true;
  1591. }
  1592. // Don't go backwards.
  1593. // @todo Don't think is the real solution....
  1594. $lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
  1595. if ($pos < $lastAutoPos)
  1596. $no_autolink_area = true;
  1597. $lastAutoPos = $pos;
  1598. if (!$no_autolink_area)
  1599. {
  1600. // Parse any URLs.... have to get rid of the @ problems some things cause... stupid email addresses.
  1601. if (!isset($disabled['url']) && (strpos($data, '://') !== false || strpos($data, 'www.') !== false) && strpos($data, '[url') === false)
  1602. {
  1603. // Switch out quotes really quick because they can cause problems.
  1604. $data = strtr($data, array('&#039;' => '\'', '&nbsp;' => $context['utf8'] ? "\xC2\xA0" : "\xA0", '&quot;' => '>">', '"' => '<"<', '&lt;' => '<lt<'));
  1605. // Only do this if the preg survives.
  1606. if (is_string($result = preg_replace(array(
  1607. '~(?<=[\s>\.(;\'"]|^)((?:http|https)://[\w\-_%@:|]+(?:\.[\w\-_%]+)*(?::\d+)?(?:/[\w\-_\~%\.@!,\?&;=#(){}+:\'\\\\]*)*[/\w\-_\~%@\?;=#}\\\\]?)~i',
  1608. '~(?<=[\s>\.(;\'"]|^)((?:ftp|ftps)://[\w\-_%@:|]+(?:\.[\w\-_%]+)*(?::\d+)?(?:/[\w\-_\~%\.@,\?&;=#(){}+:\'\\\\]*)*[/\w\-_\~%@\?;=#}\\\\]?)~i',
  1609. '~(?<=[\s>(\'<]|^)(www(?:\.[\w\-_]+)+(?::\d+)?(?:/[\w\-_\~%\.@!,\?&;=#(){}+:\'\\\\]*)*[/\w\-_\~%@\?;=#}\\\\])~i'
  1610. ), array(
  1611. '[url]$1[/url]',
  1612. '[ftp]$1[/ftp]',
  1613. '[url=http://$1]$1[/url]'
  1614. ), $data)))
  1615. $data = $result;
  1616. $data = strtr($data, array('\'' => '&#039;', $context['utf8'] ? "\xC2\xA0" : "\xA0" => '&nbsp;', '>">' => '&quot;', '<"<' => '"', '<lt<' => '&lt;'));
  1617. }
  1618. // Next, emails...
  1619. if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false)
  1620. {
  1621. $data = preg_replace('~(?<=[\?\s' . $non_breaking_space . '\[\]()*\\\;>]|^)([\w\-\.]{1,80}@[\w\-]+\.[\w\-\.]+[\w\-])(?=[?,\s' . $non_breaking_space . '\[\]()*\\\]|$|<br>|&nbsp;|&gt;|&lt;|&quot;|&#039;|\.(?:\.|;|&nbsp;|\s|$|<br>))~' . ($context['utf8'] ? 'u' : ''), '[email]$1[/email]', $data);
  1622. $data = preg_replace('~(?<=<br>)([\w\-\.]{1,80}@[\w\-]+\.[\w\-\.]+[\w\-])(?=[?\.,;\s' . $non_breaking_space . '\[\]()*\\\]|$|<br>|&nbsp;|&gt;|&lt;|&quot;|&#039;)~' . ($context['utf8'] ? 'u' : ''), '[email]$1[/email]', $data);
  1623. }
  1624. }
  1625. }
  1626. $data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
  1627. // If it wasn't changed, no copying or other boring stuff has to happen!
  1628. if ($data != substr($message, $last_pos, $pos - $last_pos))
  1629. {
  1630. $message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
  1631. // Since we changed it, look again in case we added or removed a tag. But we don't want to skip any.
  1632. $old_pos = strlen($data) + $last_pos;
  1633. $pos = strpos($message, '[', $last_pos);
  1634. $pos = $pos === false ? $old_pos : min($pos, $old_pos);
  1635. }
  1636. }
  1637. // Are we there yet? Are we there yet?
  1638. if ($pos >= strlen($message) - 1)
  1639. break;
  1640. $tags = strtolower($message[$pos + 1]);
  1641. if ($tags == '/' && !empty($open_tags))
  1642. {
  1643. $pos2 = strpos($message, ']', $pos + 1);
  1644. if ($pos2 == $pos + 2)
  1645. continue;
  1646. $look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
  1647. $to_close = array();
  1648. $block_level = null;
  1649. do
  1650. {
  1651. $tag = array_pop($open_tags);
  1652. if (!$tag)
  1653. break;
  1654. if (!empty($tag['block_level']))
  1655. {
  1656. // Only find out if we need to.
  1657. if ($block_level === false)
  1658. {
  1659. array_push($open_tags, $tag);
  1660. break;
  1661. }
  1662. // The idea is, if we are LOOKING for a block level tag, we can close them on the way.
  1663. if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
  1664. {
  1665. foreach ($bbc_codes[$look_for[0]] as $temp)
  1666. if ($temp['tag'] == $look_for)
  1667. {
  1668. $block_level = !empty($temp['block_level']);
  1669. break;
  1670. }
  1671. }
  1672. if ($block_level !== true)
  1673. {
  1674. $block_level = false;
  1675. array_push($open_tags, $tag);
  1676. break;
  1677. }
  1678. }
  1679. $to_close[] = $tag;
  1680. }
  1681. while ($tag['tag'] != $look_for);
  1682. // Did we just eat through everything and not find it?
  1683. if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
  1684. {
  1685. $open_tags = $to_close;
  1686. continue;
  1687. }
  1688. elseif (!empty($to_close) && $tag['tag'] != $look_for)
  1689. {
  1690. if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
  1691. {
  1692. foreach ($bbc_codes[$look_for[0]] as $temp)
  1693. if ($temp['tag'] == $look_for)
  1694. {
  1695. $block_level = !empty($temp['block_level']);
  1696. break;
  1697. }
  1698. }
  1699. // We're not looking for a block level tag (or maybe even a tag that exists...)
  1700. if (!$block_level)
  1701. {
  1702. foreach ($to_close as $tag)
  1703. array_push($open_tags, $tag);
  1704. continue;
  1705. }
  1706. }
  1707. foreach ($to_close as $tag)
  1708. {
  1709. $message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
  1710. $pos += strlen($tag['after']) + 2;
  1711. $pos2 = $pos - 1;
  1712. // See the comment at the end of the big loop - just eating whitespace ;).
  1713. if (!empty($tag['block_level']) && substr($message, $pos, 6) == '<br>')
  1714. $message = substr($message, 0, $pos) . substr($message, $pos + 6);
  1715. if (!empty($tag['trim']) && $tag['trim'] != 'inside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos), $matches) != 0)
  1716. $message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
  1717. }
  1718. if (!empty($to_close))
  1719. {
  1720. $to_close = array();
  1721. $pos--;
  1722. }
  1723. continue;
  1724. }
  1725. // No tags for this character, so just keep going (fastest possible course.)
  1726. if (!isset($bbc_codes[$tags]))
  1727. continue;
  1728. $inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
  1729. $tag = null;
  1730. foreach ($bbc_codes[$tags] as $possible)
  1731. {
  1732. $pt_strlen = strlen($possible['tag']);
  1733. // Not a match?
  1734. if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
  1735. continue;
  1736. $next_c = $message[$pos + 1 + $pt_strlen];
  1737. // A test validation?
  1738. if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
  1739. continue;
  1740. // Do we want parameters?
  1741. elseif (!empty($possible['parameters']))
  1742. {
  1743. if ($next_c != ' ')
  1744. continue;
  1745. }
  1746. elseif (isset($possible['type']))
  1747. {
  1748. // Do we need an equal sign?
  1749. if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
  1750. continue;
  1751. // Maybe we just want a /...
  1752. if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
  1753. continue;
  1754. // An immediate ]?
  1755. if ($possible['type'] == 'unparsed_content' && $next_c != ']')
  1756. continue;
  1757. }
  1758. // No type means 'parsed_content', which demands an immediate ] without parameters!
  1759. elseif ($next_c != ']')
  1760. continue;
  1761. // Check allowed tree?
  1762. if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
  1763. continue;
  1764. elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
  1765. continue;
  1766. // If this is in the list of disallowed child tags, don't parse it.
  1767. elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
  1768. continue;
  1769. $pos1 = $pos + 1 + $pt_strlen + 1;
  1770. // Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
  1771. if ($possible['tag'] == 'quote')
  1772. {
  1773. // Start with standard
  1774. $quote_alt = false;
  1775. foreach ($open_tags as $open_quote)
  1776. {
  1777. // Every parent quote this quote has flips the styling
  1778. if ($open_quote['tag'] == 'quote')
  1779. $quote_alt = !$quote_alt;
  1780. }
  1781. // Add a class to the quote to style alternating blockquotes
  1782. $possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
  1783. }
  1784. // This is long, but it makes things much easier and cleaner.
  1785. if (!empty($possible['parameters']))
  1786. {
  1787. $preg = array();
  1788. foreach ($possible['parameters'] as $p => $info)
  1789. $preg[] = '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . ')' . (empty($info['optional']) ? '' : '?');
  1790. // Okay, this may look ugly and it is, but it's not going to happen much and it is the best way of allowing any order of parameters but still parsing them right.
  1791. $match = false;
  1792. $orders = permute($preg);
  1793. foreach ($orders as $p)
  1794. if (preg_match('~^' . implode('', $p) . '\]~i', substr($message, $pos1 - 1), $matches) != 0)
  1795. {
  1796. $match = true;
  1797. break;
  1798. }
  1799. // Didn't match our parameter list, try the next possible.
  1800. if (!$match)
  1801. continue;
  1802. $params = array();
  1803. for ($i = 1, $n = count($matches); $i < $n; $i += 2)
  1804. {
  1805. $key = strtok(ltrim($matches[$i]), '=');
  1806. if (isset($possible['parameters'][$key]['value']))
  1807. $params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
  1808. elseif (isset($possible['parameters'][$key]['validate']))
  1809. $params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
  1810. else
  1811. $params['{' . $key . '}'] = $matches[$i + 1];
  1812. // Just to make sure: replace any $ or { so they can't interpolate wrongly.
  1813. $params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
  1814. }
  1815. foreach ($possible['parameters'] as $p => $info)
  1816. {
  1817. if (!isset($params['{' . $p . '}']))
  1818. $params['{' . $p . '}'] = '';
  1819. }
  1820. $tag = $possible;
  1821. // Put the parameters into the string.
  1822. if (isset($tag['before']))
  1823. $tag['before'] = strtr($tag['before'], $params);
  1824. if (isset($tag['after']))
  1825. $tag['after'] = strtr($tag['after'], $params);
  1826. if (isset($tag['content']))
  1827. $tag['content'] = strtr($tag['content'], $params);
  1828. $pos1 += strlen($matches[0]) - 1;
  1829. }
  1830. else
  1831. $tag = $possible;
  1832. break;
  1833. }
  1834. // Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
  1835. if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
  1836. {
  1837. if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
  1838. continue;
  1839. $tag = $itemcodes[$message[$pos + 1]];
  1840. // First let's set up the tree: it needs to be in a list, or after an li.
  1841. if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
  1842. {
  1843. $open_tags[] = array(
  1844. 'tag' => 'list',
  1845. 'after' => '</ul>',
  1846. 'block_level' => true,
  1847. 'require_children' => array('li'),
  1848. 'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
  1849. );
  1850. $code = '<ul class="bbc_list">';
  1851. }
  1852. // We're in a list item already: another itemcode? Close it first.
  1853. elseif ($inside['tag'] == 'li')
  1854. {
  1855. array_pop($open_tags);
  1856. $code = '</li>';
  1857. }
  1858. else
  1859. $code = '';
  1860. // Now we open a new tag.
  1861. $open_tags[] = array(
  1862. 'tag' => 'li',
  1863. 'after' => '</li>',
  1864. 'trim' => 'outside',
  1865. 'block_level' => true,
  1866. 'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
  1867. );
  1868. // First, open the tag...
  1869. $code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
  1870. $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
  1871. $pos += strlen($code) - 1 + 2;
  1872. // Next, find the next break (if any.) If there's more itemcode after it, keep it going - otherwise close!
  1873. $pos2 = strpos($message, '<br>', $pos);
  1874. $pos3 = strpos($message, '[/', $pos);
  1875. if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
  1876. {
  1877. preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 6), $matches);
  1878. $message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
  1879. $open_tags[count($open_tags) - 2]['after'] = '</ul>';
  1880. }
  1881. // Tell the [list] that it needs to close specially.
  1882. else
  1883. {
  1884. // Move the li over, because we're not sure what we'll hit.
  1885. $open_tags[count($open_tags) - 1]['after'] = '';
  1886. $open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
  1887. }
  1888. continue;
  1889. }
  1890. // Implicitly close lists and tables if something other than what's required is in them. This is needed for itemcode.
  1891. if ($tag === null && $inside !== null && !empty($inside['require_children']))
  1892. {
  1893. array_pop($open_tags);
  1894. $message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
  1895. $pos += strlen($inside['after']) - 1 + 2;
  1896. }
  1897. // No tag? Keep looking, then. Silly people using brackets without actual tags.
  1898. if ($tag === null)
  1899. continue;
  1900. // Propagate the list to the child (so wrapping the disallowed tag won't work either.)
  1901. if (isset($inside['disallow_children']))
  1902. $tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
  1903. // Is this tag disabled?
  1904. if (isset($disabled[$tag['tag']]))
  1905. {
  1906. if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
  1907. {
  1908. $tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
  1909. $tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
  1910. $tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
  1911. }
  1912. elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
  1913. {
  1914. $tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
  1915. $tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
  1916. }
  1917. else
  1918. $tag['content'] = $tag['disabled_content'];
  1919. }
  1920. // we use this alot
  1921. $tag_strlen = strlen($tag['tag']);
  1922. // The only special case is 'html', which doesn't need to close things.
  1923. if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
  1924. {
  1925. $n = count($open_tags) - 1;
  1926. while (empty($open_tags[$n]['block_level']) && $n >= 0)
  1927. $n--;
  1928. // Close all the non block level tags so this tag isn't surrounded by them.
  1929. for ($i = count($open_tags) - 1; $i > $n; $i--)
  1930. {
  1931. $message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
  1932. $ot_strlen = strlen($open_tags[$i]['after']);
  1933. $pos += $ot_strlen + 2;
  1934. $pos1 += $ot_strlen + 2;
  1935. // Trim or eat trailing stuff... see comment at the end of the big loop.
  1936. if (!empty($open_tags[$i]['block_level']) && substr($message, $pos, 6) == '<br>')
  1937. $message = substr($message, 0, $pos) . substr($message, $pos + 6);
  1938. if (!empty($open_tags[$i]['trim']) && $tag['trim'] != 'inside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos), $matches) != 0)
  1939. $message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
  1940. array_pop($open_tags);
  1941. }
  1942. }
  1943. // No type means 'parsed_content'.
  1944. if (!isset($tag['type']))
  1945. {
  1946. // @todo Check for end tag first, so people can say "I like that [i] tag"?
  1947. $open_tags[] = $tag;
  1948. $message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
  1949. $pos += strlen($tag['before']) - 1 + 2;
  1950. }
  1951. // Don't parse the content, just skip it.
  1952. elseif ($tag['type'] == 'unparsed_content')
  1953. {
  1954. $pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
  1955. if ($pos2 === false)
  1956. continue;
  1957. $data = substr($message, $pos1, $pos2 - $pos1);
  1958. if (!empty($tag['block_level']) && substr($data, 0, 6) == '<br>')
  1959. $data = substr($data, 6);
  1960. if (isset($tag['validate']))
  1961. $tag['validate']($tag, $data, $disabled);
  1962. $code = strtr($tag['content'], array('$1' => $data));
  1963. $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
  1964. $pos += strlen($code) - 1 + 2;
  1965. $last_pos = $pos + 1;
  1966. }
  1967. // Don't parse the content, just skip it.
  1968. elseif ($tag['type'] == 'unparsed_equals_content')
  1969. {
  1970. // The value may be quoted for some tags - check.
  1971. if (isset($tag['quoted']))
  1972. {
  1973. $quoted = substr($message, $pos1, 6) == '&quot;';
  1974. if ($tag['quoted'] != 'optional' && !$quoted)
  1975. continue;
  1976. if ($quoted)
  1977. $pos1 += 6;
  1978. }
  1979. else
  1980. $quoted = false;
  1981. $pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
  1982. if ($pos2 === false)
  1983. continue;
  1984. $pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
  1985. if ($pos3 === false)
  1986. continue;
  1987. $data = array(
  1988. substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))),
  1989. substr($message, $pos1, $pos2 - $pos1)
  1990. );
  1991. if (!empty($tag['block_level']) && substr($data[0], 0, 6) == '<br>')
  1992. $data[0] = substr($data[0], 6);
  1993. // Validation for my parking, please!
  1994. if (isset($tag['validate']))
  1995. $tag['validate']($tag, $data, $disabled);
  1996. $code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
  1997. $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
  1998. $pos += strlen($code) - 1 + 2;
  1999. }
  2000. // A closed tag, with no content or value.
  2001. elseif ($tag['type'] == 'closed')
  2002. {
  2003. $pos2 = strpos($message, ']', $pos);
  2004. $message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
  2005. $pos += strlen($tag['content']) - 1 + 2;
  2006. }
  2007. // This one is sorta ugly... :/. Unfortunately, it's needed for flash.
  2008. elseif ($tag['type'] == 'unparsed_commas_content')
  2009. {
  2010. $pos2 = strpos($message, ']', $pos1);
  2011. if ($pos2 === false)
  2012. continue;
  2013. $pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
  2014. if ($pos3 === false)
  2015. continue;
  2016. // We want $1 to be the content, and the rest to be csv.
  2017. $data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
  2018. $data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
  2019. if (isset($tag['validate']))
  2020. $tag['validate']($tag, $data, $disabled);
  2021. $code = $tag['content'];
  2022. foreach ($data as $k => $d)
  2023. $code = strtr($code, array('$' . ($k + 1) => trim($d)));
  2024. $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
  2025. $pos += strlen($code) - 1 + 2;
  2026. }
  2027. // This has parsed content, and a csv value which is unparsed.
  2028. elseif ($tag['type'] == 'unparsed_commas')
  2029. {
  2030. $pos2 = strpos($message, ']', $pos1);
  2031. if ($pos2 === false)
  2032. continue;
  2033. $data = explode(',', substr($message, $pos1, $pos2 - $pos1));
  2034. if (isset($tag['validate']))
  2035. $tag['validate']($tag, $data, $disabled);
  2036. // Fix after, for disabled code mainly.
  2037. foreach ($data as $k => $d)
  2038. $tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
  2039. $open_tags[] = $tag;
  2040. // Replace them out, $1, $2, $3, $4, etc.
  2041. $code = $tag['before'];
  2042. foreach ($data as $k => $d)
  2043. $code = strtr($code, array('$' . ($k + 1) => trim($d)));
  2044. $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
  2045. $pos += strlen($code) - 1 + 2;
  2046. }
  2047. // A tag set to a value, parsed or not.
  2048. elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
  2049. {
  2050. // The value may be quoted for some tags - check.
  2051. if (isset($tag['quoted']))
  2052. {
  2053. $quoted = substr($message, $pos1, 6) == '&quot;';
  2054. if ($tag['quoted'] != 'optional' && !$quoted)
  2055. continue;
  2056. if ($quoted)
  2057. $pos1 += 6;
  2058. }
  2059. else
  2060. $quoted = false;
  2061. $pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
  2062. if ($pos2 === false)
  2063. continue;
  2064. $data = substr($message, $pos1, $pos2 - $pos1);
  2065. // Validation for my parking, please!
  2066. if (isset($tag['validate']))
  2067. $tag['validate']($tag, $data, $disabled);
  2068. // For parsed content, we must recurse to avoid security problems.
  2069. if ($tag['type'] != 'unparsed_equals')
  2070. $data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
  2071. $tag['after'] = strtr($tag['after'], array('$1' => $data));
  2072. $open_tags[] = $tag;
  2073. $code = strtr($tag['before'], array('$1' => $data));
  2074. $message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7));
  2075. $pos += strlen($code) - 1 + 2;
  2076. }
  2077. // If this is block level, eat any breaks after it.
  2078. if (!empty($tag['block_level']) && substr($message, $pos + 1, 6) == '<br>')
  2079. $message = substr($message, 0, $pos + 1) . substr($message, $pos + 7);
  2080. // Are we trimming outside this tag?
  2081. if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
  2082. $message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
  2083. }
  2084. // Close any remaining tags.
  2085. while ($tag = array_pop($open_tags))
  2086. $message .= "\n" . $tag['after'] . "\n";
  2087. // Parse the smileys within the parts where it can be done safely.
  2088. if ($smileys === true)
  2089. {
  2090. $message_parts = explode("\n", $message);
  2091. for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
  2092. parsesmileys($message_parts[$i]);
  2093. $message = implode('', $message_parts);
  2094. }
  2095. // No smileys, just get rid of the markers.
  2096. else
  2097. $message = strtr($message, array("\n" => ''));
  2098. if ($message[0] === ' ')
  2099. $message = '&nbsp;' . substr($message, 1);
  2100. // Cleanup whitespace.
  2101. $message = strtr($message, array(' ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
  2102. // Allow mods access to what parse_bbc created
  2103. call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
  2104. // Cache the output if it took some time...
  2105. if (isset($cache_key, $cache_t) && array_sum(explode(' ', microtime())) - array_sum(explode(' ', $cache_t)) > 0.05)
  2106. cache_put_data($cache_key, $message, 240);
  2107. // If this was a force parse revert if needed.
  2108. if (!empty($parse_tags))
  2109. {
  2110. if (empty($temp_bbc))
  2111. $bbc_codes = array();
  2112. else
  2113. {
  2114. $bbc_codes = $temp_bbc;
  2115. unset($temp_bbc);
  2116. }
  2117. }
  2118. return $message;
  2119. }
  2120. /**
  2121. * Parse smileys in the passed message.
  2122. *
  2123. * The smiley parsing function which makes pretty faces appear :).
  2124. * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
  2125. * These are specifically not parsed in code tags [url=mailto:[email protected]]
  2126. * Caches the smileys from the database or array in memory.
  2127. * Doesn't return anything, but rather modifies message directly.
  2128. *
  2129. * @param string &$message
  2130. */
  2131. function parsesmileys(&$message)
  2132. {
  2133. global $modSettings, $txt, $user_info, $context, $smcFunc;
  2134. static $smileyPregSearch = null, $smileyPregReplacements = array();
  2135. // No smiley set at all?!
  2136. if ($user_info['smiley_set'] == 'none' || trim($message) == '')
  2137. return;
  2138. // If smileyPregSearch hasn't been set, do it now.
  2139. if (empty($smileyPregSearch))
  2140. {
  2141. // Use the default smileys if it is disabled. (better for "portability" of smileys.)
  2142. if (empty($modSettings['smiley_enable']))
  2143. {
  2144. $smileysfrom = array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', '0:)');
  2145. $smileysto = array('evil.gif', 'cheesy.gif', 'rolleyes.gif', 'angry.gif', 'laugh.gif', 'smiley.gif', 'wink.gif', 'grin.gif', 'sad.gif', 'shocked.gif', 'cool.gif', 'tongue.gif', 'huh.gif', 'embarrassed.gif', 'lipsrsealed.gif', 'kiss.gif', 'cry.gif', 'undecided.gif', 'azn.gif', 'afro.gif', 'police.gif', 'angel.gif');
  2146. $smileysdescs = array('', $txt['icon_cheesy'], $txt['icon_rolleyes'], $txt['icon_angry'], '', $txt['icon_smiley'], $txt['icon_wink'], $txt['icon_grin'], $txt['icon_sad'], $txt['icon_shocked'], $txt['icon_cool'], $txt['icon_tongue'], $txt['icon_huh'], $txt['icon_embarrassed'], $txt['icon_lips'], $txt['icon_kiss'], $txt['icon_cry'], $txt['icon_undecided'], '', '', '', '');
  2147. }
  2148. else
  2149. {
  2150. // Load the smileys in reverse order by length so they don't get parsed wrong.
  2151. if (($temp = cache_get_data('parsing_smileys', 480)) == null)
  2152. {
  2153. $result = $smcFunc['db_query']('', '
  2154. SELECT code, filename, description
  2155. FROM {db_prefix}smileys
  2156. ORDER BY LENGTH(code) DESC',
  2157. array(
  2158. )
  2159. );
  2160. $smileysfrom = array();
  2161. $smileysto = array();
  2162. $smileysdescs = array();
  2163. while ($row = $smcFunc['db_fetch_assoc']($result))
  2164. {
  2165. $smileysfrom[] = $row['code'];
  2166. $smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
  2167. $smileysdescs[] = $row['description'];
  2168. }
  2169. $smcFunc['db_free_result']($result);
  2170. cache_put_data('parsing_smileys', array($smileysfrom, $smileysto, $smileysdescs), 480);
  2171. }
  2172. else
  2173. list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
  2174. }
  2175. // The non-breaking-space is a complex thing...
  2176. $non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
  2177. // This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
  2178. $smileyPregReplacements = array();
  2179. $searchParts = array();
  2180. $smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
  2181. for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
  2182. {
  2183. $specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
  2184. $smileyCode = '<img src="' . $smileys_path . $smileysto[$i] . '" alt="' . strtr($specialChars, array(':' => '&#58;', '(' => '&#40;', ')' => '&#41;', '$' => '&#36;', '[' => '&#091;')). '" title="' . strtr($smcFunc['htmlspecialchars']($smileysdescs[$i]), array(':' => '&#58;', '(' => '&#40;', ')' => '&#41;', '$' => '&#36;', '[' => '&#091;')) . '" class="smiley">';
  2185. $smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
  2186. $searchParts[] = preg_quote($smileysfrom[$i], '~');
  2187. if ($smileysfrom[$i] != $specialChars)
  2188. {
  2189. $smileyPregReplacements[$specialChars] = $smileyCode;
  2190. $searchParts[] = preg_quote($specialChars, '~');
  2191. }
  2192. }
  2193. $smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|^)(' . implode('|', $searchParts) . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
  2194. }
  2195. // Replace away!
  2196. /*
  2197. * TODO: When SMF supports only PHP 5.3+, we can change this to "uses" keyword and simpifly this.
  2198. */
  2199. $callback = pregReplaceCurry('smielyPregReplaceCallback', 2);
  2200. $message = preg_replace_callback($smileyPregSearch, $callback($smileyPregReplacements), $message);
  2201. }
  2202. /**
  2203. * Preg Replacment Curry.
  2204. *
  2205. * This allows use to do delayed argument binding and bring in
  2206. * the replacement variables for some preg replacments.
  2207. *
  2208. * Original code from: http://php.net/manual/en/function.preg-replace-callback.php#88013
  2209. * This is needed until SMF only supports PHP 5.3+ and we change to "use"
  2210. *
  2211. * @param string $func
  2212. * @param string $arity
  2213. * @return function a lambda function bound to $func.
  2214. */
  2215. function pregReplaceCurry($func, $arity)
  2216. {
  2217. return create_function('', "
  2218. \$args = func_get_args();
  2219. if(count(\$args) >= $arity)
  2220. return call_user_func_array('$func', \$args);
  2221. \$args = var_export(\$args, 1);
  2222. return create_function('','
  2223. \$a = func_get_args();
  2224. \$z = ' . \$args . ';
  2225. \$a = array_merge(\$z,\$a);
  2226. return call_user_func_array(\'$func\', \$a);
  2227. ');
  2228. ");
  2229. }
  2230. /**
  2231. * Smiely Replacment Callback.
  2232. *
  2233. * Our callback that does the actual smiley replacments.
  2234. *
  2235. * Original code from: http://php.net/manual/en/function.preg-replace-callback.php#88013
  2236. * This is needed until SMF only supports PHP 5.3+ and we change to "use"
  2237. *
  2238. * @param string $replacements
  2239. * @param string $matches
  2240. * @return string the replaced results.
  2241. */
  2242. function smielyPregReplaceCallback($replacements, $matches)
  2243. {
  2244. return $replacements[$matches[1]];
  2245. }
  2246. /**
  2247. * Highlight any code.
  2248. *
  2249. * Uses PHP's highlight_string() to highlight PHP syntax
  2250. * does special handling to keep the tabs in the code available.
  2251. * used to parse PHP code from inside [code] and [php] tags.
  2252. *
  2253. * @param string $code
  2254. * @return string the code with highlighted HTML.
  2255. */
  2256. function highlight_php_code($code)
  2257. {
  2258. global $context;
  2259. // Remove special characters.
  2260. $code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
  2261. $oldlevel = error_reporting(0);
  2262. $buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
  2263. error_reporting($oldlevel);
  2264. // Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
  2265. $buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
  2266. return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
  2267. }
  2268. /**
  2269. * Make sure the browser doesn't come back and repost the form data.
  2270. * Should be used whenever anything is posted.
  2271. *
  2272. * @param string $setLocation = ''
  2273. * @param bool $refresh = false
  2274. */
  2275. function redirectexit($setLocation = '', $refresh = false)
  2276. {
  2277. global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
  2278. // In case we have mail to send, better do that - as obExit doesn't always quite make it...
  2279. if (!empty($context['flush_mail']))
  2280. // @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
  2281. AddMailQueue(true);
  2282. $add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
  2283. if (WIRELESS)
  2284. {
  2285. // Add the scripturl on if needed.
  2286. if ($add)
  2287. $setLocation = $scripturl . '?' . $setLocation;
  2288. $char = strpos($setLocation, '?') === false ? '?' : ';';
  2289. if (strpos($setLocation, '#') !== false)
  2290. $setLocation = strtr($setLocation, array('#' => $char . WIRELESS_PROTOCOL . '#'));
  2291. else
  2292. $setLocation .= $char . WIRELESS_PROTOCOL;
  2293. }
  2294. elseif ($add)
  2295. $setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
  2296. // Put the session ID in.
  2297. if (defined('SID') && SID != '')
  2298. $setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
  2299. // Keep that debug in their for template debugging!
  2300. elseif (isset($_GET['debug']))
  2301. $setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
  2302. if (!empty($modSettings['queryless_urls']) && (empty($context['server']['is_cgi']) || ini_get('cgi.fix_pathinfo') == 1 || @get_cfg_var('cgi.fix_pathinfo') == 1) && (!empty($context['server']['is_apache']) || !empty($context['server']['is_lighttpd']) || !empty($context['server']['is_litespeed'])))
  2303. {
  2304. if (defined('SID') && SID != '')
  2305. $setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~', create_function('$m', 'global $scripturl; return $scripturl . \'/\' . strtr("$m[1]", \'&;=\', \'//,\') . \'.html?\' . SID. (isset($m[2]) ? "$m[2]" : "");'), $setLocation);
  2306. else
  2307. $setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~', create_function('$m', 'global $scripturl; return $scripturl . \'/\' . strtr("$m[1]", \'&;=\', \'//,\') . \'.html\' . (isset($m[2]) ? "$m[2]" : "");'), $setLocation);
  2308. }
  2309. // Maybe integrations want to change where we are heading?
  2310. call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh));
  2311. // We send a Refresh header only in special cases because Location looks better. (and is quicker...)
  2312. if ($refresh && !WIRELESS)
  2313. header('Refresh: 0; URL=' . strtr($setLocation, array(' ' => '%20')));
  2314. else
  2315. header('Location: ' . str_replace(' ', '%20', $setLocation));
  2316. // Debugging.
  2317. if (isset($db_show_debug) && $db_show_debug === true)
  2318. $_SESSION['debug_redirect'] = $db_cache;
  2319. obExit(false);
  2320. }
  2321. /**
  2322. * Ends execution. Takes care of template loading and remembering the previous URL.
  2323. * @param bool $header = null
  2324. * @param bool $do_footer = null
  2325. * @param bool $from_index = false
  2326. * @param bool $from_fatal_error = false
  2327. */
  2328. function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
  2329. {
  2330. global $context, $settings, $modSettings, $txt, $smcFunc;
  2331. static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
  2332. // Attempt to prevent a recursive loop.
  2333. ++$level;
  2334. if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
  2335. exit;
  2336. if ($from_fatal_error)
  2337. $has_fatal_error = true;
  2338. // Clear out the stat cache.
  2339. trackStats();
  2340. // If we have mail to send, send it.
  2341. if (!empty($context['flush_mail']))
  2342. // @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
  2343. AddMailQueue(true);
  2344. $do_header = $header === null ? !$header_done : $header;
  2345. if ($do_footer === null)
  2346. $do_footer = $do_header;
  2347. // Has the template/header been done yet?
  2348. if ($do_header)
  2349. {
  2350. // Was the page title set last minute? Also update the HTML safe one.
  2351. if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
  2352. $context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
  2353. // Start up the session URL fixer.
  2354. ob_start('ob_sessrewrite');
  2355. if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
  2356. $buffers = explode(',', $settings['output_buffers']);
  2357. elseif (!empty($settings['output_buffers']))
  2358. $buffers = $settings['output_buffers'];
  2359. else
  2360. $buffers = array();
  2361. if (isset($modSettings['integrate_buffer']))
  2362. $buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
  2363. if (!empty($buffers))
  2364. foreach ($buffers as $function)
  2365. {
  2366. $function = trim($function);
  2367. $call = strpos($function, '::') !== false ? explode('::', $function) : $function;
  2368. // Is it valid?
  2369. if (is_callable($call))
  2370. ob_start($call);
  2371. }
  2372. // Display the screen in the logical order.
  2373. template_header();
  2374. $header_done = true;
  2375. }
  2376. if ($do_footer)
  2377. {
  2378. if (WIRELESS && !isset($context['sub_template']))
  2379. fatal_lang_error('wireless_error_notyet', false);
  2380. // Just show the footer, then.
  2381. loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
  2382. // Anything special to put out?
  2383. if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
  2384. echo $context['insert_after_template'];
  2385. // Just so we don't get caught in an endless loop of errors from the footer...
  2386. if (!$footer_done)
  2387. {
  2388. $footer_done = true;
  2389. template_footer();
  2390. // (since this is just debugging... it's okay that it's after </html>.)
  2391. if (!isset($_REQUEST['xml']))
  2392. displayDebug();
  2393. }
  2394. }
  2395. // Remember this URL in case someone doesn't like sending HTTP_REFERER.
  2396. if (strpos($_SERVER['REQUEST_URL'], 'action=dlattach') === false && strpos($_SERVER['REQUEST_URL'], 'action=viewsmfile') === false)
  2397. $_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
  2398. // For session check verification.... don't switch browsers...
  2399. $_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
  2400. // Hand off the output to the portal, etc. we're integrated with.
  2401. call_integration_hook('integrate_exit', array($do_footer && !WIRELESS));
  2402. // Don't exit if we're coming from index.php; that will pass through normally.
  2403. if (!$from_index || WIRELESS)
  2404. exit;
  2405. }
  2406. /**
  2407. * Get the size of a specified image with better error handling.
  2408. * @todo see if it's better in Subs-Graphics, but one step at the time.
  2409. * Uses getimagesize() to determine the size of a file.
  2410. * Attempts to connect to the server first so it won't time out.
  2411. *
  2412. * @param string $url
  2413. * @return array or false, the image size as array (width, height), or false on failure
  2414. */
  2415. function url_image_size($url)
  2416. {
  2417. global $sourcedir;
  2418. // Make sure it is a proper URL.
  2419. $url = str_replace(' ', '%20', $url);
  2420. // Can we pull this from the cache... please please?
  2421. if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
  2422. return $temp;
  2423. $t = microtime();
  2424. // Get the host to pester...
  2425. preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
  2426. // Can't figure it out, just try the image size.
  2427. if ($url == '' || $url == 'http://' || $url == 'https://')
  2428. {
  2429. return false;
  2430. }
  2431. elseif (!isset($match[1]))
  2432. {
  2433. $size = @getimagesize($url);
  2434. }
  2435. else
  2436. {
  2437. // Try to connect to the server... give it half a second.
  2438. $temp = 0;
  2439. $fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
  2440. // Successful? Continue...
  2441. if ($fp != false)
  2442. {
  2443. // Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
  2444. fwrite($fp, 'HEAD /' . $match[2] . ' HTTP/1.1' . "\r\n" . 'Host: ' . $match[1] . "\r\n" . 'User-Agent: PHP/SMF' . "\r\n" . 'Connection: close' . "\r\n\r\n");
  2445. // Read in the HTTP/1.1 or whatever.
  2446. $test = substr(fgets($fp, 11), -1);
  2447. fclose($fp);
  2448. // See if it returned a 404/403 or something.
  2449. if ($test < 4)
  2450. {
  2451. $size = @getimagesize($url);
  2452. // This probably means allow_url_fopen is off, let's try GD.
  2453. if ($size === false && function_exists('imagecreatefromstring'))
  2454. {
  2455. include_once($sourcedir . '/Subs-Package.php');
  2456. // It's going to hate us for doing this, but another request...
  2457. $image = @imagecreatefromstring(fetch_web_data($url));
  2458. if ($image !== false)
  2459. {
  2460. $size = array(imagesx($image), imagesy($image));
  2461. imagedestroy($image);
  2462. }
  2463. }
  2464. }
  2465. }
  2466. }
  2467. // If we didn't get it, we failed.
  2468. if (!isset($size))
  2469. $size = false;
  2470. // If this took a long time, we may never have to do it again, but then again we might...
  2471. if (array_sum(explode(' ', microtime())) - array_sum(explode(' ', $t)) > 0.8)
  2472. cache_put_data('url_image_size-' . md5($url), $size, 240);
  2473. // Didn't work.
  2474. return $size;
  2475. }
  2476. /**
  2477. * Sets the class of the current topic based on, normal, sticky, locked, etc
  2478. *
  2479. * @param array &$topic_context
  2480. */
  2481. function determineTopicClass(&$topic_context)
  2482. {
  2483. // Set topic class depending on locked status and number of replies.
  2484. $topic_context['class'] = 'normal';
  2485. $topic_context['class'] .= $topic_context['is_poll'] ? '_poll' : '_post';
  2486. if ($topic_context['is_locked'])
  2487. $topic_context['class'] .= '_locked';
  2488. if ($topic_context['is_sticky'])
  2489. $topic_context['class'] .= '_sticky';
  2490. // This is so old themes will still work.
  2491. $topic_context['extended_class'] = &$topic_context['class'];
  2492. }
  2493. /**
  2494. * Sets up the basic theme context stuff.
  2495. * @param bool $forceload = false
  2496. */
  2497. function setupThemeContext($forceload = false)
  2498. {
  2499. global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
  2500. global $user_settings, $smcFunc;
  2501. static $loaded = false;
  2502. // Under SSI this function can be called more then once. That can cause some problems.
  2503. // So only run the function once unless we are forced to run it again.
  2504. if ($loaded && !$forceload)
  2505. return;
  2506. $loaded = true;
  2507. $context['in_maintenance'] = !empty($maintenance);
  2508. $context['current_time'] = timeformat(time(), false);
  2509. $context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
  2510. // Get some news...
  2511. $context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
  2512. for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
  2513. {
  2514. if (trim($context['news_lines'][$i]) == '')
  2515. continue;
  2516. // Clean it up for presentation ;).
  2517. $context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
  2518. }
  2519. if (!empty($context['news_lines']))
  2520. $context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
  2521. if (!$user_info['is_guest'])
  2522. {
  2523. $context['user']['messages'] = &$user_info['messages'];
  2524. $context['user']['unread_messages'] = &$user_info['unread_messages'];
  2525. $context['user']['alerts'] = &$user_info['alerts'];
  2526. // Personal message popup...
  2527. if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
  2528. $context['user']['popup_messages'] = true;
  2529. else
  2530. $context['user']['popup_messages'] = false;
  2531. $_SESSION['unread_messages'] = $user_info['unread_messages'];
  2532. if (allowedTo('moderate_forum'))
  2533. $context['unapproved_members'] = (!empty($modSettings['registration_method']) && ($modSettings['registration_method'] == 2 || (!empty($modSettings['coppaType']) && $modSettings['coppaType'] == 2))) || !empty($modSettings['approveAccountDeletion']) ? $modSettings['unapprovedMembers'] : 0;
  2534. $context['user']['avatar'] = array();
  2535. // Figure out the avatar... uploaded?
  2536. if ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
  2537. $context['user']['avatar']['href'] = $user_info['avatar']['custom_dir'] ? $modSettings['custom_avatar_url'] . '/' . $user_info['avatar']['filename'] : $scripturl . '?action=dlattach;attach=' . $user_info['avatar']['id_attach'] . ';type=avatar';
  2538. // Full URL?
  2539. elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
  2540. $context['user']['avatar']['href'] = $user_info['avatar']['url'];
  2541. // Otherwise we assume it's server stored?
  2542. elseif ($user_info['avatar']['url'] != '')
  2543. $context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
  2544. if (!empty($context['user']['avatar']))
  2545. $context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
  2546. // Figure out how long they've been logged in.
  2547. $context['user']['total_time_logged_in'] = array(
  2548. 'days' => floor($user_info['total_time_logged_in'] / 86400),
  2549. 'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
  2550. 'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
  2551. );
  2552. }
  2553. else
  2554. {
  2555. $context['user']['messages'] = 0;
  2556. $context['user']['unread_messages'] = 0;
  2557. $context['user']['avatar'] = array();
  2558. $context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
  2559. $context['user']['popup_messages'] = false;
  2560. if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
  2561. $txt['welcome_guest'] .= $txt['welcome_guest_activate'];
  2562. // If we've upgraded recently, go easy on the passwords.
  2563. if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
  2564. $context['disable_login_hashing'] = true;
  2565. }
  2566. // Setup the main menu items.
  2567. setupMenuContext();
  2568. // This is here because old index templates might still use it.
  2569. $context['show_news'] = !empty($settings['enable_news']);
  2570. // This is done to allow theme authors to customize it as they want.
  2571. $context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
  2572. // 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
  2573. if ($context['show_pm_popup'])
  2574. addInlineJavascript('
  2575. jQuery(document).ready(function($) {
  2576. new smc_Popup({
  2577. heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
  2578. content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
  2579. icon_class: \'generic_icons mail_new\'
  2580. });
  2581. });');
  2582. // Now add the capping code for avatars.
  2583. if (!empty($modSettings['avatar_max_width_external']) && !empty($modSettings['avatar_max_height_external']) && !empty($modSettings['avatar_action_too_large']) && $modSettings['avatar_action_too_large'] == 'option_css_resize')
  2584. {
  2585. if (!isset($context['css_header']))
  2586. $context['css_header'] = '';
  2587. $context['css_header'] .= '
  2588. img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }';
  2589. }
  2590. // This looks weird, but it's because BoardIndex.php references the variable.
  2591. $context['common_stats']['latest_member'] = array(
  2592. 'id' => $modSettings['latestMember'],
  2593. 'name' => $modSettings['latestRealName'],
  2594. 'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
  2595. 'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
  2596. );
  2597. $context['common_stats'] = array(
  2598. 'total_posts' => comma_format($modSettings['totalMessages']),
  2599. 'total_topics' => comma_format($modSettings['totalTopics']),
  2600. 'total_members' => comma_format($modSettings['totalMembers']),
  2601. 'latest_member' => $context['common_stats']['latest_member'],
  2602. );
  2603. $context['common_stats']['boardindex_total_posts'] = sprintf($txt['boardindex_total_posts'], $context['common_stats']['total_posts'], $context['common_stats']['total_topics'], $context['common_stats']['total_members']);
  2604. if (empty($settings['theme_version']))
  2605. addJavascriptVar('smf_scripturl', $scripturl);
  2606. if (!isset($context['page_title']))
  2607. $context['page_title'] = '';
  2608. // Set some specific vars.
  2609. $context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
  2610. $context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
  2611. }
  2612. /**
  2613. * Helper function to set the system memory to a needed value
  2614. * - If the needed memory is greater than current, will attempt to get more
  2615. * - if in_use is set to true, will also try to take the current memory usage in to account
  2616. *
  2617. * @param string $needed The amount of memory to request, if needed, like 256M
  2618. * @param bool $in_use Set to true to account for current memory usage of the script
  2619. * @return boolean, true if we have at least the needed memory
  2620. */
  2621. function setMemoryLimit($needed, $in_use = false)
  2622. {
  2623. // everything in bytes
  2624. $memory_used = 0;
  2625. $memory_current = memoryReturnBytes(ini_get('memory_limit'));
  2626. $memory_needed = memoryReturnBytes($needed);
  2627. // should we account for how much is currently being used?
  2628. if ($in_use)
  2629. $memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
  2630. // if more is needed, request it
  2631. if ($memory_current < $memory_needed)
  2632. {
  2633. @ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
  2634. $memory_current = memoryReturnBytes(ini_get('memory_limit'));
  2635. }
  2636. $memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
  2637. // return success or not
  2638. return (bool) ($memory_current >= $memory_needed);
  2639. }
  2640. /**
  2641. * Helper function to convert memory string settings to bytes
  2642. *
  2643. * @param string $val The byte string, like 256M or 1G
  2644. * @return integer The string converted to a proper integer in bytes
  2645. */
  2646. function memoryReturnBytes($val)
  2647. {
  2648. if (is_integer($val))
  2649. return $val;
  2650. // Separate the number from the designator
  2651. $val = trim($val);
  2652. $num = intval(substr($val, 0, strlen($val) - 1));
  2653. $last = strtolower(substr($val, -1));
  2654. // convert to bytes
  2655. switch ($last)
  2656. {
  2657. case 'g':
  2658. $num *= 1024;
  2659. case 'm':
  2660. $num *= 1024;
  2661. case 'k':
  2662. $num *= 1024;
  2663. }
  2664. return $num;
  2665. }
  2666. /**
  2667. * This is the only template included in the sources.
  2668. */
  2669. function template_rawdata()
  2670. {
  2671. global $context;
  2672. echo $context['raw_data'];
  2673. }
  2674. /**
  2675. * The header template
  2676. */
  2677. function template_header()
  2678. {
  2679. global $txt, $modSettings, $context, $settings, $user_info, $boarddir, $cachedir;
  2680. setupThemeContext();
  2681. // Print stuff to prevent caching of pages (except on attachment errors, etc.)
  2682. if (empty($context['no_last_modified']))
  2683. {
  2684. header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
  2685. header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
  2686. // Are we debugging the template/html content?
  2687. if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie') && !WIRELESS)
  2688. header('Content-Type: application/xhtml+xml');
  2689. elseif (!isset($_REQUEST['xml']) && !WIRELESS)
  2690. header('Content-Type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
  2691. }
  2692. if (!WIRELESS || WIRELESS_PROTOCOL != 'wap')
  2693. header('Content-Type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
  2694. // We need to splice this in after the body layer, or after the main layer for older stuff.
  2695. if ($context['in_maintenance'] && $context['user']['is_admin'])
  2696. {
  2697. $position = array_search('body', $context['template_layers']);
  2698. if ($position === false)
  2699. $position = array_search('main', $context['template_layers']);
  2700. if ($position !== false)
  2701. {
  2702. $before = array_slice($context['template_layers'], 0, $position + 1);
  2703. $after = array_slice($context['template_layers'], $position + 1);
  2704. $context['template_layers'] = array_merge($before, array('maint_warning'), $after);
  2705. }
  2706. }
  2707. $checked_securityFiles = false;
  2708. $showed_banned = false;
  2709. foreach ($context['template_layers'] as $layer)
  2710. {
  2711. loadSubTemplate($layer . '_above', true);
  2712. // May seem contrived, but this is done in case the body and main layer aren't there...
  2713. if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
  2714. {
  2715. $checked_securityFiles = true;
  2716. $securityFiles = array('install.php', 'webinstall.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
  2717. // Add your own files.
  2718. call_integration_hook('integrate_security_files', array(&$securityFiles));
  2719. foreach ($securityFiles as $i => $securityFile)
  2720. {
  2721. if (!file_exists($boarddir . '/' . $securityFile))
  2722. unset($securityFiles[$i]);
  2723. }
  2724. // We are already checking so many files...just few more doesn't make any difference! :P
  2725. if (!empty($modSettings['currentAttachmentUploadDir']))
  2726. {
  2727. if (!is_array($modSettings['attachmentUploadDir']))
  2728. $modSettings['attachmentUploadDir'] = @unserialize($modSettings['attachmentUploadDir']);
  2729. $path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
  2730. }
  2731. else
  2732. {
  2733. $path = $modSettings['attachmentUploadDir'];
  2734. $id_folder_thumb = 1;
  2735. }
  2736. secureDirectory($path, true);
  2737. secureDirectory($cachedir);
  2738. // If agreement is enabled, at least the english version shall exists
  2739. if ($modSettings['requireAgreement'])
  2740. $agreement = !file_exists($boarddir . '/agreement.txt');
  2741. if (!empty($securityFiles) || (!empty($modSettings['cache_enable']) && !is_writable($cachedir)) || !empty($agreement))
  2742. {
  2743. echo '
  2744. <div class="errorbox">
  2745. <p class="alert">!!</p>
  2746. <h3>', empty($securityFiles) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
  2747. <p>';
  2748. foreach ($securityFiles as $securityFile)
  2749. {
  2750. echo '
  2751. ', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
  2752. if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
  2753. echo '
  2754. ', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
  2755. }
  2756. if (!empty($modSettings['cache_enable']) && !is_writable($cachedir))
  2757. echo '
  2758. <strong>', $txt['cache_writable'], '</strong><br>';
  2759. if (!empty($agreement))
  2760. echo '
  2761. <strong>', $txt['agreement_missing'], '</strong><br>';
  2762. echo '
  2763. </p>
  2764. </div>';
  2765. }
  2766. }
  2767. // If the user is banned from posting inform them of it.
  2768. elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
  2769. {
  2770. $showed_banned = true;
  2771. echo '
  2772. <div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
  2773. ', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
  2774. if (!empty($_SESSION['ban']['cannot_post']['reason']))
  2775. echo '
  2776. <div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
  2777. if (!empty($_SESSION['ban']['expire_time']))
  2778. echo '
  2779. <div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
  2780. else
  2781. echo '
  2782. <div>', $txt['your_ban_expires_never'], '</div>';
  2783. echo '
  2784. </div>';
  2785. }
  2786. }
  2787. if (isset($settings['use_default_images']) && $settings['use_default_images'] == 'defaults' && isset($settings['default_template']))
  2788. {
  2789. $settings['theme_url'] = $settings['default_theme_url'];
  2790. $settings['images_url'] = $settings['default_images_url'];
  2791. $settings['theme_dir'] = $settings['default_theme_dir'];
  2792. }
  2793. }
  2794. /**
  2795. * Show the copyright.
  2796. */
  2797. function theme_copyright()
  2798. {
  2799. global $forum_copyright, $software_year, $forum_version;
  2800. // Don't display copyright for things like SSI.
  2801. if (!isset($forum_version) || !isset($software_year))
  2802. return;
  2803. // Put in the version...
  2804. $forum_copyright = sprintf($forum_copyright, $forum_version, $software_year);
  2805. echo '
  2806. <span class="smalltext" style="display: inline; visibility: visible; font-family: Verdana, Arial, sans-serif;">' . $forum_copyright . '
  2807. </span>';
  2808. }
  2809. /**
  2810. * The template footer
  2811. */
  2812. function template_footer()
  2813. {
  2814. global $context, $settings, $modSettings, $time_start, $db_count;
  2815. // Show the load time? (only makes sense for the footer.)
  2816. $context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
  2817. $context['load_time'] = comma_format(round(array_sum(explode(' ', microtime())) - array_sum(explode(' ', $time_start)), 3));
  2818. $context['load_queries'] = $db_count;
  2819. if (isset($settings['use_default_images']) && $settings['use_default_images'] == 'defaults' && isset($settings['default_template']))
  2820. {
  2821. $settings['theme_url'] = $settings['actual_theme_url'];
  2822. $settings['images_url'] = $settings['actual_images_url'];
  2823. $settings['theme_dir'] = $settings['actual_theme_dir'];
  2824. }
  2825. foreach (array_reverse($context['template_layers']) as $layer)
  2826. loadSubTemplate($layer . '_below', true);
  2827. }
  2828. /**
  2829. * Output the Javascript files
  2830. * - tabbing in this function is to make the HTML source look good proper
  2831. * - if defered is set function will output all JS (source & inline) set to load at page end
  2832. *
  2833. * @param bool $do_defered = false
  2834. */
  2835. function template_javascript($do_defered = false)
  2836. {
  2837. global $context, $modSettings, $settings;
  2838. // Use this hook to minify/optimize Javascript files and vars
  2839. call_integration_hook('integrate_pre_javascript_output');
  2840. // Ouput the declared Javascript variables.
  2841. if (!empty($context['javascript_vars']) && !$do_defered)
  2842. {
  2843. echo '
  2844. <script><!-- // --><![CDATA[';
  2845. foreach ($context['javascript_vars'] as $key => $value)
  2846. {
  2847. if (empty($value))
  2848. {
  2849. echo '
  2850. var ', $key, ';';
  2851. }
  2852. else
  2853. {
  2854. echo '
  2855. var ', $key, ' = ', $value, ';';
  2856. }
  2857. }
  2858. echo '
  2859. // ]]></script>';
  2860. }
  2861. // While we have Javascript files to place in the template
  2862. foreach ($context['javascript_files'] as $id => $js_file)
  2863. {
  2864. if ((!$do_defered && empty($js_file['options']['defer'])) || ($do_defered && !empty($js_file['options']['defer'])))
  2865. echo '
  2866. <script src="', $js_file['filename'], '"', !empty($js_file['options']['async']) ? ' async="async"' : '', '></script>';
  2867. // If we are loading JQuery and we are set to 'auto' load, put in our remote success or load local check
  2868. if ($id == 'jquery' && (!isset($modSettings['jquery_source']) || !in_array($modSettings['jquery_source'], array('local', 'cdn'))))
  2869. echo '
  2870. <script><!-- // --><![CDATA[
  2871. window.jQuery || document.write(\'<script src="' . $settings['default_theme_url'] . '/scripts/jquery-1.7.1.min.js"><\/script>\');
  2872. // ]]></script>';
  2873. }
  2874. // Inline JavaScript - Actually useful some times!
  2875. if (!empty($context['javascript_inline']))
  2876. {
  2877. if (!empty($context['javascript_inline']['defer']) && $do_defered)
  2878. {
  2879. echo '
  2880. <script><!-- // --><![CDATA[';
  2881. foreach ($context['javascript_inline']['defer'] as $js_code)
  2882. echo $js_code;
  2883. echo '
  2884. // ]]></script>';
  2885. }
  2886. if (!empty($context['javascript_inline']['standard']) && !$do_defered)
  2887. {
  2888. echo '
  2889. <script><!-- // --><![CDATA[';
  2890. foreach ($context['javascript_inline']['standard'] as $js_code)
  2891. echo $js_code;
  2892. echo '
  2893. // ]]></script>';
  2894. }
  2895. }
  2896. }
  2897. /**
  2898. * Output the CSS files
  2899. */
  2900. function template_css()
  2901. {
  2902. global $context, $db_show_debug, $boardurl;
  2903. // Use this hook to minify/optimize CSS files
  2904. call_integration_hook('integrate_pre_css_output');
  2905. foreach ($context['css_files'] as $id => $file)
  2906. echo '
  2907. <link rel="stylesheet" type="text/css" href="', $file['filename'], '">';
  2908. if ($db_show_debug === true)
  2909. {
  2910. // Try to keep only what's useful.
  2911. $repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
  2912. foreach ($context['css_files'] as $file)
  2913. $context['debug']['sheets'][] = strtr($file['filename'], $repl);
  2914. }
  2915. if (!empty($context['css_header']))
  2916. echo '
  2917. <style>', $context['css_header'], '</style>';
  2918. }
  2919. /**
  2920. * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
  2921. * @todo this currently returns the hash if new, and the full filename otherwise.
  2922. * Something messy like that.
  2923. * @todo and of course everything relies on this behavior and work around it. :P.
  2924. * Converters included.
  2925. *
  2926. * @param $filename
  2927. * @param $attachment_id
  2928. * @param $dir
  2929. * @param $new
  2930. * @param $file_hash
  2931. */
  2932. function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
  2933. {
  2934. global $modSettings, $smcFunc;
  2935. // Just make up a nice hash...
  2936. if ($new)
  2937. return sha1(md5($filename . time()) . mt_rand());
  2938. // Grab the file hash if it wasn't added.
  2939. // Left this for legacy.
  2940. if ($file_hash === '')
  2941. {
  2942. $request = $smcFunc['db_query']('', '
  2943. SELECT file_hash
  2944. FROM {db_prefix}attachments
  2945. WHERE id_attach = {int:id_attach}',
  2946. array(
  2947. 'id_attach' => $attachment_id,
  2948. ));
  2949. if ($smcFunc['db_num_rows']($request) === 0)
  2950. return false;
  2951. list ($file_hash) = $smcFunc['db_fetch_row']($request);
  2952. $smcFunc['db_free_result']($request);
  2953. }
  2954. // Still no hash? mmm...
  2955. if (empty($file_hash))
  2956. $file_hash = sha1(md5($filename . time()) . mt_rand());
  2957. // Are we using multiple directories?
  2958. if (!empty($modSettings['currentAttachmentUploadDir']))
  2959. {
  2960. if (!is_array($modSettings['attachmentUploadDir']))
  2961. $modSettings['attachmentUploadDir'] = unserialize($modSettings['attachmentUploadDir']);
  2962. $path = $modSettings['attachmentUploadDir'][$dir];
  2963. }
  2964. else
  2965. $path = $modSettings['attachmentUploadDir'];
  2966. return $path . '/' . $attachment_id . '_' . $file_hash .'.dat';
  2967. }
  2968. /**
  2969. * Convert a single IP to a ranged IP.
  2970. * internal function used to convert a user-readable format to a format suitable for the database.
  2971. *
  2972. * @param string $fullip
  2973. * @return array|string 'unknown' if the ip in the input was '255.255.255.255'
  2974. */
  2975. function ip2range($fullip)
  2976. {
  2977. // If its IPv6, validate it first.
  2978. if (isValidIPv6($fullip) !== false)
  2979. {
  2980. $ip_parts = explode(':', expandIPv6($fullip, false));
  2981. $ip_array = array();
  2982. if (count($ip_parts) != 8)
  2983. return array();
  2984. for ($i = 0; $i < 8; $i++)
  2985. {
  2986. if ($ip_parts[$i] == '*')
  2987. $ip_array[$i] = array('low' => '0', 'high' => hexdec('ffff'));
  2988. elseif (preg_match('/^([0-9A-Fa-f]{1,4})\-([0-9A-Fa-f]{1,4})$/', $ip_parts[$i], $range) == 1)
  2989. $ip_array[$i] = array('low' => hexdec($range[1]), 'high' => hexdec($range[2]));
  2990. elseif (is_numeric(hexdec($ip_parts[$i])))
  2991. $ip_array[$i] = array('low' => hexdec($ip_parts[$i]), 'high' => hexdec($ip_parts[$i]));
  2992. }
  2993. return $ip_array;
  2994. }
  2995. // Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
  2996. if ($fullip == 'unknown')
  2997. $fullip = '255.255.255.255';
  2998. $ip_parts = explode('.', $fullip);
  2999. $ip_array = array();
  3000. if (count($ip_parts) != 4)
  3001. return array();
  3002. for ($i = 0; $i < 4; $i++)
  3003. {
  3004. if ($ip_parts[$i] == '*')
  3005. $ip_array[$i] = array('low' => '0', 'high' => '255');
  3006. elseif (preg_match('/^(\d{1,3})\-(\d{1,3})$/', $ip_parts[$i], $range) == 1)
  3007. $ip_array[$i] = array('low' => $range[1], 'high' => $range[2]);
  3008. elseif (is_numeric($ip_parts[$i]))
  3009. $ip_array[$i] = array('low' => $ip_parts[$i], 'high' => $ip_parts[$i]);
  3010. }
  3011. // Makes it simpiler to work with.
  3012. $ip_array[4] = array('low' => 0, 'high' => 0);
  3013. $ip_array[5] = array('low' => 0, 'high' => 0);
  3014. $ip_array[6] = array('low' => 0, 'high' => 0);
  3015. $ip_array[7] = array('low' => 0, 'high' => 0);
  3016. return $ip_array;
  3017. }
  3018. /**
  3019. * Lookup an IP; try shell_exec first because we can do a timeout on it.
  3020. *
  3021. * @param string $ip
  3022. */
  3023. function host_from_ip($ip)
  3024. {
  3025. global $modSettings;
  3026. if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
  3027. return $host;
  3028. $t = microtime();
  3029. // Try the Linux host command, perhaps?
  3030. if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
  3031. {
  3032. if (!isset($modSettings['host_to_dis']))
  3033. $test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
  3034. else
  3035. $test = @shell_exec('host ' . @escapeshellarg($ip));
  3036. // Did host say it didn't find anything?
  3037. if (strpos($test, 'not found') !== false)
  3038. $host = '';
  3039. // Invalid server option?
  3040. elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
  3041. updateSettings(array('host_to_dis' => 1));
  3042. // Maybe it found something, after all?
  3043. elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
  3044. $host = $match[1];
  3045. }
  3046. // This is nslookup; usually only Windows, but possibly some Unix?
  3047. if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
  3048. {
  3049. $test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
  3050. if (strpos($test, 'Non-existent domain') !== false)
  3051. $host = '';
  3052. elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
  3053. $host = $match[1];
  3054. }
  3055. // This is the last try :/.
  3056. if (!isset($host) || $host === false)
  3057. $host = @gethostbyaddr($ip);
  3058. // It took a long time, so let's cache it!
  3059. if (array_sum(explode(' ', microtime())) - array_sum(explode(' ', $t)) > 0.5)
  3060. cache_put_data('hostlookup-' . $ip, $host, 600);
  3061. return $host;
  3062. }
  3063. /**
  3064. * Chops a string into words and prepares them to be inserted into (or searched from) the database.
  3065. *
  3066. * @param string $text
  3067. * @param int $max_chars = 20
  3068. * @param bool $encrypt = false
  3069. */
  3070. function text2words($text, $max_chars = 20, $encrypt = false)
  3071. {
  3072. global $smcFunc, $context;
  3073. // Step 1: Remove entities/things we don't consider words:
  3074. $words = preg_replace('~(?:[\x0B\0' . ($context['utf8'] ? '\x{A0}' : '\xA0') . '\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~' . ($context['utf8'] ? 'u' : ''), ' ', strtr($text, array('<br>' => ' ')));
  3075. // Step 2: Entities we left to letters, where applicable, lowercase.
  3076. $words = un_htmlspecialchars($smcFunc['strtolower']($words));
  3077. // Step 3: Ready to split apart and index!
  3078. $words = explode(' ', $words);
  3079. if ($encrypt)
  3080. {
  3081. $possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
  3082. $returned_ints = array();
  3083. foreach ($words as $word)
  3084. {
  3085. if (($word = trim($word, '-_\'')) !== '')
  3086. {
  3087. $encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
  3088. $total = 0;
  3089. for ($i = 0; $i < $max_chars; $i++)
  3090. $total += $possible_chars[ord($encrypted{$i})] * pow(63, $i);
  3091. $returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
  3092. }
  3093. }
  3094. return array_unique($returned_ints);
  3095. }
  3096. else
  3097. {
  3098. // Trim characters before and after and add slashes for database insertion.
  3099. $returned_words = array();
  3100. foreach ($words as $word)
  3101. if (($word = trim($word, '-_\'')) !== '')
  3102. $returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
  3103. // Filter out all words that occur more than once.
  3104. return array_unique($returned_words);
  3105. }
  3106. }
  3107. /**
  3108. * Creates an image/text button
  3109. *
  3110. * @param string $name
  3111. * @param string $alt
  3112. * @param string $label = ''
  3113. * @param boolean $custom = ''
  3114. * @param boolean $force_use = false
  3115. * @return string
  3116. */
  3117. function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
  3118. {
  3119. global $settings, $txt, $context;
  3120. // Does the current loaded theme have this and we are not forcing the usage of this function?
  3121. if (function_exists('template_create_button') && !$force_use)
  3122. return template_create_button($name, $alt, $label = '', $custom = '');
  3123. if (!$settings['use_image_buttons'])
  3124. return $txt[$alt];
  3125. elseif (!empty($settings['use_buttons']))
  3126. return '<img src="' . $settings['images_url'] . '/buttons/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
  3127. else
  3128. return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
  3129. }
  3130. /**
  3131. * Empty out the cache in use as best it can
  3132. *
  3133. * It may only remove the files of a certain type (if the $type parameter is given)
  3134. * Type can be user, data or left blank
  3135. * - user clears out user data
  3136. * - data clears out system / opcode data
  3137. * - If no type is specified will perfom a complete cache clearing
  3138. * For cache engines that do not distinguish on types, a full cache flush will be done
  3139. *
  3140. * @param string $type = ''
  3141. */
  3142. function clean_cache($type = '')
  3143. {
  3144. global $cachedir, $sourcedir, $cache_accelerator, $modSettings, $memcached;
  3145. switch ($cache_accelerator)
  3146. {
  3147. case 'memcached':
  3148. if (function_exists('memcache_flush') || function_exists('memcached_flush') && isset($modSettings['cache_memcached']) && trim($modSettings['cache_memcached']) != '')
  3149. {
  3150. // Not connected yet?
  3151. if (empty($memcached))
  3152. get_memcached_server();
  3153. if (!$memcached)
  3154. return;
  3155. // clear it out
  3156. if (function_exists('memcache_flush'))
  3157. memcache_flush($memcached);
  3158. else
  3159. memcached_flush($memcached);
  3160. }
  3161. break;
  3162. case 'apc':
  3163. if (function_exists('apc_clear_cache'))
  3164. {
  3165. // if passed a type, clear that type out
  3166. if ($type === '' || $type === 'data')
  3167. {
  3168. apc_clear_cache('user');
  3169. apc_clear_cache('system');
  3170. }
  3171. elseif ($type === 'user')
  3172. apc_clear_cache('user');
  3173. }
  3174. break;
  3175. case 'zend':
  3176. if (function_exists('zend_shm_cache_clear'))
  3177. zend_shm_cache_clear('SMF');
  3178. break;
  3179. case 'xcache':
  3180. if (function_exists('xcache_clear_cache'))
  3181. {
  3182. //
  3183. if ($type === '')
  3184. {
  3185. xcache_clear_cache(XC_TYPE_VAR, 0);
  3186. xcache_clear_cache(XC_TYPE_PHP, 0);
  3187. }
  3188. if ($type === 'user')
  3189. xcache_clear_cache(XC_TYPE_VAR, 0);
  3190. if ($type === 'data')
  3191. xcache_clear_cache(XC_TYPE_PHP, 0);
  3192. }
  3193. break;
  3194. default:
  3195. // No directory = no game.
  3196. if (!is_dir($cachedir))
  3197. return;
  3198. // Remove the files in SMF's own disk cache, if any
  3199. $dh = opendir($cachedir);
  3200. while ($file = readdir($dh))
  3201. {
  3202. if ($file != '.' && $file != '..' && $file != 'index.php' && $file != '.htaccess' && (!$type || substr($file, 0, strlen($type)) == $type))
  3203. @unlink($cachedir . '/' . $file);
  3204. }
  3205. closedir($dh);
  3206. break;
  3207. }
  3208. // Invalidate cache, to be sure!
  3209. // ... as long as Load.php can be modified, anyway.
  3210. @touch($sourcedir . '/' . 'Load.php');
  3211. call_integration_hook('integrate_clean_cache');
  3212. clearstatcache();
  3213. }
  3214. /**
  3215. * Sets up all of the top menu buttons
  3216. * Saves them in the cache if it is available and on
  3217. * Places the results in $context
  3218. *
  3219. */
  3220. function setupMenuContext()
  3221. {
  3222. global $context, $modSettings, $user_info, $txt, $scripturl;
  3223. // Set up the menu privileges.
  3224. $context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
  3225. $context['allow_admin'] = allowedTo(array('admin_forum', 'manage_boards', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_attachments', 'manage_smileys'));
  3226. $context['allow_memberlist'] = allowedTo('view_mlist');
  3227. $context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
  3228. $context['allow_moderation_center'] = $context['user']['can_mod'];
  3229. $context['allow_pm'] = allowedTo('pm_read');
  3230. $cacheTime = $modSettings['lastActive'] * 60;
  3231. // Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
  3232. if (!isset($context['allow_calendar_event']))
  3233. {
  3234. $context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
  3235. // If you don't allow events not linked to posts and you're not an admin, we have more work to do...
  3236. if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
  3237. {
  3238. $boards_can_post = boardsAllowedTo('post_new');
  3239. $context['allow_calendar_event'] &= !empty($boards_can_post);
  3240. }
  3241. }
  3242. // There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
  3243. if (!$context['user']['is_guest'])
  3244. {
  3245. addInlineJavascript('
  3246. var user_menus = new smc_PopupMenu();
  3247. user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
  3248. user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup");', true);
  3249. if ($context['allow_pm'])
  3250. addInlineJavascript('
  3251. user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
  3252. }
  3253. // All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
  3254. if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
  3255. {
  3256. $buttons = array(
  3257. 'home' => array(
  3258. 'title' => $txt['home'],
  3259. 'href' => $scripturl,
  3260. 'show' => true,
  3261. 'sub_buttons' => array(
  3262. ),
  3263. 'is_last' => $context['right_to_left'],
  3264. ),
  3265. 'search' => array(
  3266. 'title' => $txt['search'],
  3267. 'href' => $scripturl . '?action=search',
  3268. 'show' => $context['allow_search'],
  3269. 'sub_buttons' => array(
  3270. ),
  3271. ),
  3272. 'admin' => array(
  3273. 'title' => $txt['admin'],
  3274. 'href' => $scripturl . '?action=admin',
  3275. 'show' => $context['allow_admin'],
  3276. 'sub_buttons' => array(
  3277. 'featuresettings' => array(
  3278. 'title' => $txt['modSettings_title'],
  3279. 'href' => $scripturl . '?action=admin;area=featuresettings',
  3280. 'show' => allowedTo('admin_forum'),
  3281. ),
  3282. 'packages' => array(
  3283. 'title' => $txt['package'],
  3284. 'href' => $scripturl . '?action=admin;area=packages',
  3285. 'show' => allowedTo('admin_forum'),
  3286. ),
  3287. 'errorlog' => array(
  3288. 'title' => $txt['errlog'],
  3289. 'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
  3290. 'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
  3291. ),
  3292. 'permissions' => array(
  3293. 'title' => $txt['edit_permissions'],
  3294. 'href' => $scripturl . '?action=admin;area=permissions',
  3295. 'show' => allowedTo('manage_permissions'),
  3296. ),
  3297. 'memberapprove' => array(
  3298. 'title' => $txt['approve_members_waiting'],
  3299. 'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
  3300. 'show' => !empty($context['unapproved_members']),
  3301. 'is_last' => true,
  3302. ),
  3303. ),
  3304. ),
  3305. 'moderate' => array(
  3306. 'title' => $txt['moderate'],
  3307. 'href' => $scripturl . '?action=moderate',
  3308. 'show' => $context['allow_moderation_center'],
  3309. 'sub_buttons' => array(
  3310. 'modlog' => array(
  3311. 'title' => $txt['modlog_view'],
  3312. 'href' => $scripturl . '?action=moderate;area=modlog',
  3313. 'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
  3314. ),
  3315. 'poststopics' => array(
  3316. 'title' => $txt['mc_unapproved_poststopics'],
  3317. 'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
  3318. 'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
  3319. ),
  3320. 'attachments' => array(
  3321. 'title' => $txt['mc_unapproved_attachments'],
  3322. 'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
  3323. 'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
  3324. ),
  3325. 'reports' => array(
  3326. 'title' => $txt['mc_reported_posts'],
  3327. 'href' => $scripturl . '?action=moderate;area=reportedposts',
  3328. 'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
  3329. ),
  3330. 'reported_members' => array(
  3331. 'title' => $txt['mc_reported_members'],
  3332. 'href' => $scripturl . '?action=moderate;area=reportedmembers',
  3333. 'show' => allowedTo('moderate_forum'),
  3334. 'is_last' => true,
  3335. )
  3336. ),
  3337. ),
  3338. 'calendar' => array(
  3339. 'title' => $txt['calendar'],
  3340. 'href' => $scripturl . '?action=calendar',
  3341. 'show' => $context['allow_calendar'],
  3342. 'sub_buttons' => array(
  3343. 'view' => array(
  3344. 'title' => $txt['calendar_menu'],
  3345. 'href' => $scripturl . '?action=calendar',
  3346. 'show' => $context['allow_calendar_event'],
  3347. ),
  3348. 'post' => array(
  3349. 'title' => $txt['calendar_post_event'],
  3350. 'href' => $scripturl . '?action=calendar;sa=post',
  3351. 'show' => $context['allow_calendar_event'],
  3352. 'is_last' => true,
  3353. ),
  3354. ),
  3355. ),
  3356. 'mlist' => array(
  3357. 'title' => $txt['members_title'],
  3358. 'href' => $scripturl . '?action=mlist',
  3359. 'show' => $context['allow_memberlist'],
  3360. 'sub_buttons' => array(
  3361. 'mlist_view' => array(
  3362. 'title' => $txt['mlist_menu_view'],
  3363. 'href' => $scripturl . '?action=mlist',
  3364. 'show' => true,
  3365. ),
  3366. 'mlist_search' => array(
  3367. 'title' => $txt['mlist_search'],
  3368. 'href' => $scripturl . '?action=mlist;sa=search',
  3369. 'show' => true,
  3370. 'is_last' => true,
  3371. ),
  3372. ),
  3373. ),
  3374. 'register' => array(
  3375. 'title' => $txt['register'],
  3376. 'href' => $scripturl . '?action=register',
  3377. 'show' => $user_info['is_guest'] && $context['can_register'],
  3378. 'sub_buttons' => array(
  3379. ),
  3380. 'is_last' => !$context['right_to_left'],
  3381. ),
  3382. 'logout' => array(
  3383. 'title' => $txt['logout'],
  3384. 'href' => $scripturl . '?action=logout;%1$s=%2$s',
  3385. 'show' => !$user_info['is_guest'],
  3386. 'sub_buttons' => array(
  3387. ),
  3388. 'is_last' => !$context['right_to_left'],
  3389. ),
  3390. );
  3391. // Allow editing menu buttons easily.
  3392. call_integration_hook('integrate_menu_buttons', array(&$buttons));
  3393. // Now we put the buttons in the context so the theme can use them.
  3394. $menu_buttons = array();
  3395. foreach ($buttons as $act => $button)
  3396. if (!empty($button['show']))
  3397. {
  3398. $button['active_button'] = false;
  3399. // This button needs some action.
  3400. if (isset($button['action_hook']))
  3401. $needs_action_hook = true;
  3402. // Make sure the last button truely is the last button.
  3403. if (!empty($button['is_last']))
  3404. {
  3405. if (isset($last_button))
  3406. unset($menu_buttons[$last_button]['is_last']);
  3407. $last_button = $act;
  3408. }
  3409. // Go through the sub buttons if there are any.
  3410. if (!empty($button['sub_buttons']))
  3411. foreach ($button['sub_buttons'] as $key => $subbutton)
  3412. {
  3413. if (empty($subbutton['show']))
  3414. unset($button['sub_buttons'][$key]);
  3415. // 2nd level sub buttons next...
  3416. if (!empty($subbutton['sub_buttons']))
  3417. {
  3418. foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
  3419. {
  3420. if (empty($sub_button2['show']))
  3421. unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
  3422. }
  3423. }
  3424. }
  3425. $menu_buttons[$act] = $button;
  3426. }
  3427. if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
  3428. cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
  3429. }
  3430. $context['menu_buttons'] = $menu_buttons;
  3431. // Logging out requires the session id in the url.
  3432. if (isset($context['menu_buttons']['logout']))
  3433. $context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
  3434. // Figure out which action we are doing so we can set the active tab.
  3435. // Default to home.
  3436. $current_action = 'home';
  3437. if (isset($context['menu_buttons'][$context['current_action']]))
  3438. $current_action = $context['current_action'];
  3439. elseif ($context['current_action'] == 'search2')
  3440. $current_action = 'search';
  3441. elseif ($context['current_action'] == 'theme')
  3442. $current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
  3443. elseif ($context['current_action'] == 'register2')
  3444. $current_action = 'register';
  3445. elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
  3446. $current_action = 'login';
  3447. elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
  3448. $current_action = 'moderate';
  3449. // There are certain exceptions to the above where we don't want anything on the menu highlighted.
  3450. if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
  3451. {
  3452. $current_action = !empty($_GET['area']) && $_GET['area'] == 'alerts_popup' ? 'self_alerts' : 'self_profile';
  3453. $context[$current_action] = true;
  3454. }
  3455. elseif ($context['current_action'] == 'pm')
  3456. {
  3457. $current_action = 'self_pm';
  3458. $context['self_pm'] = true;
  3459. }
  3460. // Not all actions are simple.
  3461. if (!empty($needs_action_hook))
  3462. call_integration_hook('integrate_current_action', array(&$current_action));
  3463. if (isset($context['menu_buttons'][$current_action]))
  3464. $context['menu_buttons'][$current_action]['active_button'] = true;
  3465. $total_mod_reports = 0;
  3466. if (!empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1' && !empty($context['open_mod_reports']))
  3467. {
  3468. $total_mod_reports = $context['open_mod_reports'];
  3469. $context['menu_buttons']['moderate']['sub_buttons']['reports']['title'] .= ' <span class="amt">' . $context['open_mod_reports'] . '</span>';
  3470. }
  3471. /**
  3472. * @todo For some reason, $context['open_member_reports'] isn't getting set
  3473. */
  3474. if (allowedTo('moderate_forum') && !empty($context['open_member_reports']))
  3475. {
  3476. $total_mod_reports += $context['open_member_reports'];
  3477. $context['menu_buttons']['moderate']['sub_buttons']['reported_members']['title'] .= ' <span class="amt">' . $context['open_member_reports'] . '</span>';
  3478. }
  3479. if (!empty($context['unapproved_members']))
  3480. {
  3481. $context['menu_buttons']['admin']['sub_buttons']['memberapprove']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
  3482. $context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
  3483. }
  3484. // Do we have any open reports?
  3485. if ($total_mod_reports > 0)
  3486. {
  3487. $context['menu_buttons']['moderate']['title'] .= ' <span class="amt">' . $total_mod_reports . '</span>';
  3488. }
  3489. }
  3490. /**
  3491. * Generate a random seed and ensure it's stored in settings.
  3492. */
  3493. function smf_seed_generator()
  3494. {
  3495. global $modSettings;
  3496. // Never existed?
  3497. if (empty($modSettings['rand_seed']))
  3498. {
  3499. $modSettings['rand_seed'] = microtime() * 1000000;
  3500. updateSettings(array('rand_seed' => $modSettings['rand_seed']));
  3501. }
  3502. // Change the seed.
  3503. updateSettings(array('rand_seed' => mt_rand()));
  3504. }
  3505. /**
  3506. * Process functions of an integration hook.
  3507. * calls all functions of the given hook.
  3508. * supports static class method calls.
  3509. *
  3510. * @param string $hook
  3511. * @param array $parameters = array()
  3512. * @return array the results of the functions
  3513. */
  3514. function call_integration_hook($hook, $parameters = array())
  3515. {
  3516. global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
  3517. global $context, $txt;
  3518. if ($db_show_debug === true)
  3519. $context['debug']['hooks'][] = $hook;
  3520. // Need to have some control.
  3521. if (!isset($context['instances']))
  3522. $context['instances'] = array();
  3523. $results = array();
  3524. if (empty($modSettings[$hook]))
  3525. return $results;
  3526. $functions = explode(',', $modSettings[$hook]);
  3527. // Loop through each function.
  3528. foreach ($functions as $function)
  3529. {
  3530. $function = trim($function);
  3531. // Found a call to a method.
  3532. if (strpos($function, '::') !== false)
  3533. {
  3534. $call = explode('::', $function);
  3535. // Get the file and the class::method.
  3536. if (strpos($call[1], ':') !== false)
  3537. {
  3538. list($func, $file) = explode(':', $call[1]);
  3539. // Need to temp delete the #
  3540. if (strpos($file, '#') !== false)
  3541. $file = str_replace('#', '', $file);
  3542. // Match the wildcards to their regular vars.
  3543. if (empty($settings['theme_dir']))
  3544. $absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
  3545. else
  3546. $absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
  3547. // Load the file if it can be loaded.
  3548. if (file_exists($absPath))
  3549. require_once($absPath);
  3550. // No? tell the admin about it.
  3551. else
  3552. {
  3553. loadLanguage('Errors');
  3554. log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
  3555. }
  3556. // Check if a new object will be created.
  3557. if (strpos($call[1], '#') !== false)
  3558. {
  3559. // Don't need to create a new instance for every method.
  3560. if (empty($context['instances'][$call[0]]) || !($context['instances'][$call[0]] instanceof $call[0]))
  3561. {
  3562. $context['instances'][$call[0]] = new $call[0];
  3563. // Add another one to the list.
  3564. if ($db_show_debug === true)
  3565. $context['debug']['instances'][$call[0]] = $hook;
  3566. }
  3567. $call = array($context['instances'][$call[0]], $func);
  3568. }
  3569. // Right then, this is a call to a static method.
  3570. else
  3571. $call = array($call[0], $func);
  3572. }
  3573. }
  3574. else
  3575. {
  3576. $call = $function;
  3577. if (strpos($function, ':') !== false)
  3578. {
  3579. list($func, $file) = explode(':', $function);
  3580. if (empty($settings['theme_dir']))
  3581. $absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
  3582. else
  3583. $absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
  3584. if (file_exists($absPath))
  3585. require_once($absPath);
  3586. $call = $func;
  3587. }
  3588. }
  3589. // Is it valid?
  3590. if (is_callable($call))
  3591. $results[$function] = call_user_func_array($call, $parameters);
  3592. // Whatever it was suppose to call, it failed :(
  3593. elseif (!empty($func) && !empty($absPath))
  3594. {
  3595. loadLanguage('Errors');
  3596. log_error(sprintf($txt['hook_fail_call_to'], $func, $absPath), 'general');
  3597. }
  3598. }
  3599. return $results;
  3600. }
  3601. /**
  3602. * Add a function for integration hook.
  3603. * does nothing if the function is already added.
  3604. *
  3605. * @param string $hook The complete hook name.
  3606. * @param string $function Function name, can be a call to a method via Class::method.
  3607. * @param string $file Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
  3608. * @param bool $object Boolean Indicates if your class will be instantiated when its respective hook is called, your function must be a method.
  3609. * @param bool $permanent = true if true, updates the value in settings table.
  3610. */
  3611. function add_integration_function($hook, $function, $file = '', $object = false, $permanent = true)
  3612. {
  3613. global $smcFunc, $modSettings;
  3614. $integration_call = (!empty($file) && $file !== true) ? ($function . ':' . $file . ($object ? '#' : '')) : $function;
  3615. // Is it going to be permanent?
  3616. if ($permanent)
  3617. {
  3618. $request = $smcFunc['db_query']('', '
  3619. SELECT value
  3620. FROM {db_prefix}settings
  3621. WHERE variable = {string:variable}',
  3622. array(
  3623. 'variable' => $hook,
  3624. )
  3625. );
  3626. list($current_functions) = $smcFunc['db_fetch_row']($request);
  3627. $smcFunc['db_free_result']($request);
  3628. if (!empty($current_functions))
  3629. {
  3630. $current_functions = explode(',', $current_functions);
  3631. if (in_array($integration_call, $current_functions))
  3632. return;
  3633. $permanent_functions = array_merge($current_functions, array($integration_call));
  3634. }
  3635. else
  3636. $permanent_functions = array($integration_call);
  3637. updateSettings(array($hook => implode(',', $permanent_functions)));
  3638. }
  3639. // Make current function list usable.
  3640. $functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
  3641. // Do nothing, if it's already there.
  3642. if (in_array($integration_call, $functions))
  3643. return;
  3644. $functions[] = $integration_call;
  3645. $modSettings[$hook] = implode(',', $functions);
  3646. }
  3647. /**
  3648. * Remove an integration hook function.
  3649. * Removes the given function from the given hook.
  3650. * Does nothing if the function is not available.
  3651. *
  3652. * @param string $hook
  3653. * @param string $function
  3654. * @param string $file
  3655. */
  3656. function remove_integration_function($hook, $function, $file = '', $object = false)
  3657. {
  3658. global $smcFunc, $modSettings;
  3659. $integration_call = (!empty($file)) ? $function . ':' . $file .($object ? '#' : '') : $function;
  3660. // Get the permanent functions.
  3661. $request = $smcFunc['db_query']('', '
  3662. SELECT value
  3663. FROM {db_prefix}settings
  3664. WHERE variable = {string:variable}',
  3665. array(
  3666. 'variable' => $hook,
  3667. )
  3668. );
  3669. list($current_functions) = $smcFunc['db_fetch_row']($request);
  3670. $smcFunc['db_free_result']($request);
  3671. if (!empty($current_functions))
  3672. {
  3673. $current_functions = explode(',', $current_functions);
  3674. if (in_array($integration_call, $current_functions))
  3675. updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
  3676. }
  3677. // Turn the function list into something usable.
  3678. $functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
  3679. // You can only remove it if it's available.
  3680. if (!in_array($integration_call, $functions))
  3681. return;
  3682. $functions = array_diff($functions, array($integration_call));
  3683. $modSettings[$hook] = implode(',', $functions);
  3684. }
  3685. /**
  3686. * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
  3687. * superset of ISO 8859-1, defining several characters between DEC 128 and 159
  3688. * that are not normally displayable. This converts the popular ones that
  3689. * appear from a cut and paste from windows.
  3690. *
  3691. * @param string $string
  3692. * @return string $string
  3693. */
  3694. function sanitizeMSCutPaste($string)
  3695. {
  3696. global $context;
  3697. if (empty($string))
  3698. return $string;
  3699. // UTF-8 occurences of MS special characters
  3700. $findchars_utf8 = array(
  3701. "\xe2\80\x9a", // single low-9 quotation mark
  3702. "\xe2\80\x9e", // double low-9 quotation mark
  3703. "\xe2\80\xa6", // horizontal ellipsis
  3704. "\xe2\x80\x98", // left single curly quote
  3705. "\xe2\x80\x99", // right single curly quote
  3706. "\xe2\x80\x9c", // left double curly quote
  3707. "\xe2\x80\x9d", // right double curly quote
  3708. "\xe2\x80\x93", // en dash
  3709. "\xe2\x80\x94", // em dash
  3710. );
  3711. // windows 1252 / iso equivalents
  3712. $findchars_iso = array(
  3713. chr(130),
  3714. chr(132),
  3715. chr(133),
  3716. chr(145),
  3717. chr(146),
  3718. chr(147),
  3719. chr(148),
  3720. chr(150),
  3721. chr(151),
  3722. );
  3723. // safe replacements
  3724. $replacechars = array(
  3725. ',', // &sbquo;
  3726. ',,', // &bdquo;
  3727. '...', // &hellip;
  3728. "'", // &lsquo;
  3729. "'", // &rsquo;
  3730. '"', // &ldquo;
  3731. '"', // &rdquo;
  3732. '-', // &ndash;
  3733. '--', // &mdash;
  3734. );
  3735. if ($context['utf8'])
  3736. $string = str_replace($findchars_utf8, $replacechars, $string);
  3737. else
  3738. $string = str_replace($findchars_iso, $replacechars, $string);
  3739. return $string;
  3740. }
  3741. /**
  3742. * Decode numeric html entities to their ascii or UTF8 equivalent character.
  3743. *
  3744. * Callback function for preg_replace_callback in subs-members
  3745. * Uses capture group 2 in the supplied array
  3746. * Does basic scan to ensure characters are inside a valid range
  3747. *
  3748. * @param array $matches
  3749. * @return string $string
  3750. */
  3751. function replaceEntities__callback($matches)
  3752. {
  3753. global $context;
  3754. if (!isset($matches[2]))
  3755. return '';
  3756. $num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
  3757. // remove left to right / right to left overrides
  3758. if ($num === 0x202D || $num === 0x202E)
  3759. return '';
  3760. // Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
  3761. if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
  3762. return '&#' . $num . ';';
  3763. if (empty($context['utf8']))
  3764. {
  3765. // no control characters
  3766. if ($num < 0x20)
  3767. return '';
  3768. // text is text
  3769. elseif ($num < 0x80)
  3770. return chr($num);
  3771. // all others get html-ised
  3772. else
  3773. return '&#' . $matches[2] . ';';
  3774. }
  3775. else
  3776. {
  3777. // <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
  3778. // 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
  3779. if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
  3780. return '';
  3781. // <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and puncuation
  3782. elseif ($num < 0x80)
  3783. return chr($num);
  3784. // <0x800 (2048)
  3785. elseif ($num < 0x800)
  3786. return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
  3787. // < 0x10000 (65536)
  3788. elseif ($num < 0x10000)
  3789. return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
  3790. // <= 0x10FFFF (1114111)
  3791. else
  3792. return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
  3793. }
  3794. }
  3795. /**
  3796. * Converts html entities to utf8 equivalents
  3797. *
  3798. * Callback function for preg_replace_callback
  3799. * Uses capture group 1 in the supplied array
  3800. * Does basic checks to keep characters inside a viewable range.
  3801. *
  3802. * @param array $matches
  3803. * @return string $string
  3804. */
  3805. function fixchar__callback($matches)
  3806. {
  3807. if (!isset($matches[1]))
  3808. return '';
  3809. $num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
  3810. // <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
  3811. // 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
  3812. if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
  3813. return '';
  3814. // <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and puncuation
  3815. elseif ($num < 0x80)
  3816. return chr($num);
  3817. // <0x800 (2048)
  3818. elseif ($num < 0x800)
  3819. return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
  3820. // < 0x10000 (65536)
  3821. elseif ($num < 0x10000)
  3822. return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
  3823. // <= 0x10FFFF (1114111)
  3824. else
  3825. return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
  3826. }
  3827. /**
  3828. * Strips out invalid html entities, replaces others with html style &#123; codes
  3829. *
  3830. * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
  3831. * strpos, strlen, substr etc
  3832. *
  3833. * @param array $matches
  3834. * @return string $string
  3835. */
  3836. function entity_fix__callback($matches)
  3837. {
  3838. if (!isset($matches[2]))
  3839. return '';
  3840. $num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
  3841. // we don't allow control characters, characters out of range, byte markers, etc
  3842. if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
  3843. return '';
  3844. else
  3845. return '&#' . $num . ';';
  3846. }
  3847. ?>