HTMLPurifier.standalone.php 588 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508135091351013511135121351313514135151351613517135181351913520135211352213523135241352513526135271352813529135301353113532135331353413535135361353713538135391354013541135421354313544135451354613547135481354913550135511355213553135541355513556135571355813559135601356113562135631356413565135661356713568135691357013571135721357313574135751357613577135781357913580135811358213583135841358513586135871358813589135901359113592135931359413595135961359713598135991360013601136021360313604136051360613607136081360913610136111361213613136141361513616136171361813619136201362113622136231362413625136261362713628136291363013631136321363313634136351363613637136381363913640136411364213643136441364513646136471364813649136501365113652136531365413655136561365713658136591366013661136621366313664136651366613667136681366913670136711367213673136741367513676136771367813679136801368113682136831368413685136861368713688136891369013691136921369313694136951369613697136981369913700137011370213703137041370513706137071370813709137101371113712137131371413715137161371713718137191372013721137221372313724137251372613727137281372913730137311373213733137341373513736137371373813739137401374113742137431374413745137461374713748137491375013751137521375313754137551375613757137581375913760137611376213763137641376513766137671376813769137701377113772137731377413775137761377713778137791378013781137821378313784137851378613787137881378913790137911379213793137941379513796137971379813799138001380113802138031380413805138061380713808138091381013811138121381313814138151381613817138181381913820138211382213823138241382513826138271382813829138301383113832138331383413835138361383713838138391384013841138421384313844138451384613847138481384913850138511385213853138541385513856138571385813859138601386113862138631386413865138661386713868138691387013871138721387313874138751387613877138781387913880138811388213883138841388513886138871388813889138901389113892138931389413895138961389713898138991390013901139021390313904139051390613907139081390913910139111391213913139141391513916139171391813919139201392113922139231392413925139261392713928139291393013931139321393313934139351393613937139381393913940139411394213943139441394513946139471394813949139501395113952139531395413955139561395713958139591396013961139621396313964139651396613967139681396913970139711397213973139741397513976139771397813979139801398113982139831398413985139861398713988139891399013991139921399313994139951399613997139981399914000140011400214003140041400514006140071400814009140101401114012140131401414015140161401714018140191402014021140221402314024140251402614027140281402914030140311403214033140341403514036140371403814039140401404114042140431404414045140461404714048140491405014051140521405314054140551405614057140581405914060140611406214063140641406514066140671406814069140701407114072140731407414075140761407714078140791408014081140821408314084140851408614087140881408914090140911409214093140941409514096140971409814099141001410114102141031410414105141061410714108141091411014111141121411314114141151411614117141181411914120141211412214123141241412514126141271412814129141301413114132141331413414135141361413714138141391414014141141421414314144141451414614147141481414914150141511415214153141541415514156141571415814159141601416114162141631416414165141661416714168141691417014171141721417314174141751417614177141781417914180141811418214183141841418514186141871418814189141901419114192141931419414195141961419714198141991420014201142021420314204142051420614207142081420914210142111421214213142141421514216142171421814219142201422114222142231422414225142261422714228142291423014231142321423314234142351423614237142381423914240142411424214243142441424514246142471424814249142501425114252142531425414255142561425714258142591426014261142621426314264142651426614267142681426914270142711427214273142741427514276142771427814279142801428114282142831428414285142861428714288142891429014291142921429314294142951429614297142981429914300143011430214303143041430514306143071430814309143101431114312143131431414315143161431714318143191432014321143221432314324143251432614327143281432914330143311433214333143341433514336143371433814339143401434114342143431434414345143461434714348143491435014351143521435314354143551435614357143581435914360143611436214363143641436514366143671436814369143701437114372143731437414375143761437714378143791438014381143821438314384143851438614387143881438914390143911439214393143941439514396143971439814399144001440114402144031440414405144061440714408144091441014411144121441314414144151441614417144181441914420144211442214423144241442514426144271442814429144301443114432144331443414435144361443714438144391444014441144421444314444144451444614447144481444914450144511445214453144541445514456144571445814459144601446114462144631446414465144661446714468144691447014471144721447314474144751447614477144781447914480144811448214483144841448514486144871448814489144901449114492144931449414495144961449714498144991450014501145021450314504145051450614507145081450914510145111451214513145141451514516145171451814519145201452114522145231452414525145261452714528145291453014531145321453314534145351453614537145381453914540145411454214543145441454514546145471454814549145501455114552145531455414555145561455714558145591456014561145621456314564145651456614567145681456914570145711457214573145741457514576145771457814579145801458114582145831458414585145861458714588145891459014591145921459314594145951459614597145981459914600146011460214603146041460514606146071460814609146101461114612146131461414615146161461714618146191462014621146221462314624146251462614627146281462914630146311463214633146341463514636146371463814639146401464114642146431464414645146461464714648146491465014651146521465314654146551465614657146581465914660146611466214663146641466514666146671466814669146701467114672146731467414675146761467714678146791468014681146821468314684146851468614687146881468914690146911469214693146941469514696146971469814699147001470114702147031470414705147061470714708147091471014711147121471314714147151471614717147181471914720147211472214723147241472514726147271472814729147301473114732147331473414735147361473714738147391474014741147421474314744147451474614747147481474914750147511475214753147541475514756147571475814759147601476114762147631476414765147661476714768147691477014771147721477314774147751477614777147781477914780147811478214783147841478514786147871478814789147901479114792147931479414795147961479714798147991480014801148021480314804148051480614807148081480914810148111481214813148141481514816148171481814819148201482114822148231482414825148261482714828148291483014831148321483314834148351483614837148381483914840148411484214843148441484514846148471484814849148501485114852148531485414855148561485714858148591486014861148621486314864148651486614867148681486914870148711487214873148741487514876148771487814879148801488114882148831488414885148861488714888148891489014891148921489314894148951489614897148981489914900149011490214903149041490514906149071490814909149101491114912149131491414915149161491714918149191492014921149221492314924149251492614927149281492914930149311493214933149341493514936149371493814939149401494114942149431494414945149461494714948149491495014951149521495314954149551495614957149581495914960149611496214963149641496514966149671496814969149701497114972149731497414975149761497714978149791498014981149821498314984149851498614987149881498914990149911499214993149941499514996149971499814999150001500115002150031500415005150061500715008150091501015011150121501315014150151501615017150181501915020150211502215023150241502515026150271502815029150301503115032150331503415035150361503715038150391504015041150421504315044150451504615047150481504915050150511505215053150541505515056150571505815059150601506115062150631506415065150661506715068150691507015071150721507315074150751507615077150781507915080150811508215083150841508515086150871508815089150901509115092150931509415095150961509715098150991510015101151021510315104151051510615107151081510915110151111511215113151141511515116151171511815119151201512115122151231512415125151261512715128151291513015131151321513315134151351513615137151381513915140151411514215143151441514515146151471514815149151501515115152151531515415155151561515715158151591516015161151621516315164151651516615167151681516915170151711517215173151741517515176151771517815179151801518115182151831518415185151861518715188151891519015191151921519315194151951519615197151981519915200152011520215203152041520515206152071520815209152101521115212152131521415215152161521715218152191522015221152221522315224152251522615227152281522915230152311523215233152341523515236152371523815239152401524115242152431524415245152461524715248152491525015251152521525315254152551525615257152581525915260152611526215263152641526515266152671526815269152701527115272152731527415275152761527715278152791528015281152821528315284152851528615287152881528915290152911529215293152941529515296152971529815299153001530115302153031530415305153061530715308153091531015311153121531315314153151531615317153181531915320153211532215323153241532515326153271532815329153301533115332153331533415335153361533715338153391534015341153421534315344153451534615347153481534915350153511535215353153541535515356153571535815359153601536115362153631536415365153661536715368153691537015371153721537315374153751537615377153781537915380153811538215383153841538515386153871538815389153901539115392153931539415395153961539715398153991540015401154021540315404154051540615407154081540915410154111541215413154141541515416154171541815419154201542115422154231542415425154261542715428154291543015431154321543315434154351543615437154381543915440154411544215443154441544515446154471544815449154501545115452154531545415455154561545715458154591546015461154621546315464154651546615467154681546915470154711547215473154741547515476154771547815479154801548115482154831548415485154861548715488154891549015491154921549315494154951549615497154981549915500155011550215503155041550515506155071550815509155101551115512155131551415515155161551715518155191552015521155221552315524155251552615527155281552915530155311553215533155341553515536155371553815539155401554115542155431554415545155461554715548155491555015551155521555315554155551555615557155581555915560155611556215563155641556515566155671556815569155701557115572155731557415575155761557715578155791558015581155821558315584155851558615587155881558915590155911559215593155941559515596155971559815599156001560115602156031560415605156061560715608156091561015611156121561315614156151561615617156181561915620156211562215623156241562515626156271562815629156301563115632156331563415635156361563715638156391564015641156421564315644156451564615647156481564915650156511565215653156541565515656156571565815659156601566115662156631566415665156661566715668156691567015671156721567315674156751567615677156781567915680156811568215683156841568515686156871568815689156901569115692156931569415695156961569715698156991570015701157021570315704157051570615707157081570915710157111571215713157141571515716157171571815719157201572115722157231572415725157261572715728157291573015731157321573315734157351573615737157381573915740157411574215743157441574515746157471574815749157501575115752157531575415755157561575715758157591576015761157621576315764157651576615767157681576915770157711577215773157741577515776157771577815779157801578115782157831578415785157861578715788157891579015791157921579315794157951579615797157981579915800158011580215803158041580515806158071580815809158101581115812158131581415815158161581715818158191582015821158221582315824158251582615827158281582915830158311583215833158341583515836158371583815839158401584115842158431584415845158461584715848158491585015851158521585315854158551585615857158581585915860158611586215863158641586515866158671586815869158701587115872158731587415875158761587715878158791588015881158821588315884158851588615887158881588915890158911589215893158941589515896158971589815899159001590115902159031590415905159061590715908159091591015911159121591315914159151591615917159181591915920159211592215923159241592515926159271592815929159301593115932159331593415935159361593715938159391594015941159421594315944159451594615947159481594915950159511595215953159541595515956159571595815959159601596115962159631596415965159661596715968159691597015971159721597315974159751597615977159781597915980159811598215983159841598515986159871598815989159901599115992159931599415995159961599715998159991600016001160021600316004160051600616007160081600916010160111601216013160141601516016160171601816019160201602116022160231602416025160261602716028160291603016031160321603316034160351603616037160381603916040160411604216043160441604516046160471604816049160501605116052160531605416055160561605716058160591606016061160621606316064160651606616067160681606916070160711607216073160741607516076160771607816079160801608116082160831608416085160861608716088160891609016091160921609316094160951609616097160981609916100161011610216103161041610516106161071610816109161101611116112161131611416115161161611716118161191612016121161221612316124161251612616127161281612916130161311613216133161341613516136161371613816139161401614116142161431614416145161461614716148161491615016151161521615316154161551615616157161581615916160161611616216163161641616516166161671616816169161701617116172161731617416175161761617716178161791618016181161821618316184161851618616187161881618916190161911619216193161941619516196161971619816199162001620116202162031620416205162061620716208162091621016211162121621316214162151621616217162181621916220162211622216223162241622516226162271622816229162301623116232162331623416235162361623716238162391624016241162421624316244162451624616247162481624916250162511625216253162541625516256162571625816259162601626116262162631626416265162661626716268162691627016271162721627316274162751627616277162781627916280162811628216283162841628516286162871628816289162901629116292162931629416295162961629716298162991630016301163021630316304163051630616307163081630916310163111631216313163141631516316163171631816319163201632116322163231632416325163261632716328163291633016331163321633316334163351633616337163381633916340163411634216343163441634516346163471634816349163501635116352163531635416355163561635716358163591636016361163621636316364163651636616367163681636916370163711637216373163741637516376163771637816379163801638116382163831638416385163861638716388163891639016391163921639316394163951639616397163981639916400164011640216403164041640516406164071640816409164101641116412164131641416415164161641716418164191642016421164221642316424164251642616427164281642916430164311643216433164341643516436164371643816439164401644116442164431644416445164461644716448164491645016451164521645316454164551645616457164581645916460164611646216463164641646516466164671646816469164701647116472164731647416475164761647716478164791648016481164821648316484164851648616487164881648916490164911649216493164941649516496164971649816499165001650116502165031650416505165061650716508165091651016511165121651316514165151651616517165181651916520165211652216523165241652516526165271652816529165301653116532165331653416535165361653716538165391654016541165421654316544165451654616547165481654916550165511655216553165541655516556165571655816559165601656116562165631656416565165661656716568165691657016571165721657316574165751657616577165781657916580165811658216583165841658516586165871658816589165901659116592165931659416595165961659716598165991660016601166021660316604166051660616607166081660916610166111661216613166141661516616166171661816619166201662116622166231662416625166261662716628166291663016631166321663316634166351663616637166381663916640166411664216643166441664516646166471664816649166501665116652166531665416655166561665716658166591666016661166621666316664166651666616667166681666916670166711667216673166741667516676166771667816679166801668116682166831668416685166861668716688166891669016691166921669316694166951669616697166981669916700167011670216703167041670516706167071670816709167101671116712167131671416715167161671716718167191672016721167221672316724167251672616727167281672916730167311673216733167341673516736167371673816739167401674116742167431674416745167461674716748167491675016751167521675316754167551675616757167581675916760167611676216763167641676516766167671676816769167701677116772167731677416775167761677716778167791678016781167821678316784167851678616787167881678916790167911679216793167941679516796167971679816799168001680116802168031680416805168061680716808168091681016811168121681316814168151681616817168181681916820168211682216823168241682516826168271682816829168301683116832168331683416835168361683716838168391684016841168421684316844168451684616847168481684916850168511685216853168541685516856168571685816859168601686116862168631686416865168661686716868168691687016871168721687316874168751687616877168781687916880168811688216883168841688516886168871688816889168901689116892168931689416895168961689716898168991690016901169021690316904169051690616907169081690916910169111691216913169141691516916169171691816919169201692116922169231692416925169261692716928169291693016931169321693316934169351693616937169381693916940169411694216943169441694516946169471694816949169501695116952169531695416955169561695716958169591696016961169621696316964169651696616967169681696916970169711697216973169741697516976169771697816979169801698116982169831698416985169861698716988169891699016991169921699316994169951699616997169981699917000170011700217003170041700517006170071700817009170101701117012170131701417015170161701717018170191702017021170221702317024170251702617027170281702917030170311703217033170341703517036170371703817039170401704117042170431704417045170461704717048170491705017051170521705317054170551705617057170581705917060170611706217063170641706517066170671706817069170701707117072170731707417075170761707717078170791708017081170821708317084170851708617087170881708917090170911709217093170941709517096170971709817099171001710117102171031710417105171061710717108171091711017111171121711317114171151711617117171181711917120171211712217123171241712517126171271712817129171301713117132171331713417135171361713717138171391714017141171421714317144171451714617147171481714917150171511715217153171541715517156171571715817159171601716117162171631716417165171661716717168171691717017171171721717317174171751717617177171781717917180171811718217183171841718517186171871718817189171901719117192171931719417195171961719717198171991720017201172021720317204172051720617207172081720917210172111721217213172141721517216172171721817219172201722117222172231722417225172261722717228172291723017231172321723317234172351723617237172381723917240172411724217243172441724517246172471724817249172501725117252172531725417255172561725717258172591726017261172621726317264172651726617267172681726917270172711727217273172741727517276172771727817279172801728117282172831728417285172861728717288172891729017291172921729317294172951729617297172981729917300173011730217303173041730517306173071730817309173101731117312173131731417315173161731717318173191732017321173221732317324173251732617327173281732917330173311733217333173341733517336173371733817339173401734117342173431734417345173461734717348173491735017351173521735317354173551735617357173581735917360173611736217363173641736517366173671736817369173701737117372173731737417375173761737717378173791738017381173821738317384173851738617387173881738917390173911739217393173941739517396173971739817399174001740117402174031740417405174061740717408174091741017411174121741317414174151741617417174181741917420174211742217423174241742517426174271742817429174301743117432174331743417435174361743717438174391744017441174421744317444174451744617447174481744917450174511745217453174541745517456174571745817459174601746117462174631746417465174661746717468174691747017471174721747317474174751747617477174781747917480174811748217483174841748517486174871748817489174901749117492174931749417495174961749717498174991750017501175021750317504175051750617507175081750917510175111751217513175141751517516175171751817519175201752117522175231752417525175261752717528175291753017531175321753317534175351753617537175381753917540175411754217543175441754517546175471754817549175501755117552175531755417555175561755717558175591756017561175621756317564175651756617567175681756917570175711757217573175741757517576175771757817579175801758117582175831758417585175861758717588175891759017591175921759317594175951759617597175981759917600176011760217603176041760517606176071760817609176101761117612176131761417615176161761717618176191762017621176221762317624176251762617627
  1. <?php
  2. /**
  3. * @file
  4. * This file was auto-generated by generate-includes.php and includes all of
  5. * the core files required by HTML Purifier. Use this if performance is a
  6. * primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS
  7. * FILE, changes will be overwritten the next time the script is run.
  8. *
  9. * @version 4.5.0
  10. *
  11. * @warning
  12. * You must *not* include any other HTML Purifier files before this file,
  13. * because 'require' not 'require_once' is used.
  14. *
  15. * @warning
  16. * This file requires that the include path contains the HTML Purifier
  17. * library directory; this is not auto-set.
  18. */
  19. /*! @mainpage
  20. *
  21. * HTML Purifier is an HTML filter that will take an arbitrary snippet of
  22. * HTML and rigorously test, validate and filter it into a version that
  23. * is safe for output onto webpages. It achieves this by:
  24. *
  25. * -# Lexing (parsing into tokens) the document,
  26. * -# Executing various strategies on the tokens:
  27. * -# Removing all elements not in the whitelist,
  28. * -# Making the tokens well-formed,
  29. * -# Fixing the nesting of the nodes, and
  30. * -# Validating attributes of the nodes; and
  31. * -# Generating HTML from the purified tokens.
  32. *
  33. * However, most users will only need to interface with the HTMLPurifier
  34. * and HTMLPurifier_Config.
  35. */
  36. /*
  37. HTML Purifier 4.5.0 - Standards Compliant HTML Filtering
  38. Copyright (C) 2006-2008 Edward Z. Yang
  39. This library is free software; you can redistribute it and/or
  40. modify it under the terms of the GNU Lesser General Public
  41. License as published by the Free Software Foundation; either
  42. version 2.1 of the License, or (at your option) any later version.
  43. This library is distributed in the hope that it will be useful,
  44. but WITHOUT ANY WARRANTY; without even the implied warranty of
  45. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  46. Lesser General Public License for more details.
  47. You should have received a copy of the GNU Lesser General Public
  48. License along with this library; if not, write to the Free Software
  49. Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  50. */
  51. /**
  52. * Facade that coordinates HTML Purifier's subsystems in order to purify HTML.
  53. *
  54. * @note There are several points in which configuration can be specified
  55. * for HTML Purifier. The precedence of these (from lowest to
  56. * highest) is as follows:
  57. * -# Instance: new HTMLPurifier($config)
  58. * -# Invocation: purify($html, $config)
  59. * These configurations are entirely independent of each other and
  60. * are *not* merged (this behavior may change in the future).
  61. *
  62. * @todo We need an easier way to inject strategies using the configuration
  63. * object.
  64. */
  65. class HTMLPurifier
  66. {
  67. /** Version of HTML Purifier */
  68. public $version = '4.5.0';
  69. /** Constant with version of HTML Purifier */
  70. const VERSION = '4.5.0';
  71. /** Global configuration object */
  72. public $config;
  73. /** Array of extra HTMLPurifier_Filter objects to run on HTML, for backwards compatibility */
  74. private $filters = array();
  75. /** Single instance of HTML Purifier */
  76. private static $instance;
  77. protected $strategy, $generator;
  78. /**
  79. * Resultant HTMLPurifier_Context of last run purification. Is an array
  80. * of contexts if the last called method was purifyArray().
  81. */
  82. public $context;
  83. /**
  84. * Initializes the purifier.
  85. * @param $config Optional HTMLPurifier_Config object for all instances of
  86. * the purifier, if omitted, a default configuration is
  87. * supplied (which can be overridden on a per-use basis).
  88. * The parameter can also be any type that
  89. * HTMLPurifier_Config::create() supports.
  90. */
  91. public function __construct($config = null) {
  92. $this->config = HTMLPurifier_Config::create($config);
  93. $this->strategy = new HTMLPurifier_Strategy_Core();
  94. }
  95. /**
  96. * Adds a filter to process the output. First come first serve
  97. * @param $filter HTMLPurifier_Filter object
  98. */
  99. public function addFilter($filter) {
  100. trigger_error('HTMLPurifier->addFilter() is deprecated, use configuration directives in the Filter namespace or Filter.Custom', E_USER_WARNING);
  101. $this->filters[] = $filter;
  102. }
  103. /**
  104. * Filters an HTML snippet/document to be XSS-free and standards-compliant.
  105. *
  106. * @param $html String of HTML to purify
  107. * @param $config HTMLPurifier_Config object for this operation, if omitted,
  108. * defaults to the config object specified during this
  109. * object's construction. The parameter can also be any type
  110. * that HTMLPurifier_Config::create() supports.
  111. * @return Purified HTML
  112. */
  113. public function purify($html, $config = null) {
  114. // :TODO: make the config merge in, instead of replace
  115. $config = $config ? HTMLPurifier_Config::create($config) : $this->config;
  116. // implementation is partially environment dependant, partially
  117. // configuration dependant
  118. $lexer = HTMLPurifier_Lexer::create($config);
  119. $context = new HTMLPurifier_Context();
  120. // setup HTML generator
  121. $this->generator = new HTMLPurifier_Generator($config, $context);
  122. $context->register('Generator', $this->generator);
  123. // set up global context variables
  124. if ($config->get('Core.CollectErrors')) {
  125. // may get moved out if other facilities use it
  126. $language_factory = HTMLPurifier_LanguageFactory::instance();
  127. $language = $language_factory->create($config, $context);
  128. $context->register('Locale', $language);
  129. $error_collector = new HTMLPurifier_ErrorCollector($context);
  130. $context->register('ErrorCollector', $error_collector);
  131. }
  132. // setup id_accumulator context, necessary due to the fact that
  133. // AttrValidator can be called from many places
  134. $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
  135. $context->register('IDAccumulator', $id_accumulator);
  136. $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context);
  137. // setup filters
  138. $filter_flags = $config->getBatch('Filter');
  139. $custom_filters = $filter_flags['Custom'];
  140. unset($filter_flags['Custom']);
  141. $filters = array();
  142. foreach ($filter_flags as $filter => $flag) {
  143. if (!$flag) continue;
  144. if (strpos($filter, '.') !== false) continue;
  145. $class = "HTMLPurifier_Filter_$filter";
  146. $filters[] = new $class;
  147. }
  148. foreach ($custom_filters as $filter) {
  149. // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat
  150. $filters[] = $filter;
  151. }
  152. $filters = array_merge($filters, $this->filters);
  153. // maybe prepare(), but later
  154. for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) {
  155. $html = $filters[$i]->preFilter($html, $config, $context);
  156. }
  157. // purified HTML
  158. $html =
  159. $this->generator->generateFromTokens(
  160. // list of tokens
  161. $this->strategy->execute(
  162. // list of un-purified tokens
  163. $lexer->tokenizeHTML(
  164. // un-purified HTML
  165. $html, $config, $context
  166. ),
  167. $config, $context
  168. )
  169. );
  170. for ($i = $filter_size - 1; $i >= 0; $i--) {
  171. $html = $filters[$i]->postFilter($html, $config, $context);
  172. }
  173. $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context);
  174. $this->context =& $context;
  175. return $html;
  176. }
  177. /**
  178. * Filters an array of HTML snippets
  179. * @param $config Optional HTMLPurifier_Config object for this operation.
  180. * See HTMLPurifier::purify() for more details.
  181. * @return Array of purified HTML
  182. */
  183. public function purifyArray($array_of_html, $config = null) {
  184. $context_array = array();
  185. foreach ($array_of_html as $key => $html) {
  186. $array_of_html[$key] = $this->purify($html, $config);
  187. $context_array[$key] = $this->context;
  188. }
  189. $this->context = $context_array;
  190. return $array_of_html;
  191. }
  192. /**
  193. * Singleton for enforcing just one HTML Purifier in your system
  194. * @param $prototype Optional prototype HTMLPurifier instance to
  195. * overload singleton with, or HTMLPurifier_Config
  196. * instance to configure the generated version with.
  197. */
  198. public static function instance($prototype = null) {
  199. if (!self::$instance || $prototype) {
  200. if ($prototype instanceof HTMLPurifier) {
  201. self::$instance = $prototype;
  202. } elseif ($prototype) {
  203. self::$instance = new HTMLPurifier($prototype);
  204. } else {
  205. self::$instance = new HTMLPurifier();
  206. }
  207. }
  208. return self::$instance;
  209. }
  210. /**
  211. * @note Backwards compatibility, see instance()
  212. */
  213. public static function getInstance($prototype = null) {
  214. return HTMLPurifier::instance($prototype);
  215. }
  216. }
  217. /**
  218. * Defines common attribute collections that modules reference
  219. */
  220. class HTMLPurifier_AttrCollections
  221. {
  222. /**
  223. * Associative array of attribute collections, indexed by name
  224. */
  225. public $info = array();
  226. /**
  227. * Performs all expansions on internal data for use by other inclusions
  228. * It also collects all attribute collection extensions from
  229. * modules
  230. * @param $attr_types HTMLPurifier_AttrTypes instance
  231. * @param $modules Hash array of HTMLPurifier_HTMLModule members
  232. */
  233. public function __construct($attr_types, $modules) {
  234. // load extensions from the modules
  235. foreach ($modules as $module) {
  236. foreach ($module->attr_collections as $coll_i => $coll) {
  237. if (!isset($this->info[$coll_i])) {
  238. $this->info[$coll_i] = array();
  239. }
  240. foreach ($coll as $attr_i => $attr) {
  241. if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) {
  242. // merge in includes
  243. $this->info[$coll_i][$attr_i] = array_merge(
  244. $this->info[$coll_i][$attr_i], $attr);
  245. continue;
  246. }
  247. $this->info[$coll_i][$attr_i] = $attr;
  248. }
  249. }
  250. }
  251. // perform internal expansions and inclusions
  252. foreach ($this->info as $name => $attr) {
  253. // merge attribute collections that include others
  254. $this->performInclusions($this->info[$name]);
  255. // replace string identifiers with actual attribute objects
  256. $this->expandIdentifiers($this->info[$name], $attr_types);
  257. }
  258. }
  259. /**
  260. * Takes a reference to an attribute associative array and performs
  261. * all inclusions specified by the zero index.
  262. * @param &$attr Reference to attribute array
  263. */
  264. public function performInclusions(&$attr) {
  265. if (!isset($attr[0])) return;
  266. $merge = $attr[0];
  267. $seen = array(); // recursion guard
  268. // loop through all the inclusions
  269. for ($i = 0; isset($merge[$i]); $i++) {
  270. if (isset($seen[$merge[$i]])) continue;
  271. $seen[$merge[$i]] = true;
  272. // foreach attribute of the inclusion, copy it over
  273. if (!isset($this->info[$merge[$i]])) continue;
  274. foreach ($this->info[$merge[$i]] as $key => $value) {
  275. if (isset($attr[$key])) continue; // also catches more inclusions
  276. $attr[$key] = $value;
  277. }
  278. if (isset($this->info[$merge[$i]][0])) {
  279. // recursion
  280. $merge = array_merge($merge, $this->info[$merge[$i]][0]);
  281. }
  282. }
  283. unset($attr[0]);
  284. }
  285. /**
  286. * Expands all string identifiers in an attribute array by replacing
  287. * them with the appropriate values inside HTMLPurifier_AttrTypes
  288. * @param &$attr Reference to attribute array
  289. * @param $attr_types HTMLPurifier_AttrTypes instance
  290. */
  291. public function expandIdentifiers(&$attr, $attr_types) {
  292. // because foreach will process new elements we add, make sure we
  293. // skip duplicates
  294. $processed = array();
  295. foreach ($attr as $def_i => $def) {
  296. // skip inclusions
  297. if ($def_i === 0) continue;
  298. if (isset($processed[$def_i])) continue;
  299. // determine whether or not attribute is required
  300. if ($required = (strpos($def_i, '*') !== false)) {
  301. // rename the definition
  302. unset($attr[$def_i]);
  303. $def_i = trim($def_i, '*');
  304. $attr[$def_i] = $def;
  305. }
  306. $processed[$def_i] = true;
  307. // if we've already got a literal object, move on
  308. if (is_object($def)) {
  309. // preserve previous required
  310. $attr[$def_i]->required = ($required || $attr[$def_i]->required);
  311. continue;
  312. }
  313. if ($def === false) {
  314. unset($attr[$def_i]);
  315. continue;
  316. }
  317. if ($t = $attr_types->get($def)) {
  318. $attr[$def_i] = $t;
  319. $attr[$def_i]->required = $required;
  320. } else {
  321. unset($attr[$def_i]);
  322. }
  323. }
  324. }
  325. }
  326. /**
  327. * Base class for all validating attribute definitions.
  328. *
  329. * This family of classes forms the core for not only HTML attribute validation,
  330. * but also any sort of string that needs to be validated or cleaned (which
  331. * means CSS properties and composite definitions are defined here too).
  332. * Besides defining (through code) what precisely makes the string valid,
  333. * subclasses are also responsible for cleaning the code if possible.
  334. */
  335. abstract class HTMLPurifier_AttrDef
  336. {
  337. /**
  338. * Tells us whether or not an HTML attribute is minimized. Has no
  339. * meaning in other contexts.
  340. */
  341. public $minimized = false;
  342. /**
  343. * Tells us whether or not an HTML attribute is required. Has no
  344. * meaning in other contexts
  345. */
  346. public $required = false;
  347. /**
  348. * Validates and cleans passed string according to a definition.
  349. *
  350. * @param $string String to be validated and cleaned.
  351. * @param $config Mandatory HTMLPurifier_Config object.
  352. * @param $context Mandatory HTMLPurifier_AttrContext object.
  353. */
  354. abstract public function validate($string, $config, $context);
  355. /**
  356. * Convenience method that parses a string as if it were CDATA.
  357. *
  358. * This method process a string in the manner specified at
  359. * <http://www.w3.org/TR/html4/types.html#h-6.2> by removing
  360. * leading and trailing whitespace, ignoring line feeds, and replacing
  361. * carriage returns and tabs with spaces. While most useful for HTML
  362. * attributes specified as CDATA, it can also be applied to most CSS
  363. * values.
  364. *
  365. * @note This method is not entirely standards compliant, as trim() removes
  366. * more types of whitespace than specified in the spec. In practice,
  367. * this is rarely a problem, as those extra characters usually have
  368. * already been removed by HTMLPurifier_Encoder.
  369. *
  370. * @warning This processing is inconsistent with XML's whitespace handling
  371. * as specified by section 3.3.3 and referenced XHTML 1.0 section
  372. * 4.7. However, note that we are NOT necessarily
  373. * parsing XML, thus, this behavior may still be correct. We
  374. * assume that newlines have been normalized.
  375. */
  376. public function parseCDATA($string) {
  377. $string = trim($string);
  378. $string = str_replace(array("\n", "\t", "\r"), ' ', $string);
  379. return $string;
  380. }
  381. /**
  382. * Factory method for creating this class from a string.
  383. * @param $string String construction info
  384. * @return Created AttrDef object corresponding to $string
  385. */
  386. public function make($string) {
  387. // default implementation, return a flyweight of this object.
  388. // If $string has an effect on the returned object (i.e. you
  389. // need to overload this method), it is best
  390. // to clone or instantiate new copies. (Instantiation is safer.)
  391. return $this;
  392. }
  393. /**
  394. * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work
  395. * properly. THIS IS A HACK!
  396. */
  397. protected function mungeRgb($string) {
  398. return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string);
  399. }
  400. /**
  401. * Parses a possibly escaped CSS string and returns the "pure"
  402. * version of it.
  403. */
  404. protected function expandCSSEscape($string) {
  405. // flexibly parse it
  406. $ret = '';
  407. for ($i = 0, $c = strlen($string); $i < $c; $i++) {
  408. if ($string[$i] === '\\') {
  409. $i++;
  410. if ($i >= $c) {
  411. $ret .= '\\';
  412. break;
  413. }
  414. if (ctype_xdigit($string[$i])) {
  415. $code = $string[$i];
  416. for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) {
  417. if (!ctype_xdigit($string[$i])) break;
  418. $code .= $string[$i];
  419. }
  420. // We have to be extremely careful when adding
  421. // new characters, to make sure we're not breaking
  422. // the encoding.
  423. $char = HTMLPurifier_Encoder::unichr(hexdec($code));
  424. if (HTMLPurifier_Encoder::cleanUTF8($char) === '') continue;
  425. $ret .= $char;
  426. if ($i < $c && trim($string[$i]) !== '') $i--;
  427. continue;
  428. }
  429. if ($string[$i] === "\n") continue;
  430. }
  431. $ret .= $string[$i];
  432. }
  433. return $ret;
  434. }
  435. }
  436. /**
  437. * Processes an entire attribute array for corrections needing multiple values.
  438. *
  439. * Occasionally, a certain attribute will need to be removed and popped onto
  440. * another value. Instead of creating a complex return syntax for
  441. * HTMLPurifier_AttrDef, we just pass the whole attribute array to a
  442. * specialized object and have that do the special work. That is the
  443. * family of HTMLPurifier_AttrTransform.
  444. *
  445. * An attribute transformation can be assigned to run before or after
  446. * HTMLPurifier_AttrDef validation. See HTMLPurifier_HTMLDefinition for
  447. * more details.
  448. */
  449. abstract class HTMLPurifier_AttrTransform
  450. {
  451. /**
  452. * Abstract: makes changes to the attributes dependent on multiple values.
  453. *
  454. * @param $attr Assoc array of attributes, usually from
  455. * HTMLPurifier_Token_Tag::$attr
  456. * @param $config Mandatory HTMLPurifier_Config object.
  457. * @param $context Mandatory HTMLPurifier_Context object
  458. * @returns Processed attribute array.
  459. */
  460. abstract public function transform($attr, $config, $context);
  461. /**
  462. * Prepends CSS properties to the style attribute, creating the
  463. * attribute if it doesn't exist.
  464. * @param $attr Attribute array to process (passed by reference)
  465. * @param $css CSS to prepend
  466. */
  467. public function prependCSS(&$attr, $css) {
  468. $attr['style'] = isset($attr['style']) ? $attr['style'] : '';
  469. $attr['style'] = $css . $attr['style'];
  470. }
  471. /**
  472. * Retrieves and removes an attribute
  473. * @param $attr Attribute array to process (passed by reference)
  474. * @param $key Key of attribute to confiscate
  475. */
  476. public function confiscateAttr(&$attr, $key) {
  477. if (!isset($attr[$key])) return null;
  478. $value = $attr[$key];
  479. unset($attr[$key]);
  480. return $value;
  481. }
  482. }
  483. /**
  484. * Provides lookup array of attribute types to HTMLPurifier_AttrDef objects
  485. */
  486. class HTMLPurifier_AttrTypes
  487. {
  488. /**
  489. * Lookup array of attribute string identifiers to concrete implementations
  490. */
  491. protected $info = array();
  492. /**
  493. * Constructs the info array, supplying default implementations for attribute
  494. * types.
  495. */
  496. public function __construct() {
  497. // XXX This is kind of poor, since we don't actually /clone/
  498. // instances; instead, we use the supplied make() attribute. So,
  499. // the underlying class must know how to deal with arguments.
  500. // With the old implementation of Enum, that ignored its
  501. // arguments when handling a make dispatch, the IAlign
  502. // definition wouldn't work.
  503. // pseudo-types, must be instantiated via shorthand
  504. $this->info['Enum'] = new HTMLPurifier_AttrDef_Enum();
  505. $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool();
  506. $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text();
  507. $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID();
  508. $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length();
  509. $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength();
  510. $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens();
  511. $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels();
  512. $this->info['Text'] = new HTMLPurifier_AttrDef_Text();
  513. $this->info['URI'] = new HTMLPurifier_AttrDef_URI();
  514. $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang();
  515. $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color();
  516. $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right');
  517. $this->info['LAlign'] = self::makeEnum('top,bottom,left,right');
  518. $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget();
  519. // unimplemented aliases
  520. $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text();
  521. $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text();
  522. $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text();
  523. $this->info['Character'] = new HTMLPurifier_AttrDef_Text();
  524. // "proprietary" types
  525. $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class();
  526. // number is really a positive integer (one or more digits)
  527. // FIXME: ^^ not always, see start and value of list items
  528. $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true);
  529. }
  530. private static function makeEnum($in) {
  531. return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in)));
  532. }
  533. /**
  534. * Retrieves a type
  535. * @param $type String type name
  536. * @return Object AttrDef for type
  537. */
  538. public function get($type) {
  539. // determine if there is any extra info tacked on
  540. if (strpos($type, '#') !== false) list($type, $string) = explode('#', $type, 2);
  541. else $string = '';
  542. if (!isset($this->info[$type])) {
  543. trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR);
  544. return;
  545. }
  546. return $this->info[$type]->make($string);
  547. }
  548. /**
  549. * Sets a new implementation for a type
  550. * @param $type String type name
  551. * @param $impl Object AttrDef for type
  552. */
  553. public function set($type, $impl) {
  554. $this->info[$type] = $impl;
  555. }
  556. }
  557. /**
  558. * Validates the attributes of a token. Doesn't manage required attributes
  559. * very well. The only reason we factored this out was because RemoveForeignElements
  560. * also needed it besides ValidateAttributes.
  561. */
  562. class HTMLPurifier_AttrValidator
  563. {
  564. /**
  565. * Validates the attributes of a token, returning a modified token
  566. * that has valid tokens
  567. * @param $token Reference to token to validate. We require a reference
  568. * because the operation this class performs on the token are
  569. * not atomic, so the context CurrentToken to be updated
  570. * throughout
  571. * @param $config Instance of HTMLPurifier_Config
  572. * @param $context Instance of HTMLPurifier_Context
  573. */
  574. public function validateToken(&$token, &$config, $context) {
  575. $definition = $config->getHTMLDefinition();
  576. $e =& $context->get('ErrorCollector', true);
  577. // initialize IDAccumulator if necessary
  578. $ok =& $context->get('IDAccumulator', true);
  579. if (!$ok) {
  580. $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
  581. $context->register('IDAccumulator', $id_accumulator);
  582. }
  583. // initialize CurrentToken if necessary
  584. $current_token =& $context->get('CurrentToken', true);
  585. if (!$current_token) $context->register('CurrentToken', $token);
  586. if (
  587. !$token instanceof HTMLPurifier_Token_Start &&
  588. !$token instanceof HTMLPurifier_Token_Empty
  589. ) return $token;
  590. // create alias to global definition array, see also $defs
  591. // DEFINITION CALL
  592. $d_defs = $definition->info_global_attr;
  593. // don't update token until the very end, to ensure an atomic update
  594. $attr = $token->attr;
  595. // do global transformations (pre)
  596. // nothing currently utilizes this
  597. foreach ($definition->info_attr_transform_pre as $transform) {
  598. $attr = $transform->transform($o = $attr, $config, $context);
  599. if ($e) {
  600. if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
  601. }
  602. }
  603. // do local transformations only applicable to this element (pre)
  604. // ex. <p align="right"> to <p style="text-align:right;">
  605. foreach ($definition->info[$token->name]->attr_transform_pre as $transform) {
  606. $attr = $transform->transform($o = $attr, $config, $context);
  607. if ($e) {
  608. if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
  609. }
  610. }
  611. // create alias to this element's attribute definition array, see
  612. // also $d_defs (global attribute definition array)
  613. // DEFINITION CALL
  614. $defs = $definition->info[$token->name]->attr;
  615. $attr_key = false;
  616. $context->register('CurrentAttr', $attr_key);
  617. // iterate through all the attribute keypairs
  618. // Watch out for name collisions: $key has previously been used
  619. foreach ($attr as $attr_key => $value) {
  620. // call the definition
  621. if ( isset($defs[$attr_key]) ) {
  622. // there is a local definition defined
  623. if ($defs[$attr_key] === false) {
  624. // We've explicitly been told not to allow this element.
  625. // This is usually when there's a global definition
  626. // that must be overridden.
  627. // Theoretically speaking, we could have a
  628. // AttrDef_DenyAll, but this is faster!
  629. $result = false;
  630. } else {
  631. // validate according to the element's definition
  632. $result = $defs[$attr_key]->validate(
  633. $value, $config, $context
  634. );
  635. }
  636. } elseif ( isset($d_defs[$attr_key]) ) {
  637. // there is a global definition defined, validate according
  638. // to the global definition
  639. $result = $d_defs[$attr_key]->validate(
  640. $value, $config, $context
  641. );
  642. } else {
  643. // system never heard of the attribute? DELETE!
  644. $result = false;
  645. }
  646. // put the results into effect
  647. if ($result === false || $result === null) {
  648. // this is a generic error message that should replaced
  649. // with more specific ones when possible
  650. if ($e) $e->send(E_ERROR, 'AttrValidator: Attribute removed');
  651. // remove the attribute
  652. unset($attr[$attr_key]);
  653. } elseif (is_string($result)) {
  654. // generally, if a substitution is happening, there
  655. // was some sort of implicit correction going on. We'll
  656. // delegate it to the attribute classes to say exactly what.
  657. // simple substitution
  658. $attr[$attr_key] = $result;
  659. } else {
  660. // nothing happens
  661. }
  662. // we'd also want slightly more complicated substitution
  663. // involving an array as the return value,
  664. // although we're not sure how colliding attributes would
  665. // resolve (certain ones would be completely overriden,
  666. // others would prepend themselves).
  667. }
  668. $context->destroy('CurrentAttr');
  669. // post transforms
  670. // global (error reporting untested)
  671. foreach ($definition->info_attr_transform_post as $transform) {
  672. $attr = $transform->transform($o = $attr, $config, $context);
  673. if ($e) {
  674. if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
  675. }
  676. }
  677. // local (error reporting untested)
  678. foreach ($definition->info[$token->name]->attr_transform_post as $transform) {
  679. $attr = $transform->transform($o = $attr, $config, $context);
  680. if ($e) {
  681. if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
  682. }
  683. }
  684. $token->attr = $attr;
  685. // destroy CurrentToken if we made it ourselves
  686. if (!$current_token) $context->destroy('CurrentToken');
  687. }
  688. }
  689. // constants are slow, so we use as few as possible
  690. if (!defined('HTMLPURIFIER_PREFIX')) {
  691. define('HTMLPURIFIER_PREFIX', dirname(__FILE__) . '/standalone');
  692. set_include_path(HTMLPURIFIER_PREFIX . PATH_SEPARATOR . get_include_path());
  693. }
  694. // accomodations for versions earlier than 5.0.2
  695. // borrowed from PHP_Compat, LGPL licensed, by Aidan Lister <aidan@php.net>
  696. if (!defined('PHP_EOL')) {
  697. switch (strtoupper(substr(PHP_OS, 0, 3))) {
  698. case 'WIN':
  699. define('PHP_EOL', "\r\n");
  700. break;
  701. case 'DAR':
  702. define('PHP_EOL', "\r");
  703. break;
  704. default:
  705. define('PHP_EOL', "\n");
  706. }
  707. }
  708. /**
  709. * Bootstrap class that contains meta-functionality for HTML Purifier such as
  710. * the autoload function.
  711. *
  712. * @note
  713. * This class may be used without any other files from HTML Purifier.
  714. */
  715. class HTMLPurifier_Bootstrap
  716. {
  717. /**
  718. * Autoload function for HTML Purifier
  719. * @param $class Class to load
  720. */
  721. public static function autoload($class) {
  722. $file = HTMLPurifier_Bootstrap::getPath($class);
  723. if (!$file) return false;
  724. // Technically speaking, it should be ok and more efficient to
  725. // just do 'require', but Antonio Parraga reports that with
  726. // Zend extensions such as Zend debugger and APC, this invariant
  727. // may be broken. Since we have efficient alternatives, pay
  728. // the cost here and avoid the bug.
  729. require_once HTMLPURIFIER_PREFIX . '/' . $file;
  730. return true;
  731. }
  732. /**
  733. * Returns the path for a specific class.
  734. */
  735. public static function getPath($class) {
  736. if (strncmp('HTMLPurifier', $class, 12) !== 0) return false;
  737. // Custom implementations
  738. if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) {
  739. $code = str_replace('_', '-', substr($class, 22));
  740. $file = 'HTMLPurifier/Language/classes/' . $code . '.php';
  741. } else {
  742. $file = str_replace('_', '/', $class) . '.php';
  743. }
  744. if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) return false;
  745. return $file;
  746. }
  747. /**
  748. * "Pre-registers" our autoloader on the SPL stack.
  749. */
  750. public static function registerAutoload() {
  751. $autoload = array('HTMLPurifier_Bootstrap', 'autoload');
  752. if ( ($funcs = spl_autoload_functions()) === false ) {
  753. spl_autoload_register($autoload);
  754. } elseif (function_exists('spl_autoload_unregister')) {
  755. if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
  756. // prepend flag exists, no need for shenanigans
  757. spl_autoload_register($autoload, true, true);
  758. } else {
  759. $buggy = version_compare(PHP_VERSION, '5.2.11', '<');
  760. $compat = version_compare(PHP_VERSION, '5.1.2', '<=') &&
  761. version_compare(PHP_VERSION, '5.1.0', '>=');
  762. foreach ($funcs as $func) {
  763. if ($buggy && is_array($func)) {
  764. // :TRICKY: There are some compatibility issues and some
  765. // places where we need to error out
  766. $reflector = new ReflectionMethod($func[0], $func[1]);
  767. if (!$reflector->isStatic()) {
  768. throw new Exception('
  769. HTML Purifier autoloader registrar is not compatible
  770. with non-static object methods due to PHP Bug #44144;
  771. Please do not use HTMLPurifier.autoload.php (or any
  772. file that includes this file); instead, place the code:
  773. spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\'))
  774. after your own autoloaders.
  775. ');
  776. }
  777. // Suprisingly, spl_autoload_register supports the
  778. // Class::staticMethod callback format, although call_user_func doesn't
  779. if ($compat) $func = implode('::', $func);
  780. }
  781. spl_autoload_unregister($func);
  782. }
  783. spl_autoload_register($autoload);
  784. foreach ($funcs as $func) spl_autoload_register($func);
  785. }
  786. }
  787. }
  788. }
  789. /**
  790. * Super-class for definition datatype objects, implements serialization
  791. * functions for the class.
  792. */
  793. abstract class HTMLPurifier_Definition
  794. {
  795. /**
  796. * Has setup() been called yet?
  797. */
  798. public $setup = false;
  799. /**
  800. * If true, write out the final definition object to the cache after
  801. * setup. This will be true only if all invocations to get a raw
  802. * definition object are also optimized. This does not cause file
  803. * system thrashing because on subsequent calls the cached object
  804. * is used and any writes to the raw definition object are short
  805. * circuited. See enduser-customize.html for the high-level
  806. * picture.
  807. */
  808. public $optimized = null;
  809. /**
  810. * What type of definition is it?
  811. */
  812. public $type;
  813. /**
  814. * Sets up the definition object into the final form, something
  815. * not done by the constructor
  816. * @param $config HTMLPurifier_Config instance
  817. */
  818. abstract protected function doSetup($config);
  819. /**
  820. * Setup function that aborts if already setup
  821. * @param $config HTMLPurifier_Config instance
  822. */
  823. public function setup($config) {
  824. if ($this->setup) return;
  825. $this->setup = true;
  826. $this->doSetup($config);
  827. }
  828. }
  829. /**
  830. * Defines allowed CSS attributes and what their values are.
  831. * @see HTMLPurifier_HTMLDefinition
  832. */
  833. class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
  834. {
  835. public $type = 'CSS';
  836. /**
  837. * Assoc array of attribute name to definition object.
  838. */
  839. public $info = array();
  840. /**
  841. * Constructs the info array. The meat of this class.
  842. */
  843. protected function doSetup($config) {
  844. $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum(
  845. array('left', 'right', 'center', 'justify'), false);
  846. $border_style =
  847. $this->info['border-bottom-style'] =
  848. $this->info['border-right-style'] =
  849. $this->info['border-left-style'] =
  850. $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum(
  851. array('none', 'hidden', 'dotted', 'dashed', 'solid', 'double',
  852. 'groove', 'ridge', 'inset', 'outset'), false);
  853. $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style);
  854. $this->info['clear'] = new HTMLPurifier_AttrDef_Enum(
  855. array('none', 'left', 'right', 'both'), false);
  856. $this->info['float'] = new HTMLPurifier_AttrDef_Enum(
  857. array('none', 'left', 'right'), false);
  858. $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum(
  859. array('normal', 'italic', 'oblique'), false);
  860. $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum(
  861. array('normal', 'small-caps'), false);
  862. $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite(
  863. array(
  864. new HTMLPurifier_AttrDef_Enum(array('none')),
  865. new HTMLPurifier_AttrDef_CSS_URI()
  866. )
  867. );
  868. $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum(
  869. array('inside', 'outside'), false);
  870. $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum(
  871. array('disc', 'circle', 'square', 'decimal', 'lower-roman',
  872. 'upper-roman', 'lower-alpha', 'upper-alpha', 'none'), false);
  873. $this->info['list-style-image'] = $uri_or_none;
  874. $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config);
  875. $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum(
  876. array('capitalize', 'uppercase', 'lowercase', 'none'), false);
  877. $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color();
  878. $this->info['background-image'] = $uri_or_none;
  879. $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum(
  880. array('repeat', 'repeat-x', 'repeat-y', 'no-repeat')
  881. );
  882. $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum(
  883. array('scroll', 'fixed')
  884. );
  885. $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition();
  886. $border_color =
  887. $this->info['border-top-color'] =
  888. $this->info['border-bottom-color'] =
  889. $this->info['border-left-color'] =
  890. $this->info['border-right-color'] =
  891. $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  892. new HTMLPurifier_AttrDef_Enum(array('transparent')),
  893. new HTMLPurifier_AttrDef_CSS_Color()
  894. ));
  895. $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config);
  896. $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color);
  897. $border_width =
  898. $this->info['border-top-width'] =
  899. $this->info['border-bottom-width'] =
  900. $this->info['border-left-width'] =
  901. $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  902. new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')),
  903. new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative
  904. ));
  905. $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width);
  906. $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  907. new HTMLPurifier_AttrDef_Enum(array('normal')),
  908. new HTMLPurifier_AttrDef_CSS_Length()
  909. ));
  910. $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  911. new HTMLPurifier_AttrDef_Enum(array('normal')),
  912. new HTMLPurifier_AttrDef_CSS_Length()
  913. ));
  914. $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  915. new HTMLPurifier_AttrDef_Enum(array('xx-small', 'x-small',
  916. 'small', 'medium', 'large', 'x-large', 'xx-large',
  917. 'larger', 'smaller')),
  918. new HTMLPurifier_AttrDef_CSS_Percentage(),
  919. new HTMLPurifier_AttrDef_CSS_Length()
  920. ));
  921. $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  922. new HTMLPurifier_AttrDef_Enum(array('normal')),
  923. new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives
  924. new HTMLPurifier_AttrDef_CSS_Length('0'),
  925. new HTMLPurifier_AttrDef_CSS_Percentage(true)
  926. ));
  927. $margin =
  928. $this->info['margin-top'] =
  929. $this->info['margin-bottom'] =
  930. $this->info['margin-left'] =
  931. $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  932. new HTMLPurifier_AttrDef_CSS_Length(),
  933. new HTMLPurifier_AttrDef_CSS_Percentage(),
  934. new HTMLPurifier_AttrDef_Enum(array('auto'))
  935. ));
  936. $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin);
  937. // non-negative
  938. $padding =
  939. $this->info['padding-top'] =
  940. $this->info['padding-bottom'] =
  941. $this->info['padding-left'] =
  942. $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  943. new HTMLPurifier_AttrDef_CSS_Length('0'),
  944. new HTMLPurifier_AttrDef_CSS_Percentage(true)
  945. ));
  946. $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding);
  947. $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  948. new HTMLPurifier_AttrDef_CSS_Length(),
  949. new HTMLPurifier_AttrDef_CSS_Percentage()
  950. ));
  951. $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite(array(
  952. new HTMLPurifier_AttrDef_CSS_Length('0'),
  953. new HTMLPurifier_AttrDef_CSS_Percentage(true),
  954. new HTMLPurifier_AttrDef_Enum(array('auto'))
  955. ));
  956. $max = $config->get('CSS.MaxImgLength');
  957. $this->info['width'] =
  958. $this->info['height'] =
  959. $max === null ?
  960. $trusted_wh :
  961. new HTMLPurifier_AttrDef_Switch('img',
  962. // For img tags:
  963. new HTMLPurifier_AttrDef_CSS_Composite(array(
  964. new HTMLPurifier_AttrDef_CSS_Length('0', $max),
  965. new HTMLPurifier_AttrDef_Enum(array('auto'))
  966. )),
  967. // For everyone else:
  968. $trusted_wh
  969. );
  970. $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration();
  971. $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily();
  972. // this could use specialized code
  973. $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum(
  974. array('normal', 'bold', 'bolder', 'lighter', '100', '200', '300',
  975. '400', '500', '600', '700', '800', '900'), false);
  976. // MUST be called after other font properties, as it references
  977. // a CSSDefinition object
  978. $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config);
  979. // same here
  980. $this->info['border'] =
  981. $this->info['border-bottom'] =
  982. $this->info['border-top'] =
  983. $this->info['border-left'] =
  984. $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config);
  985. $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum(array(
  986. 'collapse', 'separate'));
  987. $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum(array(
  988. 'top', 'bottom'));
  989. $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum(array(
  990. 'auto', 'fixed'));
  991. $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  992. new HTMLPurifier_AttrDef_Enum(array('baseline', 'sub', 'super',
  993. 'top', 'text-top', 'middle', 'bottom', 'text-bottom')),
  994. new HTMLPurifier_AttrDef_CSS_Length(),
  995. new HTMLPurifier_AttrDef_CSS_Percentage()
  996. ));
  997. $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2);
  998. // These CSS properties don't work on many browsers, but we live
  999. // in THE FUTURE!
  1000. $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum(array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line'));
  1001. if ($config->get('CSS.Proprietary')) {
  1002. $this->doSetupProprietary($config);
  1003. }
  1004. if ($config->get('CSS.AllowTricky')) {
  1005. $this->doSetupTricky($config);
  1006. }
  1007. if ($config->get('CSS.Trusted')) {
  1008. $this->doSetupTrusted($config);
  1009. }
  1010. $allow_important = $config->get('CSS.AllowImportant');
  1011. // wrap all attr-defs with decorator that handles !important
  1012. foreach ($this->info as $k => $v) {
  1013. $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important);
  1014. }
  1015. $this->setupConfigStuff($config);
  1016. }
  1017. protected function doSetupProprietary($config) {
  1018. // Internet Explorer only scrollbar colors
  1019. $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
  1020. $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color();
  1021. $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
  1022. $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color();
  1023. $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color();
  1024. $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
  1025. // technically not proprietary, but CSS3, and no one supports it
  1026. $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
  1027. $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
  1028. $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
  1029. // only opacity, for now
  1030. $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter();
  1031. // more CSS3
  1032. $this->info['page-break-after'] =
  1033. $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum(array('auto','always','avoid','left','right'));
  1034. $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto','avoid'));
  1035. }
  1036. protected function doSetupTricky($config) {
  1037. $this->info['display'] = new HTMLPurifier_AttrDef_Enum(array(
  1038. 'inline', 'block', 'list-item', 'run-in', 'compact',
  1039. 'marker', 'table', 'inline-block', 'inline-table', 'table-row-group',
  1040. 'table-header-group', 'table-footer-group', 'table-row',
  1041. 'table-column-group', 'table-column', 'table-cell', 'table-caption', 'none'
  1042. ));
  1043. $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum(array(
  1044. 'visible', 'hidden', 'collapse'
  1045. ));
  1046. $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll'));
  1047. }
  1048. protected function doSetupTrusted($config) {
  1049. $this->info['position'] = new HTMLPurifier_AttrDef_Enum(array(
  1050. 'static', 'relative', 'absolute', 'fixed'
  1051. ));
  1052. $this->info['top'] =
  1053. $this->info['left'] =
  1054. $this->info['right'] =
  1055. $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  1056. new HTMLPurifier_AttrDef_CSS_Length(),
  1057. new HTMLPurifier_AttrDef_CSS_Percentage(),
  1058. new HTMLPurifier_AttrDef_Enum(array('auto')),
  1059. ));
  1060. $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
  1061. new HTMLPurifier_AttrDef_Integer(),
  1062. new HTMLPurifier_AttrDef_Enum(array('auto')),
  1063. ));
  1064. }
  1065. /**
  1066. * Performs extra config-based processing. Based off of
  1067. * HTMLPurifier_HTMLDefinition.
  1068. * @todo Refactor duplicate elements into common class (probably using
  1069. * composition, not inheritance).
  1070. */
  1071. protected function setupConfigStuff($config) {
  1072. // setup allowed elements
  1073. $support = "(for information on implementing this, see the ".
  1074. "support forums) ";
  1075. $allowed_properties = $config->get('CSS.AllowedProperties');
  1076. if ($allowed_properties !== null) {
  1077. foreach ($this->info as $name => $d) {
  1078. if(!isset($allowed_properties[$name])) unset($this->info[$name]);
  1079. unset($allowed_properties[$name]);
  1080. }
  1081. // emit errors
  1082. foreach ($allowed_properties as $name => $d) {
  1083. // :TODO: Is this htmlspecialchars() call really necessary?
  1084. $name = htmlspecialchars($name);
  1085. trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING);
  1086. }
  1087. }
  1088. $forbidden_properties = $config->get('CSS.ForbiddenProperties');
  1089. if ($forbidden_properties !== null) {
  1090. foreach ($this->info as $name => $d) {
  1091. if (isset($forbidden_properties[$name])) {
  1092. unset($this->info[$name]);
  1093. }
  1094. }
  1095. }
  1096. }
  1097. }
  1098. /**
  1099. * Defines allowed child nodes and validates tokens against it.
  1100. */
  1101. abstract class HTMLPurifier_ChildDef
  1102. {
  1103. /**
  1104. * Type of child definition, usually right-most part of class name lowercase.
  1105. * Used occasionally in terms of context.
  1106. */
  1107. public $type;
  1108. /**
  1109. * Bool that indicates whether or not an empty array of children is okay
  1110. *
  1111. * This is necessary for redundant checking when changes affecting
  1112. * a child node may cause a parent node to now be disallowed.
  1113. */
  1114. public $allow_empty;
  1115. /**
  1116. * Lookup array of all elements that this definition could possibly allow
  1117. */
  1118. public $elements = array();
  1119. /**
  1120. * Get lookup of tag names that should not close this element automatically.
  1121. * All other elements will do so.
  1122. */
  1123. public function getAllowedElements($config) {
  1124. return $this->elements;
  1125. }
  1126. /**
  1127. * Validates nodes according to definition and returns modification.
  1128. *
  1129. * @param $tokens_of_children Array of HTMLPurifier_Token
  1130. * @param $config HTMLPurifier_Config object
  1131. * @param $context HTMLPurifier_Context object
  1132. * @return bool true to leave nodes as is
  1133. * @return bool false to remove parent node
  1134. * @return array of replacement child tokens
  1135. */
  1136. abstract public function validateChildren($tokens_of_children, $config, $context);
  1137. }
  1138. /**
  1139. * Configuration object that triggers customizable behavior.
  1140. *
  1141. * @warning This class is strongly defined: that means that the class
  1142. * will fail if an undefined directive is retrieved or set.
  1143. *
  1144. * @note Many classes that could (although many times don't) use the
  1145. * configuration object make it a mandatory parameter. This is
  1146. * because a configuration object should always be forwarded,
  1147. * otherwise, you run the risk of missing a parameter and then
  1148. * being stumped when a configuration directive doesn't work.
  1149. *
  1150. * @todo Reconsider some of the public member variables
  1151. */
  1152. class HTMLPurifier_Config
  1153. {
  1154. /**
  1155. * HTML Purifier's version
  1156. */
  1157. public $version = '4.5.0';
  1158. /**
  1159. * Bool indicator whether or not to automatically finalize
  1160. * the object if a read operation is done
  1161. */
  1162. public $autoFinalize = true;
  1163. // protected member variables
  1164. /**
  1165. * Namespace indexed array of serials for specific namespaces (see
  1166. * getSerial() for more info).
  1167. */
  1168. protected $serials = array();
  1169. /**
  1170. * Serial for entire configuration object
  1171. */
  1172. protected $serial;
  1173. /**
  1174. * Parser for variables
  1175. */
  1176. protected $parser = null;
  1177. /**
  1178. * Reference HTMLPurifier_ConfigSchema for value checking
  1179. * @note This is public for introspective purposes. Please don't
  1180. * abuse!
  1181. */
  1182. public $def;
  1183. /**
  1184. * Indexed array of definitions
  1185. */
  1186. protected $definitions;
  1187. /**
  1188. * Bool indicator whether or not config is finalized
  1189. */
  1190. protected $finalized = false;
  1191. /**
  1192. * Property list containing configuration directives.
  1193. */
  1194. protected $plist;
  1195. /**
  1196. * Whether or not a set is taking place due to an
  1197. * alias lookup.
  1198. */
  1199. private $aliasMode;
  1200. /**
  1201. * Set to false if you do not want line and file numbers in errors
  1202. * (useful when unit testing). This will also compress some errors
  1203. * and exceptions.
  1204. */
  1205. public $chatty = true;
  1206. /**
  1207. * Current lock; only gets to this namespace are allowed.
  1208. */
  1209. private $lock;
  1210. /**
  1211. * @param $definition HTMLPurifier_ConfigSchema that defines what directives
  1212. * are allowed.
  1213. */
  1214. public function __construct($definition, $parent = null) {
  1215. $parent = $parent ? $parent : $definition->defaultPlist;
  1216. $this->plist = new HTMLPurifier_PropertyList($parent);
  1217. $this->def = $definition; // keep a copy around for checking
  1218. $this->parser = new HTMLPurifier_VarParser_Flexible();
  1219. }
  1220. /**
  1221. * Convenience constructor that creates a config object based on a mixed var
  1222. * @param mixed $config Variable that defines the state of the config
  1223. * object. Can be: a HTMLPurifier_Config() object,
  1224. * an array of directives based on loadArray(),
  1225. * or a string filename of an ini file.
  1226. * @param HTMLPurifier_ConfigSchema Schema object
  1227. * @return Configured HTMLPurifier_Config object
  1228. */
  1229. public static function create($config, $schema = null) {
  1230. if ($config instanceof HTMLPurifier_Config) {
  1231. // pass-through
  1232. return $config;
  1233. }
  1234. if (!$schema) {
  1235. $ret = HTMLPurifier_Config::createDefault();
  1236. } else {
  1237. $ret = new HTMLPurifier_Config($schema);
  1238. }
  1239. if (is_string($config)) $ret->loadIni($config);
  1240. elseif (is_array($config)) $ret->loadArray($config);
  1241. return $ret;
  1242. }
  1243. /**
  1244. * Creates a new config object that inherits from a previous one.
  1245. * @param HTMLPurifier_Config $config Configuration object to inherit
  1246. * from.
  1247. * @return HTMLPurifier_Config object with $config as its parent.
  1248. */
  1249. public static function inherit(HTMLPurifier_Config $config) {
  1250. return new HTMLPurifier_Config($config->def, $config->plist);
  1251. }
  1252. /**
  1253. * Convenience constructor that creates a default configuration object.
  1254. * @return Default HTMLPurifier_Config object.
  1255. */
  1256. public static function createDefault() {
  1257. $definition = HTMLPurifier_ConfigSchema::instance();
  1258. $config = new HTMLPurifier_Config($definition);
  1259. return $config;
  1260. }
  1261. /**
  1262. * Retreives a value from the configuration.
  1263. * @param $key String key
  1264. */
  1265. public function get($key, $a = null) {
  1266. if ($a !== null) {
  1267. $this->triggerError("Using deprecated API: use \$config->get('$key.$a') instead", E_USER_WARNING);
  1268. $key = "$key.$a";
  1269. }
  1270. if (!$this->finalized) $this->autoFinalize();
  1271. if (!isset($this->def->info[$key])) {
  1272. // can't add % due to SimpleTest bug
  1273. $this->triggerError('Cannot retrieve value of undefined directive ' . htmlspecialchars($key),
  1274. E_USER_WARNING);
  1275. return;
  1276. }
  1277. if (isset($this->def->info[$key]->isAlias)) {
  1278. $d = $this->def->info[$key];
  1279. $this->triggerError('Cannot get value from aliased directive, use real name ' . $d->key,
  1280. E_USER_ERROR);
  1281. return;
  1282. }
  1283. if ($this->lock) {
  1284. list($ns) = explode('.', $key);
  1285. if ($ns !== $this->lock) {
  1286. $this->triggerError('Cannot get value of namespace ' . $ns . ' when lock for ' . $this->lock . ' is active, this probably indicates a Definition setup method is accessing directives that are not within its namespace', E_USER_ERROR);
  1287. return;
  1288. }
  1289. }
  1290. return $this->plist->get($key);
  1291. }
  1292. /**
  1293. * Retreives an array of directives to values from a given namespace
  1294. * @param $namespace String namespace
  1295. */
  1296. public function getBatch($namespace) {
  1297. if (!$this->finalized) $this->autoFinalize();
  1298. $full = $this->getAll();
  1299. if (!isset($full[$namespace])) {
  1300. $this->triggerError('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace),
  1301. E_USER_WARNING);
  1302. return;
  1303. }
  1304. return $full[$namespace];
  1305. }
  1306. /**
  1307. * Returns a SHA-1 signature of a segment of the configuration object
  1308. * that uniquely identifies that particular configuration
  1309. * @note Revision is handled specially and is removed from the batch
  1310. * before processing!
  1311. * @param $namespace Namespace to get serial for
  1312. */
  1313. public function getBatchSerial($namespace) {
  1314. if (empty($this->serials[$namespace])) {
  1315. $batch = $this->getBatch($namespace);
  1316. unset($batch['DefinitionRev']);
  1317. $this->serials[$namespace] = sha1(serialize($batch));
  1318. }
  1319. return $this->serials[$namespace];
  1320. }
  1321. /**
  1322. * Returns a SHA-1 signature for the entire configuration object
  1323. * that uniquely identifies that particular configuration
  1324. */
  1325. public function getSerial() {
  1326. if (empty($this->serial)) {
  1327. $this->serial = sha1(serialize($this->getAll()));
  1328. }
  1329. return $this->serial;
  1330. }
  1331. /**
  1332. * Retrieves all directives, organized by namespace
  1333. * @warning This is a pretty inefficient function, avoid if you can
  1334. */
  1335. public function getAll() {
  1336. if (!$this->finalized) $this->autoFinalize();
  1337. $ret = array();
  1338. foreach ($this->plist->squash() as $name => $value) {
  1339. list($ns, $key) = explode('.', $name, 2);
  1340. $ret[$ns][$key] = $value;
  1341. }
  1342. return $ret;
  1343. }
  1344. /**
  1345. * Sets a value to configuration.
  1346. * @param $key String key
  1347. * @param $value Mixed value
  1348. */
  1349. public function set($key, $value, $a = null) {
  1350. if (strpos($key, '.') === false) {
  1351. $namespace = $key;
  1352. $directive = $value;
  1353. $value = $a;
  1354. $key = "$key.$directive";
  1355. $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE);
  1356. } else {
  1357. list($namespace) = explode('.', $key);
  1358. }
  1359. if ($this->isFinalized('Cannot set directive after finalization')) return;
  1360. if (!isset($this->def->info[$key])) {
  1361. $this->triggerError('Cannot set undefined directive ' . htmlspecialchars($key) . ' to value',
  1362. E_USER_WARNING);
  1363. return;
  1364. }
  1365. $def = $this->def->info[$key];
  1366. if (isset($def->isAlias)) {
  1367. if ($this->aliasMode) {
  1368. $this->triggerError('Double-aliases not allowed, please fix '.
  1369. 'ConfigSchema bug with' . $key, E_USER_ERROR);
  1370. return;
  1371. }
  1372. $this->aliasMode = true;
  1373. $this->set($def->key, $value);
  1374. $this->aliasMode = false;
  1375. $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE);
  1376. return;
  1377. }
  1378. // Raw type might be negative when using the fully optimized form
  1379. // of stdclass, which indicates allow_null == true
  1380. $rtype = is_int($def) ? $def : $def->type;
  1381. if ($rtype < 0) {
  1382. $type = -$rtype;
  1383. $allow_null = true;
  1384. } else {
  1385. $type = $rtype;
  1386. $allow_null = isset($def->allow_null);
  1387. }
  1388. try {
  1389. $value = $this->parser->parse($value, $type, $allow_null);
  1390. } catch (HTMLPurifier_VarParserException $e) {
  1391. $this->triggerError('Value for ' . $key . ' is of invalid type, should be ' . HTMLPurifier_VarParser::getTypeName($type), E_USER_WARNING);
  1392. return;
  1393. }
  1394. if (is_string($value) && is_object($def)) {
  1395. // resolve value alias if defined
  1396. if (isset($def->aliases[$value])) {
  1397. $value = $def->aliases[$value];
  1398. }
  1399. // check to see if the value is allowed
  1400. if (isset($def->allowed) && !isset($def->allowed[$value])) {
  1401. $this->triggerError('Value not supported, valid values are: ' .
  1402. $this->_listify($def->allowed), E_USER_WARNING);
  1403. return;
  1404. }
  1405. }
  1406. $this->plist->set($key, $value);
  1407. // reset definitions if the directives they depend on changed
  1408. // this is a very costly process, so it's discouraged
  1409. // with finalization
  1410. if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') {
  1411. $this->definitions[$namespace] = null;
  1412. }
  1413. $this->serials[$namespace] = false;
  1414. }
  1415. /**
  1416. * Convenience function for error reporting
  1417. */
  1418. private function _listify($lookup) {
  1419. $list = array();
  1420. foreach ($lookup as $name => $b) $list[] = $name;
  1421. return implode(', ', $list);
  1422. }
  1423. /**
  1424. * Retrieves object reference to the HTML definition.
  1425. * @param $raw Return a copy that has not been setup yet. Must be
  1426. * called before it's been setup, otherwise won't work.
  1427. * @param $optimized If true, this method may return null, to
  1428. * indicate that a cached version of the modified
  1429. * definition object is available and no further edits
  1430. * are necessary. Consider using
  1431. * maybeGetRawHTMLDefinition, which is more explicitly
  1432. * named, instead.
  1433. */
  1434. public function getHTMLDefinition($raw = false, $optimized = false) {
  1435. return $this->getDefinition('HTML', $raw, $optimized);
  1436. }
  1437. /**
  1438. * Retrieves object reference to the CSS definition
  1439. * @param $raw Return a copy that has not been setup yet. Must be
  1440. * called before it's been setup, otherwise won't work.
  1441. * @param $optimized If true, this method may return null, to
  1442. * indicate that a cached version of the modified
  1443. * definition object is available and no further edits
  1444. * are necessary. Consider using
  1445. * maybeGetRawCSSDefinition, which is more explicitly
  1446. * named, instead.
  1447. */
  1448. public function getCSSDefinition($raw = false, $optimized = false) {
  1449. return $this->getDefinition('CSS', $raw, $optimized);
  1450. }
  1451. /**
  1452. * Retrieves object reference to the URI definition
  1453. * @param $raw Return a copy that has not been setup yet. Must be
  1454. * called before it's been setup, otherwise won't work.
  1455. * @param $optimized If true, this method may return null, to
  1456. * indicate that a cached version of the modified
  1457. * definition object is available and no further edits
  1458. * are necessary. Consider using
  1459. * maybeGetRawURIDefinition, which is more explicitly
  1460. * named, instead.
  1461. */
  1462. public function getURIDefinition($raw = false, $optimized = false) {
  1463. return $this->getDefinition('URI', $raw, $optimized);
  1464. }
  1465. /**
  1466. * Retrieves a definition
  1467. * @param $type Type of definition: HTML, CSS, etc
  1468. * @param $raw Whether or not definition should be returned raw
  1469. * @param $optimized Only has an effect when $raw is true. Whether
  1470. * or not to return null if the result is already present in
  1471. * the cache. This is off by default for backwards
  1472. * compatibility reasons, but you need to do things this
  1473. * way in order to ensure that caching is done properly.
  1474. * Check out enduser-customize.html for more details.
  1475. * We probably won't ever change this default, as much as the
  1476. * maybe semantics is the "right thing to do."
  1477. */
  1478. public function getDefinition($type, $raw = false, $optimized = false) {
  1479. if ($optimized && !$raw) {
  1480. throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false");
  1481. }
  1482. if (!$this->finalized) $this->autoFinalize();
  1483. // temporarily suspend locks, so we can handle recursive definition calls
  1484. $lock = $this->lock;
  1485. $this->lock = null;
  1486. $factory = HTMLPurifier_DefinitionCacheFactory::instance();
  1487. $cache = $factory->create($type, $this);
  1488. $this->lock = $lock;
  1489. if (!$raw) {
  1490. // full definition
  1491. // ---------------
  1492. // check if definition is in memory
  1493. if (!empty($this->definitions[$type])) {
  1494. $def = $this->definitions[$type];
  1495. // check if the definition is setup
  1496. if ($def->setup) {
  1497. return $def;
  1498. } else {
  1499. $def->setup($this);
  1500. if ($def->optimized) $cache->add($def, $this);
  1501. return $def;
  1502. }
  1503. }
  1504. // check if definition is in cache
  1505. $def = $cache->get($this);
  1506. if ($def) {
  1507. // definition in cache, save to memory and return it
  1508. $this->definitions[$type] = $def;
  1509. return $def;
  1510. }
  1511. // initialize it
  1512. $def = $this->initDefinition($type);
  1513. // set it up
  1514. $this->lock = $type;
  1515. $def->setup($this);
  1516. $this->lock = null;
  1517. // save in cache
  1518. $cache->add($def, $this);
  1519. // return it
  1520. return $def;
  1521. } else {
  1522. // raw definition
  1523. // --------------
  1524. // check preconditions
  1525. $def = null;
  1526. if ($optimized) {
  1527. if (is_null($this->get($type . '.DefinitionID'))) {
  1528. // fatally error out if definition ID not set
  1529. throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID");
  1530. }
  1531. }
  1532. if (!empty($this->definitions[$type])) {
  1533. $def = $this->definitions[$type];
  1534. if ($def->setup && !$optimized) {
  1535. $extra = $this->chatty ? " (try moving this code block earlier in your initialization)" : "";
  1536. throw new HTMLPurifier_Exception("Cannot retrieve raw definition after it has already been setup" . $extra);
  1537. }
  1538. if ($def->optimized === null) {
  1539. $extra = $this->chatty ? " (try flushing your cache)" : "";
  1540. throw new HTMLPurifier_Exception("Optimization status of definition is unknown" . $extra);
  1541. }
  1542. if ($def->optimized !== $optimized) {
  1543. $msg = $optimized ? "optimized" : "unoptimized";
  1544. $extra = $this->chatty ? " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" : "";
  1545. throw new HTMLPurifier_Exception("Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra);
  1546. }
  1547. }
  1548. // check if definition was in memory
  1549. if ($def) {
  1550. if ($def->setup) {
  1551. // invariant: $optimized === true (checked above)
  1552. return null;
  1553. } else {
  1554. return $def;
  1555. }
  1556. }
  1557. // if optimized, check if definition was in cache
  1558. // (because we do the memory check first, this formulation
  1559. // is prone to cache slamming, but I think
  1560. // guaranteeing that either /all/ of the raw
  1561. // setup code or /none/ of it is run is more important.)
  1562. if ($optimized) {
  1563. // This code path only gets run once; once we put
  1564. // something in $definitions (which is guaranteed by the
  1565. // trailing code), we always short-circuit above.
  1566. $def = $cache->get($this);
  1567. if ($def) {
  1568. // save the full definition for later, but don't
  1569. // return it yet
  1570. $this->definitions[$type] = $def;
  1571. return null;
  1572. }
  1573. }
  1574. // check invariants for creation
  1575. if (!$optimized) {
  1576. if (!is_null($this->get($type . '.DefinitionID'))) {
  1577. if ($this->chatty) {
  1578. $this->triggerError("Due to a documentation error in previous version of HTML Purifier, your definitions are not being cached. If this is OK, you can remove the %$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, modify your code to use maybeGetRawDefinition, and test if the returned value is null before making any edits (if it is null, that means that a cached version is available, and no raw operations are necessary). See <a href='http://htmlpurifier.org/docs/enduser-customize.html#optimized'>Customize</a> for more details", E_USER_WARNING);
  1579. } else {
  1580. $this->triggerError("Useless DefinitionID declaration", E_USER_WARNING);
  1581. }
  1582. }
  1583. }
  1584. // initialize it
  1585. $def = $this->initDefinition($type);
  1586. $def->optimized = $optimized;
  1587. return $def;
  1588. }
  1589. throw new HTMLPurifier_Exception("The impossible happened!");
  1590. }
  1591. private function initDefinition($type) {
  1592. // quick checks failed, let's create the object
  1593. if ($type == 'HTML') {
  1594. $def = new HTMLPurifier_HTMLDefinition();
  1595. } elseif ($type == 'CSS') {
  1596. $def = new HTMLPurifier_CSSDefinition();
  1597. } elseif ($type == 'URI') {
  1598. $def = new HTMLPurifier_URIDefinition();
  1599. } else {
  1600. throw new HTMLPurifier_Exception("Definition of $type type not supported");
  1601. }
  1602. $this->definitions[$type] = $def;
  1603. return $def;
  1604. }
  1605. public function maybeGetRawDefinition($name) {
  1606. return $this->getDefinition($name, true, true);
  1607. }
  1608. public function maybeGetRawHTMLDefinition() {
  1609. return $this->getDefinition('HTML', true, true);
  1610. }
  1611. public function maybeGetRawCSSDefinition() {
  1612. return $this->getDefinition('CSS', true, true);
  1613. }
  1614. public function maybeGetRawURIDefinition() {
  1615. return $this->getDefinition('URI', true, true);
  1616. }
  1617. /**
  1618. * Loads configuration values from an array with the following structure:
  1619. * Namespace.Directive => Value
  1620. * @param $config_array Configuration associative array
  1621. */
  1622. public function loadArray($config_array) {
  1623. if ($this->isFinalized('Cannot load directives after finalization')) return;
  1624. foreach ($config_array as $key => $value) {
  1625. $key = str_replace('_', '.', $key);
  1626. if (strpos($key, '.') !== false) {
  1627. $this->set($key, $value);
  1628. } else {
  1629. $namespace = $key;
  1630. $namespace_values = $value;
  1631. foreach ($namespace_values as $directive => $value) {
  1632. $this->set($namespace .'.'. $directive, $value);
  1633. }
  1634. }
  1635. }
  1636. }
  1637. /**
  1638. * Returns a list of array(namespace, directive) for all directives
  1639. * that are allowed in a web-form context as per an allowed
  1640. * namespaces/directives list.
  1641. * @param $allowed List of allowed namespaces/directives
  1642. */
  1643. public static function getAllowedDirectivesForForm($allowed, $schema = null) {
  1644. if (!$schema) {
  1645. $schema = HTMLPurifier_ConfigSchema::instance();
  1646. }
  1647. if ($allowed !== true) {
  1648. if (is_string($allowed)) $allowed = array($allowed);
  1649. $allowed_ns = array();
  1650. $allowed_directives = array();
  1651. $blacklisted_directives = array();
  1652. foreach ($allowed as $ns_or_directive) {
  1653. if (strpos($ns_or_directive, '.') !== false) {
  1654. // directive
  1655. if ($ns_or_directive[0] == '-') {
  1656. $blacklisted_directives[substr($ns_or_directive, 1)] = true;
  1657. } else {
  1658. $allowed_directives[$ns_or_directive] = true;
  1659. }
  1660. } else {
  1661. // namespace
  1662. $allowed_ns[$ns_or_directive] = true;
  1663. }
  1664. }
  1665. }
  1666. $ret = array();
  1667. foreach ($schema->info as $key => $def) {
  1668. list($ns, $directive) = explode('.', $key, 2);
  1669. if ($allowed !== true) {
  1670. if (isset($blacklisted_directives["$ns.$directive"])) continue;
  1671. if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) continue;
  1672. }
  1673. if (isset($def->isAlias)) continue;
  1674. if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') continue;
  1675. $ret[] = array($ns, $directive);
  1676. }
  1677. return $ret;
  1678. }
  1679. /**
  1680. * Loads configuration values from $_GET/$_POST that were posted
  1681. * via ConfigForm
  1682. * @param $array $_GET or $_POST array to import
  1683. * @param $index Index/name that the config variables are in
  1684. * @param $allowed List of allowed namespaces/directives
  1685. * @param $mq_fix Boolean whether or not to enable magic quotes fix
  1686. * @param $schema Instance of HTMLPurifier_ConfigSchema to use, if not global copy
  1687. */
  1688. public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
  1689. $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema);
  1690. $config = HTMLPurifier_Config::create($ret, $schema);
  1691. return $config;
  1692. }
  1693. /**
  1694. * Merges in configuration values from $_GET/$_POST to object. NOT STATIC.
  1695. * @note Same parameters as loadArrayFromForm
  1696. */
  1697. public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) {
  1698. $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def);
  1699. $this->loadArray($ret);
  1700. }
  1701. /**
  1702. * Prepares an array from a form into something usable for the more
  1703. * strict parts of HTMLPurifier_Config
  1704. */
  1705. public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
  1706. if ($index !== false) $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array();
  1707. $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc();
  1708. $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema);
  1709. $ret = array();
  1710. foreach ($allowed as $key) {
  1711. list($ns, $directive) = $key;
  1712. $skey = "$ns.$directive";
  1713. if (!empty($array["Null_$skey"])) {
  1714. $ret[$ns][$directive] = null;
  1715. continue;
  1716. }
  1717. if (!isset($array[$skey])) continue;
  1718. $value = $mq ? stripslashes($array[$skey]) : $array[$skey];
  1719. $ret[$ns][$directive] = $value;
  1720. }
  1721. return $ret;
  1722. }
  1723. /**
  1724. * Loads configuration values from an ini file
  1725. * @param $filename Name of ini file
  1726. */
  1727. public function loadIni($filename) {
  1728. if ($this->isFinalized('Cannot load directives after finalization')) return;
  1729. $array = parse_ini_file($filename, true);
  1730. $this->loadArray($array);
  1731. }
  1732. /**
  1733. * Checks whether or not the configuration object is finalized.
  1734. * @param $error String error message, or false for no error
  1735. */
  1736. public function isFinalized($error = false) {
  1737. if ($this->finalized && $error) {
  1738. $this->triggerError($error, E_USER_ERROR);
  1739. }
  1740. return $this->finalized;
  1741. }
  1742. /**
  1743. * Finalizes configuration only if auto finalize is on and not
  1744. * already finalized
  1745. */
  1746. public function autoFinalize() {
  1747. if ($this->autoFinalize) {
  1748. $this->finalize();
  1749. } else {
  1750. $this->plist->squash(true);
  1751. }
  1752. }
  1753. /**
  1754. * Finalizes a configuration object, prohibiting further change
  1755. */
  1756. public function finalize() {
  1757. $this->finalized = true;
  1758. $this->parser = null;
  1759. }
  1760. /**
  1761. * Produces a nicely formatted error message by supplying the
  1762. * stack frame information OUTSIDE of HTMLPurifier_Config.
  1763. */
  1764. protected function triggerError($msg, $no) {
  1765. // determine previous stack frame
  1766. $extra = '';
  1767. if ($this->chatty) {
  1768. $trace = debug_backtrace();
  1769. // zip(tail(trace), trace) -- but PHP is not Haskell har har
  1770. for ($i = 0, $c = count($trace); $i < $c - 1; $i++) {
  1771. // XXX this is not correct on some versions of HTML Purifier
  1772. if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') {
  1773. continue;
  1774. }
  1775. $frame = $trace[$i];
  1776. $extra = " invoked on line {$frame['line']} in file {$frame['file']}";
  1777. break;
  1778. }
  1779. }
  1780. trigger_error($msg . $extra, $no);
  1781. }
  1782. /**
  1783. * Returns a serialized form of the configuration object that can
  1784. * be reconstituted.
  1785. */
  1786. public function serialize() {
  1787. $this->getDefinition('HTML');
  1788. $this->getDefinition('CSS');
  1789. $this->getDefinition('URI');
  1790. return serialize($this);
  1791. }
  1792. }
  1793. /**
  1794. * Configuration definition, defines directives and their defaults.
  1795. */
  1796. class HTMLPurifier_ConfigSchema {
  1797. /**
  1798. * Defaults of the directives and namespaces.
  1799. * @note This shares the exact same structure as HTMLPurifier_Config::$conf
  1800. */
  1801. public $defaults = array();
  1802. /**
  1803. * The default property list. Do not edit this property list.
  1804. */
  1805. public $defaultPlist;
  1806. /**
  1807. * Definition of the directives. The structure of this is:
  1808. *
  1809. * array(
  1810. * 'Namespace' => array(
  1811. * 'Directive' => new stdclass(),
  1812. * )
  1813. * )
  1814. *
  1815. * The stdclass may have the following properties:
  1816. *
  1817. * - If isAlias isn't set:
  1818. * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions
  1819. * - allow_null: If set, this directive allows null values
  1820. * - aliases: If set, an associative array of value aliases to real values
  1821. * - allowed: If set, a lookup array of allowed (string) values
  1822. * - If isAlias is set:
  1823. * - namespace: Namespace this directive aliases to
  1824. * - name: Directive name this directive aliases to
  1825. *
  1826. * In certain degenerate cases, stdclass will actually be an integer. In
  1827. * that case, the value is equivalent to an stdclass with the type
  1828. * property set to the integer. If the integer is negative, type is
  1829. * equal to the absolute value of integer, and allow_null is true.
  1830. *
  1831. * This class is friendly with HTMLPurifier_Config. If you need introspection
  1832. * about the schema, you're better of using the ConfigSchema_Interchange,
  1833. * which uses more memory but has much richer information.
  1834. */
  1835. public $info = array();
  1836. /**
  1837. * Application-wide singleton
  1838. */
  1839. static protected $singleton;
  1840. public function __construct() {
  1841. $this->defaultPlist = new HTMLPurifier_PropertyList();
  1842. }
  1843. /**
  1844. * Unserializes the default ConfigSchema.
  1845. */
  1846. public static function makeFromSerial() {
  1847. $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser');
  1848. $r = unserialize($contents);
  1849. if (!$r) {
  1850. $hash = sha1($contents);
  1851. trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR);
  1852. }
  1853. return $r;
  1854. }
  1855. /**
  1856. * Retrieves an instance of the application-wide configuration definition.
  1857. */
  1858. public static function instance($prototype = null) {
  1859. if ($prototype !== null) {
  1860. HTMLPurifier_ConfigSchema::$singleton = $prototype;
  1861. } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) {
  1862. HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial();
  1863. }
  1864. return HTMLPurifier_ConfigSchema::$singleton;
  1865. }
  1866. /**
  1867. * Defines a directive for configuration
  1868. * @warning Will fail of directive's namespace is defined.
  1869. * @warning This method's signature is slightly different from the legacy
  1870. * define() static method! Beware!
  1871. * @param $namespace Namespace the directive is in
  1872. * @param $name Key of directive
  1873. * @param $default Default value of directive
  1874. * @param $type Allowed type of the directive. See
  1875. * HTMLPurifier_DirectiveDef::$type for allowed values
  1876. * @param $allow_null Whether or not to allow null values
  1877. */
  1878. public function add($key, $default, $type, $allow_null) {
  1879. $obj = new stdclass();
  1880. $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type];
  1881. if ($allow_null) $obj->allow_null = true;
  1882. $this->info[$key] = $obj;
  1883. $this->defaults[$key] = $default;
  1884. $this->defaultPlist->set($key, $default);
  1885. }
  1886. /**
  1887. * Defines a directive value alias.
  1888. *
  1889. * Directive value aliases are convenient for developers because it lets
  1890. * them set a directive to several values and get the same result.
  1891. * @param $namespace Directive's namespace
  1892. * @param $name Name of Directive
  1893. * @param $aliases Hash of aliased values to the real alias
  1894. */
  1895. public function addValueAliases($key, $aliases) {
  1896. if (!isset($this->info[$key]->aliases)) {
  1897. $this->info[$key]->aliases = array();
  1898. }
  1899. foreach ($aliases as $alias => $real) {
  1900. $this->info[$key]->aliases[$alias] = $real;
  1901. }
  1902. }
  1903. /**
  1904. * Defines a set of allowed values for a directive.
  1905. * @warning This is slightly different from the corresponding static
  1906. * method definition.
  1907. * @param $namespace Namespace of directive
  1908. * @param $name Name of directive
  1909. * @param $allowed Lookup array of allowed values
  1910. */
  1911. public function addAllowedValues($key, $allowed) {
  1912. $this->info[$key]->allowed = $allowed;
  1913. }
  1914. /**
  1915. * Defines a directive alias for backwards compatibility
  1916. * @param $namespace
  1917. * @param $name Directive that will be aliased
  1918. * @param $new_namespace
  1919. * @param $new_name Directive that the alias will be to
  1920. */
  1921. public function addAlias($key, $new_key) {
  1922. $obj = new stdclass;
  1923. $obj->key = $new_key;
  1924. $obj->isAlias = true;
  1925. $this->info[$key] = $obj;
  1926. }
  1927. /**
  1928. * Replaces any stdclass that only has the type property with type integer.
  1929. */
  1930. public function postProcess() {
  1931. foreach ($this->info as $key => $v) {
  1932. if (count((array) $v) == 1) {
  1933. $this->info[$key] = $v->type;
  1934. } elseif (count((array) $v) == 2 && isset($v->allow_null)) {
  1935. $this->info[$key] = -$v->type;
  1936. }
  1937. }
  1938. }
  1939. }
  1940. /**
  1941. * @todo Unit test
  1942. */
  1943. class HTMLPurifier_ContentSets
  1944. {
  1945. /**
  1946. * List of content set strings (pipe seperators) indexed by name.
  1947. */
  1948. public $info = array();
  1949. /**
  1950. * List of content set lookups (element => true) indexed by name.
  1951. * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets
  1952. */
  1953. public $lookup = array();
  1954. /**
  1955. * Synchronized list of defined content sets (keys of info)
  1956. */
  1957. protected $keys = array();
  1958. /**
  1959. * Synchronized list of defined content values (values of info)
  1960. */
  1961. protected $values = array();
  1962. /**
  1963. * Merges in module's content sets, expands identifiers in the content
  1964. * sets and populates the keys, values and lookup member variables.
  1965. * @param $modules List of HTMLPurifier_HTMLModule
  1966. */
  1967. public function __construct($modules) {
  1968. if (!is_array($modules)) $modules = array($modules);
  1969. // populate content_sets based on module hints
  1970. // sorry, no way of overloading
  1971. foreach ($modules as $module_i => $module) {
  1972. foreach ($module->content_sets as $key => $value) {
  1973. $temp = $this->convertToLookup($value);
  1974. if (isset($this->lookup[$key])) {
  1975. // add it into the existing content set
  1976. $this->lookup[$key] = array_merge($this->lookup[$key], $temp);
  1977. } else {
  1978. $this->lookup[$key] = $temp;
  1979. }
  1980. }
  1981. }
  1982. $old_lookup = false;
  1983. while ($old_lookup !== $this->lookup) {
  1984. $old_lookup = $this->lookup;
  1985. foreach ($this->lookup as $i => $set) {
  1986. $add = array();
  1987. foreach ($set as $element => $x) {
  1988. if (isset($this->lookup[$element])) {
  1989. $add += $this->lookup[$element];
  1990. unset($this->lookup[$i][$element]);
  1991. }
  1992. }
  1993. $this->lookup[$i] += $add;
  1994. }
  1995. }
  1996. foreach ($this->lookup as $key => $lookup) {
  1997. $this->info[$key] = implode(' | ', array_keys($lookup));
  1998. }
  1999. $this->keys = array_keys($this->info);
  2000. $this->values = array_values($this->info);
  2001. }
  2002. /**
  2003. * Accepts a definition; generates and assigns a ChildDef for it
  2004. * @param $def HTMLPurifier_ElementDef reference
  2005. * @param $module Module that defined the ElementDef
  2006. */
  2007. public function generateChildDef(&$def, $module) {
  2008. if (!empty($def->child)) return; // already done!
  2009. $content_model = $def->content_model;
  2010. if (is_string($content_model)) {
  2011. // Assume that $this->keys is alphanumeric
  2012. $def->content_model = preg_replace_callback(
  2013. '/\b(' . implode('|', $this->keys) . ')\b/',
  2014. array($this, 'generateChildDefCallback'),
  2015. $content_model
  2016. );
  2017. //$def->content_model = str_replace(
  2018. // $this->keys, $this->values, $content_model);
  2019. }
  2020. $def->child = $this->getChildDef($def, $module);
  2021. }
  2022. public function generateChildDefCallback($matches) {
  2023. return $this->info[$matches[0]];
  2024. }
  2025. /**
  2026. * Instantiates a ChildDef based on content_model and content_model_type
  2027. * member variables in HTMLPurifier_ElementDef
  2028. * @note This will also defer to modules for custom HTMLPurifier_ChildDef
  2029. * subclasses that need content set expansion
  2030. * @param $def HTMLPurifier_ElementDef to have ChildDef extracted
  2031. * @return HTMLPurifier_ChildDef corresponding to ElementDef
  2032. */
  2033. public function getChildDef($def, $module) {
  2034. $value = $def->content_model;
  2035. if (is_object($value)) {
  2036. trigger_error(
  2037. 'Literal object child definitions should be stored in '.
  2038. 'ElementDef->child not ElementDef->content_model',
  2039. E_USER_NOTICE
  2040. );
  2041. return $value;
  2042. }
  2043. switch ($def->content_model_type) {
  2044. case 'required':
  2045. return new HTMLPurifier_ChildDef_Required($value);
  2046. case 'optional':
  2047. return new HTMLPurifier_ChildDef_Optional($value);
  2048. case 'empty':
  2049. return new HTMLPurifier_ChildDef_Empty();
  2050. case 'custom':
  2051. return new HTMLPurifier_ChildDef_Custom($value);
  2052. }
  2053. // defer to its module
  2054. $return = false;
  2055. if ($module->defines_child_def) { // save a func call
  2056. $return = $module->getChildDef($def);
  2057. }
  2058. if ($return !== false) return $return;
  2059. // error-out
  2060. trigger_error(
  2061. 'Could not determine which ChildDef class to instantiate',
  2062. E_USER_ERROR
  2063. );
  2064. return false;
  2065. }
  2066. /**
  2067. * Converts a string list of elements separated by pipes into
  2068. * a lookup array.
  2069. * @param $string List of elements
  2070. * @return Lookup array of elements
  2071. */
  2072. protected function convertToLookup($string) {
  2073. $array = explode('|', str_replace(' ', '', $string));
  2074. $ret = array();
  2075. foreach ($array as $i => $k) {
  2076. $ret[$k] = true;
  2077. }
  2078. return $ret;
  2079. }
  2080. }
  2081. /**
  2082. * Registry object that contains information about the current context.
  2083. * @warning Is a bit buggy when variables are set to null: it thinks
  2084. * they don't exist! So use false instead, please.
  2085. * @note Since the variables Context deals with may not be objects,
  2086. * references are very important here! Do not remove!
  2087. */
  2088. class HTMLPurifier_Context
  2089. {
  2090. /**
  2091. * Private array that stores the references.
  2092. */
  2093. private $_storage = array();
  2094. /**
  2095. * Registers a variable into the context.
  2096. * @param $name String name
  2097. * @param $ref Reference to variable to be registered
  2098. */
  2099. public function register($name, &$ref) {
  2100. if (isset($this->_storage[$name])) {
  2101. trigger_error("Name $name produces collision, cannot re-register",
  2102. E_USER_ERROR);
  2103. return;
  2104. }
  2105. $this->_storage[$name] =& $ref;
  2106. }
  2107. /**
  2108. * Retrieves a variable reference from the context.
  2109. * @param $name String name
  2110. * @param $ignore_error Boolean whether or not to ignore error
  2111. */
  2112. public function &get($name, $ignore_error = false) {
  2113. if (!isset($this->_storage[$name])) {
  2114. if (!$ignore_error) {
  2115. trigger_error("Attempted to retrieve non-existent variable $name",
  2116. E_USER_ERROR);
  2117. }
  2118. $var = null; // so we can return by reference
  2119. return $var;
  2120. }
  2121. return $this->_storage[$name];
  2122. }
  2123. /**
  2124. * Destorys a variable in the context.
  2125. * @param $name String name
  2126. */
  2127. public function destroy($name) {
  2128. if (!isset($this->_storage[$name])) {
  2129. trigger_error("Attempted to destroy non-existent variable $name",
  2130. E_USER_ERROR);
  2131. return;
  2132. }
  2133. unset($this->_storage[$name]);
  2134. }
  2135. /**
  2136. * Checks whether or not the variable exists.
  2137. * @param $name String name
  2138. */
  2139. public function exists($name) {
  2140. return isset($this->_storage[$name]);
  2141. }
  2142. /**
  2143. * Loads a series of variables from an associative array
  2144. * @param $context_array Assoc array of variables to load
  2145. */
  2146. public function loadArray($context_array) {
  2147. foreach ($context_array as $key => $discard) {
  2148. $this->register($key, $context_array[$key]);
  2149. }
  2150. }
  2151. }
  2152. /**
  2153. * Abstract class representing Definition cache managers that implements
  2154. * useful common methods and is a factory.
  2155. * @todo Create a separate maintenance file advanced users can use to
  2156. * cache their custom HTMLDefinition, which can be loaded
  2157. * via a configuration directive
  2158. * @todo Implement memcached
  2159. */
  2160. abstract class HTMLPurifier_DefinitionCache
  2161. {
  2162. public $type;
  2163. /**
  2164. * @param $name Type of definition objects this instance of the
  2165. * cache will handle.
  2166. */
  2167. public function __construct($type) {
  2168. $this->type = $type;
  2169. }
  2170. /**
  2171. * Generates a unique identifier for a particular configuration
  2172. * @param Instance of HTMLPurifier_Config
  2173. */
  2174. public function generateKey($config) {
  2175. return $config->version . ',' . // possibly replace with function calls
  2176. $config->getBatchSerial($this->type) . ',' .
  2177. $config->get($this->type . '.DefinitionRev');
  2178. }
  2179. /**
  2180. * Tests whether or not a key is old with respect to the configuration's
  2181. * version and revision number.
  2182. * @param $key Key to test
  2183. * @param $config Instance of HTMLPurifier_Config to test against
  2184. */
  2185. public function isOld($key, $config) {
  2186. if (substr_count($key, ',') < 2) return true;
  2187. list($version, $hash, $revision) = explode(',', $key, 3);
  2188. $compare = version_compare($version, $config->version);
  2189. // version mismatch, is always old
  2190. if ($compare != 0) return true;
  2191. // versions match, ids match, check revision number
  2192. if (
  2193. $hash == $config->getBatchSerial($this->type) &&
  2194. $revision < $config->get($this->type . '.DefinitionRev')
  2195. ) return true;
  2196. return false;
  2197. }
  2198. /**
  2199. * Checks if a definition's type jives with the cache's type
  2200. * @note Throws an error on failure
  2201. * @param $def Definition object to check
  2202. * @return Boolean true if good, false if not
  2203. */
  2204. public function checkDefType($def) {
  2205. if ($def->type !== $this->type) {
  2206. trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}");
  2207. return false;
  2208. }
  2209. return true;
  2210. }
  2211. /**
  2212. * Adds a definition object to the cache
  2213. */
  2214. abstract public function add($def, $config);
  2215. /**
  2216. * Unconditionally saves a definition object to the cache
  2217. */
  2218. abstract public function set($def, $config);
  2219. /**
  2220. * Replace an object in the cache
  2221. */
  2222. abstract public function replace($def, $config);
  2223. /**
  2224. * Retrieves a definition object from the cache
  2225. */
  2226. abstract public function get($config);
  2227. /**
  2228. * Removes a definition object to the cache
  2229. */
  2230. abstract public function remove($config);
  2231. /**
  2232. * Clears all objects from cache
  2233. */
  2234. abstract public function flush($config);
  2235. /**
  2236. * Clears all expired (older version or revision) objects from cache
  2237. * @note Be carefuly implementing this method as flush. Flush must
  2238. * not interfere with other Definition types, and cleanup()
  2239. * should not be repeatedly called by userland code.
  2240. */
  2241. abstract public function cleanup($config);
  2242. }
  2243. /**
  2244. * Responsible for creating definition caches.
  2245. */
  2246. class HTMLPurifier_DefinitionCacheFactory
  2247. {
  2248. protected $caches = array('Serializer' => array());
  2249. protected $implementations = array();
  2250. protected $decorators = array();
  2251. /**
  2252. * Initialize default decorators
  2253. */
  2254. public function setup() {
  2255. $this->addDecorator('Cleanup');
  2256. }
  2257. /**
  2258. * Retrieves an instance of global definition cache factory.
  2259. */
  2260. public static function instance($prototype = null) {
  2261. static $instance;
  2262. if ($prototype !== null) {
  2263. $instance = $prototype;
  2264. } elseif ($instance === null || $prototype === true) {
  2265. $instance = new HTMLPurifier_DefinitionCacheFactory();
  2266. $instance->setup();
  2267. }
  2268. return $instance;
  2269. }
  2270. /**
  2271. * Registers a new definition cache object
  2272. * @param $short Short name of cache object, for reference
  2273. * @param $long Full class name of cache object, for construction
  2274. */
  2275. public function register($short, $long) {
  2276. $this->implementations[$short] = $long;
  2277. }
  2278. /**
  2279. * Factory method that creates a cache object based on configuration
  2280. * @param $name Name of definitions handled by cache
  2281. * @param $config Instance of HTMLPurifier_Config
  2282. */
  2283. public function create($type, $config) {
  2284. $method = $config->get('Cache.DefinitionImpl');
  2285. if ($method === null) {
  2286. return new HTMLPurifier_DefinitionCache_Null($type);
  2287. }
  2288. if (!empty($this->caches[$method][$type])) {
  2289. return $this->caches[$method][$type];
  2290. }
  2291. if (
  2292. isset($this->implementations[$method]) &&
  2293. class_exists($class = $this->implementations[$method], false)
  2294. ) {
  2295. $cache = new $class($type);
  2296. } else {
  2297. if ($method != 'Serializer') {
  2298. trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING);
  2299. }
  2300. $cache = new HTMLPurifier_DefinitionCache_Serializer($type);
  2301. }
  2302. foreach ($this->decorators as $decorator) {
  2303. $new_cache = $decorator->decorate($cache);
  2304. // prevent infinite recursion in PHP 4
  2305. unset($cache);
  2306. $cache = $new_cache;
  2307. }
  2308. $this->caches[$method][$type] = $cache;
  2309. return $this->caches[$method][$type];
  2310. }
  2311. /**
  2312. * Registers a decorator to add to all new cache objects
  2313. * @param
  2314. */
  2315. public function addDecorator($decorator) {
  2316. if (is_string($decorator)) {
  2317. $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator";
  2318. $decorator = new $class;
  2319. }
  2320. $this->decorators[$decorator->name] = $decorator;
  2321. }
  2322. }
  2323. /**
  2324. * Represents a document type, contains information on which modules
  2325. * need to be loaded.
  2326. * @note This class is inspected by Printer_HTMLDefinition->renderDoctype.
  2327. * If structure changes, please update that function.
  2328. */
  2329. class HTMLPurifier_Doctype
  2330. {
  2331. /**
  2332. * Full name of doctype
  2333. */
  2334. public $name;
  2335. /**
  2336. * List of standard modules (string identifiers or literal objects)
  2337. * that this doctype uses
  2338. */
  2339. public $modules = array();
  2340. /**
  2341. * List of modules to use for tidying up code
  2342. */
  2343. public $tidyModules = array();
  2344. /**
  2345. * Is the language derived from XML (i.e. XHTML)?
  2346. */
  2347. public $xml = true;
  2348. /**
  2349. * List of aliases for this doctype
  2350. */
  2351. public $aliases = array();
  2352. /**
  2353. * Public DTD identifier
  2354. */
  2355. public $dtdPublic;
  2356. /**
  2357. * System DTD identifier
  2358. */
  2359. public $dtdSystem;
  2360. public function __construct($name = null, $xml = true, $modules = array(),
  2361. $tidyModules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
  2362. ) {
  2363. $this->name = $name;
  2364. $this->xml = $xml;
  2365. $this->modules = $modules;
  2366. $this->tidyModules = $tidyModules;
  2367. $this->aliases = $aliases;
  2368. $this->dtdPublic = $dtd_public;
  2369. $this->dtdSystem = $dtd_system;
  2370. }
  2371. }
  2372. class HTMLPurifier_DoctypeRegistry
  2373. {
  2374. /**
  2375. * Hash of doctype names to doctype objects
  2376. */
  2377. protected $doctypes;
  2378. /**
  2379. * Lookup table of aliases to real doctype names
  2380. */
  2381. protected $aliases;
  2382. /**
  2383. * Registers a doctype to the registry
  2384. * @note Accepts a fully-formed doctype object, or the
  2385. * parameters for constructing a doctype object
  2386. * @param $doctype Name of doctype or literal doctype object
  2387. * @param $modules Modules doctype will load
  2388. * @param $modules_for_modes Modules doctype will load for certain modes
  2389. * @param $aliases Alias names for doctype
  2390. * @return Editable registered doctype
  2391. */
  2392. public function register($doctype, $xml = true, $modules = array(),
  2393. $tidy_modules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
  2394. ) {
  2395. if (!is_array($modules)) $modules = array($modules);
  2396. if (!is_array($tidy_modules)) $tidy_modules = array($tidy_modules);
  2397. if (!is_array($aliases)) $aliases = array($aliases);
  2398. if (!is_object($doctype)) {
  2399. $doctype = new HTMLPurifier_Doctype(
  2400. $doctype, $xml, $modules, $tidy_modules, $aliases, $dtd_public, $dtd_system
  2401. );
  2402. }
  2403. $this->doctypes[$doctype->name] = $doctype;
  2404. $name = $doctype->name;
  2405. // hookup aliases
  2406. foreach ($doctype->aliases as $alias) {
  2407. if (isset($this->doctypes[$alias])) continue;
  2408. $this->aliases[$alias] = $name;
  2409. }
  2410. // remove old aliases
  2411. if (isset($this->aliases[$name])) unset($this->aliases[$name]);
  2412. return $doctype;
  2413. }
  2414. /**
  2415. * Retrieves reference to a doctype of a certain name
  2416. * @note This function resolves aliases
  2417. * @note When possible, use the more fully-featured make()
  2418. * @param $doctype Name of doctype
  2419. * @return Editable doctype object
  2420. */
  2421. public function get($doctype) {
  2422. if (isset($this->aliases[$doctype])) $doctype = $this->aliases[$doctype];
  2423. if (!isset($this->doctypes[$doctype])) {
  2424. trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR);
  2425. $anon = new HTMLPurifier_Doctype($doctype);
  2426. return $anon;
  2427. }
  2428. return $this->doctypes[$doctype];
  2429. }
  2430. /**
  2431. * Creates a doctype based on a configuration object,
  2432. * will perform initialization on the doctype
  2433. * @note Use this function to get a copy of doctype that config
  2434. * can hold on to (this is necessary in order to tell
  2435. * Generator whether or not the current document is XML
  2436. * based or not).
  2437. */
  2438. public function make($config) {
  2439. return clone $this->get($this->getDoctypeFromConfig($config));
  2440. }
  2441. /**
  2442. * Retrieves the doctype from the configuration object
  2443. */
  2444. public function getDoctypeFromConfig($config) {
  2445. // recommended test
  2446. $doctype = $config->get('HTML.Doctype');
  2447. if (!empty($doctype)) return $doctype;
  2448. $doctype = $config->get('HTML.CustomDoctype');
  2449. if (!empty($doctype)) return $doctype;
  2450. // backwards-compatibility
  2451. if ($config->get('HTML.XHTML')) {
  2452. $doctype = 'XHTML 1.0';
  2453. } else {
  2454. $doctype = 'HTML 4.01';
  2455. }
  2456. if ($config->get('HTML.Strict')) {
  2457. $doctype .= ' Strict';
  2458. } else {
  2459. $doctype .= ' Transitional';
  2460. }
  2461. return $doctype;
  2462. }
  2463. }
  2464. /**
  2465. * Structure that stores an HTML element definition. Used by
  2466. * HTMLPurifier_HTMLDefinition and HTMLPurifier_HTMLModule.
  2467. * @note This class is inspected by HTMLPurifier_Printer_HTMLDefinition.
  2468. * Please update that class too.
  2469. * @warning If you add new properties to this class, you MUST update
  2470. * the mergeIn() method.
  2471. */
  2472. class HTMLPurifier_ElementDef
  2473. {
  2474. /**
  2475. * Does the definition work by itself, or is it created solely
  2476. * for the purpose of merging into another definition?
  2477. */
  2478. public $standalone = true;
  2479. /**
  2480. * Associative array of attribute name to HTMLPurifier_AttrDef
  2481. * @note Before being processed by HTMLPurifier_AttrCollections
  2482. * when modules are finalized during
  2483. * HTMLPurifier_HTMLDefinition->setup(), this array may also
  2484. * contain an array at index 0 that indicates which attribute
  2485. * collections to load into the full array. It may also
  2486. * contain string indentifiers in lieu of HTMLPurifier_AttrDef,
  2487. * see HTMLPurifier_AttrTypes on how they are expanded during
  2488. * HTMLPurifier_HTMLDefinition->setup() processing.
  2489. */
  2490. public $attr = array();
  2491. // XXX: Design note: currently, it's not possible to override
  2492. // previously defined AttrTransforms without messing around with
  2493. // the final generated config. This is by design; a previous version
  2494. // used an associated list of attr_transform, but it was extremely
  2495. // easy to accidentally override other attribute transforms by
  2496. // forgetting to specify an index (and just using 0.) While we
  2497. // could check this by checking the index number and complaining,
  2498. // there is a second problem which is that it is not at all easy to
  2499. // tell when something is getting overridden. Combine this with a
  2500. // codebase where this isn't really being used, and it's perfect for
  2501. // nuking.
  2502. /**
  2503. * List of tags HTMLPurifier_AttrTransform to be done before validation
  2504. */
  2505. public $attr_transform_pre = array();
  2506. /**
  2507. * List of tags HTMLPurifier_AttrTransform to be done after validation
  2508. */
  2509. public $attr_transform_post = array();
  2510. /**
  2511. * HTMLPurifier_ChildDef of this tag.
  2512. */
  2513. public $child;
  2514. /**
  2515. * Abstract string representation of internal ChildDef rules. See
  2516. * HTMLPurifier_ContentSets for how this is parsed and then transformed
  2517. * into an HTMLPurifier_ChildDef.
  2518. * @warning This is a temporary variable that is not available after
  2519. * being processed by HTMLDefinition
  2520. */
  2521. public $content_model;
  2522. /**
  2523. * Value of $child->type, used to determine which ChildDef to use,
  2524. * used in combination with $content_model.
  2525. * @warning This must be lowercase
  2526. * @warning This is a temporary variable that is not available after
  2527. * being processed by HTMLDefinition
  2528. */
  2529. public $content_model_type;
  2530. /**
  2531. * Does the element have a content model (#PCDATA | Inline)*? This
  2532. * is important for chameleon ins and del processing in
  2533. * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't
  2534. * have to worry about this one.
  2535. */
  2536. public $descendants_are_inline = false;
  2537. /**
  2538. * List of the names of required attributes this element has. Dynamically
  2539. * populated by HTMLPurifier_HTMLDefinition::getElement
  2540. */
  2541. public $required_attr = array();
  2542. /**
  2543. * Lookup table of tags excluded from all descendants of this tag.
  2544. * @note SGML permits exclusions for all descendants, but this is
  2545. * not possible with DTDs or XML Schemas. W3C has elected to
  2546. * use complicated compositions of content_models to simulate
  2547. * exclusion for children, but we go the simpler, SGML-style
  2548. * route of flat-out exclusions, which correctly apply to
  2549. * all descendants and not just children. Note that the XHTML
  2550. * Modularization Abstract Modules are blithely unaware of such
  2551. * distinctions.
  2552. */
  2553. public $excludes = array();
  2554. /**
  2555. * This tag is explicitly auto-closed by the following tags.
  2556. */
  2557. public $autoclose = array();
  2558. /**
  2559. * If a foreign element is found in this element, test if it is
  2560. * allowed by this sub-element; if it is, instead of closing the
  2561. * current element, place it inside this element.
  2562. */
  2563. public $wrap;
  2564. /**
  2565. * Whether or not this is a formatting element affected by the
  2566. * "Active Formatting Elements" algorithm.
  2567. */
  2568. public $formatting;
  2569. /**
  2570. * Low-level factory constructor for creating new standalone element defs
  2571. */
  2572. public static function create($content_model, $content_model_type, $attr) {
  2573. $def = new HTMLPurifier_ElementDef();
  2574. $def->content_model = $content_model;
  2575. $def->content_model_type = $content_model_type;
  2576. $def->attr = $attr;
  2577. return $def;
  2578. }
  2579. /**
  2580. * Merges the values of another element definition into this one.
  2581. * Values from the new element def take precedence if a value is
  2582. * not mergeable.
  2583. */
  2584. public function mergeIn($def) {
  2585. // later keys takes precedence
  2586. foreach($def->attr as $k => $v) {
  2587. if ($k === 0) {
  2588. // merge in the includes
  2589. // sorry, no way to override an include
  2590. foreach ($v as $v2) {
  2591. $this->attr[0][] = $v2;
  2592. }
  2593. continue;
  2594. }
  2595. if ($v === false) {
  2596. if (isset($this->attr[$k])) unset($this->attr[$k]);
  2597. continue;
  2598. }
  2599. $this->attr[$k] = $v;
  2600. }
  2601. $this->_mergeAssocArray($this->excludes, $def->excludes);
  2602. $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre);
  2603. $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post);
  2604. if(!empty($def->content_model)) {
  2605. $this->content_model =
  2606. str_replace("#SUPER", $this->content_model, $def->content_model);
  2607. $this->child = false;
  2608. }
  2609. if(!empty($def->content_model_type)) {
  2610. $this->content_model_type = $def->content_model_type;
  2611. $this->child = false;
  2612. }
  2613. if(!is_null($def->child)) $this->child = $def->child;
  2614. if(!is_null($def->formatting)) $this->formatting = $def->formatting;
  2615. if($def->descendants_are_inline) $this->descendants_are_inline = $def->descendants_are_inline;
  2616. }
  2617. /**
  2618. * Merges one array into another, removes values which equal false
  2619. * @param $a1 Array by reference that is merged into
  2620. * @param $a2 Array that merges into $a1
  2621. */
  2622. private function _mergeAssocArray(&$a1, $a2) {
  2623. foreach ($a2 as $k => $v) {
  2624. if ($v === false) {
  2625. if (isset($a1[$k])) unset($a1[$k]);
  2626. continue;
  2627. }
  2628. $a1[$k] = $v;
  2629. }
  2630. }
  2631. }
  2632. /**
  2633. * A UTF-8 specific character encoder that handles cleaning and transforming.
  2634. * @note All functions in this class should be static.
  2635. */
  2636. class HTMLPurifier_Encoder
  2637. {
  2638. /**
  2639. * Constructor throws fatal error if you attempt to instantiate class
  2640. */
  2641. private function __construct() {
  2642. trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR);
  2643. }
  2644. /**
  2645. * Error-handler that mutes errors, alternative to shut-up operator.
  2646. */
  2647. public static function muteErrorHandler() {}
  2648. /**
  2649. * iconv wrapper which mutes errors, but doesn't work around bugs.
  2650. */
  2651. public static function unsafeIconv($in, $out, $text) {
  2652. set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
  2653. $r = iconv($in, $out, $text);
  2654. restore_error_handler();
  2655. return $r;
  2656. }
  2657. /**
  2658. * iconv wrapper which mutes errors and works around bugs.
  2659. */
  2660. public static function iconv($in, $out, $text, $max_chunk_size = 8000) {
  2661. $code = self::testIconvTruncateBug();
  2662. if ($code == self::ICONV_OK) {
  2663. return self::unsafeIconv($in, $out, $text);
  2664. } elseif ($code == self::ICONV_TRUNCATES) {
  2665. // we can only work around this if the input character set
  2666. // is utf-8
  2667. if ($in == 'utf-8') {
  2668. if ($max_chunk_size < 4) {
  2669. trigger_error('max_chunk_size is too small', E_USER_WARNING);
  2670. return false;
  2671. }
  2672. // split into 8000 byte chunks, but be careful to handle
  2673. // multibyte boundaries properly
  2674. if (($c = strlen($text)) <= $max_chunk_size) {
  2675. return self::unsafeIconv($in, $out, $text);
  2676. }
  2677. $r = '';
  2678. $i = 0;
  2679. while (true) {
  2680. if ($i + $max_chunk_size >= $c) {
  2681. $r .= self::unsafeIconv($in, $out, substr($text, $i));
  2682. break;
  2683. }
  2684. // wibble the boundary
  2685. if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) {
  2686. $chunk_size = $max_chunk_size;
  2687. } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) {
  2688. $chunk_size = $max_chunk_size - 1;
  2689. } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) {
  2690. $chunk_size = $max_chunk_size - 2;
  2691. } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) {
  2692. $chunk_size = $max_chunk_size - 3;
  2693. } else {
  2694. return false; // rather confusing UTF-8...
  2695. }
  2696. $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths
  2697. $r .= self::unsafeIconv($in, $out, $chunk);
  2698. $i += $chunk_size;
  2699. }
  2700. return $r;
  2701. } else {
  2702. return false;
  2703. }
  2704. } else {
  2705. return false;
  2706. }
  2707. }
  2708. /**
  2709. * Cleans a UTF-8 string for well-formedness and SGML validity
  2710. *
  2711. * It will parse according to UTF-8 and return a valid UTF8 string, with
  2712. * non-SGML codepoints excluded.
  2713. *
  2714. * @note Just for reference, the non-SGML code points are 0 to 31 and
  2715. * 127 to 159, inclusive. However, we allow code points 9, 10
  2716. * and 13, which are the tab, line feed and carriage return
  2717. * respectively. 128 and above the code points map to multibyte
  2718. * UTF-8 representations.
  2719. *
  2720. * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and
  2721. * hsivonen@iki.fi at <http://iki.fi/hsivonen/php-utf8/> under the
  2722. * LGPL license. Notes on what changed are inside, but in general,
  2723. * the original code transformed UTF-8 text into an array of integer
  2724. * Unicode codepoints. Understandably, transforming that back to
  2725. * a string would be somewhat expensive, so the function was modded to
  2726. * directly operate on the string. However, this discourages code
  2727. * reuse, and the logic enumerated here would be useful for any
  2728. * function that needs to be able to understand UTF-8 characters.
  2729. * As of right now, only smart lossless character encoding converters
  2730. * would need that, and I'm probably not going to implement them.
  2731. * Once again, PHP 6 should solve all our problems.
  2732. */
  2733. public static function cleanUTF8($str, $force_php = false) {
  2734. // UTF-8 validity is checked since PHP 4.3.5
  2735. // This is an optimization: if the string is already valid UTF-8, no
  2736. // need to do PHP stuff. 99% of the time, this will be the case.
  2737. // The regexp matches the XML char production, as well as well as excluding
  2738. // non-SGML codepoints U+007F to U+009F
  2739. if (preg_match('/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', $str)) {
  2740. return $str;
  2741. }
  2742. $mState = 0; // cached expected number of octets after the current octet
  2743. // until the beginning of the next UTF8 character sequence
  2744. $mUcs4 = 0; // cached Unicode character
  2745. $mBytes = 1; // cached expected number of octets in the current sequence
  2746. // original code involved an $out that was an array of Unicode
  2747. // codepoints. Instead of having to convert back into UTF-8, we've
  2748. // decided to directly append valid UTF-8 characters onto a string
  2749. // $out once they're done. $char accumulates raw bytes, while $mUcs4
  2750. // turns into the Unicode code point, so there's some redundancy.
  2751. $out = '';
  2752. $char = '';
  2753. $len = strlen($str);
  2754. for($i = 0; $i < $len; $i++) {
  2755. $in = ord($str{$i});
  2756. $char .= $str[$i]; // append byte to char
  2757. if (0 == $mState) {
  2758. // When mState is zero we expect either a US-ASCII character
  2759. // or a multi-octet sequence.
  2760. if (0 == (0x80 & ($in))) {
  2761. // US-ASCII, pass straight through.
  2762. if (($in <= 31 || $in == 127) &&
  2763. !($in == 9 || $in == 13 || $in == 10) // save \r\t\n
  2764. ) {
  2765. // control characters, remove
  2766. } else {
  2767. $out .= $char;
  2768. }
  2769. // reset
  2770. $char = '';
  2771. $mBytes = 1;
  2772. } elseif (0xC0 == (0xE0 & ($in))) {
  2773. // First octet of 2 octet sequence
  2774. $mUcs4 = ($in);
  2775. $mUcs4 = ($mUcs4 & 0x1F) << 6;
  2776. $mState = 1;
  2777. $mBytes = 2;
  2778. } elseif (0xE0 == (0xF0 & ($in))) {
  2779. // First octet of 3 octet sequence
  2780. $mUcs4 = ($in);
  2781. $mUcs4 = ($mUcs4 & 0x0F) << 12;
  2782. $mState = 2;
  2783. $mBytes = 3;
  2784. } elseif (0xF0 == (0xF8 & ($in))) {
  2785. // First octet of 4 octet sequence
  2786. $mUcs4 = ($in);
  2787. $mUcs4 = ($mUcs4 & 0x07) << 18;
  2788. $mState = 3;
  2789. $mBytes = 4;
  2790. } elseif (0xF8 == (0xFC & ($in))) {
  2791. // First octet of 5 octet sequence.
  2792. //
  2793. // This is illegal because the encoded codepoint must be
  2794. // either:
  2795. // (a) not the shortest form or
  2796. // (b) outside the Unicode range of 0-0x10FFFF.
  2797. // Rather than trying to resynchronize, we will carry on
  2798. // until the end of the sequence and let the later error
  2799. // handling code catch it.
  2800. $mUcs4 = ($in);
  2801. $mUcs4 = ($mUcs4 & 0x03) << 24;
  2802. $mState = 4;
  2803. $mBytes = 5;
  2804. } elseif (0xFC == (0xFE & ($in))) {
  2805. // First octet of 6 octet sequence, see comments for 5
  2806. // octet sequence.
  2807. $mUcs4 = ($in);
  2808. $mUcs4 = ($mUcs4 & 1) << 30;
  2809. $mState = 5;
  2810. $mBytes = 6;
  2811. } else {
  2812. // Current octet is neither in the US-ASCII range nor a
  2813. // legal first octet of a multi-octet sequence.
  2814. $mState = 0;
  2815. $mUcs4 = 0;
  2816. $mBytes = 1;
  2817. $char = '';
  2818. }
  2819. } else {
  2820. // When mState is non-zero, we expect a continuation of the
  2821. // multi-octet sequence
  2822. if (0x80 == (0xC0 & ($in))) {
  2823. // Legal continuation.
  2824. $shift = ($mState - 1) * 6;
  2825. $tmp = $in;
  2826. $tmp = ($tmp & 0x0000003F) << $shift;
  2827. $mUcs4 |= $tmp;
  2828. if (0 == --$mState) {
  2829. // End of the multi-octet sequence. mUcs4 now contains
  2830. // the final Unicode codepoint to be output
  2831. // Check for illegal sequences and codepoints.
  2832. // From Unicode 3.1, non-shortest form is illegal
  2833. if (((2 == $mBytes) && ($mUcs4 < 0x0080)) ||
  2834. ((3 == $mBytes) && ($mUcs4 < 0x0800)) ||
  2835. ((4 == $mBytes) && ($mUcs4 < 0x10000)) ||
  2836. (4 < $mBytes) ||
  2837. // From Unicode 3.2, surrogate characters = illegal
  2838. (($mUcs4 & 0xFFFFF800) == 0xD800) ||
  2839. // Codepoints outside the Unicode range are illegal
  2840. ($mUcs4 > 0x10FFFF)
  2841. ) {
  2842. } elseif (0xFEFF != $mUcs4 && // omit BOM
  2843. // check for valid Char unicode codepoints
  2844. (
  2845. 0x9 == $mUcs4 ||
  2846. 0xA == $mUcs4 ||
  2847. 0xD == $mUcs4 ||
  2848. (0x20 <= $mUcs4 && 0x7E >= $mUcs4) ||
  2849. // 7F-9F is not strictly prohibited by XML,
  2850. // but it is non-SGML, and thus we don't allow it
  2851. (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) ||
  2852. (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4)
  2853. )
  2854. ) {
  2855. $out .= $char;
  2856. }
  2857. // initialize UTF8 cache (reset)
  2858. $mState = 0;
  2859. $mUcs4 = 0;
  2860. $mBytes = 1;
  2861. $char = '';
  2862. }
  2863. } else {
  2864. // ((0xC0 & (*in) != 0x80) && (mState != 0))
  2865. // Incomplete multi-octet sequence.
  2866. // used to result in complete fail, but we'll reset
  2867. $mState = 0;
  2868. $mUcs4 = 0;
  2869. $mBytes = 1;
  2870. $char ='';
  2871. }
  2872. }
  2873. }
  2874. return $out;
  2875. }
  2876. /**
  2877. * Translates a Unicode codepoint into its corresponding UTF-8 character.
  2878. * @note Based on Feyd's function at
  2879. * <http://forums.devnetwork.net/viewtopic.php?p=191404#191404>,
  2880. * which is in public domain.
  2881. * @note While we're going to do code point parsing anyway, a good
  2882. * optimization would be to refuse to translate code points that
  2883. * are non-SGML characters. However, this could lead to duplication.
  2884. * @note This is very similar to the unichr function in
  2885. * maintenance/generate-entity-file.php (although this is superior,
  2886. * due to its sanity checks).
  2887. */
  2888. // +----------+----------+----------+----------+
  2889. // | 33222222 | 22221111 | 111111 | |
  2890. // | 10987654 | 32109876 | 54321098 | 76543210 | bit
  2891. // +----------+----------+----------+----------+
  2892. // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F
  2893. // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF
  2894. // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF
  2895. // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF
  2896. // +----------+----------+----------+----------+
  2897. // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF)
  2898. // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes
  2899. // +----------+----------+----------+----------+
  2900. public static function unichr($code) {
  2901. if($code > 1114111 or $code < 0 or
  2902. ($code >= 55296 and $code <= 57343) ) {
  2903. // bits are set outside the "valid" range as defined
  2904. // by UNICODE 4.1.0
  2905. return '';
  2906. }
  2907. $x = $y = $z = $w = 0;
  2908. if ($code < 128) {
  2909. // regular ASCII character
  2910. $x = $code;
  2911. } else {
  2912. // set up bits for UTF-8
  2913. $x = ($code & 63) | 128;
  2914. if ($code < 2048) {
  2915. $y = (($code & 2047) >> 6) | 192;
  2916. } else {
  2917. $y = (($code & 4032) >> 6) | 128;
  2918. if($code < 65536) {
  2919. $z = (($code >> 12) & 15) | 224;
  2920. } else {
  2921. $z = (($code >> 12) & 63) | 128;
  2922. $w = (($code >> 18) & 7) | 240;
  2923. }
  2924. }
  2925. }
  2926. // set up the actual character
  2927. $ret = '';
  2928. if($w) $ret .= chr($w);
  2929. if($z) $ret .= chr($z);
  2930. if($y) $ret .= chr($y);
  2931. $ret .= chr($x);
  2932. return $ret;
  2933. }
  2934. public static function iconvAvailable() {
  2935. static $iconv = null;
  2936. if ($iconv === null) {
  2937. $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE;
  2938. }
  2939. return $iconv;
  2940. }
  2941. /**
  2942. * Converts a string to UTF-8 based on configuration.
  2943. */
  2944. public static function convertToUTF8($str, $config, $context) {
  2945. $encoding = $config->get('Core.Encoding');
  2946. if ($encoding === 'utf-8') return $str;
  2947. static $iconv = null;
  2948. if ($iconv === null) $iconv = self::iconvAvailable();
  2949. if ($iconv && !$config->get('Test.ForceNoIconv')) {
  2950. // unaffected by bugs, since UTF-8 support all characters
  2951. $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str);
  2952. if ($str === false) {
  2953. // $encoding is not a valid encoding
  2954. trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR);
  2955. return '';
  2956. }
  2957. // If the string is bjorked by Shift_JIS or a similar encoding
  2958. // that doesn't support all of ASCII, convert the naughty
  2959. // characters to their true byte-wise ASCII/UTF-8 equivalents.
  2960. $str = strtr($str, self::testEncodingSupportsASCII($encoding));
  2961. return $str;
  2962. } elseif ($encoding === 'iso-8859-1') {
  2963. $str = utf8_encode($str);
  2964. return $str;
  2965. }
  2966. $bug = HTMLPurifier_Encoder::testIconvTruncateBug();
  2967. if ($bug == self::ICONV_OK) {
  2968. trigger_error('Encoding not supported, please install iconv', E_USER_ERROR);
  2969. } else {
  2970. trigger_error('You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', E_USER_ERROR);
  2971. }
  2972. }
  2973. /**
  2974. * Converts a string from UTF-8 based on configuration.
  2975. * @note Currently, this is a lossy conversion, with unexpressable
  2976. * characters being omitted.
  2977. */
  2978. public static function convertFromUTF8($str, $config, $context) {
  2979. $encoding = $config->get('Core.Encoding');
  2980. if ($escape = $config->get('Core.EscapeNonASCIICharacters')) {
  2981. $str = self::convertToASCIIDumbLossless($str);
  2982. }
  2983. if ($encoding === 'utf-8') return $str;
  2984. static $iconv = null;
  2985. if ($iconv === null) $iconv = self::iconvAvailable();
  2986. if ($iconv && !$config->get('Test.ForceNoIconv')) {
  2987. // Undo our previous fix in convertToUTF8, otherwise iconv will barf
  2988. $ascii_fix = self::testEncodingSupportsASCII($encoding);
  2989. if (!$escape && !empty($ascii_fix)) {
  2990. $clear_fix = array();
  2991. foreach ($ascii_fix as $utf8 => $native) $clear_fix[$utf8] = '';
  2992. $str = strtr($str, $clear_fix);
  2993. }
  2994. $str = strtr($str, array_flip($ascii_fix));
  2995. // Normal stuff
  2996. $str = self::iconv('utf-8', $encoding . '//IGNORE', $str);
  2997. return $str;
  2998. } elseif ($encoding === 'iso-8859-1') {
  2999. $str = utf8_decode($str);
  3000. return $str;
  3001. }
  3002. trigger_error('Encoding not supported', E_USER_ERROR);
  3003. // You might be tempted to assume that the ASCII representation
  3004. // might be OK, however, this is *not* universally true over all
  3005. // encodings. So we take the conservative route here, rather
  3006. // than forcibly turn on %Core.EscapeNonASCIICharacters
  3007. }
  3008. /**
  3009. * Lossless (character-wise) conversion of HTML to ASCII
  3010. * @param $str UTF-8 string to be converted to ASCII
  3011. * @returns ASCII encoded string with non-ASCII character entity-ized
  3012. * @warning Adapted from MediaWiki, claiming fair use: this is a common
  3013. * algorithm. If you disagree with this license fudgery,
  3014. * implement it yourself.
  3015. * @note Uses decimal numeric entities since they are best supported.
  3016. * @note This is a DUMB function: it has no concept of keeping
  3017. * character entities that the projected character encoding
  3018. * can allow. We could possibly implement a smart version
  3019. * but that would require it to also know which Unicode
  3020. * codepoints the charset supported (not an easy task).
  3021. * @note Sort of with cleanUTF8() but it assumes that $str is
  3022. * well-formed UTF-8
  3023. */
  3024. public static function convertToASCIIDumbLossless($str) {
  3025. $bytesleft = 0;
  3026. $result = '';
  3027. $working = 0;
  3028. $len = strlen($str);
  3029. for( $i = 0; $i < $len; $i++ ) {
  3030. $bytevalue = ord( $str[$i] );
  3031. if( $bytevalue <= 0x7F ) { //0xxx xxxx
  3032. $result .= chr( $bytevalue );
  3033. $bytesleft = 0;
  3034. } elseif( $bytevalue <= 0xBF ) { //10xx xxxx
  3035. $working = $working << 6;
  3036. $working += ($bytevalue & 0x3F);
  3037. $bytesleft--;
  3038. if( $bytesleft <= 0 ) {
  3039. $result .= "&#" . $working . ";";
  3040. }
  3041. } elseif( $bytevalue <= 0xDF ) { //110x xxxx
  3042. $working = $bytevalue & 0x1F;
  3043. $bytesleft = 1;
  3044. } elseif( $bytevalue <= 0xEF ) { //1110 xxxx
  3045. $working = $bytevalue & 0x0F;
  3046. $bytesleft = 2;
  3047. } else { //1111 0xxx
  3048. $working = $bytevalue & 0x07;
  3049. $bytesleft = 3;
  3050. }
  3051. }
  3052. return $result;
  3053. }
  3054. /** No bugs detected in iconv. */
  3055. const ICONV_OK = 0;
  3056. /** Iconv truncates output if converting from UTF-8 to another
  3057. * character set with //IGNORE, and a non-encodable character is found */
  3058. const ICONV_TRUNCATES = 1;
  3059. /** Iconv does not support //IGNORE, making it unusable for
  3060. * transcoding purposes */
  3061. const ICONV_UNUSABLE = 2;
  3062. /**
  3063. * glibc iconv has a known bug where it doesn't handle the magic
  3064. * //IGNORE stanza correctly. In particular, rather than ignore
  3065. * characters, it will return an EILSEQ after consuming some number
  3066. * of characters, and expect you to restart iconv as if it were
  3067. * an E2BIG. Old versions of PHP did not respect the errno, and
  3068. * returned the fragment, so as a result you would see iconv
  3069. * mysteriously truncating output. We can work around this by
  3070. * manually chopping our input into segments of about 8000
  3071. * characters, as long as PHP ignores the error code. If PHP starts
  3072. * paying attention to the error code, iconv becomes unusable.
  3073. *
  3074. * @returns Error code indicating severity of bug.
  3075. */
  3076. public static function testIconvTruncateBug() {
  3077. static $code = null;
  3078. if ($code === null) {
  3079. // better not use iconv, otherwise infinite loop!
  3080. $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000));
  3081. if ($r === false) {
  3082. $code = self::ICONV_UNUSABLE;
  3083. } elseif (($c = strlen($r)) < 9000) {
  3084. $code = self::ICONV_TRUNCATES;
  3085. } elseif ($c > 9000) {
  3086. trigger_error('Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: include your iconv version as per phpversion()', E_USER_ERROR);
  3087. } else {
  3088. $code = self::ICONV_OK;
  3089. }
  3090. }
  3091. return $code;
  3092. }
  3093. /**
  3094. * This expensive function tests whether or not a given character
  3095. * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will
  3096. * fail this test, and require special processing. Variable width
  3097. * encodings shouldn't ever fail.
  3098. *
  3099. * @param string $encoding Encoding name to test, as per iconv format
  3100. * @param bool $bypass Whether or not to bypass the precompiled arrays.
  3101. * @return Array of UTF-8 characters to their corresponding ASCII,
  3102. * which can be used to "undo" any overzealous iconv action.
  3103. */
  3104. public static function testEncodingSupportsASCII($encoding, $bypass = false) {
  3105. // All calls to iconv here are unsafe, proof by case analysis:
  3106. // If ICONV_OK, no difference.
  3107. // If ICONV_TRUNCATE, all calls involve one character inputs,
  3108. // so bug is not triggered.
  3109. // If ICONV_UNUSABLE, this call is irrelevant
  3110. static $encodings = array();
  3111. if (!$bypass) {
  3112. if (isset($encodings[$encoding])) return $encodings[$encoding];
  3113. $lenc = strtolower($encoding);
  3114. switch ($lenc) {
  3115. case 'shift_jis':
  3116. return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~');
  3117. case 'johab':
  3118. return array("\xE2\x82\xA9" => '\\');
  3119. }
  3120. if (strpos($lenc, 'iso-8859-') === 0) return array();
  3121. }
  3122. $ret = array();
  3123. if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) return false;
  3124. for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars
  3125. $c = chr($i); // UTF-8 char
  3126. $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion
  3127. if (
  3128. $r === '' ||
  3129. // This line is needed for iconv implementations that do not
  3130. // omit characters that do not exist in the target character set
  3131. ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c)
  3132. ) {
  3133. // Reverse engineer: what's the UTF-8 equiv of this byte
  3134. // sequence? This assumes that there's no variable width
  3135. // encoding that doesn't support ASCII.
  3136. $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c;
  3137. }
  3138. }
  3139. $encodings[$encoding] = $ret;
  3140. return $ret;
  3141. }
  3142. }
  3143. /**
  3144. * Object that provides entity lookup table from entity name to character
  3145. */
  3146. class HTMLPurifier_EntityLookup {
  3147. /**
  3148. * Assoc array of entity name to character represented.
  3149. */
  3150. public $table;
  3151. /**
  3152. * Sets up the entity lookup table from the serialized file contents.
  3153. * @note The serialized contents are versioned, but were generated
  3154. * using the maintenance script generate_entity_file.php
  3155. * @warning This is not in constructor to help enforce the Singleton
  3156. */
  3157. public function setup($file = false) {
  3158. if (!$file) {
  3159. $file = HTMLPURIFIER_PREFIX . '/HTMLPurifier/EntityLookup/entities.ser';
  3160. }
  3161. $this->table = unserialize(file_get_contents($file));
  3162. }
  3163. /**
  3164. * Retrieves sole instance of the object.
  3165. * @param Optional prototype of custom lookup table to overload with.
  3166. */
  3167. public static function instance($prototype = false) {
  3168. // no references, since PHP doesn't copy unless modified
  3169. static $instance = null;
  3170. if ($prototype) {
  3171. $instance = $prototype;
  3172. } elseif (!$instance) {
  3173. $instance = new HTMLPurifier_EntityLookup();
  3174. $instance->setup();
  3175. }
  3176. return $instance;
  3177. }
  3178. }
  3179. // if want to implement error collecting here, we'll need to use some sort
  3180. // of global data (probably trigger_error) because it's impossible to pass
  3181. // $config or $context to the callback functions.
  3182. /**
  3183. * Handles referencing and derefencing character entities
  3184. */
  3185. class HTMLPurifier_EntityParser
  3186. {
  3187. /**
  3188. * Reference to entity lookup table.
  3189. */
  3190. protected $_entity_lookup;
  3191. /**
  3192. * Callback regex string for parsing entities.
  3193. */
  3194. protected $_substituteEntitiesRegex =
  3195. '/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/';
  3196. // 1. hex 2. dec 3. string (XML style)
  3197. /**
  3198. * Decimal to parsed string conversion table for special entities.
  3199. */
  3200. protected $_special_dec2str =
  3201. array(
  3202. 34 => '"',
  3203. 38 => '&',
  3204. 39 => "'",
  3205. 60 => '<',
  3206. 62 => '>'
  3207. );
  3208. /**
  3209. * Stripped entity names to decimal conversion table for special entities.
  3210. */
  3211. protected $_special_ent2dec =
  3212. array(
  3213. 'quot' => 34,
  3214. 'amp' => 38,
  3215. 'lt' => 60,
  3216. 'gt' => 62
  3217. );
  3218. /**
  3219. * Substitutes non-special entities with their parsed equivalents. Since
  3220. * running this whenever you have parsed character is t3h 5uck, we run
  3221. * it before everything else.
  3222. *
  3223. * @param $string String to have non-special entities parsed.
  3224. * @returns Parsed string.
  3225. */
  3226. public function substituteNonSpecialEntities($string) {
  3227. // it will try to detect missing semicolons, but don't rely on it
  3228. return preg_replace_callback(
  3229. $this->_substituteEntitiesRegex,
  3230. array($this, 'nonSpecialEntityCallback'),
  3231. $string
  3232. );
  3233. }
  3234. /**
  3235. * Callback function for substituteNonSpecialEntities() that does the work.
  3236. *
  3237. * @param $matches PCRE matches array, with 0 the entire match, and
  3238. * either index 1, 2 or 3 set with a hex value, dec value,
  3239. * or string (respectively).
  3240. * @returns Replacement string.
  3241. */
  3242. protected function nonSpecialEntityCallback($matches) {
  3243. // replaces all but big five
  3244. $entity = $matches[0];
  3245. $is_num = (@$matches[0][1] === '#');
  3246. if ($is_num) {
  3247. $is_hex = (@$entity[2] === 'x');
  3248. $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
  3249. // abort for special characters
  3250. if (isset($this->_special_dec2str[$code])) return $entity;
  3251. return HTMLPurifier_Encoder::unichr($code);
  3252. } else {
  3253. if (isset($this->_special_ent2dec[$matches[3]])) return $entity;
  3254. if (!$this->_entity_lookup) {
  3255. $this->_entity_lookup = HTMLPurifier_EntityLookup::instance();
  3256. }
  3257. if (isset($this->_entity_lookup->table[$matches[3]])) {
  3258. return $this->_entity_lookup->table[$matches[3]];
  3259. } else {
  3260. return $entity;
  3261. }
  3262. }
  3263. }
  3264. /**
  3265. * Substitutes only special entities with their parsed equivalents.
  3266. *
  3267. * @notice We try to avoid calling this function because otherwise, it
  3268. * would have to be called a lot (for every parsed section).
  3269. *
  3270. * @param $string String to have non-special entities parsed.
  3271. * @returns Parsed string.
  3272. */
  3273. public function substituteSpecialEntities($string) {
  3274. return preg_replace_callback(
  3275. $this->_substituteEntitiesRegex,
  3276. array($this, 'specialEntityCallback'),
  3277. $string);
  3278. }
  3279. /**
  3280. * Callback function for substituteSpecialEntities() that does the work.
  3281. *
  3282. * This callback has same syntax as nonSpecialEntityCallback().
  3283. *
  3284. * @param $matches PCRE-style matches array, with 0 the entire match, and
  3285. * either index 1, 2 or 3 set with a hex value, dec value,
  3286. * or string (respectively).
  3287. * @returns Replacement string.
  3288. */
  3289. protected function specialEntityCallback($matches) {
  3290. $entity = $matches[0];
  3291. $is_num = (@$matches[0][1] === '#');
  3292. if ($is_num) {
  3293. $is_hex = (@$entity[2] === 'x');
  3294. $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
  3295. return isset($this->_special_dec2str[$int]) ?
  3296. $this->_special_dec2str[$int] :
  3297. $entity;
  3298. } else {
  3299. return isset($this->_special_ent2dec[$matches[3]]) ?
  3300. $this->_special_ent2dec[$matches[3]] :
  3301. $entity;
  3302. }
  3303. }
  3304. }
  3305. /**
  3306. * Error collection class that enables HTML Purifier to report HTML
  3307. * problems back to the user
  3308. */
  3309. class HTMLPurifier_ErrorCollector
  3310. {
  3311. /**
  3312. * Identifiers for the returned error array. These are purposely numeric
  3313. * so list() can be used.
  3314. */
  3315. const LINENO = 0;
  3316. const SEVERITY = 1;
  3317. const MESSAGE = 2;
  3318. const CHILDREN = 3;
  3319. protected $errors;
  3320. protected $_current;
  3321. protected $_stacks = array(array());
  3322. protected $locale;
  3323. protected $generator;
  3324. protected $context;
  3325. protected $lines = array();
  3326. public function __construct($context) {
  3327. $this->locale =& $context->get('Locale');
  3328. $this->context = $context;
  3329. $this->_current =& $this->_stacks[0];
  3330. $this->errors =& $this->_stacks[0];
  3331. }
  3332. /**
  3333. * Sends an error message to the collector for later use
  3334. * @param $severity int Error severity, PHP error style (don't use E_USER_)
  3335. * @param $msg string Error message text
  3336. * @param $subst1 string First substitution for $msg
  3337. * @param $subst2 string ...
  3338. */
  3339. public function send($severity, $msg) {
  3340. $args = array();
  3341. if (func_num_args() > 2) {
  3342. $args = func_get_args();
  3343. array_shift($args);
  3344. unset($args[0]);
  3345. }
  3346. $token = $this->context->get('CurrentToken', true);
  3347. $line = $token ? $token->line : $this->context->get('CurrentLine', true);
  3348. $col = $token ? $token->col : $this->context->get('CurrentCol', true);
  3349. $attr = $this->context->get('CurrentAttr', true);
  3350. // perform special substitutions, also add custom parameters
  3351. $subst = array();
  3352. if (!is_null($token)) {
  3353. $args['CurrentToken'] = $token;
  3354. }
  3355. if (!is_null($attr)) {
  3356. $subst['$CurrentAttr.Name'] = $attr;
  3357. if (isset($token->attr[$attr])) $subst['$CurrentAttr.Value'] = $token->attr[$attr];
  3358. }
  3359. if (empty($args)) {
  3360. $msg = $this->locale->getMessage($msg);
  3361. } else {
  3362. $msg = $this->locale->formatMessage($msg, $args);
  3363. }
  3364. if (!empty($subst)) $msg = strtr($msg, $subst);
  3365. // (numerically indexed)
  3366. $error = array(
  3367. self::LINENO => $line,
  3368. self::SEVERITY => $severity,
  3369. self::MESSAGE => $msg,
  3370. self::CHILDREN => array()
  3371. );
  3372. $this->_current[] = $error;
  3373. // NEW CODE BELOW ...
  3374. $struct = null;
  3375. // Top-level errors are either:
  3376. // TOKEN type, if $value is set appropriately, or
  3377. // "syntax" type, if $value is null
  3378. $new_struct = new HTMLPurifier_ErrorStruct();
  3379. $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN;
  3380. if ($token) $new_struct->value = clone $token;
  3381. if (is_int($line) && is_int($col)) {
  3382. if (isset($this->lines[$line][$col])) {
  3383. $struct = $this->lines[$line][$col];
  3384. } else {
  3385. $struct = $this->lines[$line][$col] = $new_struct;
  3386. }
  3387. // These ksorts may present a performance problem
  3388. ksort($this->lines[$line], SORT_NUMERIC);
  3389. } else {
  3390. if (isset($this->lines[-1])) {
  3391. $struct = $this->lines[-1];
  3392. } else {
  3393. $struct = $this->lines[-1] = $new_struct;
  3394. }
  3395. }
  3396. ksort($this->lines, SORT_NUMERIC);
  3397. // Now, check if we need to operate on a lower structure
  3398. if (!empty($attr)) {
  3399. $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr);
  3400. if (!$struct->value) {
  3401. $struct->value = array($attr, 'PUT VALUE HERE');
  3402. }
  3403. }
  3404. if (!empty($cssprop)) {
  3405. $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop);
  3406. if (!$struct->value) {
  3407. // if we tokenize CSS this might be a little more difficult to do
  3408. $struct->value = array($cssprop, 'PUT VALUE HERE');
  3409. }
  3410. }
  3411. // Ok, structs are all setup, now time to register the error
  3412. $struct->addError($severity, $msg);
  3413. }
  3414. /**
  3415. * Retrieves raw error data for custom formatter to use
  3416. * @param List of arrays in format of array(line of error,
  3417. * error severity, error message,
  3418. * recursive sub-errors array)
  3419. */
  3420. public function getRaw() {
  3421. return $this->errors;
  3422. }
  3423. /**
  3424. * Default HTML formatting implementation for error messages
  3425. * @param $config Configuration array, vital for HTML output nature
  3426. * @param $errors Errors array to display; used for recursion.
  3427. */
  3428. public function getHTMLFormatted($config, $errors = null) {
  3429. $ret = array();
  3430. $this->generator = new HTMLPurifier_Generator($config, $this->context);
  3431. if ($errors === null) $errors = $this->errors;
  3432. // 'At line' message needs to be removed
  3433. // generation code for new structure goes here. It needs to be recursive.
  3434. foreach ($this->lines as $line => $col_array) {
  3435. if ($line == -1) continue;
  3436. foreach ($col_array as $col => $struct) {
  3437. $this->_renderStruct($ret, $struct, $line, $col);
  3438. }
  3439. }
  3440. if (isset($this->lines[-1])) {
  3441. $this->_renderStruct($ret, $this->lines[-1]);
  3442. }
  3443. if (empty($errors)) {
  3444. return '<p>' . $this->locale->getMessage('ErrorCollector: No errors') . '</p>';
  3445. } else {
  3446. return '<ul><li>' . implode('</li><li>', $ret) . '</li></ul>';
  3447. }
  3448. }
  3449. private function _renderStruct(&$ret, $struct, $line = null, $col = null) {
  3450. $stack = array($struct);
  3451. $context_stack = array(array());
  3452. while ($current = array_pop($stack)) {
  3453. $context = array_pop($context_stack);
  3454. foreach ($current->errors as $error) {
  3455. list($severity, $msg) = $error;
  3456. $string = '';
  3457. $string .= '<div>';
  3458. // W3C uses an icon to indicate the severity of the error.
  3459. $error = $this->locale->getErrorName($severity);
  3460. $string .= "<span class=\"error e$severity\"><strong>$error</strong></span> ";
  3461. if (!is_null($line) && !is_null($col)) {
  3462. $string .= "<em class=\"location\">Line $line, Column $col: </em> ";
  3463. } else {
  3464. $string .= '<em class="location">End of Document: </em> ';
  3465. }
  3466. $string .= '<strong class="description">' . $this->generator->escape($msg) . '</strong> ';
  3467. $string .= '</div>';
  3468. // Here, have a marker for the character on the column appropriate.
  3469. // Be sure to clip extremely long lines.
  3470. //$string .= '<pre>';
  3471. //$string .= '';
  3472. //$string .= '</pre>';
  3473. $ret[] = $string;
  3474. }
  3475. foreach ($current->children as $type => $array) {
  3476. $context[] = $current;
  3477. $stack = array_merge($stack, array_reverse($array, true));
  3478. for ($i = count($array); $i > 0; $i--) {
  3479. $context_stack[] = $context;
  3480. }
  3481. }
  3482. }
  3483. }
  3484. }
  3485. /**
  3486. * Records errors for particular segments of an HTML document such as tokens,
  3487. * attributes or CSS properties. They can contain error structs (which apply
  3488. * to components of what they represent), but their main purpose is to hold
  3489. * errors applying to whatever struct is being used.
  3490. */
  3491. class HTMLPurifier_ErrorStruct
  3492. {
  3493. /**
  3494. * Possible values for $children first-key. Note that top-level structures
  3495. * are automatically token-level.
  3496. */
  3497. const TOKEN = 0;
  3498. const ATTR = 1;
  3499. const CSSPROP = 2;
  3500. /**
  3501. * Type of this struct.
  3502. */
  3503. public $type;
  3504. /**
  3505. * Value of the struct we are recording errors for. There are various
  3506. * values for this:
  3507. * - TOKEN: Instance of HTMLPurifier_Token
  3508. * - ATTR: array('attr-name', 'value')
  3509. * - CSSPROP: array('prop-name', 'value')
  3510. */
  3511. public $value;
  3512. /**
  3513. * Errors registered for this structure.
  3514. */
  3515. public $errors = array();
  3516. /**
  3517. * Child ErrorStructs that are from this structure. For example, a TOKEN
  3518. * ErrorStruct would contain ATTR ErrorStructs. This is a multi-dimensional
  3519. * array in structure: [TYPE]['identifier']
  3520. */
  3521. public $children = array();
  3522. public function getChild($type, $id) {
  3523. if (!isset($this->children[$type][$id])) {
  3524. $this->children[$type][$id] = new HTMLPurifier_ErrorStruct();
  3525. $this->children[$type][$id]->type = $type;
  3526. }
  3527. return $this->children[$type][$id];
  3528. }
  3529. public function addError($severity, $message) {
  3530. $this->errors[] = array($severity, $message);
  3531. }
  3532. }
  3533. /**
  3534. * Global exception class for HTML Purifier; any exceptions we throw
  3535. * are from here.
  3536. */
  3537. class HTMLPurifier_Exception extends Exception
  3538. {
  3539. }
  3540. /**
  3541. * Represents a pre or post processing filter on HTML Purifier's output
  3542. *
  3543. * Sometimes, a little ad-hoc fixing of HTML has to be done before
  3544. * it gets sent through HTML Purifier: you can use filters to acheive
  3545. * this effect. For instance, YouTube videos can be preserved using
  3546. * this manner. You could have used a decorator for this task, but
  3547. * PHP's support for them is not terribly robust, so we're going
  3548. * to just loop through the filters.
  3549. *
  3550. * Filters should be exited first in, last out. If there are three filters,
  3551. * named 1, 2 and 3, the order of execution should go 1->preFilter,
  3552. * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter,
  3553. * 1->postFilter.
  3554. *
  3555. * @note Methods are not declared abstract as it is perfectly legitimate
  3556. * for an implementation not to want anything to happen on a step
  3557. */
  3558. class HTMLPurifier_Filter
  3559. {
  3560. /**
  3561. * Name of the filter for identification purposes
  3562. */
  3563. public $name;
  3564. /**
  3565. * Pre-processor function, handles HTML before HTML Purifier
  3566. */
  3567. public function preFilter($html, $config, $context) {
  3568. return $html;
  3569. }
  3570. /**
  3571. * Post-processor function, handles HTML after HTML Purifier
  3572. */
  3573. public function postFilter($html, $config, $context) {
  3574. return $html;
  3575. }
  3576. }
  3577. /**
  3578. * Generates HTML from tokens.
  3579. * @todo Refactor interface so that configuration/context is determined
  3580. * upon instantiation, no need for messy generateFromTokens() calls
  3581. * @todo Make some of the more internal functions protected, and have
  3582. * unit tests work around that
  3583. */
  3584. class HTMLPurifier_Generator
  3585. {
  3586. /**
  3587. * Whether or not generator should produce XML output
  3588. */
  3589. private $_xhtml = true;
  3590. /**
  3591. * :HACK: Whether or not generator should comment the insides of <script> tags
  3592. */
  3593. private $_scriptFix = false;
  3594. /**
  3595. * Cache of HTMLDefinition during HTML output to determine whether or
  3596. * not attributes should be minimized.
  3597. */
  3598. private $_def;
  3599. /**
  3600. * Cache of %Output.SortAttr
  3601. */
  3602. private $_sortAttr;
  3603. /**
  3604. * Cache of %Output.FlashCompat
  3605. */
  3606. private $_flashCompat;
  3607. /**
  3608. * Cache of %Output.FixInnerHTML
  3609. */
  3610. private $_innerHTMLFix;
  3611. /**
  3612. * Stack for keeping track of object information when outputting IE
  3613. * compatibility code.
  3614. */
  3615. private $_flashStack = array();
  3616. /**
  3617. * Configuration for the generator
  3618. */
  3619. protected $config;
  3620. /**
  3621. * @param $config Instance of HTMLPurifier_Config
  3622. * @param $context Instance of HTMLPurifier_Context
  3623. */
  3624. public function __construct($config, $context) {
  3625. $this->config = $config;
  3626. $this->_scriptFix = $config->get('Output.CommentScriptContents');
  3627. $this->_innerHTMLFix = $config->get('Output.FixInnerHTML');
  3628. $this->_sortAttr = $config->get('Output.SortAttr');
  3629. $this->_flashCompat = $config->get('Output.FlashCompat');
  3630. $this->_def = $config->getHTMLDefinition();
  3631. $this->_xhtml = $this->_def->doctype->xml;
  3632. }
  3633. /**
  3634. * Generates HTML from an array of tokens.
  3635. * @param $tokens Array of HTMLPurifier_Token
  3636. * @param $config HTMLPurifier_Config object
  3637. * @return Generated HTML
  3638. */
  3639. public function generateFromTokens($tokens) {
  3640. if (!$tokens) return '';
  3641. // Basic algorithm
  3642. $html = '';
  3643. for ($i = 0, $size = count($tokens); $i < $size; $i++) {
  3644. if ($this->_scriptFix && $tokens[$i]->name === 'script'
  3645. && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) {
  3646. // script special case
  3647. // the contents of the script block must be ONE token
  3648. // for this to work.
  3649. $html .= $this->generateFromToken($tokens[$i++]);
  3650. $html .= $this->generateScriptFromToken($tokens[$i++]);
  3651. }
  3652. $html .= $this->generateFromToken($tokens[$i]);
  3653. }
  3654. // Tidy cleanup
  3655. if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) {
  3656. $tidy = new Tidy;
  3657. $tidy->parseString($html, array(
  3658. 'indent'=> true,
  3659. 'output-xhtml' => $this->_xhtml,
  3660. 'show-body-only' => true,
  3661. 'indent-spaces' => 2,
  3662. 'wrap' => 68,
  3663. ), 'utf8');
  3664. $tidy->cleanRepair();
  3665. $html = (string) $tidy; // explicit cast necessary
  3666. }
  3667. // Normalize newlines to system defined value
  3668. if ($this->config->get('Core.NormalizeNewlines')) {
  3669. $nl = $this->config->get('Output.Newline');
  3670. if ($nl === null) $nl = PHP_EOL;
  3671. if ($nl !== "\n") $html = str_replace("\n", $nl, $html);
  3672. }
  3673. return $html;
  3674. }
  3675. /**
  3676. * Generates HTML from a single token.
  3677. * @param $token HTMLPurifier_Token object.
  3678. * @return Generated HTML
  3679. */
  3680. public function generateFromToken($token) {
  3681. if (!$token instanceof HTMLPurifier_Token) {
  3682. trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING);
  3683. return '';
  3684. } elseif ($token instanceof HTMLPurifier_Token_Start) {
  3685. $attr = $this->generateAttributes($token->attr, $token->name);
  3686. if ($this->_flashCompat) {
  3687. if ($token->name == "object") {
  3688. $flash = new stdclass();
  3689. $flash->attr = $token->attr;
  3690. $flash->param = array();
  3691. $this->_flashStack[] = $flash;
  3692. }
  3693. }
  3694. return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>';
  3695. } elseif ($token instanceof HTMLPurifier_Token_End) {
  3696. $_extra = '';
  3697. if ($this->_flashCompat) {
  3698. if ($token->name == "object" && !empty($this->_flashStack)) {
  3699. // doesn't do anything for now
  3700. }
  3701. }
  3702. return $_extra . '</' . $token->name . '>';
  3703. } elseif ($token instanceof HTMLPurifier_Token_Empty) {
  3704. if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) {
  3705. $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value'];
  3706. }
  3707. $attr = $this->generateAttributes($token->attr, $token->name);
  3708. return '<' . $token->name . ($attr ? ' ' : '') . $attr .
  3709. ( $this->_xhtml ? ' /': '' ) // <br /> v. <br>
  3710. . '>';
  3711. } elseif ($token instanceof HTMLPurifier_Token_Text) {
  3712. return $this->escape($token->data, ENT_NOQUOTES);
  3713. } elseif ($token instanceof HTMLPurifier_Token_Comment) {
  3714. return '<!--' . $token->data . '-->';
  3715. } else {
  3716. return '';
  3717. }
  3718. }
  3719. /**
  3720. * Special case processor for the contents of script tags
  3721. * @warning This runs into problems if there's already a literal
  3722. * --> somewhere inside the script contents.
  3723. */
  3724. public function generateScriptFromToken($token) {
  3725. if (!$token instanceof HTMLPurifier_Token_Text) return $this->generateFromToken($token);
  3726. // Thanks <http://lachy.id.au/log/2005/05/script-comments>
  3727. $data = preg_replace('#//\s*$#', '', $token->data);
  3728. return '<!--//--><![CDATA[//><!--' . "\n" . trim($data) . "\n" . '//--><!]]>';
  3729. }
  3730. /**
  3731. * Generates attribute declarations from attribute array.
  3732. * @note This does not include the leading or trailing space.
  3733. * @param $assoc_array_of_attributes Attribute array
  3734. * @param $element Name of element attributes are for, used to check
  3735. * attribute minimization.
  3736. * @return Generate HTML fragment for insertion.
  3737. */
  3738. public function generateAttributes($assoc_array_of_attributes, $element = false) {
  3739. $html = '';
  3740. if ($this->_sortAttr) ksort($assoc_array_of_attributes);
  3741. foreach ($assoc_array_of_attributes as $key => $value) {
  3742. if (!$this->_xhtml) {
  3743. // Remove namespaced attributes
  3744. if (strpos($key, ':') !== false) continue;
  3745. // Check if we should minimize the attribute: val="val" -> val
  3746. if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) {
  3747. $html .= $key . ' ';
  3748. continue;
  3749. }
  3750. }
  3751. // Workaround for Internet Explorer innerHTML bug.
  3752. // Essentially, Internet Explorer, when calculating
  3753. // innerHTML, omits quotes if there are no instances of
  3754. // angled brackets, quotes or spaces. However, when parsing
  3755. // HTML (for example, when you assign to innerHTML), it
  3756. // treats backticks as quotes. Thus,
  3757. // <img alt="``" />
  3758. // becomes
  3759. // <img alt=`` />
  3760. // becomes
  3761. // <img alt='' />
  3762. // Fortunately, all we need to do is trigger an appropriate
  3763. // quoting style, which we do by adding an extra space.
  3764. // This also is consistent with the W3C spec, which states
  3765. // that user agents may ignore leading or trailing
  3766. // whitespace (in fact, most don't, at least for attributes
  3767. // like alt, but an extra space at the end is barely
  3768. // noticeable). Still, we have a configuration knob for
  3769. // this, since this transformation is not necesary if you
  3770. // don't process user input with innerHTML or you don't plan
  3771. // on supporting Internet Explorer.
  3772. if ($this->_innerHTMLFix) {
  3773. if (strpos($value, '`') !== false) {
  3774. // check if correct quoting style would not already be
  3775. // triggered
  3776. if (strcspn($value, '"\' <>') === strlen($value)) {
  3777. // protect!
  3778. $value .= ' ';
  3779. }
  3780. }
  3781. }
  3782. $html .= $key.'="'.$this->escape($value).'" ';
  3783. }
  3784. return rtrim($html);
  3785. }
  3786. /**
  3787. * Escapes raw text data.
  3788. * @todo This really ought to be protected, but until we have a facility
  3789. * for properly generating HTML here w/o using tokens, it stays
  3790. * public.
  3791. * @param $string String data to escape for HTML.
  3792. * @param $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is
  3793. * permissible for non-attribute output.
  3794. * @return String escaped data.
  3795. */
  3796. public function escape($string, $quote = null) {
  3797. // Workaround for APC bug on Mac Leopard reported by sidepodcast
  3798. // http://htmlpurifier.org/phorum/read.php?3,4823,4846
  3799. if ($quote === null) $quote = ENT_COMPAT;
  3800. return htmlspecialchars($string, $quote, 'UTF-8');
  3801. }
  3802. }
  3803. /**
  3804. * Definition of the purified HTML that describes allowed children,
  3805. * attributes, and many other things.
  3806. *
  3807. * Conventions:
  3808. *
  3809. * All member variables that are prefixed with info
  3810. * (including the main $info array) are used by HTML Purifier internals
  3811. * and should not be directly edited when customizing the HTMLDefinition.
  3812. * They can usually be set via configuration directives or custom
  3813. * modules.
  3814. *
  3815. * On the other hand, member variables without the info prefix are used
  3816. * internally by the HTMLDefinition and MUST NOT be used by other HTML
  3817. * Purifier internals. Many of them, however, are public, and may be
  3818. * edited by userspace code to tweak the behavior of HTMLDefinition.
  3819. *
  3820. * @note This class is inspected by Printer_HTMLDefinition; please
  3821. * update that class if things here change.
  3822. *
  3823. * @warning Directives that change this object's structure must be in
  3824. * the HTML or Attr namespace!
  3825. */
  3826. class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition
  3827. {
  3828. // FULLY-PUBLIC VARIABLES ---------------------------------------------
  3829. /**
  3830. * Associative array of element names to HTMLPurifier_ElementDef
  3831. */
  3832. public $info = array();
  3833. /**
  3834. * Associative array of global attribute name to attribute definition.
  3835. */
  3836. public $info_global_attr = array();
  3837. /**
  3838. * String name of parent element HTML will be going into.
  3839. */
  3840. public $info_parent = 'div';
  3841. /**
  3842. * Definition for parent element, allows parent element to be a
  3843. * tag that's not allowed inside the HTML fragment.
  3844. */
  3845. public $info_parent_def;
  3846. /**
  3847. * String name of element used to wrap inline elements in block context
  3848. * @note This is rarely used except for BLOCKQUOTEs in strict mode
  3849. */
  3850. public $info_block_wrapper = 'p';
  3851. /**
  3852. * Associative array of deprecated tag name to HTMLPurifier_TagTransform
  3853. */
  3854. public $info_tag_transform = array();
  3855. /**
  3856. * Indexed list of HTMLPurifier_AttrTransform to be performed before validation.
  3857. */
  3858. public $info_attr_transform_pre = array();
  3859. /**
  3860. * Indexed list of HTMLPurifier_AttrTransform to be performed after validation.
  3861. */
  3862. public $info_attr_transform_post = array();
  3863. /**
  3864. * Nested lookup array of content set name (Block, Inline) to
  3865. * element name to whether or not it belongs in that content set.
  3866. */
  3867. public $info_content_sets = array();
  3868. /**
  3869. * Indexed list of HTMLPurifier_Injector to be used.
  3870. */
  3871. public $info_injector = array();
  3872. /**
  3873. * Doctype object
  3874. */
  3875. public $doctype;
  3876. // RAW CUSTOMIZATION STUFF --------------------------------------------
  3877. /**
  3878. * Adds a custom attribute to a pre-existing element
  3879. * @note This is strictly convenience, and does not have a corresponding
  3880. * method in HTMLPurifier_HTMLModule
  3881. * @param $element_name String element name to add attribute to
  3882. * @param $attr_name String name of attribute
  3883. * @param $def Attribute definition, can be string or object, see
  3884. * HTMLPurifier_AttrTypes for details
  3885. */
  3886. public function addAttribute($element_name, $attr_name, $def) {
  3887. $module = $this->getAnonymousModule();
  3888. if (!isset($module->info[$element_name])) {
  3889. $element = $module->addBlankElement($element_name);
  3890. } else {
  3891. $element = $module->info[$element_name];
  3892. }
  3893. $element->attr[$attr_name] = $def;
  3894. }
  3895. /**
  3896. * Adds a custom element to your HTML definition
  3897. * @note See HTMLPurifier_HTMLModule::addElement for detailed
  3898. * parameter and return value descriptions.
  3899. */
  3900. public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) {
  3901. $module = $this->getAnonymousModule();
  3902. // assume that if the user is calling this, the element
  3903. // is safe. This may not be a good idea
  3904. $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes);
  3905. return $element;
  3906. }
  3907. /**
  3908. * Adds a blank element to your HTML definition, for overriding
  3909. * existing behavior
  3910. * @note See HTMLPurifier_HTMLModule::addBlankElement for detailed
  3911. * parameter and return value descriptions.
  3912. */
  3913. public function addBlankElement($element_name) {
  3914. $module = $this->getAnonymousModule();
  3915. $element = $module->addBlankElement($element_name);
  3916. return $element;
  3917. }
  3918. /**
  3919. * Retrieves a reference to the anonymous module, so you can
  3920. * bust out advanced features without having to make your own
  3921. * module.
  3922. */
  3923. public function getAnonymousModule() {
  3924. if (!$this->_anonModule) {
  3925. $this->_anonModule = new HTMLPurifier_HTMLModule();
  3926. $this->_anonModule->name = 'Anonymous';
  3927. }
  3928. return $this->_anonModule;
  3929. }
  3930. private $_anonModule = null;
  3931. // PUBLIC BUT INTERNAL VARIABLES --------------------------------------
  3932. public $type = 'HTML';
  3933. public $manager; /**< Instance of HTMLPurifier_HTMLModuleManager */
  3934. /**
  3935. * Performs low-cost, preliminary initialization.
  3936. */
  3937. public function __construct() {
  3938. $this->manager = new HTMLPurifier_HTMLModuleManager();
  3939. }
  3940. protected function doSetup($config) {
  3941. $this->processModules($config);
  3942. $this->setupConfigStuff($config);
  3943. unset($this->manager);
  3944. // cleanup some of the element definitions
  3945. foreach ($this->info as $k => $v) {
  3946. unset($this->info[$k]->content_model);
  3947. unset($this->info[$k]->content_model_type);
  3948. }
  3949. }
  3950. /**
  3951. * Extract out the information from the manager
  3952. */
  3953. protected function processModules($config) {
  3954. if ($this->_anonModule) {
  3955. // for user specific changes
  3956. // this is late-loaded so we don't have to deal with PHP4
  3957. // reference wonky-ness
  3958. $this->manager->addModule($this->_anonModule);
  3959. unset($this->_anonModule);
  3960. }
  3961. $this->manager->setup($config);
  3962. $this->doctype = $this->manager->doctype;
  3963. foreach ($this->manager->modules as $module) {
  3964. foreach($module->info_tag_transform as $k => $v) {
  3965. if ($v === false) unset($this->info_tag_transform[$k]);
  3966. else $this->info_tag_transform[$k] = $v;
  3967. }
  3968. foreach($module->info_attr_transform_pre as $k => $v) {
  3969. if ($v === false) unset($this->info_attr_transform_pre[$k]);
  3970. else $this->info_attr_transform_pre[$k] = $v;
  3971. }
  3972. foreach($module->info_attr_transform_post as $k => $v) {
  3973. if ($v === false) unset($this->info_attr_transform_post[$k]);
  3974. else $this->info_attr_transform_post[$k] = $v;
  3975. }
  3976. foreach ($module->info_injector as $k => $v) {
  3977. if ($v === false) unset($this->info_injector[$k]);
  3978. else $this->info_injector[$k] = $v;
  3979. }
  3980. }
  3981. $this->info = $this->manager->getElements();
  3982. $this->info_content_sets = $this->manager->contentSets->lookup;
  3983. }
  3984. /**
  3985. * Sets up stuff based on config. We need a better way of doing this.
  3986. */
  3987. protected function setupConfigStuff($config) {
  3988. $block_wrapper = $config->get('HTML.BlockWrapper');
  3989. if (isset($this->info_content_sets['Block'][$block_wrapper])) {
  3990. $this->info_block_wrapper = $block_wrapper;
  3991. } else {
  3992. trigger_error('Cannot use non-block element as block wrapper',
  3993. E_USER_ERROR);
  3994. }
  3995. $parent = $config->get('HTML.Parent');
  3996. $def = $this->manager->getElement($parent, true);
  3997. if ($def) {
  3998. $this->info_parent = $parent;
  3999. $this->info_parent_def = $def;
  4000. } else {
  4001. trigger_error('Cannot use unrecognized element as parent',
  4002. E_USER_ERROR);
  4003. $this->info_parent_def = $this->manager->getElement($this->info_parent, true);
  4004. }
  4005. // support template text
  4006. $support = "(for information on implementing this, see the ".
  4007. "support forums) ";
  4008. // setup allowed elements -----------------------------------------
  4009. $allowed_elements = $config->get('HTML.AllowedElements');
  4010. $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early
  4011. if (!is_array($allowed_elements) && !is_array($allowed_attributes)) {
  4012. $allowed = $config->get('HTML.Allowed');
  4013. if (is_string($allowed)) {
  4014. list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed);
  4015. }
  4016. }
  4017. if (is_array($allowed_elements)) {
  4018. foreach ($this->info as $name => $d) {
  4019. if(!isset($allowed_elements[$name])) unset($this->info[$name]);
  4020. unset($allowed_elements[$name]);
  4021. }
  4022. // emit errors
  4023. foreach ($allowed_elements as $element => $d) {
  4024. $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful!
  4025. trigger_error("Element '$element' is not supported $support", E_USER_WARNING);
  4026. }
  4027. }
  4028. // setup allowed attributes ---------------------------------------
  4029. $allowed_attributes_mutable = $allowed_attributes; // by copy!
  4030. if (is_array($allowed_attributes)) {
  4031. // This actually doesn't do anything, since we went away from
  4032. // global attributes. It's possible that userland code uses
  4033. // it, but HTMLModuleManager doesn't!
  4034. foreach ($this->info_global_attr as $attr => $x) {
  4035. $keys = array($attr, "*@$attr", "*.$attr");
  4036. $delete = true;
  4037. foreach ($keys as $key) {
  4038. if ($delete && isset($allowed_attributes[$key])) {
  4039. $delete = false;
  4040. }
  4041. if (isset($allowed_attributes_mutable[$key])) {
  4042. unset($allowed_attributes_mutable[$key]);
  4043. }
  4044. }
  4045. if ($delete) unset($this->info_global_attr[$attr]);
  4046. }
  4047. foreach ($this->info as $tag => $info) {
  4048. foreach ($info->attr as $attr => $x) {
  4049. $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr");
  4050. $delete = true;
  4051. foreach ($keys as $key) {
  4052. if ($delete && isset($allowed_attributes[$key])) {
  4053. $delete = false;
  4054. }
  4055. if (isset($allowed_attributes_mutable[$key])) {
  4056. unset($allowed_attributes_mutable[$key]);
  4057. }
  4058. }
  4059. if ($delete) {
  4060. if ($this->info[$tag]->attr[$attr]->required) {
  4061. trigger_error("Required attribute '$attr' in element '$tag' was not allowed, which means '$tag' will not be allowed either", E_USER_WARNING);
  4062. }
  4063. unset($this->info[$tag]->attr[$attr]);
  4064. }
  4065. }
  4066. }
  4067. // emit errors
  4068. foreach ($allowed_attributes_mutable as $elattr => $d) {
  4069. $bits = preg_split('/[.@]/', $elattr, 2);
  4070. $c = count($bits);
  4071. switch ($c) {
  4072. case 2:
  4073. if ($bits[0] !== '*') {
  4074. $element = htmlspecialchars($bits[0]);
  4075. $attribute = htmlspecialchars($bits[1]);
  4076. if (!isset($this->info[$element])) {
  4077. trigger_error("Cannot allow attribute '$attribute' if element '$element' is not allowed/supported $support");
  4078. } else {
  4079. trigger_error("Attribute '$attribute' in element '$element' not supported $support",
  4080. E_USER_WARNING);
  4081. }
  4082. break;
  4083. }
  4084. // otherwise fall through
  4085. case 1:
  4086. $attribute = htmlspecialchars($bits[0]);
  4087. trigger_error("Global attribute '$attribute' is not ".
  4088. "supported in any elements $support",
  4089. E_USER_WARNING);
  4090. break;
  4091. }
  4092. }
  4093. }
  4094. // setup forbidden elements ---------------------------------------
  4095. $forbidden_elements = $config->get('HTML.ForbiddenElements');
  4096. $forbidden_attributes = $config->get('HTML.ForbiddenAttributes');
  4097. foreach ($this->info as $tag => $info) {
  4098. if (isset($forbidden_elements[$tag])) {
  4099. unset($this->info[$tag]);
  4100. continue;
  4101. }
  4102. foreach ($info->attr as $attr => $x) {
  4103. if (
  4104. isset($forbidden_attributes["$tag@$attr"]) ||
  4105. isset($forbidden_attributes["*@$attr"]) ||
  4106. isset($forbidden_attributes[$attr])
  4107. ) {
  4108. unset($this->info[$tag]->attr[$attr]);
  4109. continue;
  4110. } // this segment might get removed eventually
  4111. elseif (isset($forbidden_attributes["$tag.$attr"])) {
  4112. // $tag.$attr are not user supplied, so no worries!
  4113. trigger_error("Error with $tag.$attr: tag.attr syntax not supported for HTML.ForbiddenAttributes; use tag@attr instead", E_USER_WARNING);
  4114. }
  4115. }
  4116. }
  4117. foreach ($forbidden_attributes as $key => $v) {
  4118. if (strlen($key) < 2) continue;
  4119. if ($key[0] != '*') continue;
  4120. if ($key[1] == '.') {
  4121. trigger_error("Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", E_USER_WARNING);
  4122. }
  4123. }
  4124. // setup injectors -----------------------------------------------------
  4125. foreach ($this->info_injector as $i => $injector) {
  4126. if ($injector->checkNeeded($config) !== false) {
  4127. // remove injector that does not have it's required
  4128. // elements/attributes present, and is thus not needed.
  4129. unset($this->info_injector[$i]);
  4130. }
  4131. }
  4132. }
  4133. /**
  4134. * Parses a TinyMCE-flavored Allowed Elements and Attributes list into
  4135. * separate lists for processing. Format is element[attr1|attr2],element2...
  4136. * @warning Although it's largely drawn from TinyMCE's implementation,
  4137. * it is different, and you'll probably have to modify your lists
  4138. * @param $list String list to parse
  4139. * @param array($allowed_elements, $allowed_attributes)
  4140. * @todo Give this its own class, probably static interface
  4141. */
  4142. public function parseTinyMCEAllowedList($list) {
  4143. $list = str_replace(array(' ', "\t"), '', $list);
  4144. $elements = array();
  4145. $attributes = array();
  4146. $chunks = preg_split('/(,|[\n\r]+)/', $list);
  4147. foreach ($chunks as $chunk) {
  4148. if (empty($chunk)) continue;
  4149. // remove TinyMCE element control characters
  4150. if (!strpos($chunk, '[')) {
  4151. $element = $chunk;
  4152. $attr = false;
  4153. } else {
  4154. list($element, $attr) = explode('[', $chunk);
  4155. }
  4156. if ($element !== '*') $elements[$element] = true;
  4157. if (!$attr) continue;
  4158. $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ]
  4159. $attr = explode('|', $attr);
  4160. foreach ($attr as $key) {
  4161. $attributes["$element.$key"] = true;
  4162. }
  4163. }
  4164. return array($elements, $attributes);
  4165. }
  4166. }
  4167. /**
  4168. * Represents an XHTML 1.1 module, with information on elements, tags
  4169. * and attributes.
  4170. * @note Even though this is technically XHTML 1.1, it is also used for
  4171. * regular HTML parsing. We are using modulization as a convenient
  4172. * way to represent the internals of HTMLDefinition, and our
  4173. * implementation is by no means conforming and does not directly
  4174. * use the normative DTDs or XML schemas.
  4175. * @note The public variables in a module should almost directly
  4176. * correspond to the variables in HTMLPurifier_HTMLDefinition.
  4177. * However, the prefix info carries no special meaning in these
  4178. * objects (include it anyway if that's the correspondence though).
  4179. * @todo Consider making some member functions protected
  4180. */
  4181. class HTMLPurifier_HTMLModule
  4182. {
  4183. // -- Overloadable ----------------------------------------------------
  4184. /**
  4185. * Short unique string identifier of the module
  4186. */
  4187. public $name;
  4188. /**
  4189. * Informally, a list of elements this module changes. Not used in
  4190. * any significant way.
  4191. */
  4192. public $elements = array();
  4193. /**
  4194. * Associative array of element names to element definitions.
  4195. * Some definitions may be incomplete, to be merged in later
  4196. * with the full definition.
  4197. */
  4198. public $info = array();
  4199. /**
  4200. * Associative array of content set names to content set additions.
  4201. * This is commonly used to, say, add an A element to the Inline
  4202. * content set. This corresponds to an internal variable $content_sets
  4203. * and NOT info_content_sets member variable of HTMLDefinition.
  4204. */
  4205. public $content_sets = array();
  4206. /**
  4207. * Associative array of attribute collection names to attribute
  4208. * collection additions. More rarely used for adding attributes to
  4209. * the global collections. Example is the StyleAttribute module adding
  4210. * the style attribute to the Core. Corresponds to HTMLDefinition's
  4211. * attr_collections->info, since the object's data is only info,
  4212. * with extra behavior associated with it.
  4213. */
  4214. public $attr_collections = array();
  4215. /**
  4216. * Associative array of deprecated tag name to HTMLPurifier_TagTransform
  4217. */
  4218. public $info_tag_transform = array();
  4219. /**
  4220. * List of HTMLPurifier_AttrTransform to be performed before validation.
  4221. */
  4222. public $info_attr_transform_pre = array();
  4223. /**
  4224. * List of HTMLPurifier_AttrTransform to be performed after validation.
  4225. */
  4226. public $info_attr_transform_post = array();
  4227. /**
  4228. * List of HTMLPurifier_Injector to be performed during well-formedness fixing.
  4229. * An injector will only be invoked if all of it's pre-requisites are met;
  4230. * if an injector fails setup, there will be no error; it will simply be
  4231. * silently disabled.
  4232. */
  4233. public $info_injector = array();
  4234. /**
  4235. * Boolean flag that indicates whether or not getChildDef is implemented.
  4236. * For optimization reasons: may save a call to a function. Be sure
  4237. * to set it if you do implement getChildDef(), otherwise it will have
  4238. * no effect!
  4239. */
  4240. public $defines_child_def = false;
  4241. /**
  4242. * Boolean flag whether or not this module is safe. If it is not safe, all
  4243. * of its members are unsafe. Modules are safe by default (this might be
  4244. * slightly dangerous, but it doesn't make much sense to force HTML Purifier,
  4245. * which is based off of safe HTML, to explicitly say, "This is safe," even
  4246. * though there are modules which are "unsafe")
  4247. *
  4248. * @note Previously, safety could be applied at an element level granularity.
  4249. * We've removed this ability, so in order to add "unsafe" elements
  4250. * or attributes, a dedicated module with this property set to false
  4251. * must be used.
  4252. */
  4253. public $safe = true;
  4254. /**
  4255. * Retrieves a proper HTMLPurifier_ChildDef subclass based on
  4256. * content_model and content_model_type member variables of
  4257. * the HTMLPurifier_ElementDef class. There is a similar function
  4258. * in HTMLPurifier_HTMLDefinition.
  4259. * @param $def HTMLPurifier_ElementDef instance
  4260. * @return HTMLPurifier_ChildDef subclass
  4261. */
  4262. public function getChildDef($def) {return false;}
  4263. // -- Convenience -----------------------------------------------------
  4264. /**
  4265. * Convenience function that sets up a new element
  4266. * @param $element Name of element to add
  4267. * @param $type What content set should element be registered to?
  4268. * Set as false to skip this step.
  4269. * @param $contents Allowed children in form of:
  4270. * "$content_model_type: $content_model"
  4271. * @param $attr_includes What attribute collections to register to
  4272. * element?
  4273. * @param $attr What unique attributes does the element define?
  4274. * @note See ElementDef for in-depth descriptions of these parameters.
  4275. * @return Created element definition object, so you
  4276. * can set advanced parameters
  4277. */
  4278. public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) {
  4279. $this->elements[] = $element;
  4280. // parse content_model
  4281. list($content_model_type, $content_model) = $this->parseContents($contents);
  4282. // merge in attribute inclusions
  4283. $this->mergeInAttrIncludes($attr, $attr_includes);
  4284. // add element to content sets
  4285. if ($type) $this->addElementToContentSet($element, $type);
  4286. // create element
  4287. $this->info[$element] = HTMLPurifier_ElementDef::create(
  4288. $content_model, $content_model_type, $attr
  4289. );
  4290. // literal object $contents means direct child manipulation
  4291. if (!is_string($contents)) $this->info[$element]->child = $contents;
  4292. return $this->info[$element];
  4293. }
  4294. /**
  4295. * Convenience function that creates a totally blank, non-standalone
  4296. * element.
  4297. * @param $element Name of element to create
  4298. * @return Created element
  4299. */
  4300. public function addBlankElement($element) {
  4301. if (!isset($this->info[$element])) {
  4302. $this->elements[] = $element;
  4303. $this->info[$element] = new HTMLPurifier_ElementDef();
  4304. $this->info[$element]->standalone = false;
  4305. } else {
  4306. trigger_error("Definition for $element already exists in module, cannot redefine");
  4307. }
  4308. return $this->info[$element];
  4309. }
  4310. /**
  4311. * Convenience function that registers an element to a content set
  4312. * @param Element to register
  4313. * @param Name content set (warning: case sensitive, usually upper-case
  4314. * first letter)
  4315. */
  4316. public function addElementToContentSet($element, $type) {
  4317. if (!isset($this->content_sets[$type])) $this->content_sets[$type] = '';
  4318. else $this->content_sets[$type] .= ' | ';
  4319. $this->content_sets[$type] .= $element;
  4320. }
  4321. /**
  4322. * Convenience function that transforms single-string contents
  4323. * into separate content model and content model type
  4324. * @param $contents Allowed children in form of:
  4325. * "$content_model_type: $content_model"
  4326. * @note If contents is an object, an array of two nulls will be
  4327. * returned, and the callee needs to take the original $contents
  4328. * and use it directly.
  4329. */
  4330. public function parseContents($contents) {
  4331. if (!is_string($contents)) return array(null, null); // defer
  4332. switch ($contents) {
  4333. // check for shorthand content model forms
  4334. case 'Empty':
  4335. return array('empty', '');
  4336. case 'Inline':
  4337. return array('optional', 'Inline | #PCDATA');
  4338. case 'Flow':
  4339. return array('optional', 'Flow | #PCDATA');
  4340. }
  4341. list($content_model_type, $content_model) = explode(':', $contents);
  4342. $content_model_type = strtolower(trim($content_model_type));
  4343. $content_model = trim($content_model);
  4344. return array($content_model_type, $content_model);
  4345. }
  4346. /**
  4347. * Convenience function that merges a list of attribute includes into
  4348. * an attribute array.
  4349. * @param $attr Reference to attr array to modify
  4350. * @param $attr_includes Array of includes / string include to merge in
  4351. */
  4352. public function mergeInAttrIncludes(&$attr, $attr_includes) {
  4353. if (!is_array($attr_includes)) {
  4354. if (empty($attr_includes)) $attr_includes = array();
  4355. else $attr_includes = array($attr_includes);
  4356. }
  4357. $attr[0] = $attr_includes;
  4358. }
  4359. /**
  4360. * Convenience function that generates a lookup table with boolean
  4361. * true as value.
  4362. * @param $list List of values to turn into a lookup
  4363. * @note You can also pass an arbitrary number of arguments in
  4364. * place of the regular argument
  4365. * @return Lookup array equivalent of list
  4366. */
  4367. public function makeLookup($list) {
  4368. if (is_string($list)) $list = func_get_args();
  4369. $ret = array();
  4370. foreach ($list as $value) {
  4371. if (is_null($value)) continue;
  4372. $ret[$value] = true;
  4373. }
  4374. return $ret;
  4375. }
  4376. /**
  4377. * Lazy load construction of the module after determining whether
  4378. * or not it's needed, and also when a finalized configuration object
  4379. * is available.
  4380. * @param $config Instance of HTMLPurifier_Config
  4381. */
  4382. public function setup($config) {}
  4383. }
  4384. class HTMLPurifier_HTMLModuleManager
  4385. {
  4386. /**
  4387. * Instance of HTMLPurifier_DoctypeRegistry
  4388. */
  4389. public $doctypes;
  4390. /**
  4391. * Instance of current doctype
  4392. */
  4393. public $doctype;
  4394. /**
  4395. * Instance of HTMLPurifier_AttrTypes
  4396. */
  4397. public $attrTypes;
  4398. /**
  4399. * Active instances of modules for the specified doctype are
  4400. * indexed, by name, in this array.
  4401. */
  4402. public $modules = array();
  4403. /**
  4404. * Array of recognized HTMLPurifier_Module instances, indexed by
  4405. * module's class name. This array is usually lazy loaded, but a
  4406. * user can overload a module by pre-emptively registering it.
  4407. */
  4408. public $registeredModules = array();
  4409. /**
  4410. * List of extra modules that were added by the user using addModule().
  4411. * These get unconditionally merged into the current doctype, whatever
  4412. * it may be.
  4413. */
  4414. public $userModules = array();
  4415. /**
  4416. * Associative array of element name to list of modules that have
  4417. * definitions for the element; this array is dynamically filled.
  4418. */
  4419. public $elementLookup = array();
  4420. /** List of prefixes we should use for registering small names */
  4421. public $prefixes = array('HTMLPurifier_HTMLModule_');
  4422. public $contentSets; /**< Instance of HTMLPurifier_ContentSets */
  4423. public $attrCollections; /**< Instance of HTMLPurifier_AttrCollections */
  4424. /** If set to true, unsafe elements and attributes will be allowed */
  4425. public $trusted = false;
  4426. public function __construct() {
  4427. // editable internal objects
  4428. $this->attrTypes = new HTMLPurifier_AttrTypes();
  4429. $this->doctypes = new HTMLPurifier_DoctypeRegistry();
  4430. // setup basic modules
  4431. $common = array(
  4432. 'CommonAttributes', 'Text', 'Hypertext', 'List',
  4433. 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image',
  4434. 'StyleAttribute',
  4435. // Unsafe:
  4436. 'Scripting', 'Object', 'Forms',
  4437. // Sorta legacy, but present in strict:
  4438. 'Name',
  4439. );
  4440. $transitional = array('Legacy', 'Target', 'Iframe');
  4441. $xml = array('XMLCommonAttributes');
  4442. $non_xml = array('NonXMLCommonAttributes');
  4443. // setup basic doctypes
  4444. $this->doctypes->register(
  4445. 'HTML 4.01 Transitional', false,
  4446. array_merge($common, $transitional, $non_xml),
  4447. array('Tidy_Transitional', 'Tidy_Proprietary'),
  4448. array(),
  4449. '-//W3C//DTD HTML 4.01 Transitional//EN',
  4450. 'http://www.w3.org/TR/html4/loose.dtd'
  4451. );
  4452. $this->doctypes->register(
  4453. 'HTML 4.01 Strict', false,
  4454. array_merge($common, $non_xml),
  4455. array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
  4456. array(),
  4457. '-//W3C//DTD HTML 4.01//EN',
  4458. 'http://www.w3.org/TR/html4/strict.dtd'
  4459. );
  4460. $this->doctypes->register(
  4461. 'XHTML 1.0 Transitional', true,
  4462. array_merge($common, $transitional, $xml, $non_xml),
  4463. array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'),
  4464. array(),
  4465. '-//W3C//DTD XHTML 1.0 Transitional//EN',
  4466. 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
  4467. );
  4468. $this->doctypes->register(
  4469. 'XHTML 1.0 Strict', true,
  4470. array_merge($common, $xml, $non_xml),
  4471. array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
  4472. array(),
  4473. '-//W3C//DTD XHTML 1.0 Strict//EN',
  4474. 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
  4475. );
  4476. $this->doctypes->register(
  4477. 'XHTML 1.1', true,
  4478. // Iframe is a real XHTML 1.1 module, despite being
  4479. // "transitional"!
  4480. array_merge($common, $xml, array('Ruby', 'Iframe')),
  4481. array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1
  4482. array(),
  4483. '-//W3C//DTD XHTML 1.1//EN',
  4484. 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
  4485. );
  4486. }
  4487. /**
  4488. * Registers a module to the recognized module list, useful for
  4489. * overloading pre-existing modules.
  4490. * @param $module Mixed: string module name, with or without
  4491. * HTMLPurifier_HTMLModule prefix, or instance of
  4492. * subclass of HTMLPurifier_HTMLModule.
  4493. * @param $overload Boolean whether or not to overload previous modules.
  4494. * If this is not set, and you do overload a module,
  4495. * HTML Purifier will complain with a warning.
  4496. * @note This function will not call autoload, you must instantiate
  4497. * (and thus invoke) autoload outside the method.
  4498. * @note If a string is passed as a module name, different variants
  4499. * will be tested in this order:
  4500. * - Check for HTMLPurifier_HTMLModule_$name
  4501. * - Check all prefixes with $name in order they were added
  4502. * - Check for literal object name
  4503. * - Throw fatal error
  4504. * If your object name collides with an internal class, specify
  4505. * your module manually. All modules must have been included
  4506. * externally: registerModule will not perform inclusions for you!
  4507. */
  4508. public function registerModule($module, $overload = false) {
  4509. if (is_string($module)) {
  4510. // attempt to load the module
  4511. $original_module = $module;
  4512. $ok = false;
  4513. foreach ($this->prefixes as $prefix) {
  4514. $module = $prefix . $original_module;
  4515. if (class_exists($module)) {
  4516. $ok = true;
  4517. break;
  4518. }
  4519. }
  4520. if (!$ok) {
  4521. $module = $original_module;
  4522. if (!class_exists($module)) {
  4523. trigger_error($original_module . ' module does not exist',
  4524. E_USER_ERROR);
  4525. return;
  4526. }
  4527. }
  4528. $module = new $module();
  4529. }
  4530. if (empty($module->name)) {
  4531. trigger_error('Module instance of ' . get_class($module) . ' must have name');
  4532. return;
  4533. }
  4534. if (!$overload && isset($this->registeredModules[$module->name])) {
  4535. trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING);
  4536. }
  4537. $this->registeredModules[$module->name] = $module;
  4538. }
  4539. /**
  4540. * Adds a module to the current doctype by first registering it,
  4541. * and then tacking it on to the active doctype
  4542. */
  4543. public function addModule($module) {
  4544. $this->registerModule($module);
  4545. if (is_object($module)) $module = $module->name;
  4546. $this->userModules[] = $module;
  4547. }
  4548. /**
  4549. * Adds a class prefix that registerModule() will use to resolve a
  4550. * string name to a concrete class
  4551. */
  4552. public function addPrefix($prefix) {
  4553. $this->prefixes[] = $prefix;
  4554. }
  4555. /**
  4556. * Performs processing on modules, after being called you may
  4557. * use getElement() and getElements()
  4558. * @param $config Instance of HTMLPurifier_Config
  4559. */
  4560. public function setup($config) {
  4561. $this->trusted = $config->get('HTML.Trusted');
  4562. // generate
  4563. $this->doctype = $this->doctypes->make($config);
  4564. $modules = $this->doctype->modules;
  4565. // take out the default modules that aren't allowed
  4566. $lookup = $config->get('HTML.AllowedModules');
  4567. $special_cases = $config->get('HTML.CoreModules');
  4568. if (is_array($lookup)) {
  4569. foreach ($modules as $k => $m) {
  4570. if (isset($special_cases[$m])) continue;
  4571. if (!isset($lookup[$m])) unset($modules[$k]);
  4572. }
  4573. }
  4574. // custom modules
  4575. if ($config->get('HTML.Proprietary')) {
  4576. $modules[] = 'Proprietary';
  4577. }
  4578. if ($config->get('HTML.SafeObject')) {
  4579. $modules[] = 'SafeObject';
  4580. }
  4581. if ($config->get('HTML.SafeEmbed')) {
  4582. $modules[] = 'SafeEmbed';
  4583. }
  4584. if ($config->get('HTML.SafeScripting') !== array()) {
  4585. $modules[] = 'SafeScripting';
  4586. }
  4587. if ($config->get('HTML.Nofollow')) {
  4588. $modules[] = 'Nofollow';
  4589. }
  4590. if ($config->get('HTML.TargetBlank')) {
  4591. $modules[] = 'TargetBlank';
  4592. }
  4593. // merge in custom modules
  4594. $modules = array_merge($modules, $this->userModules);
  4595. foreach ($modules as $module) {
  4596. $this->processModule($module);
  4597. $this->modules[$module]->setup($config);
  4598. }
  4599. foreach ($this->doctype->tidyModules as $module) {
  4600. $this->processModule($module);
  4601. $this->modules[$module]->setup($config);
  4602. }
  4603. // prepare any injectors
  4604. foreach ($this->modules as $module) {
  4605. $n = array();
  4606. foreach ($module->info_injector as $i => $injector) {
  4607. if (!is_object($injector)) {
  4608. $class = "HTMLPurifier_Injector_$injector";
  4609. $injector = new $class;
  4610. }
  4611. $n[$injector->name] = $injector;
  4612. }
  4613. $module->info_injector = $n;
  4614. }
  4615. // setup lookup table based on all valid modules
  4616. foreach ($this->modules as $module) {
  4617. foreach ($module->info as $name => $def) {
  4618. if (!isset($this->elementLookup[$name])) {
  4619. $this->elementLookup[$name] = array();
  4620. }
  4621. $this->elementLookup[$name][] = $module->name;
  4622. }
  4623. }
  4624. // note the different choice
  4625. $this->contentSets = new HTMLPurifier_ContentSets(
  4626. // content set assembly deals with all possible modules,
  4627. // not just ones deemed to be "safe"
  4628. $this->modules
  4629. );
  4630. $this->attrCollections = new HTMLPurifier_AttrCollections(
  4631. $this->attrTypes,
  4632. // there is no way to directly disable a global attribute,
  4633. // but using AllowedAttributes or simply not including
  4634. // the module in your custom doctype should be sufficient
  4635. $this->modules
  4636. );
  4637. }
  4638. /**
  4639. * Takes a module and adds it to the active module collection,
  4640. * registering it if necessary.
  4641. */
  4642. public function processModule($module) {
  4643. if (!isset($this->registeredModules[$module]) || is_object($module)) {
  4644. $this->registerModule($module);
  4645. }
  4646. $this->modules[$module] = $this->registeredModules[$module];
  4647. }
  4648. /**
  4649. * Retrieves merged element definitions.
  4650. * @return Array of HTMLPurifier_ElementDef
  4651. */
  4652. public function getElements() {
  4653. $elements = array();
  4654. foreach ($this->modules as $module) {
  4655. if (!$this->trusted && !$module->safe) continue;
  4656. foreach ($module->info as $name => $v) {
  4657. if (isset($elements[$name])) continue;
  4658. $elements[$name] = $this->getElement($name);
  4659. }
  4660. }
  4661. // remove dud elements, this happens when an element that
  4662. // appeared to be safe actually wasn't
  4663. foreach ($elements as $n => $v) {
  4664. if ($v === false) unset($elements[$n]);
  4665. }
  4666. return $elements;
  4667. }
  4668. /**
  4669. * Retrieves a single merged element definition
  4670. * @param $name Name of element
  4671. * @param $trusted Boolean trusted overriding parameter: set to true
  4672. * if you want the full version of an element
  4673. * @return Merged HTMLPurifier_ElementDef
  4674. * @note You may notice that modules are getting iterated over twice (once
  4675. * in getElements() and once here). This
  4676. * is because
  4677. */
  4678. public function getElement($name, $trusted = null) {
  4679. if (!isset($this->elementLookup[$name])) {
  4680. return false;
  4681. }
  4682. // setup global state variables
  4683. $def = false;
  4684. if ($trusted === null) $trusted = $this->trusted;
  4685. // iterate through each module that has registered itself to this
  4686. // element
  4687. foreach($this->elementLookup[$name] as $module_name) {
  4688. $module = $this->modules[$module_name];
  4689. // refuse to create/merge from a module that is deemed unsafe--
  4690. // pretend the module doesn't exist--when trusted mode is not on.
  4691. if (!$trusted && !$module->safe) {
  4692. continue;
  4693. }
  4694. // clone is used because, ideally speaking, the original
  4695. // definition should not be modified. Usually, this will
  4696. // make no difference, but for consistency's sake
  4697. $new_def = clone $module->info[$name];
  4698. if (!$def && $new_def->standalone) {
  4699. $def = $new_def;
  4700. } elseif ($def) {
  4701. // This will occur even if $new_def is standalone. In practice,
  4702. // this will usually result in a full replacement.
  4703. $def->mergeIn($new_def);
  4704. } else {
  4705. // :TODO:
  4706. // non-standalone definitions that don't have a standalone
  4707. // to merge into could be deferred to the end
  4708. // HOWEVER, it is perfectly valid for a non-standalone
  4709. // definition to lack a standalone definition, even
  4710. // after all processing: this allows us to safely
  4711. // specify extra attributes for elements that may not be
  4712. // enabled all in one place. In particular, this might
  4713. // be the case for trusted elements. WARNING: care must
  4714. // be taken that the /extra/ definitions are all safe.
  4715. continue;
  4716. }
  4717. // attribute value expansions
  4718. $this->attrCollections->performInclusions($def->attr);
  4719. $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes);
  4720. // descendants_are_inline, for ChildDef_Chameleon
  4721. if (is_string($def->content_model) &&
  4722. strpos($def->content_model, 'Inline') !== false) {
  4723. if ($name != 'del' && $name != 'ins') {
  4724. // this is for you, ins/del
  4725. $def->descendants_are_inline = true;
  4726. }
  4727. }
  4728. $this->contentSets->generateChildDef($def, $module);
  4729. }
  4730. // This can occur if there is a blank definition, but no base to
  4731. // mix it in with
  4732. if (!$def) return false;
  4733. // add information on required attributes
  4734. foreach ($def->attr as $attr_name => $attr_def) {
  4735. if ($attr_def->required) {
  4736. $def->required_attr[] = $attr_name;
  4737. }
  4738. }
  4739. return $def;
  4740. }
  4741. }
  4742. /**
  4743. * Component of HTMLPurifier_AttrContext that accumulates IDs to prevent dupes
  4744. * @note In Slashdot-speak, dupe means duplicate.
  4745. * @note The default constructor does not accept $config or $context objects:
  4746. * use must use the static build() factory method to perform initialization.
  4747. */
  4748. class HTMLPurifier_IDAccumulator
  4749. {
  4750. /**
  4751. * Lookup table of IDs we've accumulated.
  4752. * @public
  4753. */
  4754. public $ids = array();
  4755. /**
  4756. * Builds an IDAccumulator, also initializing the default blacklist
  4757. * @param $config Instance of HTMLPurifier_Config
  4758. * @param $context Instance of HTMLPurifier_Context
  4759. * @return Fully initialized HTMLPurifier_IDAccumulator
  4760. */
  4761. public static function build($config, $context) {
  4762. $id_accumulator = new HTMLPurifier_IDAccumulator();
  4763. $id_accumulator->load($config->get('Attr.IDBlacklist'));
  4764. return $id_accumulator;
  4765. }
  4766. /**
  4767. * Add an ID to the lookup table.
  4768. * @param $id ID to be added.
  4769. * @return Bool status, true if success, false if there's a dupe
  4770. */
  4771. public function add($id) {
  4772. if (isset($this->ids[$id])) return false;
  4773. return $this->ids[$id] = true;
  4774. }
  4775. /**
  4776. * Load a list of IDs into the lookup table
  4777. * @param $array_of_ids Array of IDs to load
  4778. * @note This function doesn't care about duplicates
  4779. */
  4780. public function load($array_of_ids) {
  4781. foreach ($array_of_ids as $id) {
  4782. $this->ids[$id] = true;
  4783. }
  4784. }
  4785. }
  4786. /**
  4787. * Injects tokens into the document while parsing for well-formedness.
  4788. * This enables "formatter-like" functionality such as auto-paragraphing,
  4789. * smiley-ification and linkification to take place.
  4790. *
  4791. * A note on how handlers create changes; this is done by assigning a new
  4792. * value to the $token reference. These values can take a variety of forms and
  4793. * are best described HTMLPurifier_Strategy_MakeWellFormed->processToken()
  4794. * documentation.
  4795. *
  4796. * @todo Allow injectors to request a re-run on their output. This
  4797. * would help if an operation is recursive.
  4798. */
  4799. abstract class HTMLPurifier_Injector
  4800. {
  4801. /**
  4802. * Advisory name of injector, this is for friendly error messages
  4803. */
  4804. public $name;
  4805. /**
  4806. * Instance of HTMLPurifier_HTMLDefinition
  4807. */
  4808. protected $htmlDefinition;
  4809. /**
  4810. * Reference to CurrentNesting variable in Context. This is an array
  4811. * list of tokens that we are currently "inside"
  4812. */
  4813. protected $currentNesting;
  4814. /**
  4815. * Reference to InputTokens variable in Context. This is an array
  4816. * list of the input tokens that are being processed.
  4817. */
  4818. protected $inputTokens;
  4819. /**
  4820. * Reference to InputIndex variable in Context. This is an integer
  4821. * array index for $this->inputTokens that indicates what token
  4822. * is currently being processed.
  4823. */
  4824. protected $inputIndex;
  4825. /**
  4826. * Array of elements and attributes this injector creates and therefore
  4827. * need to be allowed by the definition. Takes form of
  4828. * array('element' => array('attr', 'attr2'), 'element2')
  4829. */
  4830. public $needed = array();
  4831. /**
  4832. * Index of inputTokens to rewind to.
  4833. */
  4834. protected $rewind = false;
  4835. /**
  4836. * Rewind to a spot to re-perform processing. This is useful if you
  4837. * deleted a node, and now need to see if this change affected any
  4838. * earlier nodes. Rewinding does not affect other injectors, and can
  4839. * result in infinite loops if not used carefully.
  4840. * @warning HTML Purifier will prevent you from fast-forwarding with this
  4841. * function.
  4842. */
  4843. public function rewind($index) {
  4844. $this->rewind = $index;
  4845. }
  4846. /**
  4847. * Retrieves rewind, and then unsets it.
  4848. */
  4849. public function getRewind() {
  4850. $r = $this->rewind;
  4851. $this->rewind = false;
  4852. return $r;
  4853. }
  4854. /**
  4855. * Prepares the injector by giving it the config and context objects:
  4856. * this allows references to important variables to be made within
  4857. * the injector. This function also checks if the HTML environment
  4858. * will work with the Injector (see checkNeeded()).
  4859. * @param $config Instance of HTMLPurifier_Config
  4860. * @param $context Instance of HTMLPurifier_Context
  4861. * @return Boolean false if success, string of missing needed element/attribute if failure
  4862. */
  4863. public function prepare($config, $context) {
  4864. $this->htmlDefinition = $config->getHTMLDefinition();
  4865. // Even though this might fail, some unit tests ignore this and
  4866. // still test checkNeeded, so be careful. Maybe get rid of that
  4867. // dependency.
  4868. $result = $this->checkNeeded($config);
  4869. if ($result !== false) return $result;
  4870. $this->currentNesting =& $context->get('CurrentNesting');
  4871. $this->inputTokens =& $context->get('InputTokens');
  4872. $this->inputIndex =& $context->get('InputIndex');
  4873. return false;
  4874. }
  4875. /**
  4876. * This function checks if the HTML environment
  4877. * will work with the Injector: if p tags are not allowed, the
  4878. * Auto-Paragraphing injector should not be enabled.
  4879. * @param $config Instance of HTMLPurifier_Config
  4880. * @param $context Instance of HTMLPurifier_Context
  4881. * @return Boolean false if success, string of missing needed element/attribute if failure
  4882. */
  4883. public function checkNeeded($config) {
  4884. $def = $config->getHTMLDefinition();
  4885. foreach ($this->needed as $element => $attributes) {
  4886. if (is_int($element)) $element = $attributes;
  4887. if (!isset($def->info[$element])) return $element;
  4888. if (!is_array($attributes)) continue;
  4889. foreach ($attributes as $name) {
  4890. if (!isset($def->info[$element]->attr[$name])) return "$element.$name";
  4891. }
  4892. }
  4893. return false;
  4894. }
  4895. /**
  4896. * Tests if the context node allows a certain element
  4897. * @param $name Name of element to test for
  4898. * @return True if element is allowed, false if it is not
  4899. */
  4900. public function allowsElement($name) {
  4901. if (!empty($this->currentNesting)) {
  4902. $parent_token = array_pop($this->currentNesting);
  4903. $this->currentNesting[] = $parent_token;
  4904. $parent = $this->htmlDefinition->info[$parent_token->name];
  4905. } else {
  4906. $parent = $this->htmlDefinition->info_parent_def;
  4907. }
  4908. if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) {
  4909. return false;
  4910. }
  4911. // check for exclusion
  4912. for ($i = count($this->currentNesting) - 2; $i >= 0; $i--) {
  4913. $node = $this->currentNesting[$i];
  4914. $def = $this->htmlDefinition->info[$node->name];
  4915. if (isset($def->excludes[$name])) return false;
  4916. }
  4917. return true;
  4918. }
  4919. /**
  4920. * Iterator function, which starts with the next token and continues until
  4921. * you reach the end of the input tokens.
  4922. * @warning Please prevent previous references from interfering with this
  4923. * functions by setting $i = null beforehand!
  4924. * @param &$i Current integer index variable for inputTokens
  4925. * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
  4926. */
  4927. protected function forward(&$i, &$current) {
  4928. if ($i === null) $i = $this->inputIndex + 1;
  4929. else $i++;
  4930. if (!isset($this->inputTokens[$i])) return false;
  4931. $current = $this->inputTokens[$i];
  4932. return true;
  4933. }
  4934. /**
  4935. * Similar to _forward, but accepts a third parameter $nesting (which
  4936. * should be initialized at 0) and stops when we hit the end tag
  4937. * for the node $this->inputIndex starts in.
  4938. */
  4939. protected function forwardUntilEndToken(&$i, &$current, &$nesting) {
  4940. $result = $this->forward($i, $current);
  4941. if (!$result) return false;
  4942. if ($nesting === null) $nesting = 0;
  4943. if ($current instanceof HTMLPurifier_Token_Start) $nesting++;
  4944. elseif ($current instanceof HTMLPurifier_Token_End) {
  4945. if ($nesting <= 0) return false;
  4946. $nesting--;
  4947. }
  4948. return true;
  4949. }
  4950. /**
  4951. * Iterator function, starts with the previous token and continues until
  4952. * you reach the beginning of input tokens.
  4953. * @warning Please prevent previous references from interfering with this
  4954. * functions by setting $i = null beforehand!
  4955. * @param &$i Current integer index variable for inputTokens
  4956. * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
  4957. */
  4958. protected function backward(&$i, &$current) {
  4959. if ($i === null) $i = $this->inputIndex - 1;
  4960. else $i--;
  4961. if ($i < 0) return false;
  4962. $current = $this->inputTokens[$i];
  4963. return true;
  4964. }
  4965. /**
  4966. * Initializes the iterator at the current position. Use in a do {} while;
  4967. * loop to force the _forward and _backward functions to start at the
  4968. * current location.
  4969. * @warning Please prevent previous references from interfering with this
  4970. * functions by setting $i = null beforehand!
  4971. * @param &$i Current integer index variable for inputTokens
  4972. * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
  4973. */
  4974. protected function current(&$i, &$current) {
  4975. if ($i === null) $i = $this->inputIndex;
  4976. $current = $this->inputTokens[$i];
  4977. }
  4978. /**
  4979. * Handler that is called when a text token is processed
  4980. */
  4981. public function handleText(&$token) {}
  4982. /**
  4983. * Handler that is called when a start or empty token is processed
  4984. */
  4985. public function handleElement(&$token) {}
  4986. /**
  4987. * Handler that is called when an end token is processed
  4988. */
  4989. public function handleEnd(&$token) {
  4990. $this->notifyEnd($token);
  4991. }
  4992. /**
  4993. * Notifier that is called when an end token is processed
  4994. * @note This differs from handlers in that the token is read-only
  4995. * @deprecated
  4996. */
  4997. public function notifyEnd($token) {}
  4998. }
  4999. /**
  5000. * Represents a language and defines localizable string formatting and
  5001. * other functions, as well as the localized messages for HTML Purifier.
  5002. */
  5003. class HTMLPurifier_Language
  5004. {
  5005. /**
  5006. * ISO 639 language code of language. Prefers shortest possible version
  5007. */
  5008. public $code = 'en';
  5009. /**
  5010. * Fallback language code
  5011. */
  5012. public $fallback = false;
  5013. /**
  5014. * Array of localizable messages
  5015. */
  5016. public $messages = array();
  5017. /**
  5018. * Array of localizable error codes
  5019. */
  5020. public $errorNames = array();
  5021. /**
  5022. * True if no message file was found for this language, so English
  5023. * is being used instead. Check this if you'd like to notify the
  5024. * user that they've used a non-supported language.
  5025. */
  5026. public $error = false;
  5027. /**
  5028. * Has the language object been loaded yet?
  5029. * @todo Make it private, fix usage in HTMLPurifier_LanguageTest
  5030. */
  5031. public $_loaded = false;
  5032. /**
  5033. * Instances of HTMLPurifier_Config and HTMLPurifier_Context
  5034. */
  5035. protected $config, $context;
  5036. public function __construct($config, $context) {
  5037. $this->config = $config;
  5038. $this->context = $context;
  5039. }
  5040. /**
  5041. * Loads language object with necessary info from factory cache
  5042. * @note This is a lazy loader
  5043. */
  5044. public function load() {
  5045. if ($this->_loaded) return;
  5046. $factory = HTMLPurifier_LanguageFactory::instance();
  5047. $factory->loadLanguage($this->code);
  5048. foreach ($factory->keys as $key) {
  5049. $this->$key = $factory->cache[$this->code][$key];
  5050. }
  5051. $this->_loaded = true;
  5052. }
  5053. /**
  5054. * Retrieves a localised message.
  5055. * @param $key string identifier of message
  5056. * @return string localised message
  5057. */
  5058. public function getMessage($key) {
  5059. if (!$this->_loaded) $this->load();
  5060. if (!isset($this->messages[$key])) return "[$key]";
  5061. return $this->messages[$key];
  5062. }
  5063. /**
  5064. * Retrieves a localised error name.
  5065. * @param $int integer error number, corresponding to PHP's error
  5066. * reporting
  5067. * @return string localised message
  5068. */
  5069. public function getErrorName($int) {
  5070. if (!$this->_loaded) $this->load();
  5071. if (!isset($this->errorNames[$int])) return "[Error: $int]";
  5072. return $this->errorNames[$int];
  5073. }
  5074. /**
  5075. * Converts an array list into a string readable representation
  5076. */
  5077. public function listify($array) {
  5078. $sep = $this->getMessage('Item separator');
  5079. $sep_last = $this->getMessage('Item separator last');
  5080. $ret = '';
  5081. for ($i = 0, $c = count($array); $i < $c; $i++) {
  5082. if ($i == 0) {
  5083. } elseif ($i + 1 < $c) {
  5084. $ret .= $sep;
  5085. } else {
  5086. $ret .= $sep_last;
  5087. }
  5088. $ret .= $array[$i];
  5089. }
  5090. return $ret;
  5091. }
  5092. /**
  5093. * Formats a localised message with passed parameters
  5094. * @param $key string identifier of message
  5095. * @param $args Parameters to substitute in
  5096. * @return string localised message
  5097. * @todo Implement conditionals? Right now, some messages make
  5098. * reference to line numbers, but those aren't always available
  5099. */
  5100. public function formatMessage($key, $args = array()) {
  5101. if (!$this->_loaded) $this->load();
  5102. if (!isset($this->messages[$key])) return "[$key]";
  5103. $raw = $this->messages[$key];
  5104. $subst = array();
  5105. $generator = false;
  5106. foreach ($args as $i => $value) {
  5107. if (is_object($value)) {
  5108. if ($value instanceof HTMLPurifier_Token) {
  5109. // factor this out some time
  5110. if (!$generator) $generator = $this->context->get('Generator');
  5111. if (isset($value->name)) $subst['$'.$i.'.Name'] = $value->name;
  5112. if (isset($value->data)) $subst['$'.$i.'.Data'] = $value->data;
  5113. $subst['$'.$i.'.Compact'] =
  5114. $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value);
  5115. // a more complex algorithm for compact representation
  5116. // could be introduced for all types of tokens. This
  5117. // may need to be factored out into a dedicated class
  5118. if (!empty($value->attr)) {
  5119. $stripped_token = clone $value;
  5120. $stripped_token->attr = array();
  5121. $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token);
  5122. }
  5123. $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown';
  5124. }
  5125. continue;
  5126. } elseif (is_array($value)) {
  5127. $keys = array_keys($value);
  5128. if (array_keys($keys) === $keys) {
  5129. // list
  5130. $subst['$'.$i] = $this->listify($value);
  5131. } else {
  5132. // associative array
  5133. // no $i implementation yet, sorry
  5134. $subst['$'.$i.'.Keys'] = $this->listify($keys);
  5135. $subst['$'.$i.'.Values'] = $this->listify(array_values($value));
  5136. }
  5137. continue;
  5138. }
  5139. $subst['$' . $i] = $value;
  5140. }
  5141. return strtr($raw, $subst);
  5142. }
  5143. }
  5144. /**
  5145. * Class responsible for generating HTMLPurifier_Language objects, managing
  5146. * caching and fallbacks.
  5147. * @note Thanks to MediaWiki for the general logic, although this version
  5148. * has been entirely rewritten
  5149. * @todo Serialized cache for languages
  5150. */
  5151. class HTMLPurifier_LanguageFactory
  5152. {
  5153. /**
  5154. * Cache of language code information used to load HTMLPurifier_Language objects
  5155. * Structure is: $factory->cache[$language_code][$key] = $value
  5156. * @value array map
  5157. */
  5158. public $cache;
  5159. /**
  5160. * Valid keys in the HTMLPurifier_Language object. Designates which
  5161. * variables to slurp out of a message file.
  5162. * @value array list
  5163. */
  5164. public $keys = array('fallback', 'messages', 'errorNames');
  5165. /**
  5166. * Instance of HTMLPurifier_AttrDef_Lang to validate language codes
  5167. * @value object HTMLPurifier_AttrDef_Lang
  5168. */
  5169. protected $validator;
  5170. /**
  5171. * Cached copy of dirname(__FILE__), directory of current file without
  5172. * trailing slash
  5173. * @value string filename
  5174. */
  5175. protected $dir;
  5176. /**
  5177. * Keys whose contents are a hash map and can be merged
  5178. * @value array lookup
  5179. */
  5180. protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true);
  5181. /**
  5182. * Keys whose contents are a list and can be merged
  5183. * @value array lookup
  5184. */
  5185. protected $mergeable_keys_list = array();
  5186. /**
  5187. * Retrieve sole instance of the factory.
  5188. * @param $prototype Optional prototype to overload sole instance with,
  5189. * or bool true to reset to default factory.
  5190. */
  5191. public static function instance($prototype = null) {
  5192. static $instance = null;
  5193. if ($prototype !== null) {
  5194. $instance = $prototype;
  5195. } elseif ($instance === null || $prototype == true) {
  5196. $instance = new HTMLPurifier_LanguageFactory();
  5197. $instance->setup();
  5198. }
  5199. return $instance;
  5200. }
  5201. /**
  5202. * Sets up the singleton, much like a constructor
  5203. * @note Prevents people from getting this outside of the singleton
  5204. */
  5205. public function setup() {
  5206. $this->validator = new HTMLPurifier_AttrDef_Lang();
  5207. $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier';
  5208. }
  5209. /**
  5210. * Creates a language object, handles class fallbacks
  5211. * @param $config Instance of HTMLPurifier_Config
  5212. * @param $context Instance of HTMLPurifier_Context
  5213. * @param $code Code to override configuration with. Private parameter.
  5214. */
  5215. public function create($config, $context, $code = false) {
  5216. // validate language code
  5217. if ($code === false) {
  5218. $code = $this->validator->validate(
  5219. $config->get('Core.Language'), $config, $context
  5220. );
  5221. } else {
  5222. $code = $this->validator->validate($code, $config, $context);
  5223. }
  5224. if ($code === false) $code = 'en'; // malformed code becomes English
  5225. $pcode = str_replace('-', '_', $code); // make valid PHP classname
  5226. static $depth = 0; // recursion protection
  5227. if ($code == 'en') {
  5228. $lang = new HTMLPurifier_Language($config, $context);
  5229. } else {
  5230. $class = 'HTMLPurifier_Language_' . $pcode;
  5231. $file = $this->dir . '/Language/classes/' . $code . '.php';
  5232. if (file_exists($file) || class_exists($class, false)) {
  5233. $lang = new $class($config, $context);
  5234. } else {
  5235. // Go fallback
  5236. $raw_fallback = $this->getFallbackFor($code);
  5237. $fallback = $raw_fallback ? $raw_fallback : 'en';
  5238. $depth++;
  5239. $lang = $this->create($config, $context, $fallback);
  5240. if (!$raw_fallback) {
  5241. $lang->error = true;
  5242. }
  5243. $depth--;
  5244. }
  5245. }
  5246. $lang->code = $code;
  5247. return $lang;
  5248. }
  5249. /**
  5250. * Returns the fallback language for language
  5251. * @note Loads the original language into cache
  5252. * @param $code string language code
  5253. */
  5254. public function getFallbackFor($code) {
  5255. $this->loadLanguage($code);
  5256. return $this->cache[$code]['fallback'];
  5257. }
  5258. /**
  5259. * Loads language into the cache, handles message file and fallbacks
  5260. * @param $code string language code
  5261. */
  5262. public function loadLanguage($code) {
  5263. static $languages_seen = array(); // recursion guard
  5264. // abort if we've already loaded it
  5265. if (isset($this->cache[$code])) return;
  5266. // generate filename
  5267. $filename = $this->dir . '/Language/messages/' . $code . '.php';
  5268. // default fallback : may be overwritten by the ensuing include
  5269. $fallback = ($code != 'en') ? 'en' : false;
  5270. // load primary localisation
  5271. if (!file_exists($filename)) {
  5272. // skip the include: will rely solely on fallback
  5273. $filename = $this->dir . '/Language/messages/en.php';
  5274. $cache = array();
  5275. } else {
  5276. include $filename;
  5277. $cache = compact($this->keys);
  5278. }
  5279. // load fallback localisation
  5280. if (!empty($fallback)) {
  5281. // infinite recursion guard
  5282. if (isset($languages_seen[$code])) {
  5283. trigger_error('Circular fallback reference in language ' .
  5284. $code, E_USER_ERROR);
  5285. $fallback = 'en';
  5286. }
  5287. $language_seen[$code] = true;
  5288. // load the fallback recursively
  5289. $this->loadLanguage($fallback);
  5290. $fallback_cache = $this->cache[$fallback];
  5291. // merge fallback with current language
  5292. foreach ( $this->keys as $key ) {
  5293. if (isset($cache[$key]) && isset($fallback_cache[$key])) {
  5294. if (isset($this->mergeable_keys_map[$key])) {
  5295. $cache[$key] = $cache[$key] + $fallback_cache[$key];
  5296. } elseif (isset($this->mergeable_keys_list[$key])) {
  5297. $cache[$key] = array_merge( $fallback_cache[$key], $cache[$key] );
  5298. }
  5299. } else {
  5300. $cache[$key] = $fallback_cache[$key];
  5301. }
  5302. }
  5303. }
  5304. // save to cache for later retrieval
  5305. $this->cache[$code] = $cache;
  5306. return;
  5307. }
  5308. }
  5309. /**
  5310. * Represents a measurable length, with a string numeric magnitude
  5311. * and a unit. This object is immutable.
  5312. */
  5313. class HTMLPurifier_Length
  5314. {
  5315. /**
  5316. * String numeric magnitude.
  5317. */
  5318. protected $n;
  5319. /**
  5320. * String unit. False is permitted if $n = 0.
  5321. */
  5322. protected $unit;
  5323. /**
  5324. * Whether or not this length is valid. Null if not calculated yet.
  5325. */
  5326. protected $isValid;
  5327. /**
  5328. * Lookup array of units recognized by CSS 2.1
  5329. */
  5330. protected static $allowedUnits = array(
  5331. 'em' => true, 'ex' => true, 'px' => true, 'in' => true,
  5332. 'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true
  5333. );
  5334. /**
  5335. * @param number $n Magnitude
  5336. * @param string $u Unit
  5337. */
  5338. public function __construct($n = '0', $u = false) {
  5339. $this->n = (string) $n;
  5340. $this->unit = $u !== false ? (string) $u : false;
  5341. }
  5342. /**
  5343. * @param string $s Unit string, like '2em' or '3.4in'
  5344. * @warning Does not perform validation.
  5345. */
  5346. static public function make($s) {
  5347. if ($s instanceof HTMLPurifier_Length) return $s;
  5348. $n_length = strspn($s, '1234567890.+-');
  5349. $n = substr($s, 0, $n_length);
  5350. $unit = substr($s, $n_length);
  5351. if ($unit === '') $unit = false;
  5352. return new HTMLPurifier_Length($n, $unit);
  5353. }
  5354. /**
  5355. * Validates the number and unit.
  5356. */
  5357. protected function validate() {
  5358. // Special case:
  5359. if ($this->n === '+0' || $this->n === '-0') $this->n = '0';
  5360. if ($this->n === '0' && $this->unit === false) return true;
  5361. if (!ctype_lower($this->unit)) $this->unit = strtolower($this->unit);
  5362. if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) return false;
  5363. // Hack:
  5364. $def = new HTMLPurifier_AttrDef_CSS_Number();
  5365. $result = $def->validate($this->n, false, false);
  5366. if ($result === false) return false;
  5367. $this->n = $result;
  5368. return true;
  5369. }
  5370. /**
  5371. * Returns string representation of number.
  5372. */
  5373. public function toString() {
  5374. if (!$this->isValid()) return false;
  5375. return $this->n . $this->unit;
  5376. }
  5377. /**
  5378. * Retrieves string numeric magnitude.
  5379. */
  5380. public function getN() {return $this->n;}
  5381. /**
  5382. * Retrieves string unit.
  5383. */
  5384. public function getUnit() {return $this->unit;}
  5385. /**
  5386. * Returns true if this length unit is valid.
  5387. */
  5388. public function isValid() {
  5389. if ($this->isValid === null) $this->isValid = $this->validate();
  5390. return $this->isValid;
  5391. }
  5392. /**
  5393. * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal.
  5394. * @warning If both values are too large or small, this calculation will
  5395. * not work properly
  5396. */
  5397. public function compareTo($l) {
  5398. if ($l === false) return false;
  5399. if ($l->unit !== $this->unit) {
  5400. $converter = new HTMLPurifier_UnitConverter();
  5401. $l = $converter->convert($l, $this->unit);
  5402. if ($l === false) return false;
  5403. }
  5404. return $this->n - $l->n;
  5405. }
  5406. }
  5407. /**
  5408. * Forgivingly lexes HTML (SGML-style) markup into tokens.
  5409. *
  5410. * A lexer parses a string of SGML-style markup and converts them into
  5411. * corresponding tokens. It doesn't check for well-formedness, although its
  5412. * internal mechanism may make this automatic (such as the case of
  5413. * HTMLPurifier_Lexer_DOMLex). There are several implementations to choose
  5414. * from.
  5415. *
  5416. * A lexer is HTML-oriented: it might work with XML, but it's not
  5417. * recommended, as we adhere to a subset of the specification for optimization
  5418. * reasons. This might change in the future. Also, most tokenizers are not
  5419. * expected to handle DTDs or PIs.
  5420. *
  5421. * This class should not be directly instantiated, but you may use create() to
  5422. * retrieve a default copy of the lexer. Being a supertype, this class
  5423. * does not actually define any implementation, but offers commonly used
  5424. * convenience functions for subclasses.
  5425. *
  5426. * @note The unit tests will instantiate this class for testing purposes, as
  5427. * many of the utility functions require a class to be instantiated.
  5428. * This means that, even though this class is not runnable, it will
  5429. * not be declared abstract.
  5430. *
  5431. * @par
  5432. *
  5433. * @note
  5434. * We use tokens rather than create a DOM representation because DOM would:
  5435. *
  5436. * @par
  5437. * -# Require more processing and memory to create,
  5438. * -# Is not streamable, and
  5439. * -# Has the entire document structure (html and body not needed).
  5440. *
  5441. * @par
  5442. * However, DOM is helpful in that it makes it easy to move around nodes
  5443. * without a lot of lookaheads to see when a tag is closed. This is a
  5444. * limitation of the token system and some workarounds would be nice.
  5445. */
  5446. class HTMLPurifier_Lexer
  5447. {
  5448. /**
  5449. * Whether or not this lexer implements line-number/column-number tracking.
  5450. * If it does, set to true.
  5451. */
  5452. public $tracksLineNumbers = false;
  5453. // -- STATIC ----------------------------------------------------------
  5454. /**
  5455. * Retrieves or sets the default Lexer as a Prototype Factory.
  5456. *
  5457. * By default HTMLPurifier_Lexer_DOMLex will be returned. There are
  5458. * a few exceptions involving special features that only DirectLex
  5459. * implements.
  5460. *
  5461. * @note The behavior of this class has changed, rather than accepting
  5462. * a prototype object, it now accepts a configuration object.
  5463. * To specify your own prototype, set %Core.LexerImpl to it.
  5464. * This change in behavior de-singletonizes the lexer object.
  5465. *
  5466. * @param $config Instance of HTMLPurifier_Config
  5467. * @return Concrete lexer.
  5468. */
  5469. public static function create($config) {
  5470. if (!($config instanceof HTMLPurifier_Config)) {
  5471. $lexer = $config;
  5472. trigger_error("Passing a prototype to
  5473. HTMLPurifier_Lexer::create() is deprecated, please instead
  5474. use %Core.LexerImpl", E_USER_WARNING);
  5475. } else {
  5476. $lexer = $config->get('Core.LexerImpl');
  5477. }
  5478. $needs_tracking =
  5479. $config->get('Core.MaintainLineNumbers') ||
  5480. $config->get('Core.CollectErrors');
  5481. $inst = null;
  5482. if (is_object($lexer)) {
  5483. $inst = $lexer;
  5484. } else {
  5485. if (is_null($lexer)) { do {
  5486. // auto-detection algorithm
  5487. if ($needs_tracking) {
  5488. $lexer = 'DirectLex';
  5489. break;
  5490. }
  5491. if (
  5492. class_exists('DOMDocument') &&
  5493. method_exists('DOMDocument', 'loadHTML') &&
  5494. !extension_loaded('domxml')
  5495. ) {
  5496. // check for DOM support, because while it's part of the
  5497. // core, it can be disabled compile time. Also, the PECL
  5498. // domxml extension overrides the default DOM, and is evil
  5499. // and nasty and we shan't bother to support it
  5500. $lexer = 'DOMLex';
  5501. } else {
  5502. $lexer = 'DirectLex';
  5503. }
  5504. } while(0); } // do..while so we can break
  5505. // instantiate recognized string names
  5506. switch ($lexer) {
  5507. case 'DOMLex':
  5508. $inst = new HTMLPurifier_Lexer_DOMLex();
  5509. break;
  5510. case 'DirectLex':
  5511. $inst = new HTMLPurifier_Lexer_DirectLex();
  5512. break;
  5513. case 'PH5P':
  5514. $inst = new HTMLPurifier_Lexer_PH5P();
  5515. break;
  5516. default:
  5517. throw new HTMLPurifier_Exception("Cannot instantiate unrecognized Lexer type " . htmlspecialchars($lexer));
  5518. }
  5519. }
  5520. if (!$inst) throw new HTMLPurifier_Exception('No lexer was instantiated');
  5521. // once PHP DOM implements native line numbers, or we
  5522. // hack out something using XSLT, remove this stipulation
  5523. if ($needs_tracking && !$inst->tracksLineNumbers) {
  5524. throw new HTMLPurifier_Exception('Cannot use lexer that does not support line numbers with Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)');
  5525. }
  5526. return $inst;
  5527. }
  5528. // -- CONVENIENCE MEMBERS ---------------------------------------------
  5529. public function __construct() {
  5530. $this->_entity_parser = new HTMLPurifier_EntityParser();
  5531. }
  5532. /**
  5533. * Most common entity to raw value conversion table for special entities.
  5534. */
  5535. protected $_special_entity2str =
  5536. array(
  5537. '&quot;' => '"',
  5538. '&amp;' => '&',
  5539. '&lt;' => '<',
  5540. '&gt;' => '>',
  5541. '&#39;' => "'",
  5542. '&#039;' => "'",
  5543. '&#x27;' => "'"
  5544. );
  5545. /**
  5546. * Parses special entities into the proper characters.
  5547. *
  5548. * This string will translate escaped versions of the special characters
  5549. * into the correct ones.
  5550. *
  5551. * @warning
  5552. * You should be able to treat the output of this function as
  5553. * completely parsed, but that's only because all other entities should
  5554. * have been handled previously in substituteNonSpecialEntities()
  5555. *
  5556. * @param $string String character data to be parsed.
  5557. * @returns Parsed character data.
  5558. */
  5559. public function parseData($string) {
  5560. // following functions require at least one character
  5561. if ($string === '') return '';
  5562. // subtracts amps that cannot possibly be escaped
  5563. $num_amp = substr_count($string, '&') - substr_count($string, '& ') -
  5564. ($string[strlen($string)-1] === '&' ? 1 : 0);
  5565. if (!$num_amp) return $string; // abort if no entities
  5566. $num_esc_amp = substr_count($string, '&amp;');
  5567. $string = strtr($string, $this->_special_entity2str);
  5568. // code duplication for sake of optimization, see above
  5569. $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') -
  5570. ($string[strlen($string)-1] === '&' ? 1 : 0);
  5571. if ($num_amp_2 <= $num_esc_amp) return $string;
  5572. // hmm... now we have some uncommon entities. Use the callback.
  5573. $string = $this->_entity_parser->substituteSpecialEntities($string);
  5574. return $string;
  5575. }
  5576. /**
  5577. * Lexes an HTML string into tokens.
  5578. *
  5579. * @param $string String HTML.
  5580. * @return HTMLPurifier_Token array representation of HTML.
  5581. */
  5582. public function tokenizeHTML($string, $config, $context) {
  5583. trigger_error('Call to abstract class', E_USER_ERROR);
  5584. }
  5585. /**
  5586. * Translates CDATA sections into regular sections (through escaping).
  5587. *
  5588. * @param $string HTML string to process.
  5589. * @returns HTML with CDATA sections escaped.
  5590. */
  5591. protected static function escapeCDATA($string) {
  5592. return preg_replace_callback(
  5593. '/<!\[CDATA\[(.+?)\]\]>/s',
  5594. array('HTMLPurifier_Lexer', 'CDATACallback'),
  5595. $string
  5596. );
  5597. }
  5598. /**
  5599. * Special CDATA case that is especially convoluted for <script>
  5600. */
  5601. protected static function escapeCommentedCDATA($string) {
  5602. return preg_replace_callback(
  5603. '#<!--//--><!\[CDATA\[//><!--(.+?)//--><!\]\]>#s',
  5604. array('HTMLPurifier_Lexer', 'CDATACallback'),
  5605. $string
  5606. );
  5607. }
  5608. /**
  5609. * Special Internet Explorer conditional comments should be removed.
  5610. */
  5611. protected static function removeIEConditional($string) {
  5612. return preg_replace(
  5613. '#<!--\[if [^>]+\]>.*?<!\[endif\]-->#si', // probably should generalize for all strings
  5614. '',
  5615. $string
  5616. );
  5617. }
  5618. /**
  5619. * Callback function for escapeCDATA() that does the work.
  5620. *
  5621. * @warning Though this is public in order to let the callback happen,
  5622. * calling it directly is not recommended.
  5623. * @params $matches PCRE matches array, with index 0 the entire match
  5624. * and 1 the inside of the CDATA section.
  5625. * @returns Escaped internals of the CDATA section.
  5626. */
  5627. protected static function CDATACallback($matches) {
  5628. // not exactly sure why the character set is needed, but whatever
  5629. return htmlspecialchars($matches[1], ENT_COMPAT, 'UTF-8');
  5630. }
  5631. /**
  5632. * Takes a piece of HTML and normalizes it by converting entities, fixing
  5633. * encoding, extracting bits, and other good stuff.
  5634. * @todo Consider making protected
  5635. */
  5636. public function normalize($html, $config, $context) {
  5637. // normalize newlines to \n
  5638. if ($config->get('Core.NormalizeNewlines')) {
  5639. $html = str_replace("\r\n", "\n", $html);
  5640. $html = str_replace("\r", "\n", $html);
  5641. }
  5642. if ($config->get('HTML.Trusted')) {
  5643. // escape convoluted CDATA
  5644. $html = $this->escapeCommentedCDATA($html);
  5645. }
  5646. // escape CDATA
  5647. $html = $this->escapeCDATA($html);
  5648. $html = $this->removeIEConditional($html);
  5649. // extract body from document if applicable
  5650. if ($config->get('Core.ConvertDocumentToFragment')) {
  5651. $e = false;
  5652. if ($config->get('Core.CollectErrors')) {
  5653. $e =& $context->get('ErrorCollector');
  5654. }
  5655. $new_html = $this->extractBody($html);
  5656. if ($e && $new_html != $html) {
  5657. $e->send(E_WARNING, 'Lexer: Extracted body');
  5658. }
  5659. $html = $new_html;
  5660. }
  5661. // expand entities that aren't the big five
  5662. $html = $this->_entity_parser->substituteNonSpecialEntities($html);
  5663. // clean into wellformed UTF-8 string for an SGML context: this has
  5664. // to be done after entity expansion because the entities sometimes
  5665. // represent non-SGML characters (horror, horror!)
  5666. $html = HTMLPurifier_Encoder::cleanUTF8($html);
  5667. // if processing instructions are to removed, remove them now
  5668. if ($config->get('Core.RemoveProcessingInstructions')) {
  5669. $html = preg_replace('#<\?.+?\?>#s', '', $html);
  5670. }
  5671. return $html;
  5672. }
  5673. /**
  5674. * Takes a string of HTML (fragment or document) and returns the content
  5675. * @todo Consider making protected
  5676. */
  5677. public function extractBody($html) {
  5678. $matches = array();
  5679. $result = preg_match('!<body[^>]*>(.*)</body>!is', $html, $matches);
  5680. if ($result) {
  5681. return $matches[1];
  5682. } else {
  5683. return $html;
  5684. }
  5685. }
  5686. }
  5687. /**
  5688. * Class that handles operations involving percent-encoding in URIs.
  5689. *
  5690. * @warning
  5691. * Be careful when reusing instances of PercentEncoder. The object
  5692. * you use for normalize() SHOULD NOT be used for encode(), or
  5693. * vice-versa.
  5694. */
  5695. class HTMLPurifier_PercentEncoder
  5696. {
  5697. /**
  5698. * Reserved characters to preserve when using encode().
  5699. */
  5700. protected $preserve = array();
  5701. /**
  5702. * String of characters that should be preserved while using encode().
  5703. */
  5704. public function __construct($preserve = false) {
  5705. // unreserved letters, ought to const-ify
  5706. for ($i = 48; $i <= 57; $i++) $this->preserve[$i] = true; // digits
  5707. for ($i = 65; $i <= 90; $i++) $this->preserve[$i] = true; // upper-case
  5708. for ($i = 97; $i <= 122; $i++) $this->preserve[$i] = true; // lower-case
  5709. $this->preserve[45] = true; // Dash -
  5710. $this->preserve[46] = true; // Period .
  5711. $this->preserve[95] = true; // Underscore _
  5712. $this->preserve[126]= true; // Tilde ~
  5713. // extra letters not to escape
  5714. if ($preserve !== false) {
  5715. for ($i = 0, $c = strlen($preserve); $i < $c; $i++) {
  5716. $this->preserve[ord($preserve[$i])] = true;
  5717. }
  5718. }
  5719. }
  5720. /**
  5721. * Our replacement for urlencode, it encodes all non-reserved characters,
  5722. * as well as any extra characters that were instructed to be preserved.
  5723. * @note
  5724. * Assumes that the string has already been normalized, making any
  5725. * and all percent escape sequences valid. Percents will not be
  5726. * re-escaped, regardless of their status in $preserve
  5727. * @param $string String to be encoded
  5728. * @return Encoded string.
  5729. */
  5730. public function encode($string) {
  5731. $ret = '';
  5732. for ($i = 0, $c = strlen($string); $i < $c; $i++) {
  5733. if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])]) ) {
  5734. $ret .= '%' . sprintf('%02X', $int);
  5735. } else {
  5736. $ret .= $string[$i];
  5737. }
  5738. }
  5739. return $ret;
  5740. }
  5741. /**
  5742. * Fix up percent-encoding by decoding unreserved characters and normalizing.
  5743. * @warning This function is affected by $preserve, even though the
  5744. * usual desired behavior is for this not to preserve those
  5745. * characters. Be careful when reusing instances of PercentEncoder!
  5746. * @param $string String to normalize
  5747. */
  5748. public function normalize($string) {
  5749. if ($string == '') return '';
  5750. $parts = explode('%', $string);
  5751. $ret = array_shift($parts);
  5752. foreach ($parts as $part) {
  5753. $length = strlen($part);
  5754. if ($length < 2) {
  5755. $ret .= '%25' . $part;
  5756. continue;
  5757. }
  5758. $encoding = substr($part, 0, 2);
  5759. $text = substr($part, 2);
  5760. if (!ctype_xdigit($encoding)) {
  5761. $ret .= '%25' . $part;
  5762. continue;
  5763. }
  5764. $int = hexdec($encoding);
  5765. if (isset($this->preserve[$int])) {
  5766. $ret .= chr($int) . $text;
  5767. continue;
  5768. }
  5769. $encoding = strtoupper($encoding);
  5770. $ret .= '%' . $encoding . $text;
  5771. }
  5772. return $ret;
  5773. }
  5774. }
  5775. /**
  5776. * Generic property list implementation
  5777. */
  5778. class HTMLPurifier_PropertyList
  5779. {
  5780. /**
  5781. * Internal data-structure for properties
  5782. */
  5783. protected $data = array();
  5784. /**
  5785. * Parent plist
  5786. */
  5787. protected $parent;
  5788. protected $cache;
  5789. public function __construct($parent = null) {
  5790. $this->parent = $parent;
  5791. }
  5792. /**
  5793. * Recursively retrieves the value for a key
  5794. */
  5795. public function get($name) {
  5796. if ($this->has($name)) return $this->data[$name];
  5797. // possible performance bottleneck, convert to iterative if necessary
  5798. if ($this->parent) return $this->parent->get($name);
  5799. throw new HTMLPurifier_Exception("Key '$name' not found");
  5800. }
  5801. /**
  5802. * Sets the value of a key, for this plist
  5803. */
  5804. public function set($name, $value) {
  5805. $this->data[$name] = $value;
  5806. }
  5807. /**
  5808. * Returns true if a given key exists
  5809. */
  5810. public function has($name) {
  5811. return array_key_exists($name, $this->data);
  5812. }
  5813. /**
  5814. * Resets a value to the value of it's parent, usually the default. If
  5815. * no value is specified, the entire plist is reset.
  5816. */
  5817. public function reset($name = null) {
  5818. if ($name == null) $this->data = array();
  5819. else unset($this->data[$name]);
  5820. }
  5821. /**
  5822. * Squashes this property list and all of its property lists into a single
  5823. * array, and returns the array. This value is cached by default.
  5824. * @param $force If true, ignores the cache and regenerates the array.
  5825. */
  5826. public function squash($force = false) {
  5827. if ($this->cache !== null && !$force) return $this->cache;
  5828. if ($this->parent) {
  5829. return $this->cache = array_merge($this->parent->squash($force), $this->data);
  5830. } else {
  5831. return $this->cache = $this->data;
  5832. }
  5833. }
  5834. /**
  5835. * Returns the parent plist.
  5836. */
  5837. public function getParent() {
  5838. return $this->parent;
  5839. }
  5840. /**
  5841. * Sets the parent plist.
  5842. */
  5843. public function setParent($plist) {
  5844. $this->parent = $plist;
  5845. }
  5846. }
  5847. /**
  5848. * Property list iterator. Do not instantiate this class directly.
  5849. */
  5850. class HTMLPurifier_PropertyListIterator extends FilterIterator
  5851. {
  5852. protected $l;
  5853. protected $filter;
  5854. /**
  5855. * @param $data Array of data to iterate over
  5856. * @param $filter Optional prefix to only allow values of
  5857. */
  5858. public function __construct(Iterator $iterator, $filter = null) {
  5859. parent::__construct($iterator);
  5860. $this->l = strlen($filter);
  5861. $this->filter = $filter;
  5862. }
  5863. public function accept() {
  5864. $key = $this->getInnerIterator()->key();
  5865. if( strncmp($key, $this->filter, $this->l) !== 0 ) {
  5866. return false;
  5867. }
  5868. return true;
  5869. }
  5870. }
  5871. /**
  5872. * Supertype for classes that define a strategy for modifying/purifying tokens.
  5873. *
  5874. * While HTMLPurifier's core purpose is fixing HTML into something proper,
  5875. * strategies provide plug points for extra configuration or even extra
  5876. * features, such as custom tags, custom parsing of text, etc.
  5877. */
  5878. abstract class HTMLPurifier_Strategy
  5879. {
  5880. /**
  5881. * Executes the strategy on the tokens.
  5882. *
  5883. * @param $tokens Array of HTMLPurifier_Token objects to be operated on.
  5884. * @param $config Configuration options
  5885. * @returns Processed array of token objects.
  5886. */
  5887. abstract public function execute($tokens, $config, $context);
  5888. }
  5889. /**
  5890. * This is in almost every respect equivalent to an array except
  5891. * that it keeps track of which keys were accessed.
  5892. *
  5893. * @warning For the sake of backwards compatibility with early versions
  5894. * of PHP 5, you must not use the $hash[$key] syntax; if you do
  5895. * our version of offsetGet is never called.
  5896. */
  5897. class HTMLPurifier_StringHash extends ArrayObject
  5898. {
  5899. protected $accessed = array();
  5900. /**
  5901. * Retrieves a value, and logs the access.
  5902. */
  5903. public function offsetGet($index) {
  5904. $this->accessed[$index] = true;
  5905. return parent::offsetGet($index);
  5906. }
  5907. /**
  5908. * Returns a lookup array of all array indexes that have been accessed.
  5909. * @return Array in form array($index => true).
  5910. */
  5911. public function getAccessed() {
  5912. return $this->accessed;
  5913. }
  5914. /**
  5915. * Resets the access array.
  5916. */
  5917. public function resetAccessed() {
  5918. $this->accessed = array();
  5919. }
  5920. }
  5921. /**
  5922. * Parses string hash files. File format is as such:
  5923. *
  5924. * DefaultKeyValue
  5925. * KEY: Value
  5926. * KEY2: Value2
  5927. * --MULTILINE-KEY--
  5928. * Multiline
  5929. * value.
  5930. *
  5931. * Which would output something similar to:
  5932. *
  5933. * array(
  5934. * 'ID' => 'DefaultKeyValue',
  5935. * 'KEY' => 'Value',
  5936. * 'KEY2' => 'Value2',
  5937. * 'MULTILINE-KEY' => "Multiline\nvalue.\n",
  5938. * )
  5939. *
  5940. * We use this as an easy to use file-format for configuration schema
  5941. * files, but the class itself is usage agnostic.
  5942. *
  5943. * You can use ---- to forcibly terminate parsing of a single string-hash;
  5944. * this marker is used in multi string-hashes to delimit boundaries.
  5945. */
  5946. class HTMLPurifier_StringHashParser
  5947. {
  5948. public $default = 'ID';
  5949. /**
  5950. * Parses a file that contains a single string-hash.
  5951. */
  5952. public function parseFile($file) {
  5953. if (!file_exists($file)) return false;
  5954. $fh = fopen($file, 'r');
  5955. if (!$fh) return false;
  5956. $ret = $this->parseHandle($fh);
  5957. fclose($fh);
  5958. return $ret;
  5959. }
  5960. /**
  5961. * Parses a file that contains multiple string-hashes delimited by '----'
  5962. */
  5963. public function parseMultiFile($file) {
  5964. if (!file_exists($file)) return false;
  5965. $ret = array();
  5966. $fh = fopen($file, 'r');
  5967. if (!$fh) return false;
  5968. while (!feof($fh)) {
  5969. $ret[] = $this->parseHandle($fh);
  5970. }
  5971. fclose($fh);
  5972. return $ret;
  5973. }
  5974. /**
  5975. * Internal parser that acepts a file handle.
  5976. * @note While it's possible to simulate in-memory parsing by using
  5977. * custom stream wrappers, if such a use-case arises we should
  5978. * factor out the file handle into its own class.
  5979. * @param $fh File handle with pointer at start of valid string-hash
  5980. * block.
  5981. */
  5982. protected function parseHandle($fh) {
  5983. $state = false;
  5984. $single = false;
  5985. $ret = array();
  5986. do {
  5987. $line = fgets($fh);
  5988. if ($line === false) break;
  5989. $line = rtrim($line, "\n\r");
  5990. if (!$state && $line === '') continue;
  5991. if ($line === '----') break;
  5992. if (strncmp('--#', $line, 3) === 0) {
  5993. // Comment
  5994. continue;
  5995. } elseif (strncmp('--', $line, 2) === 0) {
  5996. // Multiline declaration
  5997. $state = trim($line, '- ');
  5998. if (!isset($ret[$state])) $ret[$state] = '';
  5999. continue;
  6000. } elseif (!$state) {
  6001. $single = true;
  6002. if (strpos($line, ':') !== false) {
  6003. // Single-line declaration
  6004. list($state, $line) = explode(':', $line, 2);
  6005. $line = trim($line);
  6006. } else {
  6007. // Use default declaration
  6008. $state = $this->default;
  6009. }
  6010. }
  6011. if ($single) {
  6012. $ret[$state] = $line;
  6013. $single = false;
  6014. $state = false;
  6015. } else {
  6016. $ret[$state] .= "$line\n";
  6017. }
  6018. } while (!feof($fh));
  6019. return $ret;
  6020. }
  6021. }
  6022. /**
  6023. * Defines a mutation of an obsolete tag into a valid tag.
  6024. */
  6025. abstract class HTMLPurifier_TagTransform
  6026. {
  6027. /**
  6028. * Tag name to transform the tag to.
  6029. */
  6030. public $transform_to;
  6031. /**
  6032. * Transforms the obsolete tag into the valid tag.
  6033. * @param $tag Tag to be transformed.
  6034. * @param $config Mandatory HTMLPurifier_Config object
  6035. * @param $context Mandatory HTMLPurifier_Context object
  6036. */
  6037. abstract public function transform($tag, $config, $context);
  6038. /**
  6039. * Prepends CSS properties to the style attribute, creating the
  6040. * attribute if it doesn't exist.
  6041. * @warning Copied over from AttrTransform, be sure to keep in sync
  6042. * @param $attr Attribute array to process (passed by reference)
  6043. * @param $css CSS to prepend
  6044. */
  6045. protected function prependCSS(&$attr, $css) {
  6046. $attr['style'] = isset($attr['style']) ? $attr['style'] : '';
  6047. $attr['style'] = $css . $attr['style'];
  6048. }
  6049. }
  6050. /**
  6051. * Abstract base token class that all others inherit from.
  6052. */
  6053. class HTMLPurifier_Token {
  6054. public $line; /**< Line number node was on in source document. Null if unknown. */
  6055. public $col; /**< Column of line node was on in source document. Null if unknown. */
  6056. /**
  6057. * Lookup array of processing that this token is exempt from.
  6058. * Currently, valid values are "ValidateAttributes" and
  6059. * "MakeWellFormed_TagClosedError"
  6060. */
  6061. public $armor = array();
  6062. /**
  6063. * Used during MakeWellFormed.
  6064. */
  6065. public $skip;
  6066. public $rewind;
  6067. public $carryover;
  6068. public function __get($n) {
  6069. if ($n === 'type') {
  6070. trigger_error('Deprecated type property called; use instanceof', E_USER_NOTICE);
  6071. switch (get_class($this)) {
  6072. case 'HTMLPurifier_Token_Start': return 'start';
  6073. case 'HTMLPurifier_Token_Empty': return 'empty';
  6074. case 'HTMLPurifier_Token_End': return 'end';
  6075. case 'HTMLPurifier_Token_Text': return 'text';
  6076. case 'HTMLPurifier_Token_Comment': return 'comment';
  6077. default: return null;
  6078. }
  6079. }
  6080. }
  6081. /**
  6082. * Sets the position of the token in the source document.
  6083. */
  6084. public function position($l = null, $c = null) {
  6085. $this->line = $l;
  6086. $this->col = $c;
  6087. }
  6088. /**
  6089. * Convenience function for DirectLex settings line/col position.
  6090. */
  6091. public function rawPosition($l, $c) {
  6092. if ($c === -1) $l++;
  6093. $this->line = $l;
  6094. $this->col = $c;
  6095. }
  6096. }
  6097. /**
  6098. * Factory for token generation.
  6099. *
  6100. * @note Doing some benchmarking indicates that the new operator is much
  6101. * slower than the clone operator (even discounting the cost of the
  6102. * constructor). This class is for that optimization.
  6103. * Other then that, there's not much point as we don't
  6104. * maintain parallel HTMLPurifier_Token hierarchies (the main reason why
  6105. * you'd want to use an abstract factory).
  6106. * @todo Port DirectLex to use this
  6107. */
  6108. class HTMLPurifier_TokenFactory
  6109. {
  6110. /**
  6111. * Prototypes that will be cloned.
  6112. * @private
  6113. */
  6114. // p stands for prototype
  6115. private $p_start, $p_end, $p_empty, $p_text, $p_comment;
  6116. /**
  6117. * Generates blank prototypes for cloning.
  6118. */
  6119. public function __construct() {
  6120. $this->p_start = new HTMLPurifier_Token_Start('', array());
  6121. $this->p_end = new HTMLPurifier_Token_End('');
  6122. $this->p_empty = new HTMLPurifier_Token_Empty('', array());
  6123. $this->p_text = new HTMLPurifier_Token_Text('');
  6124. $this->p_comment= new HTMLPurifier_Token_Comment('');
  6125. }
  6126. /**
  6127. * Creates a HTMLPurifier_Token_Start.
  6128. * @param $name Tag name
  6129. * @param $attr Associative array of attributes
  6130. * @return Generated HTMLPurifier_Token_Start
  6131. */
  6132. public function createStart($name, $attr = array()) {
  6133. $p = clone $this->p_start;
  6134. $p->__construct($name, $attr);
  6135. return $p;
  6136. }
  6137. /**
  6138. * Creates a HTMLPurifier_Token_End.
  6139. * @param $name Tag name
  6140. * @return Generated HTMLPurifier_Token_End
  6141. */
  6142. public function createEnd($name) {
  6143. $p = clone $this->p_end;
  6144. $p->__construct($name);
  6145. return $p;
  6146. }
  6147. /**
  6148. * Creates a HTMLPurifier_Token_Empty.
  6149. * @param $name Tag name
  6150. * @param $attr Associative array of attributes
  6151. * @return Generated HTMLPurifier_Token_Empty
  6152. */
  6153. public function createEmpty($name, $attr = array()) {
  6154. $p = clone $this->p_empty;
  6155. $p->__construct($name, $attr);
  6156. return $p;
  6157. }
  6158. /**
  6159. * Creates a HTMLPurifier_Token_Text.
  6160. * @param $data Data of text token
  6161. * @return Generated HTMLPurifier_Token_Text
  6162. */
  6163. public function createText($data) {
  6164. $p = clone $this->p_text;
  6165. $p->__construct($data);
  6166. return $p;
  6167. }
  6168. /**
  6169. * Creates a HTMLPurifier_Token_Comment.
  6170. * @param $data Data of comment token
  6171. * @return Generated HTMLPurifier_Token_Comment
  6172. */
  6173. public function createComment($data) {
  6174. $p = clone $this->p_comment;
  6175. $p->__construct($data);
  6176. return $p;
  6177. }
  6178. }
  6179. /**
  6180. * HTML Purifier's internal representation of a URI.
  6181. * @note
  6182. * Internal data-structures are completely escaped. If the data needs
  6183. * to be used in a non-URI context (which is very unlikely), be sure
  6184. * to decode it first. The URI may not necessarily be well-formed until
  6185. * validate() is called.
  6186. */
  6187. class HTMLPurifier_URI
  6188. {
  6189. public $scheme, $userinfo, $host, $port, $path, $query, $fragment;
  6190. /**
  6191. * @note Automatically normalizes scheme and port
  6192. */
  6193. public function __construct($scheme, $userinfo, $host, $port, $path, $query, $fragment) {
  6194. $this->scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme);
  6195. $this->userinfo = $userinfo;
  6196. $this->host = $host;
  6197. $this->port = is_null($port) ? $port : (int) $port;
  6198. $this->path = $path;
  6199. $this->query = $query;
  6200. $this->fragment = $fragment;
  6201. }
  6202. /**
  6203. * Retrieves a scheme object corresponding to the URI's scheme/default
  6204. * @param $config Instance of HTMLPurifier_Config
  6205. * @param $context Instance of HTMLPurifier_Context
  6206. * @return Scheme object appropriate for validating this URI
  6207. */
  6208. public function getSchemeObj($config, $context) {
  6209. $registry = HTMLPurifier_URISchemeRegistry::instance();
  6210. if ($this->scheme !== null) {
  6211. $scheme_obj = $registry->getScheme($this->scheme, $config, $context);
  6212. if (!$scheme_obj) return false; // invalid scheme, clean it out
  6213. } else {
  6214. // no scheme: retrieve the default one
  6215. $def = $config->getDefinition('URI');
  6216. $scheme_obj = $def->getDefaultScheme($config, $context);
  6217. if (!$scheme_obj) {
  6218. // something funky happened to the default scheme object
  6219. trigger_error(
  6220. 'Default scheme object "' . $def->defaultScheme . '" was not readable',
  6221. E_USER_WARNING
  6222. );
  6223. return false;
  6224. }
  6225. }
  6226. return $scheme_obj;
  6227. }
  6228. /**
  6229. * Generic validation method applicable for all schemes. May modify
  6230. * this URI in order to get it into a compliant form.
  6231. * @param $config Instance of HTMLPurifier_Config
  6232. * @param $context Instance of HTMLPurifier_Context
  6233. * @return True if validation/filtering succeeds, false if failure
  6234. */
  6235. public function validate($config, $context) {
  6236. // ABNF definitions from RFC 3986
  6237. $chars_sub_delims = '!$&\'()*+,;=';
  6238. $chars_gen_delims = ':/?#[]@';
  6239. $chars_pchar = $chars_sub_delims . ':@';
  6240. // validate host
  6241. if (!is_null($this->host)) {
  6242. $host_def = new HTMLPurifier_AttrDef_URI_Host();
  6243. $this->host = $host_def->validate($this->host, $config, $context);
  6244. if ($this->host === false) $this->host = null;
  6245. }
  6246. // validate scheme
  6247. // NOTE: It's not appropriate to check whether or not this
  6248. // scheme is in our registry, since a URIFilter may convert a
  6249. // URI that we don't allow into one we do. So instead, we just
  6250. // check if the scheme can be dropped because there is no host
  6251. // and it is our default scheme.
  6252. if (!is_null($this->scheme) && is_null($this->host) || $this->host === '') {
  6253. // support for relative paths is pretty abysmal when the
  6254. // scheme is present, so axe it when possible
  6255. $def = $config->getDefinition('URI');
  6256. if ($def->defaultScheme === $this->scheme) {
  6257. $this->scheme = null;
  6258. }
  6259. }
  6260. // validate username
  6261. if (!is_null($this->userinfo)) {
  6262. $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':');
  6263. $this->userinfo = $encoder->encode($this->userinfo);
  6264. }
  6265. // validate port
  6266. if (!is_null($this->port)) {
  6267. if ($this->port < 1 || $this->port > 65535) $this->port = null;
  6268. }
  6269. // validate path
  6270. $path_parts = array();
  6271. $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/');
  6272. if (!is_null($this->host)) { // this catches $this->host === ''
  6273. // path-abempty (hier and relative)
  6274. // http://www.example.com/my/path
  6275. // //www.example.com/my/path (looks odd, but works, and
  6276. // recognized by most browsers)
  6277. // (this set is valid or invalid on a scheme by scheme
  6278. // basis, so we'll deal with it later)
  6279. // file:///my/path
  6280. // ///my/path
  6281. $this->path = $segments_encoder->encode($this->path);
  6282. } elseif ($this->path !== '') {
  6283. if ($this->path[0] === '/') {
  6284. // path-absolute (hier and relative)
  6285. // http:/my/path
  6286. // /my/path
  6287. if (strlen($this->path) >= 2 && $this->path[1] === '/') {
  6288. // This could happen if both the host gets stripped
  6289. // out
  6290. // http://my/path
  6291. // //my/path
  6292. $this->path = '';
  6293. } else {
  6294. $this->path = $segments_encoder->encode($this->path);
  6295. }
  6296. } elseif (!is_null($this->scheme)) {
  6297. // path-rootless (hier)
  6298. // http:my/path
  6299. // Short circuit evaluation means we don't need to check nz
  6300. $this->path = $segments_encoder->encode($this->path);
  6301. } else {
  6302. // path-noscheme (relative)
  6303. // my/path
  6304. // (once again, not checking nz)
  6305. $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@');
  6306. $c = strpos($this->path, '/');
  6307. if ($c !== false) {
  6308. $this->path =
  6309. $segment_nc_encoder->encode(substr($this->path, 0, $c)) .
  6310. $segments_encoder->encode(substr($this->path, $c));
  6311. } else {
  6312. $this->path = $segment_nc_encoder->encode($this->path);
  6313. }
  6314. }
  6315. } else {
  6316. // path-empty (hier and relative)
  6317. $this->path = ''; // just to be safe
  6318. }
  6319. // qf = query and fragment
  6320. $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?');
  6321. if (!is_null($this->query)) {
  6322. $this->query = $qf_encoder->encode($this->query);
  6323. }
  6324. if (!is_null($this->fragment)) {
  6325. $this->fragment = $qf_encoder->encode($this->fragment);
  6326. }
  6327. return true;
  6328. }
  6329. /**
  6330. * Convert URI back to string
  6331. * @return String URI appropriate for output
  6332. */
  6333. public function toString() {
  6334. // reconstruct authority
  6335. $authority = null;
  6336. // there is a rendering difference between a null authority
  6337. // (http:foo-bar) and an empty string authority
  6338. // (http:///foo-bar).
  6339. if (!is_null($this->host)) {
  6340. $authority = '';
  6341. if(!is_null($this->userinfo)) $authority .= $this->userinfo . '@';
  6342. $authority .= $this->host;
  6343. if(!is_null($this->port)) $authority .= ':' . $this->port;
  6344. }
  6345. // Reconstruct the result
  6346. // One might wonder about parsing quirks from browsers after
  6347. // this reconstruction. Unfortunately, parsing behavior depends
  6348. // on what *scheme* was employed (file:///foo is handled *very*
  6349. // differently than http:///foo), so unfortunately we have to
  6350. // defer to the schemes to do the right thing.
  6351. $result = '';
  6352. if (!is_null($this->scheme)) $result .= $this->scheme . ':';
  6353. if (!is_null($authority)) $result .= '//' . $authority;
  6354. $result .= $this->path;
  6355. if (!is_null($this->query)) $result .= '?' . $this->query;
  6356. if (!is_null($this->fragment)) $result .= '#' . $this->fragment;
  6357. return $result;
  6358. }
  6359. /**
  6360. * Returns true if this URL might be considered a 'local' URL given
  6361. * the current context. This is true when the host is null, or
  6362. * when it matches the host supplied to the configuration.
  6363. *
  6364. * Note that this does not do any scheme checking, so it is mostly
  6365. * only appropriate for metadata that doesn't care about protocol
  6366. * security. isBenign is probably what you actually want.
  6367. */
  6368. public function isLocal($config, $context) {
  6369. if ($this->host === null) return true;
  6370. $uri_def = $config->getDefinition('URI');
  6371. if ($uri_def->host === $this->host) return true;
  6372. return false;
  6373. }
  6374. /**
  6375. * Returns true if this URL should be considered a 'benign' URL,
  6376. * that is:
  6377. *
  6378. * - It is a local URL (isLocal), and
  6379. * - It has a equal or better level of security
  6380. */
  6381. public function isBenign($config, $context) {
  6382. if (!$this->isLocal($config, $context)) return false;
  6383. $scheme_obj = $this->getSchemeObj($config, $context);
  6384. if (!$scheme_obj) return false; // conservative approach
  6385. $current_scheme_obj = $config->getDefinition('URI')->getDefaultScheme($config, $context);
  6386. if ($current_scheme_obj->secure) {
  6387. if (!$scheme_obj->secure) {
  6388. return false;
  6389. }
  6390. }
  6391. return true;
  6392. }
  6393. }
  6394. class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
  6395. {
  6396. public $type = 'URI';
  6397. protected $filters = array();
  6398. protected $postFilters = array();
  6399. protected $registeredFilters = array();
  6400. /**
  6401. * HTMLPurifier_URI object of the base specified at %URI.Base
  6402. */
  6403. public $base;
  6404. /**
  6405. * String host to consider "home" base, derived off of $base
  6406. */
  6407. public $host;
  6408. /**
  6409. * Name of default scheme based on %URI.DefaultScheme and %URI.Base
  6410. */
  6411. public $defaultScheme;
  6412. public function __construct() {
  6413. $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternal());
  6414. $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources());
  6415. $this->registerFilter(new HTMLPurifier_URIFilter_DisableResources());
  6416. $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist());
  6417. $this->registerFilter(new HTMLPurifier_URIFilter_SafeIframe());
  6418. $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute());
  6419. $this->registerFilter(new HTMLPurifier_URIFilter_Munge());
  6420. }
  6421. public function registerFilter($filter) {
  6422. $this->registeredFilters[$filter->name] = $filter;
  6423. }
  6424. public function addFilter($filter, $config) {
  6425. $r = $filter->prepare($config);
  6426. if ($r === false) return; // null is ok, for backwards compat
  6427. if ($filter->post) {
  6428. $this->postFilters[$filter->name] = $filter;
  6429. } else {
  6430. $this->filters[$filter->name] = $filter;
  6431. }
  6432. }
  6433. protected function doSetup($config) {
  6434. $this->setupMemberVariables($config);
  6435. $this->setupFilters($config);
  6436. }
  6437. protected function setupFilters($config) {
  6438. foreach ($this->registeredFilters as $name => $filter) {
  6439. if ($filter->always_load) {
  6440. $this->addFilter($filter, $config);
  6441. } else {
  6442. $conf = $config->get('URI.' . $name);
  6443. if ($conf !== false && $conf !== null) {
  6444. $this->addFilter($filter, $config);
  6445. }
  6446. }
  6447. }
  6448. unset($this->registeredFilters);
  6449. }
  6450. protected function setupMemberVariables($config) {
  6451. $this->host = $config->get('URI.Host');
  6452. $base_uri = $config->get('URI.Base');
  6453. if (!is_null($base_uri)) {
  6454. $parser = new HTMLPurifier_URIParser();
  6455. $this->base = $parser->parse($base_uri);
  6456. $this->defaultScheme = $this->base->scheme;
  6457. if (is_null($this->host)) $this->host = $this->base->host;
  6458. }
  6459. if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme');
  6460. }
  6461. public function getDefaultScheme($config, $context) {
  6462. return HTMLPurifier_URISchemeRegistry::instance()->getScheme($this->defaultScheme, $config, $context);
  6463. }
  6464. public function filter(&$uri, $config, $context) {
  6465. foreach ($this->filters as $name => $f) {
  6466. $result = $f->filter($uri, $config, $context);
  6467. if (!$result) return false;
  6468. }
  6469. return true;
  6470. }
  6471. public function postFilter(&$uri, $config, $context) {
  6472. foreach ($this->postFilters as $name => $f) {
  6473. $result = $f->filter($uri, $config, $context);
  6474. if (!$result) return false;
  6475. }
  6476. return true;
  6477. }
  6478. }
  6479. /**
  6480. * Chainable filters for custom URI processing.
  6481. *
  6482. * These filters can perform custom actions on a URI filter object,
  6483. * including transformation or blacklisting. A filter named Foo
  6484. * must have a corresponding configuration directive %URI.Foo,
  6485. * unless always_load is specified to be true.
  6486. *
  6487. * The following contexts may be available while URIFilters are being
  6488. * processed:
  6489. *
  6490. * - EmbeddedURI: true if URI is an embedded resource that will
  6491. * be loaded automatically on page load
  6492. * - CurrentToken: a reference to the token that is currently
  6493. * being processed
  6494. * - CurrentAttr: the name of the attribute that is currently being
  6495. * processed
  6496. * - CurrentCSSProperty: the name of the CSS property that is
  6497. * currently being processed (if applicable)
  6498. *
  6499. * @warning This filter is called before scheme object validation occurs.
  6500. * Make sure, if you require a specific scheme object, you
  6501. * you check that it exists. This allows filters to convert
  6502. * proprietary URI schemes into regular ones.
  6503. */
  6504. abstract class HTMLPurifier_URIFilter
  6505. {
  6506. /**
  6507. * Unique identifier of filter
  6508. */
  6509. public $name;
  6510. /**
  6511. * True if this filter should be run after scheme validation.
  6512. */
  6513. public $post = false;
  6514. /**
  6515. * True if this filter should always be loaded (this permits
  6516. * a filter to be named Foo without the corresponding %URI.Foo
  6517. * directive existing.)
  6518. */
  6519. public $always_load = false;
  6520. /**
  6521. * Performs initialization for the filter. If the filter returns
  6522. * false, this means that it shouldn't be considered active.
  6523. */
  6524. public function prepare($config) {return true;}
  6525. /**
  6526. * Filter a URI object
  6527. * @param $uri Reference to URI object variable
  6528. * @param $config Instance of HTMLPurifier_Config
  6529. * @param $context Instance of HTMLPurifier_Context
  6530. * @return bool Whether or not to continue processing: false indicates
  6531. * URL is no good, true indicates continue processing. Note that
  6532. * all changes are committed directly on the URI object
  6533. */
  6534. abstract public function filter(&$uri, $config, $context);
  6535. }
  6536. /**
  6537. * Parses a URI into the components and fragment identifier as specified
  6538. * by RFC 3986.
  6539. */
  6540. class HTMLPurifier_URIParser
  6541. {
  6542. /**
  6543. * Instance of HTMLPurifier_PercentEncoder to do normalization with.
  6544. */
  6545. protected $percentEncoder;
  6546. public function __construct() {
  6547. $this->percentEncoder = new HTMLPurifier_PercentEncoder();
  6548. }
  6549. /**
  6550. * Parses a URI.
  6551. * @param $uri string URI to parse
  6552. * @return HTMLPurifier_URI representation of URI. This representation has
  6553. * not been validated yet and may not conform to RFC.
  6554. */
  6555. public function parse($uri) {
  6556. $uri = $this->percentEncoder->normalize($uri);
  6557. // Regexp is as per Appendix B.
  6558. // Note that ["<>] are an addition to the RFC's recommended
  6559. // characters, because they represent external delimeters.
  6560. $r_URI = '!'.
  6561. '(([^:/?#"<>]+):)?'. // 2. Scheme
  6562. '(//([^/?#"<>]*))?'. // 4. Authority
  6563. '([^?#"<>]*)'. // 5. Path
  6564. '(\?([^#"<>]*))?'. // 7. Query
  6565. '(#([^"<>]*))?'. // 8. Fragment
  6566. '!';
  6567. $matches = array();
  6568. $result = preg_match($r_URI, $uri, $matches);
  6569. if (!$result) return false; // *really* invalid URI
  6570. // seperate out parts
  6571. $scheme = !empty($matches[1]) ? $matches[2] : null;
  6572. $authority = !empty($matches[3]) ? $matches[4] : null;
  6573. $path = $matches[5]; // always present, can be empty
  6574. $query = !empty($matches[6]) ? $matches[7] : null;
  6575. $fragment = !empty($matches[8]) ? $matches[9] : null;
  6576. // further parse authority
  6577. if ($authority !== null) {
  6578. $r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/";
  6579. $matches = array();
  6580. preg_match($r_authority, $authority, $matches);
  6581. $userinfo = !empty($matches[1]) ? $matches[2] : null;
  6582. $host = !empty($matches[3]) ? $matches[3] : '';
  6583. $port = !empty($matches[4]) ? (int) $matches[5] : null;
  6584. } else {
  6585. $port = $host = $userinfo = null;
  6586. }
  6587. return new HTMLPurifier_URI(
  6588. $scheme, $userinfo, $host, $port, $path, $query, $fragment);
  6589. }
  6590. }
  6591. /**
  6592. * Validator for the components of a URI for a specific scheme
  6593. */
  6594. abstract class HTMLPurifier_URIScheme
  6595. {
  6596. /**
  6597. * Scheme's default port (integer). If an explicit port number is
  6598. * specified that coincides with the default port, it will be
  6599. * elided.
  6600. */
  6601. public $default_port = null;
  6602. /**
  6603. * Whether or not URIs of this schem are locatable by a browser
  6604. * http and ftp are accessible, while mailto and news are not.
  6605. */
  6606. public $browsable = false;
  6607. /**
  6608. * Whether or not data transmitted over this scheme is encrypted.
  6609. * https is secure, http is not.
  6610. */
  6611. public $secure = false;
  6612. /**
  6613. * Whether or not the URI always uses <hier_part>, resolves edge cases
  6614. * with making relative URIs absolute
  6615. */
  6616. public $hierarchical = false;
  6617. /**
  6618. * Whether or not the URI may omit a hostname when the scheme is
  6619. * explicitly specified, ala file:///path/to/file. As of writing,
  6620. * 'file' is the only scheme that browsers support his properly.
  6621. */
  6622. public $may_omit_host = false;
  6623. /**
  6624. * Validates the components of a URI for a specific scheme.
  6625. * @param $uri Reference to a HTMLPurifier_URI object
  6626. * @param $config HTMLPurifier_Config object
  6627. * @param $context HTMLPurifier_Context object
  6628. * @return Bool success or failure
  6629. */
  6630. public abstract function doValidate(&$uri, $config, $context);
  6631. /**
  6632. * Public interface for validating components of a URI. Performs a
  6633. * bunch of default actions. Don't overload this method.
  6634. * @param $uri Reference to a HTMLPurifier_URI object
  6635. * @param $config HTMLPurifier_Config object
  6636. * @param $context HTMLPurifier_Context object
  6637. * @return Bool success or failure
  6638. */
  6639. public function validate(&$uri, $config, $context) {
  6640. if ($this->default_port == $uri->port) $uri->port = null;
  6641. // kludge: browsers do funny things when the scheme but not the
  6642. // authority is set
  6643. if (!$this->may_omit_host &&
  6644. // if the scheme is present, a missing host is always in error
  6645. (!is_null($uri->scheme) && ($uri->host === '' || is_null($uri->host))) ||
  6646. // if the scheme is not present, a *blank* host is in error,
  6647. // since this translates into '///path' which most browsers
  6648. // interpret as being 'http://path'.
  6649. (is_null($uri->scheme) && $uri->host === '')
  6650. ) {
  6651. do {
  6652. if (is_null($uri->scheme)) {
  6653. if (substr($uri->path, 0, 2) != '//') {
  6654. $uri->host = null;
  6655. break;
  6656. }
  6657. // URI is '////path', so we cannot nullify the
  6658. // host to preserve semantics. Try expanding the
  6659. // hostname instead (fall through)
  6660. }
  6661. // first see if we can manually insert a hostname
  6662. $host = $config->get('URI.Host');
  6663. if (!is_null($host)) {
  6664. $uri->host = $host;
  6665. } else {
  6666. // we can't do anything sensible, reject the URL.
  6667. return false;
  6668. }
  6669. } while (false);
  6670. }
  6671. return $this->doValidate($uri, $config, $context);
  6672. }
  6673. }
  6674. /**
  6675. * Registry for retrieving specific URI scheme validator objects.
  6676. */
  6677. class HTMLPurifier_URISchemeRegistry
  6678. {
  6679. /**
  6680. * Retrieve sole instance of the registry.
  6681. * @param $prototype Optional prototype to overload sole instance with,
  6682. * or bool true to reset to default registry.
  6683. * @note Pass a registry object $prototype with a compatible interface and
  6684. * the function will copy it and return it all further times.
  6685. */
  6686. public static function instance($prototype = null) {
  6687. static $instance = null;
  6688. if ($prototype !== null) {
  6689. $instance = $prototype;
  6690. } elseif ($instance === null || $prototype == true) {
  6691. $instance = new HTMLPurifier_URISchemeRegistry();
  6692. }
  6693. return $instance;
  6694. }
  6695. /**
  6696. * Cache of retrieved schemes.
  6697. */
  6698. protected $schemes = array();
  6699. /**
  6700. * Retrieves a scheme validator object
  6701. * @param $scheme String scheme name like http or mailto
  6702. * @param $config HTMLPurifier_Config object
  6703. * @param $config HTMLPurifier_Context object
  6704. */
  6705. public function getScheme($scheme, $config, $context) {
  6706. if (!$config) $config = HTMLPurifier_Config::createDefault();
  6707. // important, otherwise attacker could include arbitrary file
  6708. $allowed_schemes = $config->get('URI.AllowedSchemes');
  6709. if (!$config->get('URI.OverrideAllowedSchemes') &&
  6710. !isset($allowed_schemes[$scheme])
  6711. ) {
  6712. return;
  6713. }
  6714. if (isset($this->schemes[$scheme])) return $this->schemes[$scheme];
  6715. if (!isset($allowed_schemes[$scheme])) return;
  6716. $class = 'HTMLPurifier_URIScheme_' . $scheme;
  6717. if (!class_exists($class)) return;
  6718. $this->schemes[$scheme] = new $class();
  6719. return $this->schemes[$scheme];
  6720. }
  6721. /**
  6722. * Registers a custom scheme to the cache, bypassing reflection.
  6723. * @param $scheme Scheme name
  6724. * @param $scheme_obj HTMLPurifier_URIScheme object
  6725. */
  6726. public function register($scheme, $scheme_obj) {
  6727. $this->schemes[$scheme] = $scheme_obj;
  6728. }
  6729. }
  6730. /**
  6731. * Class for converting between different unit-lengths as specified by
  6732. * CSS.
  6733. */
  6734. class HTMLPurifier_UnitConverter
  6735. {
  6736. const ENGLISH = 1;
  6737. const METRIC = 2;
  6738. const DIGITAL = 3;
  6739. /**
  6740. * Units information array. Units are grouped into measuring systems
  6741. * (English, Metric), and are assigned an integer representing
  6742. * the conversion factor between that unit and the smallest unit in
  6743. * the system. Numeric indexes are actually magical constants that
  6744. * encode conversion data from one system to the next, with a O(n^2)
  6745. * constraint on memory (this is generally not a problem, since
  6746. * the number of measuring systems is small.)
  6747. */
  6748. protected static $units = array(
  6749. self::ENGLISH => array(
  6750. 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
  6751. 'pt' => 4,
  6752. 'pc' => 48,
  6753. 'in' => 288,
  6754. self::METRIC => array('pt', '0.352777778', 'mm'),
  6755. ),
  6756. self::METRIC => array(
  6757. 'mm' => 1,
  6758. 'cm' => 10,
  6759. self::ENGLISH => array('mm', '2.83464567', 'pt'),
  6760. ),
  6761. );
  6762. /**
  6763. * Minimum bcmath precision for output.
  6764. */
  6765. protected $outputPrecision;
  6766. /**
  6767. * Bcmath precision for internal calculations.
  6768. */
  6769. protected $internalPrecision;
  6770. /**
  6771. * Whether or not BCMath is available
  6772. */
  6773. private $bcmath;
  6774. public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) {
  6775. $this->outputPrecision = $output_precision;
  6776. $this->internalPrecision = $internal_precision;
  6777. $this->bcmath = !$force_no_bcmath && function_exists('bcmul');
  6778. }
  6779. /**
  6780. * Converts a length object of one unit into another unit.
  6781. * @param HTMLPurifier_Length $length
  6782. * Instance of HTMLPurifier_Length to convert. You must validate()
  6783. * it before passing it here!
  6784. * @param string $to_unit
  6785. * Unit to convert to.
  6786. * @note
  6787. * About precision: This conversion function pays very special
  6788. * attention to the incoming precision of values and attempts
  6789. * to maintain a number of significant figure. Results are
  6790. * fairly accurate up to nine digits. Some caveats:
  6791. * - If a number is zero-padded as a result of this significant
  6792. * figure tracking, the zeroes will be eliminated.
  6793. * - If a number contains less than four sigfigs ($outputPrecision)
  6794. * and this causes some decimals to be excluded, those
  6795. * decimals will be added on.
  6796. */
  6797. public function convert($length, $to_unit) {
  6798. if (!$length->isValid()) return false;
  6799. $n = $length->getN();
  6800. $unit = $length->getUnit();
  6801. if ($n === '0' || $unit === false) {
  6802. return new HTMLPurifier_Length('0', false);
  6803. }
  6804. $state = $dest_state = false;
  6805. foreach (self::$units as $k => $x) {
  6806. if (isset($x[$unit])) $state = $k;
  6807. if (isset($x[$to_unit])) $dest_state = $k;
  6808. }
  6809. if (!$state || !$dest_state) return false;
  6810. // Some calculations about the initial precision of the number;
  6811. // this will be useful when we need to do final rounding.
  6812. $sigfigs = $this->getSigFigs($n);
  6813. if ($sigfigs < $this->outputPrecision) $sigfigs = $this->outputPrecision;
  6814. // BCMath's internal precision deals only with decimals. Use
  6815. // our default if the initial number has no decimals, or increase
  6816. // it by how ever many decimals, thus, the number of guard digits
  6817. // will always be greater than or equal to internalPrecision.
  6818. $log = (int) floor(log(abs($n), 10));
  6819. $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
  6820. for ($i = 0; $i < 2; $i++) {
  6821. // Determine what unit IN THIS SYSTEM we need to convert to
  6822. if ($dest_state === $state) {
  6823. // Simple conversion
  6824. $dest_unit = $to_unit;
  6825. } else {
  6826. // Convert to the smallest unit, pending a system shift
  6827. $dest_unit = self::$units[$state][$dest_state][0];
  6828. }
  6829. // Do the conversion if necessary
  6830. if ($dest_unit !== $unit) {
  6831. $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
  6832. $n = $this->mul($n, $factor, $cp);
  6833. $unit = $dest_unit;
  6834. }
  6835. // Output was zero, so bail out early. Shouldn't ever happen.
  6836. if ($n === '') {
  6837. $n = '0';
  6838. $unit = $to_unit;
  6839. break;
  6840. }
  6841. // It was a simple conversion, so bail out
  6842. if ($dest_state === $state) {
  6843. break;
  6844. }
  6845. if ($i !== 0) {
  6846. // Conversion failed! Apparently, the system we forwarded
  6847. // to didn't have this unit. This should never happen!
  6848. return false;
  6849. }
  6850. // Pre-condition: $i == 0
  6851. // Perform conversion to next system of units
  6852. $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);
  6853. $unit = self::$units[$state][$dest_state][2];
  6854. $state = $dest_state;
  6855. // One more loop around to convert the unit in the new system.
  6856. }
  6857. // Post-condition: $unit == $to_unit
  6858. if ($unit !== $to_unit) return false;
  6859. // Useful for debugging:
  6860. //echo "<pre>n";
  6861. //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n";
  6862. $n = $this->round($n, $sigfigs);
  6863. if (strpos($n, '.') !== false) $n = rtrim($n, '0');
  6864. $n = rtrim($n, '.');
  6865. return new HTMLPurifier_Length($n, $unit);
  6866. }
  6867. /**
  6868. * Returns the number of significant figures in a string number.
  6869. * @param string $n Decimal number
  6870. * @return int number of sigfigs
  6871. */
  6872. public function getSigFigs($n) {
  6873. $n = ltrim($n, '0+-');
  6874. $dp = strpos($n, '.'); // decimal position
  6875. if ($dp === false) {
  6876. $sigfigs = strlen(rtrim($n, '0'));
  6877. } else {
  6878. $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character
  6879. if ($dp !== 0) $sigfigs--;
  6880. }
  6881. return $sigfigs;
  6882. }
  6883. /**
  6884. * Adds two numbers, using arbitrary precision when available.
  6885. */
  6886. private function add($s1, $s2, $scale) {
  6887. if ($this->bcmath) return bcadd($s1, $s2, $scale);
  6888. else return $this->scale($s1 + $s2, $scale);
  6889. }
  6890. /**
  6891. * Multiples two numbers, using arbitrary precision when available.
  6892. */
  6893. private function mul($s1, $s2, $scale) {
  6894. if ($this->bcmath) return bcmul($s1, $s2, $scale);
  6895. else return $this->scale($s1 * $s2, $scale);
  6896. }
  6897. /**
  6898. * Divides two numbers, using arbitrary precision when available.
  6899. */
  6900. private function div($s1, $s2, $scale) {
  6901. if ($this->bcmath) return bcdiv($s1, $s2, $scale);
  6902. else return $this->scale($s1 / $s2, $scale);
  6903. }
  6904. /**
  6905. * Rounds a number according to the number of sigfigs it should have,
  6906. * using arbitrary precision when available.
  6907. */
  6908. private function round($n, $sigfigs) {
  6909. $new_log = (int) floor(log(abs($n), 10)); // Number of digits left of decimal - 1
  6910. $rp = $sigfigs - $new_log - 1; // Number of decimal places needed
  6911. $neg = $n < 0 ? '-' : ''; // Negative sign
  6912. if ($this->bcmath) {
  6913. if ($rp >= 0) {
  6914. $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1);
  6915. $n = bcdiv($n, '1', $rp);
  6916. } else {
  6917. // This algorithm partially depends on the standardized
  6918. // form of numbers that comes out of bcmath.
  6919. $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0);
  6920. $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1);
  6921. }
  6922. return $n;
  6923. } else {
  6924. return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);
  6925. }
  6926. }
  6927. /**
  6928. * Scales a float to $scale digits right of decimal point, like BCMath.
  6929. */
  6930. private function scale($r, $scale) {
  6931. if ($scale < 0) {
  6932. // The f sprintf type doesn't support negative numbers, so we
  6933. // need to cludge things manually. First get the string.
  6934. $r = sprintf('%.0f', (float) $r);
  6935. // Due to floating point precision loss, $r will more than likely
  6936. // look something like 4652999999999.9234. We grab one more digit
  6937. // than we need to precise from $r and then use that to round
  6938. // appropriately.
  6939. $precise = (string) round(substr($r, 0, strlen($r) + $scale), -1);
  6940. // Now we return it, truncating the zero that was rounded off.
  6941. return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);
  6942. }
  6943. return sprintf('%.' . $scale . 'f', (float) $r);
  6944. }
  6945. }
  6946. /**
  6947. * Parses string representations into their corresponding native PHP
  6948. * variable type. The base implementation does a simple type-check.
  6949. */
  6950. class HTMLPurifier_VarParser
  6951. {
  6952. const STRING = 1;
  6953. const ISTRING = 2;
  6954. const TEXT = 3;
  6955. const ITEXT = 4;
  6956. const INT = 5;
  6957. const FLOAT = 6;
  6958. const BOOL = 7;
  6959. const LOOKUP = 8;
  6960. const ALIST = 9;
  6961. const HASH = 10;
  6962. const MIXED = 11;
  6963. /**
  6964. * Lookup table of allowed types. Mainly for backwards compatibility, but
  6965. * also convenient for transforming string type names to the integer constants.
  6966. */
  6967. static public $types = array(
  6968. 'string' => self::STRING,
  6969. 'istring' => self::ISTRING,
  6970. 'text' => self::TEXT,
  6971. 'itext' => self::ITEXT,
  6972. 'int' => self::INT,
  6973. 'float' => self::FLOAT,
  6974. 'bool' => self::BOOL,
  6975. 'lookup' => self::LOOKUP,
  6976. 'list' => self::ALIST,
  6977. 'hash' => self::HASH,
  6978. 'mixed' => self::MIXED
  6979. );
  6980. /**
  6981. * Lookup table of types that are string, and can have aliases or
  6982. * allowed value lists.
  6983. */
  6984. static public $stringTypes = array(
  6985. self::STRING => true,
  6986. self::ISTRING => true,
  6987. self::TEXT => true,
  6988. self::ITEXT => true,
  6989. );
  6990. /**
  6991. * Validate a variable according to type. Throws
  6992. * HTMLPurifier_VarParserException if invalid.
  6993. * It may return NULL as a valid type if $allow_null is true.
  6994. *
  6995. * @param $var Variable to validate
  6996. * @param $type Type of variable, see HTMLPurifier_VarParser->types
  6997. * @param $allow_null Whether or not to permit null as a value
  6998. * @return Validated and type-coerced variable
  6999. */
  7000. final public function parse($var, $type, $allow_null = false) {
  7001. if (is_string($type)) {
  7002. if (!isset(HTMLPurifier_VarParser::$types[$type])) {
  7003. throw new HTMLPurifier_VarParserException("Invalid type '$type'");
  7004. } else {
  7005. $type = HTMLPurifier_VarParser::$types[$type];
  7006. }
  7007. }
  7008. $var = $this->parseImplementation($var, $type, $allow_null);
  7009. if ($allow_null && $var === null) return null;
  7010. // These are basic checks, to make sure nothing horribly wrong
  7011. // happened in our implementations.
  7012. switch ($type) {
  7013. case (self::STRING):
  7014. case (self::ISTRING):
  7015. case (self::TEXT):
  7016. case (self::ITEXT):
  7017. if (!is_string($var)) break;
  7018. if ($type == self::ISTRING || $type == self::ITEXT) $var = strtolower($var);
  7019. return $var;
  7020. case (self::INT):
  7021. if (!is_int($var)) break;
  7022. return $var;
  7023. case (self::FLOAT):
  7024. if (!is_float($var)) break;
  7025. return $var;
  7026. case (self::BOOL):
  7027. if (!is_bool($var)) break;
  7028. return $var;
  7029. case (self::LOOKUP):
  7030. case (self::ALIST):
  7031. case (self::HASH):
  7032. if (!is_array($var)) break;
  7033. if ($type === self::LOOKUP) {
  7034. foreach ($var as $k) if ($k !== true) $this->error('Lookup table contains value other than true');
  7035. } elseif ($type === self::ALIST) {
  7036. $keys = array_keys($var);
  7037. if (array_keys($keys) !== $keys) $this->error('Indices for list are not uniform');
  7038. }
  7039. return $var;
  7040. case (self::MIXED):
  7041. return $var;
  7042. default:
  7043. $this->errorInconsistent(get_class($this), $type);
  7044. }
  7045. $this->errorGeneric($var, $type);
  7046. }
  7047. /**
  7048. * Actually implements the parsing. Base implementation is to not
  7049. * do anything to $var. Subclasses should overload this!
  7050. */
  7051. protected function parseImplementation($var, $type, $allow_null) {
  7052. return $var;
  7053. }
  7054. /**
  7055. * Throws an exception.
  7056. */
  7057. protected function error($msg) {
  7058. throw new HTMLPurifier_VarParserException($msg);
  7059. }
  7060. /**
  7061. * Throws an inconsistency exception.
  7062. * @note This should not ever be called. It would be called if we
  7063. * extend the allowed values of HTMLPurifier_VarParser without
  7064. * updating subclasses.
  7065. */
  7066. protected function errorInconsistent($class, $type) {
  7067. throw new HTMLPurifier_Exception("Inconsistency in $class: ".HTMLPurifier_VarParser::getTypeName($type)." not implemented");
  7068. }
  7069. /**
  7070. * Generic error for if a type didn't work.
  7071. */
  7072. protected function errorGeneric($var, $type) {
  7073. $vtype = gettype($var);
  7074. $this->error("Expected type ".HTMLPurifier_VarParser::getTypeName($type).", got $vtype");
  7075. }
  7076. static public function getTypeName($type) {
  7077. static $lookup;
  7078. if (!$lookup) {
  7079. // Lazy load the alternative lookup table
  7080. $lookup = array_flip(HTMLPurifier_VarParser::$types);
  7081. }
  7082. if (!isset($lookup[$type])) return 'unknown';
  7083. return $lookup[$type];
  7084. }
  7085. }
  7086. /**
  7087. * Exception type for HTMLPurifier_VarParser
  7088. */
  7089. class HTMLPurifier_VarParserException extends HTMLPurifier_Exception
  7090. {
  7091. }
  7092. /**
  7093. * Validates the HTML attribute style, otherwise known as CSS.
  7094. * @note We don't implement the whole CSS specification, so it might be
  7095. * difficult to reuse this component in the context of validating
  7096. * actual stylesheet declarations.
  7097. * @note If we were really serious about validating the CSS, we would
  7098. * tokenize the styles and then parse the tokens. Obviously, we
  7099. * are not doing that. Doing that could seriously harm performance,
  7100. * but would make these components a lot more viable for a CSS
  7101. * filtering solution.
  7102. */
  7103. class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef
  7104. {
  7105. public function validate($css, $config, $context) {
  7106. $css = $this->parseCDATA($css);
  7107. $definition = $config->getCSSDefinition();
  7108. // we're going to break the spec and explode by semicolons.
  7109. // This is because semicolon rarely appears in escaped form
  7110. // Doing this is generally flaky but fast
  7111. // IT MIGHT APPEAR IN URIs, see HTMLPurifier_AttrDef_CSSURI
  7112. // for details
  7113. $declarations = explode(';', $css);
  7114. $propvalues = array();
  7115. /**
  7116. * Name of the current CSS property being validated.
  7117. */
  7118. $property = false;
  7119. $context->register('CurrentCSSProperty', $property);
  7120. foreach ($declarations as $declaration) {
  7121. if (!$declaration) continue;
  7122. if (!strpos($declaration, ':')) continue;
  7123. list($property, $value) = explode(':', $declaration, 2);
  7124. $property = trim($property);
  7125. $value = trim($value);
  7126. $ok = false;
  7127. do {
  7128. if (isset($definition->info[$property])) {
  7129. $ok = true;
  7130. break;
  7131. }
  7132. if (ctype_lower($property)) break;
  7133. $property = strtolower($property);
  7134. if (isset($definition->info[$property])) {
  7135. $ok = true;
  7136. break;
  7137. }
  7138. } while(0);
  7139. if (!$ok) continue;
  7140. // inefficient call, since the validator will do this again
  7141. if (strtolower(trim($value)) !== 'inherit') {
  7142. // inherit works for everything (but only on the base property)
  7143. $result = $definition->info[$property]->validate(
  7144. $value, $config, $context );
  7145. } else {
  7146. $result = 'inherit';
  7147. }
  7148. if ($result === false) continue;
  7149. $propvalues[$property] = $result;
  7150. }
  7151. $context->destroy('CurrentCSSProperty');
  7152. // procedure does not write the new CSS simultaneously, so it's
  7153. // slightly inefficient, but it's the only way of getting rid of
  7154. // duplicates. Perhaps config to optimize it, but not now.
  7155. $new_declarations = '';
  7156. foreach ($propvalues as $prop => $value) {
  7157. $new_declarations .= "$prop:$value;";
  7158. }
  7159. return $new_declarations ? $new_declarations : false;
  7160. }
  7161. }
  7162. /**
  7163. * Dummy AttrDef that mimics another AttrDef, BUT it generates clones
  7164. * with make.
  7165. */
  7166. class HTMLPurifier_AttrDef_Clone extends HTMLPurifier_AttrDef
  7167. {
  7168. /**
  7169. * What we're cloning
  7170. */
  7171. protected $clone;
  7172. public function __construct($clone) {
  7173. $this->clone = $clone;
  7174. }
  7175. public function validate($v, $config, $context) {
  7176. return $this->clone->validate($v, $config, $context);
  7177. }
  7178. public function make($string) {
  7179. return clone $this->clone;
  7180. }
  7181. }
  7182. // Enum = Enumerated
  7183. /**
  7184. * Validates a keyword against a list of valid values.
  7185. * @warning The case-insensitive compare of this function uses PHP's
  7186. * built-in strtolower and ctype_lower functions, which may
  7187. * cause problems with international comparisons
  7188. */
  7189. class HTMLPurifier_AttrDef_Enum extends HTMLPurifier_AttrDef
  7190. {
  7191. /**
  7192. * Lookup table of valid values.
  7193. * @todo Make protected
  7194. */
  7195. public $valid_values = array();
  7196. /**
  7197. * Bool indicating whether or not enumeration is case sensitive.
  7198. * @note In general this is always case insensitive.
  7199. */
  7200. protected $case_sensitive = false; // values according to W3C spec
  7201. /**
  7202. * @param $valid_values List of valid values
  7203. * @param $case_sensitive Bool indicating whether or not case sensitive
  7204. */
  7205. public function __construct(
  7206. $valid_values = array(), $case_sensitive = false
  7207. ) {
  7208. $this->valid_values = array_flip($valid_values);
  7209. $this->case_sensitive = $case_sensitive;
  7210. }
  7211. public function validate($string, $config, $context) {
  7212. $string = trim($string);
  7213. if (!$this->case_sensitive) {
  7214. // we may want to do full case-insensitive libraries
  7215. $string = ctype_lower($string) ? $string : strtolower($string);
  7216. }
  7217. $result = isset($this->valid_values[$string]);
  7218. return $result ? $string : false;
  7219. }
  7220. /**
  7221. * @param $string In form of comma-delimited list of case-insensitive
  7222. * valid values. Example: "foo,bar,baz". Prepend "s:" to make
  7223. * case sensitive
  7224. */
  7225. public function make($string) {
  7226. if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') {
  7227. $string = substr($string, 2);
  7228. $sensitive = true;
  7229. } else {
  7230. $sensitive = false;
  7231. }
  7232. $values = explode(',', $string);
  7233. return new HTMLPurifier_AttrDef_Enum($values, $sensitive);
  7234. }
  7235. }
  7236. /**
  7237. * Validates an integer.
  7238. * @note While this class was modeled off the CSS definition, no currently
  7239. * allowed CSS uses this type. The properties that do are: widows,
  7240. * orphans, z-index, counter-increment, counter-reset. Some of the
  7241. * HTML attributes, however, find use for a non-negative version of this.
  7242. */
  7243. class HTMLPurifier_AttrDef_Integer extends HTMLPurifier_AttrDef
  7244. {
  7245. /**
  7246. * Bool indicating whether or not negative values are allowed
  7247. */
  7248. protected $negative = true;
  7249. /**
  7250. * Bool indicating whether or not zero is allowed
  7251. */
  7252. protected $zero = true;
  7253. /**
  7254. * Bool indicating whether or not positive values are allowed
  7255. */
  7256. protected $positive = true;
  7257. /**
  7258. * @param $negative Bool indicating whether or not negative values are allowed
  7259. * @param $zero Bool indicating whether or not zero is allowed
  7260. * @param $positive Bool indicating whether or not positive values are allowed
  7261. */
  7262. public function __construct(
  7263. $negative = true, $zero = true, $positive = true
  7264. ) {
  7265. $this->negative = $negative;
  7266. $this->zero = $zero;
  7267. $this->positive = $positive;
  7268. }
  7269. public function validate($integer, $config, $context) {
  7270. $integer = $this->parseCDATA($integer);
  7271. if ($integer === '') return false;
  7272. // we could possibly simply typecast it to integer, but there are
  7273. // certain fringe cases that must not return an integer.
  7274. // clip leading sign
  7275. if ( $this->negative && $integer[0] === '-' ) {
  7276. $digits = substr($integer, 1);
  7277. if ($digits === '0') $integer = '0'; // rm minus sign for zero
  7278. } elseif( $this->positive && $integer[0] === '+' ) {
  7279. $digits = $integer = substr($integer, 1); // rm unnecessary plus
  7280. } else {
  7281. $digits = $integer;
  7282. }
  7283. // test if it's numeric
  7284. if (!ctype_digit($digits)) return false;
  7285. // perform scope tests
  7286. if (!$this->zero && $integer == 0) return false;
  7287. if (!$this->positive && $integer > 0) return false;
  7288. if (!$this->negative && $integer < 0) return false;
  7289. return $integer;
  7290. }
  7291. }
  7292. /**
  7293. * Validates the HTML attribute lang, effectively a language code.
  7294. * @note Built according to RFC 3066, which obsoleted RFC 1766
  7295. */
  7296. class HTMLPurifier_AttrDef_Lang extends HTMLPurifier_AttrDef
  7297. {
  7298. public function validate($string, $config, $context) {
  7299. $string = trim($string);
  7300. if (!$string) return false;
  7301. $subtags = explode('-', $string);
  7302. $num_subtags = count($subtags);
  7303. if ($num_subtags == 0) return false; // sanity check
  7304. // process primary subtag : $subtags[0]
  7305. $length = strlen($subtags[0]);
  7306. switch ($length) {
  7307. case 0:
  7308. return false;
  7309. case 1:
  7310. if (! ($subtags[0] == 'x' || $subtags[0] == 'i') ) {
  7311. return false;
  7312. }
  7313. break;
  7314. case 2:
  7315. case 3:
  7316. if (! ctype_alpha($subtags[0]) ) {
  7317. return false;
  7318. } elseif (! ctype_lower($subtags[0]) ) {
  7319. $subtags[0] = strtolower($subtags[0]);
  7320. }
  7321. break;
  7322. default:
  7323. return false;
  7324. }
  7325. $new_string = $subtags[0];
  7326. if ($num_subtags == 1) return $new_string;
  7327. // process second subtag : $subtags[1]
  7328. $length = strlen($subtags[1]);
  7329. if ($length == 0 || ($length == 1 && $subtags[1] != 'x') || $length > 8 || !ctype_alnum($subtags[1])) {
  7330. return $new_string;
  7331. }
  7332. if (!ctype_lower($subtags[1])) $subtags[1] = strtolower($subtags[1]);
  7333. $new_string .= '-' . $subtags[1];
  7334. if ($num_subtags == 2) return $new_string;
  7335. // process all other subtags, index 2 and up
  7336. for ($i = 2; $i < $num_subtags; $i++) {
  7337. $length = strlen($subtags[$i]);
  7338. if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) {
  7339. return $new_string;
  7340. }
  7341. if (!ctype_lower($subtags[$i])) {
  7342. $subtags[$i] = strtolower($subtags[$i]);
  7343. }
  7344. $new_string .= '-' . $subtags[$i];
  7345. }
  7346. return $new_string;
  7347. }
  7348. }
  7349. /**
  7350. * Decorator that, depending on a token, switches between two definitions.
  7351. */
  7352. class HTMLPurifier_AttrDef_Switch
  7353. {
  7354. protected $tag;
  7355. protected $withTag, $withoutTag;
  7356. /**
  7357. * @param string $tag Tag name to switch upon
  7358. * @param HTMLPurifier_AttrDef $with_tag Call if token matches tag
  7359. * @param HTMLPurifier_AttrDef $without_tag Call if token doesn't match, or there is no token
  7360. */
  7361. public function __construct($tag, $with_tag, $without_tag) {
  7362. $this->tag = $tag;
  7363. $this->withTag = $with_tag;
  7364. $this->withoutTag = $without_tag;
  7365. }
  7366. public function validate($string, $config, $context) {
  7367. $token = $context->get('CurrentToken', true);
  7368. if (!$token || $token->name !== $this->tag) {
  7369. return $this->withoutTag->validate($string, $config, $context);
  7370. } else {
  7371. return $this->withTag->validate($string, $config, $context);
  7372. }
  7373. }
  7374. }
  7375. /**
  7376. * Validates arbitrary text according to the HTML spec.
  7377. */
  7378. class HTMLPurifier_AttrDef_Text extends HTMLPurifier_AttrDef
  7379. {
  7380. public function validate($string, $config, $context) {
  7381. return $this->parseCDATA($string);
  7382. }
  7383. }
  7384. /**
  7385. * Validates a URI as defined by RFC 3986.
  7386. * @note Scheme-specific mechanics deferred to HTMLPurifier_URIScheme
  7387. */
  7388. class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef
  7389. {
  7390. protected $parser;
  7391. protected $embedsResource;
  7392. /**
  7393. * @param $embeds_resource_resource Does the URI here result in an extra HTTP request?
  7394. */
  7395. public function __construct($embeds_resource = false) {
  7396. $this->parser = new HTMLPurifier_URIParser();
  7397. $this->embedsResource = (bool) $embeds_resource;
  7398. }
  7399. public function make($string) {
  7400. $embeds = ($string === 'embedded');
  7401. return new HTMLPurifier_AttrDef_URI($embeds);
  7402. }
  7403. public function validate($uri, $config, $context) {
  7404. if ($config->get('URI.Disable')) return false;
  7405. $uri = $this->parseCDATA($uri);
  7406. // parse the URI
  7407. $uri = $this->parser->parse($uri);
  7408. if ($uri === false) return false;
  7409. // add embedded flag to context for validators
  7410. $context->register('EmbeddedURI', $this->embedsResource);
  7411. $ok = false;
  7412. do {
  7413. // generic validation
  7414. $result = $uri->validate($config, $context);
  7415. if (!$result) break;
  7416. // chained filtering
  7417. $uri_def = $config->getDefinition('URI');
  7418. $result = $uri_def->filter($uri, $config, $context);
  7419. if (!$result) break;
  7420. // scheme-specific validation
  7421. $scheme_obj = $uri->getSchemeObj($config, $context);
  7422. if (!$scheme_obj) break;
  7423. if ($this->embedsResource && !$scheme_obj->browsable) break;
  7424. $result = $scheme_obj->validate($uri, $config, $context);
  7425. if (!$result) break;
  7426. // Post chained filtering
  7427. $result = $uri_def->postFilter($uri, $config, $context);
  7428. if (!$result) break;
  7429. // survived gauntlet
  7430. $ok = true;
  7431. } while (false);
  7432. $context->destroy('EmbeddedURI');
  7433. if (!$ok) return false;
  7434. // back to string
  7435. return $uri->toString();
  7436. }
  7437. }
  7438. /**
  7439. * Validates a number as defined by the CSS spec.
  7440. */
  7441. class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef
  7442. {
  7443. /**
  7444. * Bool indicating whether or not only positive values allowed.
  7445. */
  7446. protected $non_negative = false;
  7447. /**
  7448. * @param $non_negative Bool indicating whether negatives are forbidden
  7449. */
  7450. public function __construct($non_negative = false) {
  7451. $this->non_negative = $non_negative;
  7452. }
  7453. /**
  7454. * @warning Some contexts do not pass $config, $context. These
  7455. * variables should not be used without checking HTMLPurifier_Length
  7456. */
  7457. public function validate($number, $config, $context) {
  7458. $number = $this->parseCDATA($number);
  7459. if ($number === '') return false;
  7460. if ($number === '0') return '0';
  7461. $sign = '';
  7462. switch ($number[0]) {
  7463. case '-':
  7464. if ($this->non_negative) return false;
  7465. $sign = '-';
  7466. case '+':
  7467. $number = substr($number, 1);
  7468. }
  7469. if (ctype_digit($number)) {
  7470. $number = ltrim($number, '0');
  7471. return $number ? $sign . $number : '0';
  7472. }
  7473. // Period is the only non-numeric character allowed
  7474. if (strpos($number, '.') === false) return false;
  7475. list($left, $right) = explode('.', $number, 2);
  7476. if ($left === '' && $right === '') return false;
  7477. if ($left !== '' && !ctype_digit($left)) return false;
  7478. $left = ltrim($left, '0');
  7479. $right = rtrim($right, '0');
  7480. if ($right === '') {
  7481. return $left ? $sign . $left : '0';
  7482. } elseif (!ctype_digit($right)) {
  7483. return false;
  7484. }
  7485. return $sign . $left . '.' . $right;
  7486. }
  7487. }
  7488. class HTMLPurifier_AttrDef_CSS_AlphaValue extends HTMLPurifier_AttrDef_CSS_Number
  7489. {
  7490. public function __construct() {
  7491. parent::__construct(false); // opacity is non-negative, but we will clamp it
  7492. }
  7493. public function validate($number, $config, $context) {
  7494. $result = parent::validate($number, $config, $context);
  7495. if ($result === false) return $result;
  7496. $float = (float) $result;
  7497. if ($float < 0.0) $result = '0';
  7498. if ($float > 1.0) $result = '1';
  7499. return $result;
  7500. }
  7501. }
  7502. /**
  7503. * Validates shorthand CSS property background.
  7504. * @warning Does not support url tokens that have internal spaces.
  7505. */
  7506. class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef
  7507. {
  7508. /**
  7509. * Local copy of component validators.
  7510. * @note See HTMLPurifier_AttrDef_Font::$info for a similar impl.
  7511. */
  7512. protected $info;
  7513. public function __construct($config) {
  7514. $def = $config->getCSSDefinition();
  7515. $this->info['background-color'] = $def->info['background-color'];
  7516. $this->info['background-image'] = $def->info['background-image'];
  7517. $this->info['background-repeat'] = $def->info['background-repeat'];
  7518. $this->info['background-attachment'] = $def->info['background-attachment'];
  7519. $this->info['background-position'] = $def->info['background-position'];
  7520. }
  7521. public function validate($string, $config, $context) {
  7522. // regular pre-processing
  7523. $string = $this->parseCDATA($string);
  7524. if ($string === '') return false;
  7525. // munge rgb() decl if necessary
  7526. $string = $this->mungeRgb($string);
  7527. // assumes URI doesn't have spaces in it
  7528. $bits = explode(' ', $string); // bits to process
  7529. $caught = array();
  7530. $caught['color'] = false;
  7531. $caught['image'] = false;
  7532. $caught['repeat'] = false;
  7533. $caught['attachment'] = false;
  7534. $caught['position'] = false;
  7535. $i = 0; // number of catches
  7536. $none = false;
  7537. foreach ($bits as $bit) {
  7538. if ($bit === '') continue;
  7539. foreach ($caught as $key => $status) {
  7540. if ($key != 'position') {
  7541. if ($status !== false) continue;
  7542. $r = $this->info['background-' . $key]->validate($bit, $config, $context);
  7543. } else {
  7544. $r = $bit;
  7545. }
  7546. if ($r === false) continue;
  7547. if ($key == 'position') {
  7548. if ($caught[$key] === false) $caught[$key] = '';
  7549. $caught[$key] .= $r . ' ';
  7550. } else {
  7551. $caught[$key] = $r;
  7552. }
  7553. $i++;
  7554. break;
  7555. }
  7556. }
  7557. if (!$i) return false;
  7558. if ($caught['position'] !== false) {
  7559. $caught['position'] = $this->info['background-position']->
  7560. validate($caught['position'], $config, $context);
  7561. }
  7562. $ret = array();
  7563. foreach ($caught as $value) {
  7564. if ($value === false) continue;
  7565. $ret[] = $value;
  7566. }
  7567. if (empty($ret)) return false;
  7568. return implode(' ', $ret);
  7569. }
  7570. }
  7571. /* W3C says:
  7572. [ // adjective and number must be in correct order, even if
  7573. // you could switch them without introducing ambiguity.
  7574. // some browsers support that syntax
  7575. [
  7576. <percentage> | <length> | left | center | right
  7577. ]
  7578. [
  7579. <percentage> | <length> | top | center | bottom
  7580. ]?
  7581. ] |
  7582. [ // this signifies that the vertical and horizontal adjectives
  7583. // can be arbitrarily ordered, however, there can only be two,
  7584. // one of each, or none at all
  7585. [
  7586. left | center | right
  7587. ] ||
  7588. [
  7589. top | center | bottom
  7590. ]
  7591. ]
  7592. top, left = 0%
  7593. center, (none) = 50%
  7594. bottom, right = 100%
  7595. */
  7596. /* QuirksMode says:
  7597. keyword + length/percentage must be ordered correctly, as per W3C
  7598. Internet Explorer and Opera, however, support arbitrary ordering. We
  7599. should fix it up.
  7600. Minor issue though, not strictly necessary.
  7601. */
  7602. // control freaks may appreciate the ability to convert these to
  7603. // percentages or something, but it's not necessary
  7604. /**
  7605. * Validates the value of background-position.
  7606. */
  7607. class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef
  7608. {
  7609. protected $length;
  7610. protected $percentage;
  7611. public function __construct() {
  7612. $this->length = new HTMLPurifier_AttrDef_CSS_Length();
  7613. $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage();
  7614. }
  7615. public function validate($string, $config, $context) {
  7616. $string = $this->parseCDATA($string);
  7617. $bits = explode(' ', $string);
  7618. $keywords = array();
  7619. $keywords['h'] = false; // left, right
  7620. $keywords['v'] = false; // top, bottom
  7621. $keywords['ch'] = false; // center (first word)
  7622. $keywords['cv'] = false; // center (second word)
  7623. $measures = array();
  7624. $i = 0;
  7625. $lookup = array(
  7626. 'top' => 'v',
  7627. 'bottom' => 'v',
  7628. 'left' => 'h',
  7629. 'right' => 'h',
  7630. 'center' => 'c'
  7631. );
  7632. foreach ($bits as $bit) {
  7633. if ($bit === '') continue;
  7634. // test for keyword
  7635. $lbit = ctype_lower($bit) ? $bit : strtolower($bit);
  7636. if (isset($lookup[$lbit])) {
  7637. $status = $lookup[$lbit];
  7638. if ($status == 'c') {
  7639. if ($i == 0) {
  7640. $status = 'ch';
  7641. } else {
  7642. $status = 'cv';
  7643. }
  7644. }
  7645. $keywords[$status] = $lbit;
  7646. $i++;
  7647. }
  7648. // test for length
  7649. $r = $this->length->validate($bit, $config, $context);
  7650. if ($r !== false) {
  7651. $measures[] = $r;
  7652. $i++;
  7653. }
  7654. // test for percentage
  7655. $r = $this->percentage->validate($bit, $config, $context);
  7656. if ($r !== false) {
  7657. $measures[] = $r;
  7658. $i++;
  7659. }
  7660. }
  7661. if (!$i) return false; // no valid values were caught
  7662. $ret = array();
  7663. // first keyword
  7664. if ($keywords['h']) $ret[] = $keywords['h'];
  7665. elseif ($keywords['ch']) {
  7666. $ret[] = $keywords['ch'];
  7667. $keywords['cv'] = false; // prevent re-use: center = center center
  7668. }
  7669. elseif (count($measures)) $ret[] = array_shift($measures);
  7670. if ($keywords['v']) $ret[] = $keywords['v'];
  7671. elseif ($keywords['cv']) $ret[] = $keywords['cv'];
  7672. elseif (count($measures)) $ret[] = array_shift($measures);
  7673. if (empty($ret)) return false;
  7674. return implode(' ', $ret);
  7675. }
  7676. }
  7677. /**
  7678. * Validates the border property as defined by CSS.
  7679. */
  7680. class HTMLPurifier_AttrDef_CSS_Border extends HTMLPurifier_AttrDef
  7681. {
  7682. /**
  7683. * Local copy of properties this property is shorthand for.
  7684. */
  7685. protected $info = array();
  7686. public function __construct($config) {
  7687. $def = $config->getCSSDefinition();
  7688. $this->info['border-width'] = $def->info['border-width'];
  7689. $this->info['border-style'] = $def->info['border-style'];
  7690. $this->info['border-top-color'] = $def->info['border-top-color'];
  7691. }
  7692. public function validate($string, $config, $context) {
  7693. $string = $this->parseCDATA($string);
  7694. $string = $this->mungeRgb($string);
  7695. $bits = explode(' ', $string);
  7696. $done = array(); // segments we've finished
  7697. $ret = ''; // return value
  7698. foreach ($bits as $bit) {
  7699. foreach ($this->info as $propname => $validator) {
  7700. if (isset($done[$propname])) continue;
  7701. $r = $validator->validate($bit, $config, $context);
  7702. if ($r !== false) {
  7703. $ret .= $r . ' ';
  7704. $done[$propname] = true;
  7705. break;
  7706. }
  7707. }
  7708. }
  7709. return rtrim($ret);
  7710. }
  7711. }
  7712. /**
  7713. * Validates Color as defined by CSS.
  7714. */
  7715. class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
  7716. {
  7717. public function validate($color, $config, $context) {
  7718. static $colors = null;
  7719. if ($colors === null) $colors = $config->get('Core.ColorKeywords');
  7720. $color = trim($color);
  7721. if ($color === '') return false;
  7722. $lower = strtolower($color);
  7723. if (isset($colors[$lower])) return $colors[$lower];
  7724. if (strpos($color, 'rgb(') !== false) {
  7725. // rgb literal handling
  7726. $length = strlen($color);
  7727. if (strpos($color, ')') !== $length - 1) return false;
  7728. $triad = substr($color, 4, $length - 4 - 1);
  7729. $parts = explode(',', $triad);
  7730. if (count($parts) !== 3) return false;
  7731. $type = false; // to ensure that they're all the same type
  7732. $new_parts = array();
  7733. foreach ($parts as $part) {
  7734. $part = trim($part);
  7735. if ($part === '') return false;
  7736. $length = strlen($part);
  7737. if ($part[$length - 1] === '%') {
  7738. // handle percents
  7739. if (!$type) {
  7740. $type = 'percentage';
  7741. } elseif ($type !== 'percentage') {
  7742. return false;
  7743. }
  7744. $num = (float) substr($part, 0, $length - 1);
  7745. if ($num < 0) $num = 0;
  7746. if ($num > 100) $num = 100;
  7747. $new_parts[] = "$num%";
  7748. } else {
  7749. // handle integers
  7750. if (!$type) {
  7751. $type = 'integer';
  7752. } elseif ($type !== 'integer') {
  7753. return false;
  7754. }
  7755. $num = (int) $part;
  7756. if ($num < 0) $num = 0;
  7757. if ($num > 255) $num = 255;
  7758. $new_parts[] = (string) $num;
  7759. }
  7760. }
  7761. $new_triad = implode(',', $new_parts);
  7762. $color = "rgb($new_triad)";
  7763. } else {
  7764. // hexadecimal handling
  7765. if ($color[0] === '#') {
  7766. $hex = substr($color, 1);
  7767. } else {
  7768. $hex = $color;
  7769. $color = '#' . $color;
  7770. }
  7771. $length = strlen($hex);
  7772. if ($length !== 3 && $length !== 6) return false;
  7773. if (!ctype_xdigit($hex)) return false;
  7774. }
  7775. return $color;
  7776. }
  7777. }
  7778. /**
  7779. * Allows multiple validators to attempt to validate attribute.
  7780. *
  7781. * Composite is just what it sounds like: a composite of many validators.
  7782. * This means that multiple HTMLPurifier_AttrDef objects will have a whack
  7783. * at the string. If one of them passes, that's what is returned. This is
  7784. * especially useful for CSS values, which often are a choice between
  7785. * an enumerated set of predefined values or a flexible data type.
  7786. */
  7787. class HTMLPurifier_AttrDef_CSS_Composite extends HTMLPurifier_AttrDef
  7788. {
  7789. /**
  7790. * List of HTMLPurifier_AttrDef objects that may process strings
  7791. * @todo Make protected
  7792. */
  7793. public $defs;
  7794. /**
  7795. * @param $defs List of HTMLPurifier_AttrDef objects
  7796. */
  7797. public function __construct($defs) {
  7798. $this->defs = $defs;
  7799. }
  7800. public function validate($string, $config, $context) {
  7801. foreach ($this->defs as $i => $def) {
  7802. $result = $this->defs[$i]->validate($string, $config, $context);
  7803. if ($result !== false) return $result;
  7804. }
  7805. return false;
  7806. }
  7807. }
  7808. /**
  7809. * Decorator which enables CSS properties to be disabled for specific elements.
  7810. */
  7811. class HTMLPurifier_AttrDef_CSS_DenyElementDecorator extends HTMLPurifier_AttrDef
  7812. {
  7813. public $def, $element;
  7814. /**
  7815. * @param $def Definition to wrap
  7816. * @param $element Element to deny
  7817. */
  7818. public function __construct($def, $element) {
  7819. $this->def = $def;
  7820. $this->element = $element;
  7821. }
  7822. /**
  7823. * Checks if CurrentToken is set and equal to $this->element
  7824. */
  7825. public function validate($string, $config, $context) {
  7826. $token = $context->get('CurrentToken', true);
  7827. if ($token && $token->name == $this->element) return false;
  7828. return $this->def->validate($string, $config, $context);
  7829. }
  7830. }
  7831. /**
  7832. * Microsoft's proprietary filter: CSS property
  7833. * @note Currently supports the alpha filter. In the future, this will
  7834. * probably need an extensible framework
  7835. */
  7836. class HTMLPurifier_AttrDef_CSS_Filter extends HTMLPurifier_AttrDef
  7837. {
  7838. protected $intValidator;
  7839. public function __construct() {
  7840. $this->intValidator = new HTMLPurifier_AttrDef_Integer();
  7841. }
  7842. public function validate($value, $config, $context) {
  7843. $value = $this->parseCDATA($value);
  7844. if ($value === 'none') return $value;
  7845. // if we looped this we could support multiple filters
  7846. $function_length = strcspn($value, '(');
  7847. $function = trim(substr($value, 0, $function_length));
  7848. if ($function !== 'alpha' &&
  7849. $function !== 'Alpha' &&
  7850. $function !== 'progid:DXImageTransform.Microsoft.Alpha'
  7851. ) return false;
  7852. $cursor = $function_length + 1;
  7853. $parameters_length = strcspn($value, ')', $cursor);
  7854. $parameters = substr($value, $cursor, $parameters_length);
  7855. $params = explode(',', $parameters);
  7856. $ret_params = array();
  7857. $lookup = array();
  7858. foreach ($params as $param) {
  7859. list($key, $value) = explode('=', $param);
  7860. $key = trim($key);
  7861. $value = trim($value);
  7862. if (isset($lookup[$key])) continue;
  7863. if ($key !== 'opacity') continue;
  7864. $value = $this->intValidator->validate($value, $config, $context);
  7865. if ($value === false) continue;
  7866. $int = (int) $value;
  7867. if ($int > 100) $value = '100';
  7868. if ($int < 0) $value = '0';
  7869. $ret_params[] = "$key=$value";
  7870. $lookup[$key] = true;
  7871. }
  7872. $ret_parameters = implode(',', $ret_params);
  7873. $ret_function = "$function($ret_parameters)";
  7874. return $ret_function;
  7875. }
  7876. }
  7877. /**
  7878. * Validates shorthand CSS property font.
  7879. */
  7880. class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef
  7881. {
  7882. /**
  7883. * Local copy of component validators.
  7884. *
  7885. * @note If we moved specific CSS property definitions to their own
  7886. * classes instead of having them be assembled at run time by
  7887. * CSSDefinition, this wouldn't be necessary. We'd instantiate
  7888. * our own copies.
  7889. */
  7890. protected $info = array();
  7891. public function __construct($config) {
  7892. $def = $config->getCSSDefinition();
  7893. $this->info['font-style'] = $def->info['font-style'];
  7894. $this->info['font-variant'] = $def->info['font-variant'];
  7895. $this->info['font-weight'] = $def->info['font-weight'];
  7896. $this->info['font-size'] = $def->info['font-size'];
  7897. $this->info['line-height'] = $def->info['line-height'];
  7898. $this->info['font-family'] = $def->info['font-family'];
  7899. }
  7900. public function validate($string, $config, $context) {
  7901. static $system_fonts = array(
  7902. 'caption' => true,
  7903. 'icon' => true,
  7904. 'menu' => true,
  7905. 'message-box' => true,
  7906. 'small-caption' => true,
  7907. 'status-bar' => true
  7908. );
  7909. // regular pre-processing
  7910. $string = $this->parseCDATA($string);
  7911. if ($string === '') return false;
  7912. // check if it's one of the keywords
  7913. $lowercase_string = strtolower($string);
  7914. if (isset($system_fonts[$lowercase_string])) {
  7915. return $lowercase_string;
  7916. }
  7917. $bits = explode(' ', $string); // bits to process
  7918. $stage = 0; // this indicates what we're looking for
  7919. $caught = array(); // which stage 0 properties have we caught?
  7920. $stage_1 = array('font-style', 'font-variant', 'font-weight');
  7921. $final = ''; // output
  7922. for ($i = 0, $size = count($bits); $i < $size; $i++) {
  7923. if ($bits[$i] === '') continue;
  7924. switch ($stage) {
  7925. // attempting to catch font-style, font-variant or font-weight
  7926. case 0:
  7927. foreach ($stage_1 as $validator_name) {
  7928. if (isset($caught[$validator_name])) continue;
  7929. $r = $this->info[$validator_name]->validate(
  7930. $bits[$i], $config, $context);
  7931. if ($r !== false) {
  7932. $final .= $r . ' ';
  7933. $caught[$validator_name] = true;
  7934. break;
  7935. }
  7936. }
  7937. // all three caught, continue on
  7938. if (count($caught) >= 3) $stage = 1;
  7939. if ($r !== false) break;
  7940. // attempting to catch font-size and perhaps line-height
  7941. case 1:
  7942. $found_slash = false;
  7943. if (strpos($bits[$i], '/') !== false) {
  7944. list($font_size, $line_height) =
  7945. explode('/', $bits[$i]);
  7946. if ($line_height === '') {
  7947. // ooh, there's a space after the slash!
  7948. $line_height = false;
  7949. $found_slash = true;
  7950. }
  7951. } else {
  7952. $font_size = $bits[$i];
  7953. $line_height = false;
  7954. }
  7955. $r = $this->info['font-size']->validate(
  7956. $font_size, $config, $context);
  7957. if ($r !== false) {
  7958. $final .= $r;
  7959. // attempt to catch line-height
  7960. if ($line_height === false) {
  7961. // we need to scroll forward
  7962. for ($j = $i + 1; $j < $size; $j++) {
  7963. if ($bits[$j] === '') continue;
  7964. if ($bits[$j] === '/') {
  7965. if ($found_slash) {
  7966. return false;
  7967. } else {
  7968. $found_slash = true;
  7969. continue;
  7970. }
  7971. }
  7972. $line_height = $bits[$j];
  7973. break;
  7974. }
  7975. } else {
  7976. // slash already found
  7977. $found_slash = true;
  7978. $j = $i;
  7979. }
  7980. if ($found_slash) {
  7981. $i = $j;
  7982. $r = $this->info['line-height']->validate(
  7983. $line_height, $config, $context);
  7984. if ($r !== false) {
  7985. $final .= '/' . $r;
  7986. }
  7987. }
  7988. $final .= ' ';
  7989. $stage = 2;
  7990. break;
  7991. }
  7992. return false;
  7993. // attempting to catch font-family
  7994. case 2:
  7995. $font_family =
  7996. implode(' ', array_slice($bits, $i, $size - $i));
  7997. $r = $this->info['font-family']->validate(
  7998. $font_family, $config, $context);
  7999. if ($r !== false) {
  8000. $final .= $r . ' ';
  8001. // processing completed successfully
  8002. return rtrim($final);
  8003. }
  8004. return false;
  8005. }
  8006. }
  8007. return false;
  8008. }
  8009. }
  8010. /**
  8011. * Validates a font family list according to CSS spec
  8012. */
  8013. class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
  8014. {
  8015. protected $mask = null;
  8016. public function __construct() {
  8017. $this->mask = '_- ';
  8018. for ($c = 'a'; $c <= 'z'; $c++) $this->mask .= $c;
  8019. for ($c = 'A'; $c <= 'Z'; $c++) $this->mask .= $c;
  8020. for ($c = '0'; $c <= '9'; $c++) $this->mask .= $c; // cast-y, but should be fine
  8021. // special bytes used by UTF-8
  8022. for ($i = 0x80; $i <= 0xFF; $i++) {
  8023. // We don't bother excluding invalid bytes in this range,
  8024. // because the our restriction of well-formed UTF-8 will
  8025. // prevent these from ever occurring.
  8026. $this->mask .= chr($i);
  8027. }
  8028. /*
  8029. PHP's internal strcspn implementation is
  8030. O(length of string * length of mask), making it inefficient
  8031. for large masks. However, it's still faster than
  8032. preg_match 8)
  8033. for (p = s1;;) {
  8034. spanp = s2;
  8035. do {
  8036. if (*spanp == c || p == s1_end) {
  8037. return p - s1;
  8038. }
  8039. } while (spanp++ < (s2_end - 1));
  8040. c = *++p;
  8041. }
  8042. */
  8043. // possible optimization: invert the mask.
  8044. }
  8045. public function validate($string, $config, $context) {
  8046. static $generic_names = array(
  8047. 'serif' => true,
  8048. 'sans-serif' => true,
  8049. 'monospace' => true,
  8050. 'fantasy' => true,
  8051. 'cursive' => true
  8052. );
  8053. $allowed_fonts = $config->get('CSS.AllowedFonts');
  8054. // assume that no font names contain commas in them
  8055. $fonts = explode(',', $string);
  8056. $final = '';
  8057. foreach($fonts as $font) {
  8058. $font = trim($font);
  8059. if ($font === '') continue;
  8060. // match a generic name
  8061. if (isset($generic_names[$font])) {
  8062. if ($allowed_fonts === null || isset($allowed_fonts[$font])) {
  8063. $final .= $font . ', ';
  8064. }
  8065. continue;
  8066. }
  8067. // match a quoted name
  8068. if ($font[0] === '"' || $font[0] === "'") {
  8069. $length = strlen($font);
  8070. if ($length <= 2) continue;
  8071. $quote = $font[0];
  8072. if ($font[$length - 1] !== $quote) continue;
  8073. $font = substr($font, 1, $length - 2);
  8074. }
  8075. $font = $this->expandCSSEscape($font);
  8076. // $font is a pure representation of the font name
  8077. if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) {
  8078. continue;
  8079. }
  8080. if (ctype_alnum($font) && $font !== '') {
  8081. // very simple font, allow it in unharmed
  8082. $final .= $font . ', ';
  8083. continue;
  8084. }
  8085. // bugger out on whitespace. form feed (0C) really
  8086. // shouldn't show up regardless
  8087. $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font);
  8088. // Here, there are various classes of characters which need
  8089. // to be treated differently:
  8090. // - Alphanumeric characters are essentially safe. We
  8091. // handled these above.
  8092. // - Spaces require quoting, though most parsers will do
  8093. // the right thing if there aren't any characters that
  8094. // can be misinterpreted
  8095. // - Dashes rarely occur, but they fairly unproblematic
  8096. // for parsing/rendering purposes.
  8097. // The above characters cover the majority of Western font
  8098. // names.
  8099. // - Arbitrary Unicode characters not in ASCII. Because
  8100. // most parsers give little thought to Unicode, treatment
  8101. // of these codepoints is basically uniform, even for
  8102. // punctuation-like codepoints. These characters can
  8103. // show up in non-Western pages and are supported by most
  8104. // major browsers, for example: "MS 明朝" is a
  8105. // legitimate font-name
  8106. // <http://ja.wikipedia.org/wiki/MS_明朝>. See
  8107. // the CSS3 spec for more examples:
  8108. // <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png>
  8109. // You can see live samples of these on the Internet:
  8110. // <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック>
  8111. // However, most of these fonts have ASCII equivalents:
  8112. // for example, 'MS Mincho', and it's considered
  8113. // professional to use ASCII font names instead of
  8114. // Unicode font names. Thanks Takeshi Terada for
  8115. // providing this information.
  8116. // The following characters, to my knowledge, have not been
  8117. // used to name font names.
  8118. // - Single quote. While theoretically you might find a
  8119. // font name that has a single quote in its name (serving
  8120. // as an apostrophe, e.g. Dave's Scribble), I haven't
  8121. // been able to find any actual examples of this.
  8122. // Internet Explorer's cssText translation (which I
  8123. // believe is invoked by innerHTML) normalizes any
  8124. // quoting to single quotes, and fails to escape single
  8125. // quotes. (Note that this is not IE's behavior for all
  8126. // CSS properties, just some sort of special casing for
  8127. // font-family). So a single quote *cannot* be used
  8128. // safely in the font-family context if there will be an
  8129. // innerHTML/cssText translation. Note that Firefox 3.x
  8130. // does this too.
  8131. // - Double quote. In IE, these get normalized to
  8132. // single-quotes, no matter what the encoding. (Fun
  8133. // fact, in IE8, the 'content' CSS property gained
  8134. // support, where they special cased to preserve encoded
  8135. // double quotes, but still translate unadorned double
  8136. // quotes into single quotes.) So, because their
  8137. // fixpoint behavior is identical to single quotes, they
  8138. // cannot be allowed either. Firefox 3.x displays
  8139. // single-quote style behavior.
  8140. // - Backslashes are reduced by one (so \\ -> \) every
  8141. // iteration, so they cannot be used safely. This shows
  8142. // up in IE7, IE8 and FF3
  8143. // - Semicolons, commas and backticks are handled properly.
  8144. // - The rest of the ASCII punctuation is handled properly.
  8145. // We haven't checked what browsers do to unadorned
  8146. // versions, but this is not important as long as the
  8147. // browser doesn't /remove/ surrounding quotes (as IE does
  8148. // for HTML).
  8149. //
  8150. // With these results in hand, we conclude that there are
  8151. // various levels of safety:
  8152. // - Paranoid: alphanumeric, spaces and dashes(?)
  8153. // - International: Paranoid + non-ASCII Unicode
  8154. // - Edgy: Everything except quotes, backslashes
  8155. // - NoJS: Standards compliance, e.g. sod IE. Note that
  8156. // with some judicious character escaping (since certain
  8157. // types of escaping doesn't work) this is theoretically
  8158. // OK as long as innerHTML/cssText is not called.
  8159. // We believe that international is a reasonable default
  8160. // (that we will implement now), and once we do more
  8161. // extensive research, we may feel comfortable with dropping
  8162. // it down to edgy.
  8163. // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of
  8164. // str(c)spn assumes that the string was already well formed
  8165. // Unicode (which of course it is).
  8166. if (strspn($font, $this->mask) !== strlen($font)) {
  8167. continue;
  8168. }
  8169. // Historical:
  8170. // In the absence of innerHTML/cssText, these ugly
  8171. // transforms don't pose a security risk (as \\ and \"
  8172. // might--these escapes are not supported by most browsers).
  8173. // We could try to be clever and use single-quote wrapping
  8174. // when there is a double quote present, but I have choosen
  8175. // not to implement that. (NOTE: you can reduce the amount
  8176. // of escapes by one depending on what quoting style you use)
  8177. // $font = str_replace('\\', '\\5C ', $font);
  8178. // $font = str_replace('"', '\\22 ', $font);
  8179. // $font = str_replace("'", '\\27 ', $font);
  8180. // font possibly with spaces, requires quoting
  8181. $final .= "'$font', ";
  8182. }
  8183. $final = rtrim($final, ', ');
  8184. if ($final === '') return false;
  8185. return $final;
  8186. }
  8187. }
  8188. /**
  8189. * Validates based on {ident} CSS grammar production
  8190. */
  8191. class HTMLPurifier_AttrDef_CSS_Ident extends HTMLPurifier_AttrDef
  8192. {
  8193. public function validate($string, $config, $context) {
  8194. $string = trim($string);
  8195. // early abort: '' and '0' (strings that convert to false) are invalid
  8196. if (!$string) return false;
  8197. $pattern = '/^(-?[A-Za-z_][A-Za-z_\-0-9]*)$/';
  8198. if (!preg_match($pattern, $string)) return false;
  8199. return $string;
  8200. }
  8201. }
  8202. /**
  8203. * Decorator which enables !important to be used in CSS values.
  8204. */
  8205. class HTMLPurifier_AttrDef_CSS_ImportantDecorator extends HTMLPurifier_AttrDef
  8206. {
  8207. public $def, $allow;
  8208. /**
  8209. * @param $def Definition to wrap
  8210. * @param $allow Whether or not to allow !important
  8211. */
  8212. public function __construct($def, $allow = false) {
  8213. $this->def = $def;
  8214. $this->allow = $allow;
  8215. }
  8216. /**
  8217. * Intercepts and removes !important if necessary
  8218. */
  8219. public function validate($string, $config, $context) {
  8220. // test for ! and important tokens
  8221. $string = trim($string);
  8222. $is_important = false;
  8223. // :TODO: optimization: test directly for !important and ! important
  8224. if (strlen($string) >= 9 && substr($string, -9) === 'important') {
  8225. $temp = rtrim(substr($string, 0, -9));
  8226. // use a temp, because we might want to restore important
  8227. if (strlen($temp) >= 1 && substr($temp, -1) === '!') {
  8228. $string = rtrim(substr($temp, 0, -1));
  8229. $is_important = true;
  8230. }
  8231. }
  8232. $string = $this->def->validate($string, $config, $context);
  8233. if ($this->allow && $is_important) $string .= ' !important';
  8234. return $string;
  8235. }
  8236. }
  8237. /**
  8238. * Represents a Length as defined by CSS.
  8239. */
  8240. class HTMLPurifier_AttrDef_CSS_Length extends HTMLPurifier_AttrDef
  8241. {
  8242. protected $min, $max;
  8243. /**
  8244. * @param HTMLPurifier_Length $max Minimum length, or null for no bound. String is also acceptable.
  8245. * @param HTMLPurifier_Length $max Maximum length, or null for no bound. String is also acceptable.
  8246. */
  8247. public function __construct($min = null, $max = null) {
  8248. $this->min = $min !== null ? HTMLPurifier_Length::make($min) : null;
  8249. $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null;
  8250. }
  8251. public function validate($string, $config, $context) {
  8252. $string = $this->parseCDATA($string);
  8253. // Optimizations
  8254. if ($string === '') return false;
  8255. if ($string === '0') return '0';
  8256. if (strlen($string) === 1) return false;
  8257. $length = HTMLPurifier_Length::make($string);
  8258. if (!$length->isValid()) return false;
  8259. if ($this->min) {
  8260. $c = $length->compareTo($this->min);
  8261. if ($c === false) return false;
  8262. if ($c < 0) return false;
  8263. }
  8264. if ($this->max) {
  8265. $c = $length->compareTo($this->max);
  8266. if ($c === false) return false;
  8267. if ($c > 0) return false;
  8268. }
  8269. return $length->toString();
  8270. }
  8271. }
  8272. /**
  8273. * Validates shorthand CSS property list-style.
  8274. * @warning Does not support url tokens that have internal spaces.
  8275. */
  8276. class HTMLPurifier_AttrDef_CSS_ListStyle extends HTMLPurifier_AttrDef
  8277. {
  8278. /**
  8279. * Local copy of component validators.
  8280. * @note See HTMLPurifier_AttrDef_CSS_Font::$info for a similar impl.
  8281. */
  8282. protected $info;
  8283. public function __construct($config) {
  8284. $def = $config->getCSSDefinition();
  8285. $this->info['list-style-type'] = $def->info['list-style-type'];
  8286. $this->info['list-style-position'] = $def->info['list-style-position'];
  8287. $this->info['list-style-image'] = $def->info['list-style-image'];
  8288. }
  8289. public function validate($string, $config, $context) {
  8290. // regular pre-processing
  8291. $string = $this->parseCDATA($string);
  8292. if ($string === '') return false;
  8293. // assumes URI doesn't have spaces in it
  8294. $bits = explode(' ', strtolower($string)); // bits to process
  8295. $caught = array();
  8296. $caught['type'] = false;
  8297. $caught['position'] = false;
  8298. $caught['image'] = false;
  8299. $i = 0; // number of catches
  8300. $none = false;
  8301. foreach ($bits as $bit) {
  8302. if ($i >= 3) return; // optimization bit
  8303. if ($bit === '') continue;
  8304. foreach ($caught as $key => $status) {
  8305. if ($status !== false) continue;
  8306. $r = $this->info['list-style-' . $key]->validate($bit, $config, $context);
  8307. if ($r === false) continue;
  8308. if ($r === 'none') {
  8309. if ($none) continue;
  8310. else $none = true;
  8311. if ($key == 'image') continue;
  8312. }
  8313. $caught[$key] = $r;
  8314. $i++;
  8315. break;
  8316. }
  8317. }
  8318. if (!$i) return false;
  8319. $ret = array();
  8320. // construct type
  8321. if ($caught['type']) $ret[] = $caught['type'];
  8322. // construct image
  8323. if ($caught['image']) $ret[] = $caught['image'];
  8324. // construct position
  8325. if ($caught['position']) $ret[] = $caught['position'];
  8326. if (empty($ret)) return false;
  8327. return implode(' ', $ret);
  8328. }
  8329. }
  8330. /**
  8331. * Framework class for strings that involve multiple values.
  8332. *
  8333. * Certain CSS properties such as border-width and margin allow multiple
  8334. * lengths to be specified. This class can take a vanilla border-width
  8335. * definition and multiply it, usually into a max of four.
  8336. *
  8337. * @note Even though the CSS specification isn't clear about it, inherit
  8338. * can only be used alone: it will never manifest as part of a multi
  8339. * shorthand declaration. Thus, this class does not allow inherit.
  8340. */
  8341. class HTMLPurifier_AttrDef_CSS_Multiple extends HTMLPurifier_AttrDef
  8342. {
  8343. /**
  8344. * Instance of component definition to defer validation to.
  8345. * @todo Make protected
  8346. */
  8347. public $single;
  8348. /**
  8349. * Max number of values allowed.
  8350. * @todo Make protected
  8351. */
  8352. public $max;
  8353. /**
  8354. * @param $single HTMLPurifier_AttrDef to multiply
  8355. * @param $max Max number of values allowed (usually four)
  8356. */
  8357. public function __construct($single, $max = 4) {
  8358. $this->single = $single;
  8359. $this->max = $max;
  8360. }
  8361. public function validate($string, $config, $context) {
  8362. $string = $this->parseCDATA($string);
  8363. if ($string === '') return false;
  8364. $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n
  8365. $length = count($parts);
  8366. $final = '';
  8367. for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) {
  8368. if (ctype_space($parts[$i])) continue;
  8369. $result = $this->single->validate($parts[$i], $config, $context);
  8370. if ($result !== false) {
  8371. $final .= $result . ' ';
  8372. $num++;
  8373. }
  8374. }
  8375. if ($final === '') return false;
  8376. return rtrim($final);
  8377. }
  8378. }
  8379. /**
  8380. * Validates a Percentage as defined by the CSS spec.
  8381. */
  8382. class HTMLPurifier_AttrDef_CSS_Percentage extends HTMLPurifier_AttrDef
  8383. {
  8384. /**
  8385. * Instance of HTMLPurifier_AttrDef_CSS_Number to defer number validation
  8386. */
  8387. protected $number_def;
  8388. /**
  8389. * @param Bool indicating whether to forbid negative values
  8390. */
  8391. public function __construct($non_negative = false) {
  8392. $this->number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative);
  8393. }
  8394. public function validate($string, $config, $context) {
  8395. $string = $this->parseCDATA($string);
  8396. if ($string === '') return false;
  8397. $length = strlen($string);
  8398. if ($length === 1) return false;
  8399. if ($string[$length - 1] !== '%') return false;
  8400. $number = substr($string, 0, $length - 1);
  8401. $number = $this->number_def->validate($number, $config, $context);
  8402. if ($number === false) return false;
  8403. return "$number%";
  8404. }
  8405. }
  8406. /**
  8407. * Validates the value for the CSS property text-decoration
  8408. * @note This class could be generalized into a version that acts sort of
  8409. * like Enum except you can compound the allowed values.
  8410. */
  8411. class HTMLPurifier_AttrDef_CSS_TextDecoration extends HTMLPurifier_AttrDef
  8412. {
  8413. public function validate($string, $config, $context) {
  8414. static $allowed_values = array(
  8415. 'line-through' => true,
  8416. 'overline' => true,
  8417. 'underline' => true,
  8418. );
  8419. $string = strtolower($this->parseCDATA($string));
  8420. if ($string === 'none') return $string;
  8421. $parts = explode(' ', $string);
  8422. $final = '';
  8423. foreach ($parts as $part) {
  8424. if (isset($allowed_values[$part])) {
  8425. $final .= $part . ' ';
  8426. }
  8427. }
  8428. $final = rtrim($final);
  8429. if ($final === '') return false;
  8430. return $final;
  8431. }
  8432. }
  8433. /**
  8434. * Validates a URI in CSS syntax, which uses url('http://example.com')
  8435. * @note While theoretically speaking a URI in a CSS document could
  8436. * be non-embedded, as of CSS2 there is no such usage so we're
  8437. * generalizing it. This may need to be changed in the future.
  8438. * @warning Since HTMLPurifier_AttrDef_CSS blindly uses semicolons as
  8439. * the separator, you cannot put a literal semicolon in
  8440. * in the URI. Try percent encoding it, in that case.
  8441. */
  8442. class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI
  8443. {
  8444. public function __construct() {
  8445. parent::__construct(true); // always embedded
  8446. }
  8447. public function validate($uri_string, $config, $context) {
  8448. // parse the URI out of the string and then pass it onto
  8449. // the parent object
  8450. $uri_string = $this->parseCDATA($uri_string);
  8451. if (strpos($uri_string, 'url(') !== 0) return false;
  8452. $uri_string = substr($uri_string, 4);
  8453. $new_length = strlen($uri_string) - 1;
  8454. if ($uri_string[$new_length] != ')') return false;
  8455. $uri = trim(substr($uri_string, 0, $new_length));
  8456. if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) {
  8457. $quote = $uri[0];
  8458. $new_length = strlen($uri) - 1;
  8459. if ($uri[$new_length] !== $quote) return false;
  8460. $uri = substr($uri, 1, $new_length - 1);
  8461. }
  8462. $uri = $this->expandCSSEscape($uri);
  8463. $result = parent::validate($uri, $config, $context);
  8464. if ($result === false) return false;
  8465. // extra sanity check; should have been done by URI
  8466. $result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result);
  8467. // suspicious characters are ()'; we're going to percent encode
  8468. // them for safety.
  8469. $result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result);
  8470. // there's an extra bug where ampersands lose their escaping on
  8471. // an innerHTML cycle, so a very unlucky query parameter could
  8472. // then change the meaning of the URL. Unfortunately, there's
  8473. // not much we can do about that...
  8474. return "url(\"$result\")";
  8475. }
  8476. }
  8477. /**
  8478. * Validates a boolean attribute
  8479. */
  8480. class HTMLPurifier_AttrDef_HTML_Bool extends HTMLPurifier_AttrDef
  8481. {
  8482. protected $name;
  8483. public $minimized = true;
  8484. public function __construct($name = false) {$this->name = $name;}
  8485. public function validate($string, $config, $context) {
  8486. if (empty($string)) return false;
  8487. return $this->name;
  8488. }
  8489. /**
  8490. * @param $string Name of attribute
  8491. */
  8492. public function make($string) {
  8493. return new HTMLPurifier_AttrDef_HTML_Bool($string);
  8494. }
  8495. }
  8496. /**
  8497. * Validates contents based on NMTOKENS attribute type.
  8498. */
  8499. class HTMLPurifier_AttrDef_HTML_Nmtokens extends HTMLPurifier_AttrDef
  8500. {
  8501. public function validate($string, $config, $context) {
  8502. $string = trim($string);
  8503. // early abort: '' and '0' (strings that convert to false) are invalid
  8504. if (!$string) return false;
  8505. $tokens = $this->split($string, $config, $context);
  8506. $tokens = $this->filter($tokens, $config, $context);
  8507. if (empty($tokens)) return false;
  8508. return implode(' ', $tokens);
  8509. }
  8510. /**
  8511. * Splits a space separated list of tokens into its constituent parts.
  8512. */
  8513. protected function split($string, $config, $context) {
  8514. // OPTIMIZABLE!
  8515. // do the preg_match, capture all subpatterns for reformulation
  8516. // we don't support U+00A1 and up codepoints or
  8517. // escaping because I don't know how to do that with regexps
  8518. // and plus it would complicate optimization efforts (you never
  8519. // see that anyway).
  8520. $pattern = '/(?:(?<=\s)|\A)'. // look behind for space or string start
  8521. '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)'.
  8522. '(?:(?=\s)|\z)/'; // look ahead for space or string end
  8523. preg_match_all($pattern, $string, $matches);
  8524. return $matches[1];
  8525. }
  8526. /**
  8527. * Template method for removing certain tokens based on arbitrary criteria.
  8528. * @note If we wanted to be really functional, we'd do an array_filter
  8529. * with a callback. But... we're not.
  8530. */
  8531. protected function filter($tokens, $config, $context) {
  8532. return $tokens;
  8533. }
  8534. }
  8535. /**
  8536. * Implements special behavior for class attribute (normally NMTOKENS)
  8537. */
  8538. class HTMLPurifier_AttrDef_HTML_Class extends HTMLPurifier_AttrDef_HTML_Nmtokens
  8539. {
  8540. protected function split($string, $config, $context) {
  8541. // really, this twiddle should be lazy loaded
  8542. $name = $config->getDefinition('HTML')->doctype->name;
  8543. if ($name == "XHTML 1.1" || $name == "XHTML 2.0") {
  8544. return parent::split($string, $config, $context);
  8545. } else {
  8546. return preg_split('/\s+/', $string);
  8547. }
  8548. }
  8549. protected function filter($tokens, $config, $context) {
  8550. $allowed = $config->get('Attr.AllowedClasses');
  8551. $forbidden = $config->get('Attr.ForbiddenClasses');
  8552. $ret = array();
  8553. foreach ($tokens as $token) {
  8554. if (
  8555. ($allowed === null || isset($allowed[$token])) &&
  8556. !isset($forbidden[$token]) &&
  8557. // We need this O(n) check because of PHP's array
  8558. // implementation that casts -0 to 0.
  8559. !in_array($token, $ret, true)
  8560. ) {
  8561. $ret[] = $token;
  8562. }
  8563. }
  8564. return $ret;
  8565. }
  8566. }
  8567. /**
  8568. * Validates a color according to the HTML spec.
  8569. */
  8570. class HTMLPurifier_AttrDef_HTML_Color extends HTMLPurifier_AttrDef
  8571. {
  8572. public function validate($string, $config, $context) {
  8573. static $colors = null;
  8574. if ($colors === null) $colors = $config->get('Core.ColorKeywords');
  8575. $string = trim($string);
  8576. if (empty($string)) return false;
  8577. $lower = strtolower($string);
  8578. if (isset($colors[$lower])) return $colors[$lower];
  8579. if ($string[0] === '#') $hex = substr($string, 1);
  8580. else $hex = $string;
  8581. $length = strlen($hex);
  8582. if ($length !== 3 && $length !== 6) return false;
  8583. if (!ctype_xdigit($hex)) return false;
  8584. if ($length === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
  8585. return "#$hex";
  8586. }
  8587. }
  8588. /**
  8589. * Special-case enum attribute definition that lazy loads allowed frame targets
  8590. */
  8591. class HTMLPurifier_AttrDef_HTML_FrameTarget extends HTMLPurifier_AttrDef_Enum
  8592. {
  8593. public $valid_values = false; // uninitialized value
  8594. protected $case_sensitive = false;
  8595. public function __construct() {}
  8596. public function validate($string, $config, $context) {
  8597. if ($this->valid_values === false) $this->valid_values = $config->get('Attr.AllowedFrameTargets');
  8598. return parent::validate($string, $config, $context);
  8599. }
  8600. }
  8601. /**
  8602. * Validates the HTML attribute ID.
  8603. * @warning Even though this is the id processor, it
  8604. * will ignore the directive Attr:IDBlacklist, since it will only
  8605. * go according to the ID accumulator. Since the accumulator is
  8606. * automatically generated, it will have already absorbed the
  8607. * blacklist. If you're hacking around, make sure you use load()!
  8608. */
  8609. class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef
  8610. {
  8611. // selector is NOT a valid thing to use for IDREFs, because IDREFs
  8612. // *must* target IDs that exist, whereas selector #ids do not.
  8613. /**
  8614. * Determines whether or not we're validating an ID in a CSS
  8615. * selector context.
  8616. */
  8617. protected $selector;
  8618. public function __construct($selector = false) {
  8619. $this->selector = $selector;
  8620. }
  8621. public function validate($id, $config, $context) {
  8622. if (!$this->selector && !$config->get('Attr.EnableID')) return false;
  8623. $id = trim($id); // trim it first
  8624. if ($id === '') return false;
  8625. $prefix = $config->get('Attr.IDPrefix');
  8626. if ($prefix !== '') {
  8627. $prefix .= $config->get('Attr.IDPrefixLocal');
  8628. // prevent re-appending the prefix
  8629. if (strpos($id, $prefix) !== 0) $id = $prefix . $id;
  8630. } elseif ($config->get('Attr.IDPrefixLocal') !== '') {
  8631. trigger_error('%Attr.IDPrefixLocal cannot be used unless '.
  8632. '%Attr.IDPrefix is set', E_USER_WARNING);
  8633. }
  8634. if (!$this->selector) {
  8635. $id_accumulator =& $context->get('IDAccumulator');
  8636. if (isset($id_accumulator->ids[$id])) return false;
  8637. }
  8638. // we purposely avoid using regex, hopefully this is faster
  8639. if (ctype_alpha($id)) {
  8640. $result = true;
  8641. } else {
  8642. if (!ctype_alpha(@$id[0])) return false;
  8643. $trim = trim( // primitive style of regexps, I suppose
  8644. $id,
  8645. 'A..Za..z0..9:-._'
  8646. );
  8647. $result = ($trim === '');
  8648. }
  8649. $regexp = $config->get('Attr.IDBlacklistRegexp');
  8650. if ($regexp && preg_match($regexp, $id)) {
  8651. return false;
  8652. }
  8653. if (!$this->selector && $result) $id_accumulator->add($id);
  8654. // if no change was made to the ID, return the result
  8655. // else, return the new id if stripping whitespace made it
  8656. // valid, or return false.
  8657. return $result ? $id : false;
  8658. }
  8659. }
  8660. /**
  8661. * Validates an integer representation of pixels according to the HTML spec.
  8662. */
  8663. class HTMLPurifier_AttrDef_HTML_Pixels extends HTMLPurifier_AttrDef
  8664. {
  8665. protected $max;
  8666. public function __construct($max = null) {
  8667. $this->max = $max;
  8668. }
  8669. public function validate($string, $config, $context) {
  8670. $string = trim($string);
  8671. if ($string === '0') return $string;
  8672. if ($string === '') return false;
  8673. $length = strlen($string);
  8674. if (substr($string, $length - 2) == 'px') {
  8675. $string = substr($string, 0, $length - 2);
  8676. }
  8677. if (!is_numeric($string)) return false;
  8678. $int = (int) $string;
  8679. if ($int < 0) return '0';
  8680. // upper-bound value, extremely high values can
  8681. // crash operating systems, see <http://ha.ckers.org/imagecrash.html>
  8682. // WARNING, above link WILL crash you if you're using Windows
  8683. if ($this->max !== null && $int > $this->max) return (string) $this->max;
  8684. return (string) $int;
  8685. }
  8686. public function make($string) {
  8687. if ($string === '') $max = null;
  8688. else $max = (int) $string;
  8689. $class = get_class($this);
  8690. return new $class($max);
  8691. }
  8692. }
  8693. /**
  8694. * Validates the HTML type length (not to be confused with CSS's length).
  8695. *
  8696. * This accepts integer pixels or percentages as lengths for certain
  8697. * HTML attributes.
  8698. */
  8699. class HTMLPurifier_AttrDef_HTML_Length extends HTMLPurifier_AttrDef_HTML_Pixels
  8700. {
  8701. public function validate($string, $config, $context) {
  8702. $string = trim($string);
  8703. if ($string === '') return false;
  8704. $parent_result = parent::validate($string, $config, $context);
  8705. if ($parent_result !== false) return $parent_result;
  8706. $length = strlen($string);
  8707. $last_char = $string[$length - 1];
  8708. if ($last_char !== '%') return false;
  8709. $points = substr($string, 0, $length - 1);
  8710. if (!is_numeric($points)) return false;
  8711. $points = (int) $points;
  8712. if ($points < 0) return '0%';
  8713. if ($points > 100) return '100%';
  8714. return ((string) $points) . '%';
  8715. }
  8716. }
  8717. /**
  8718. * Validates a rel/rev link attribute against a directive of allowed values
  8719. * @note We cannot use Enum because link types allow multiple
  8720. * values.
  8721. * @note Assumes link types are ASCII text
  8722. */
  8723. class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef
  8724. {
  8725. /** Name config attribute to pull. */
  8726. protected $name;
  8727. public function __construct($name) {
  8728. $configLookup = array(
  8729. 'rel' => 'AllowedRel',
  8730. 'rev' => 'AllowedRev'
  8731. );
  8732. if (!isset($configLookup[$name])) {
  8733. trigger_error('Unrecognized attribute name for link '.
  8734. 'relationship.', E_USER_ERROR);
  8735. return;
  8736. }
  8737. $this->name = $configLookup[$name];
  8738. }
  8739. public function validate($string, $config, $context) {
  8740. $allowed = $config->get('Attr.' . $this->name);
  8741. if (empty($allowed)) return false;
  8742. $string = $this->parseCDATA($string);
  8743. $parts = explode(' ', $string);
  8744. // lookup to prevent duplicates
  8745. $ret_lookup = array();
  8746. foreach ($parts as $part) {
  8747. $part = strtolower(trim($part));
  8748. if (!isset($allowed[$part])) continue;
  8749. $ret_lookup[$part] = true;
  8750. }
  8751. if (empty($ret_lookup)) return false;
  8752. $string = implode(' ', array_keys($ret_lookup));
  8753. return $string;
  8754. }
  8755. }
  8756. /**
  8757. * Validates a MultiLength as defined by the HTML spec.
  8758. *
  8759. * A multilength is either a integer (pixel count), a percentage, or
  8760. * a relative number.
  8761. */
  8762. class HTMLPurifier_AttrDef_HTML_MultiLength extends HTMLPurifier_AttrDef_HTML_Length
  8763. {
  8764. public function validate($string, $config, $context) {
  8765. $string = trim($string);
  8766. if ($string === '') return false;
  8767. $parent_result = parent::validate($string, $config, $context);
  8768. if ($parent_result !== false) return $parent_result;
  8769. $length = strlen($string);
  8770. $last_char = $string[$length - 1];
  8771. if ($last_char !== '*') return false;
  8772. $int = substr($string, 0, $length - 1);
  8773. if ($int == '') return '*';
  8774. if (!is_numeric($int)) return false;
  8775. $int = (int) $int;
  8776. if ($int < 0) return false;
  8777. if ($int == 0) return '0';
  8778. if ($int == 1) return '*';
  8779. return ((string) $int) . '*';
  8780. }
  8781. }
  8782. abstract class HTMLPurifier_AttrDef_URI_Email extends HTMLPurifier_AttrDef
  8783. {
  8784. /**
  8785. * Unpacks a mailbox into its display-name and address
  8786. */
  8787. function unpack($string) {
  8788. // needs to be implemented
  8789. }
  8790. }
  8791. // sub-implementations
  8792. /**
  8793. * Validates a host according to the IPv4, IPv6 and DNS (future) specifications.
  8794. */
  8795. class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
  8796. {
  8797. /**
  8798. * Instance of HTMLPurifier_AttrDef_URI_IPv4 sub-validator
  8799. */
  8800. protected $ipv4;
  8801. /**
  8802. * Instance of HTMLPurifier_AttrDef_URI_IPv6 sub-validator
  8803. */
  8804. protected $ipv6;
  8805. public function __construct() {
  8806. $this->ipv4 = new HTMLPurifier_AttrDef_URI_IPv4();
  8807. $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6();
  8808. }
  8809. public function validate($string, $config, $context) {
  8810. $length = strlen($string);
  8811. // empty hostname is OK; it's usually semantically equivalent:
  8812. // the default host as defined by a URI scheme is used:
  8813. //
  8814. // If the URI scheme defines a default for host, then that
  8815. // default applies when the host subcomponent is undefined
  8816. // or when the registered name is empty (zero length).
  8817. if ($string === '') return '';
  8818. if ($length > 1 && $string[0] === '[' && $string[$length-1] === ']') {
  8819. //IPv6
  8820. $ip = substr($string, 1, $length - 2);
  8821. $valid = $this->ipv6->validate($ip, $config, $context);
  8822. if ($valid === false) return false;
  8823. return '['. $valid . ']';
  8824. }
  8825. // need to do checks on unusual encodings too
  8826. $ipv4 = $this->ipv4->validate($string, $config, $context);
  8827. if ($ipv4 !== false) return $ipv4;
  8828. // A regular domain name.
  8829. // This doesn't match I18N domain names, but we don't have proper IRI support,
  8830. // so force users to insert Punycode.
  8831. // The productions describing this are:
  8832. $a = '[a-z]'; // alpha
  8833. $an = '[a-z0-9]'; // alphanum
  8834. $and = '[a-z0-9-]'; // alphanum | "-"
  8835. // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
  8836. $domainlabel = "$an($and*$an)?";
  8837. // toplabel = alpha | alpha *( alphanum | "-" ) alphanum
  8838. $toplabel = "$a($and*$an)?";
  8839. // hostname = *( domainlabel "." ) toplabel [ "." ]
  8840. if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) {
  8841. return $string;
  8842. }
  8843. // If we have Net_IDNA2 support, we can support IRIs by
  8844. // punycoding them. (This is the most portable thing to do,
  8845. // since otherwise we have to assume browsers support
  8846. if ($config->get('Core.EnableIDNA')) {
  8847. $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true));
  8848. // we need to encode each period separately
  8849. $parts = explode('.', $string);
  8850. try {
  8851. $new_parts = array();
  8852. foreach ($parts as $part) {
  8853. $encodable = false;
  8854. for ($i = 0, $c = strlen($part); $i < $c; $i++) {
  8855. if (ord($part[$i]) > 0x7a) {
  8856. $encodable = true;
  8857. break;
  8858. }
  8859. }
  8860. if (!$encodable) {
  8861. $new_parts[] = $part;
  8862. } else {
  8863. $new_parts[] = $idna->encode($part);
  8864. }
  8865. }
  8866. $string = implode('.', $new_parts);
  8867. if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) {
  8868. return $string;
  8869. }
  8870. } catch (Exception $e) {
  8871. // XXX error reporting
  8872. }
  8873. }
  8874. return false;
  8875. }
  8876. }
  8877. /**
  8878. * Validates an IPv4 address
  8879. * @author Feyd @ forums.devnetwork.net (public domain)
  8880. */
  8881. class HTMLPurifier_AttrDef_URI_IPv4 extends HTMLPurifier_AttrDef
  8882. {
  8883. /**
  8884. * IPv4 regex, protected so that IPv6 can reuse it
  8885. */
  8886. protected $ip4;
  8887. public function validate($aIP, $config, $context) {
  8888. if (!$this->ip4) $this->_loadRegex();
  8889. if (preg_match('#^' . $this->ip4 . '$#s', $aIP))
  8890. {
  8891. return $aIP;
  8892. }
  8893. return false;
  8894. }
  8895. /**
  8896. * Lazy load function to prevent regex from being stuffed in
  8897. * cache.
  8898. */
  8899. protected function _loadRegex() {
  8900. $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255
  8901. $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})";
  8902. }
  8903. }
  8904. /**
  8905. * Validates an IPv6 address.
  8906. * @author Feyd @ forums.devnetwork.net (public domain)
  8907. * @note This function requires brackets to have been removed from address
  8908. * in URI.
  8909. */
  8910. class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4
  8911. {
  8912. public function validate($aIP, $config, $context) {
  8913. if (!$this->ip4) $this->_loadRegex();
  8914. $original = $aIP;
  8915. $hex = '[0-9a-fA-F]';
  8916. $blk = '(?:' . $hex . '{1,4})';
  8917. $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128
  8918. // prefix check
  8919. if (strpos($aIP, '/') !== false)
  8920. {
  8921. if (preg_match('#' . $pre . '$#s', $aIP, $find))
  8922. {
  8923. $aIP = substr($aIP, 0, 0-strlen($find[0]));
  8924. unset($find);
  8925. }
  8926. else
  8927. {
  8928. return false;
  8929. }
  8930. }
  8931. // IPv4-compatiblity check
  8932. if (preg_match('#(?<=:'.')' . $this->ip4 . '$#s', $aIP, $find))
  8933. {
  8934. $aIP = substr($aIP, 0, 0-strlen($find[0]));
  8935. $ip = explode('.', $find[0]);
  8936. $ip = array_map('dechex', $ip);
  8937. $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3];
  8938. unset($find, $ip);
  8939. }
  8940. // compression check
  8941. $aIP = explode('::', $aIP);
  8942. $c = count($aIP);
  8943. if ($c > 2)
  8944. {
  8945. return false;
  8946. }
  8947. elseif ($c == 2)
  8948. {
  8949. list($first, $second) = $aIP;
  8950. $first = explode(':', $first);
  8951. $second = explode(':', $second);
  8952. if (count($first) + count($second) > 8)
  8953. {
  8954. return false;
  8955. }
  8956. while(count($first) < 8)
  8957. {
  8958. array_push($first, '0');
  8959. }
  8960. array_splice($first, 8 - count($second), 8, $second);
  8961. $aIP = $first;
  8962. unset($first,$second);
  8963. }
  8964. else
  8965. {
  8966. $aIP = explode(':', $aIP[0]);
  8967. }
  8968. $c = count($aIP);
  8969. if ($c != 8)
  8970. {
  8971. return false;
  8972. }
  8973. // All the pieces should be 16-bit hex strings. Are they?
  8974. foreach ($aIP as $piece)
  8975. {
  8976. if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece)))
  8977. {
  8978. return false;
  8979. }
  8980. }
  8981. return $original;
  8982. }
  8983. }
  8984. /**
  8985. * Primitive email validation class based on the regexp found at
  8986. * http://www.regular-expressions.info/email.html
  8987. */
  8988. class HTMLPurifier_AttrDef_URI_Email_SimpleCheck extends HTMLPurifier_AttrDef_URI_Email
  8989. {
  8990. public function validate($string, $config, $context) {
  8991. // no support for named mailboxes i.e. "Bob <bob@example.com>"
  8992. // that needs more percent encoding to be done
  8993. if ($string == '') return false;
  8994. $string = trim($string);
  8995. $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string);
  8996. return $result ? $string : false;
  8997. }
  8998. }
  8999. /**
  9000. * Pre-transform that changes proprietary background attribute to CSS.
  9001. */
  9002. class HTMLPurifier_AttrTransform_Background extends HTMLPurifier_AttrTransform {
  9003. public function transform($attr, $config, $context) {
  9004. if (!isset($attr['background'])) return $attr;
  9005. $background = $this->confiscateAttr($attr, 'background');
  9006. // some validation should happen here
  9007. $this->prependCSS($attr, "background-image:url($background);");
  9008. return $attr;
  9009. }
  9010. }
  9011. // this MUST be placed in post, as it assumes that any value in dir is valid
  9012. /**
  9013. * Post-trasnform that ensures that bdo tags have the dir attribute set.
  9014. */
  9015. class HTMLPurifier_AttrTransform_BdoDir extends HTMLPurifier_AttrTransform
  9016. {
  9017. public function transform($attr, $config, $context) {
  9018. if (isset($attr['dir'])) return $attr;
  9019. $attr['dir'] = $config->get('Attr.DefaultTextDir');
  9020. return $attr;
  9021. }
  9022. }
  9023. /**
  9024. * Pre-transform that changes deprecated bgcolor attribute to CSS.
  9025. */
  9026. class HTMLPurifier_AttrTransform_BgColor extends HTMLPurifier_AttrTransform {
  9027. public function transform($attr, $config, $context) {
  9028. if (!isset($attr['bgcolor'])) return $attr;
  9029. $bgcolor = $this->confiscateAttr($attr, 'bgcolor');
  9030. // some validation should happen here
  9031. $this->prependCSS($attr, "background-color:$bgcolor;");
  9032. return $attr;
  9033. }
  9034. }
  9035. /**
  9036. * Pre-transform that changes converts a boolean attribute to fixed CSS
  9037. */
  9038. class HTMLPurifier_AttrTransform_BoolToCSS extends HTMLPurifier_AttrTransform {
  9039. /**
  9040. * Name of boolean attribute that is trigger
  9041. */
  9042. protected $attr;
  9043. /**
  9044. * CSS declarations to add to style, needs trailing semicolon
  9045. */
  9046. protected $css;
  9047. /**
  9048. * @param $attr string attribute name to convert from
  9049. * @param $css string CSS declarations to add to style (needs semicolon)
  9050. */
  9051. public function __construct($attr, $css) {
  9052. $this->attr = $attr;
  9053. $this->css = $css;
  9054. }
  9055. public function transform($attr, $config, $context) {
  9056. if (!isset($attr[$this->attr])) return $attr;
  9057. unset($attr[$this->attr]);
  9058. $this->prependCSS($attr, $this->css);
  9059. return $attr;
  9060. }
  9061. }
  9062. /**
  9063. * Pre-transform that changes deprecated border attribute to CSS.
  9064. */
  9065. class HTMLPurifier_AttrTransform_Border extends HTMLPurifier_AttrTransform {
  9066. public function transform($attr, $config, $context) {
  9067. if (!isset($attr['border'])) return $attr;
  9068. $border_width = $this->confiscateAttr($attr, 'border');
  9069. // some validation should happen here
  9070. $this->prependCSS($attr, "border:{$border_width}px solid;");
  9071. return $attr;
  9072. }
  9073. }
  9074. /**
  9075. * Generic pre-transform that converts an attribute with a fixed number of
  9076. * values (enumerated) to CSS.
  9077. */
  9078. class HTMLPurifier_AttrTransform_EnumToCSS extends HTMLPurifier_AttrTransform {
  9079. /**
  9080. * Name of attribute to transform from
  9081. */
  9082. protected $attr;
  9083. /**
  9084. * Lookup array of attribute values to CSS
  9085. */
  9086. protected $enumToCSS = array();
  9087. /**
  9088. * Case sensitivity of the matching
  9089. * @warning Currently can only be guaranteed to work with ASCII
  9090. * values.
  9091. */
  9092. protected $caseSensitive = false;
  9093. /**
  9094. * @param $attr String attribute name to transform from
  9095. * @param $enumToCSS Lookup array of attribute values to CSS
  9096. * @param $case_sensitive Boolean case sensitivity indicator, default false
  9097. */
  9098. public function __construct($attr, $enum_to_css, $case_sensitive = false) {
  9099. $this->attr = $attr;
  9100. $this->enumToCSS = $enum_to_css;
  9101. $this->caseSensitive = (bool) $case_sensitive;
  9102. }
  9103. public function transform($attr, $config, $context) {
  9104. if (!isset($attr[$this->attr])) return $attr;
  9105. $value = trim($attr[$this->attr]);
  9106. unset($attr[$this->attr]);
  9107. if (!$this->caseSensitive) $value = strtolower($value);
  9108. if (!isset($this->enumToCSS[$value])) {
  9109. return $attr;
  9110. }
  9111. $this->prependCSS($attr, $this->enumToCSS[$value]);
  9112. return $attr;
  9113. }
  9114. }
  9115. // must be called POST validation
  9116. /**
  9117. * Transform that supplies default values for the src and alt attributes
  9118. * in img tags, as well as prevents the img tag from being removed
  9119. * because of a missing alt tag. This needs to be registered as both
  9120. * a pre and post attribute transform.
  9121. */
  9122. class HTMLPurifier_AttrTransform_ImgRequired extends HTMLPurifier_AttrTransform
  9123. {
  9124. public function transform($attr, $config, $context) {
  9125. $src = true;
  9126. if (!isset($attr['src'])) {
  9127. if ($config->get('Core.RemoveInvalidImg')) return $attr;
  9128. $attr['src'] = $config->get('Attr.DefaultInvalidImage');
  9129. $src = false;
  9130. }
  9131. if (!isset($attr['alt'])) {
  9132. if ($src) {
  9133. $alt = $config->get('Attr.DefaultImageAlt');
  9134. if ($alt === null) {
  9135. // truncate if the alt is too long
  9136. $attr['alt'] = substr(basename($attr['src']),0,40);
  9137. } else {
  9138. $attr['alt'] = $alt;
  9139. }
  9140. } else {
  9141. $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt');
  9142. }
  9143. }
  9144. return $attr;
  9145. }
  9146. }
  9147. /**
  9148. * Pre-transform that changes deprecated hspace and vspace attributes to CSS
  9149. */
  9150. class HTMLPurifier_AttrTransform_ImgSpace extends HTMLPurifier_AttrTransform {
  9151. protected $attr;
  9152. protected $css = array(
  9153. 'hspace' => array('left', 'right'),
  9154. 'vspace' => array('top', 'bottom')
  9155. );
  9156. public function __construct($attr) {
  9157. $this->attr = $attr;
  9158. if (!isset($this->css[$attr])) {
  9159. trigger_error(htmlspecialchars($attr) . ' is not valid space attribute');
  9160. }
  9161. }
  9162. public function transform($attr, $config, $context) {
  9163. if (!isset($attr[$this->attr])) return $attr;
  9164. $width = $this->confiscateAttr($attr, $this->attr);
  9165. // some validation could happen here
  9166. if (!isset($this->css[$this->attr])) return $attr;
  9167. $style = '';
  9168. foreach ($this->css[$this->attr] as $suffix) {
  9169. $property = "margin-$suffix";
  9170. $style .= "$property:{$width}px;";
  9171. }
  9172. $this->prependCSS($attr, $style);
  9173. return $attr;
  9174. }
  9175. }
  9176. /**
  9177. * Performs miscellaneous cross attribute validation and filtering for
  9178. * input elements. This is meant to be a post-transform.
  9179. */
  9180. class HTMLPurifier_AttrTransform_Input extends HTMLPurifier_AttrTransform {
  9181. protected $pixels;
  9182. public function __construct() {
  9183. $this->pixels = new HTMLPurifier_AttrDef_HTML_Pixels();
  9184. }
  9185. public function transform($attr, $config, $context) {
  9186. if (!isset($attr['type'])) $t = 'text';
  9187. else $t = strtolower($attr['type']);
  9188. if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') {
  9189. unset($attr['checked']);
  9190. }
  9191. if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') {
  9192. unset($attr['maxlength']);
  9193. }
  9194. if (isset($attr['size']) && $t !== 'text' && $t !== 'password') {
  9195. $result = $this->pixels->validate($attr['size'], $config, $context);
  9196. if ($result === false) unset($attr['size']);
  9197. else $attr['size'] = $result;
  9198. }
  9199. if (isset($attr['src']) && $t !== 'image') {
  9200. unset($attr['src']);
  9201. }
  9202. if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) {
  9203. $attr['value'] = '';
  9204. }
  9205. return $attr;
  9206. }
  9207. }
  9208. /**
  9209. * Post-transform that copies lang's value to xml:lang (and vice-versa)
  9210. * @note Theoretically speaking, this could be a pre-transform, but putting
  9211. * post is more efficient.
  9212. */
  9213. class HTMLPurifier_AttrTransform_Lang extends HTMLPurifier_AttrTransform
  9214. {
  9215. public function transform($attr, $config, $context) {
  9216. $lang = isset($attr['lang']) ? $attr['lang'] : false;
  9217. $xml_lang = isset($attr['xml:lang']) ? $attr['xml:lang'] : false;
  9218. if ($lang !== false && $xml_lang === false) {
  9219. $attr['xml:lang'] = $lang;
  9220. } elseif ($xml_lang !== false) {
  9221. $attr['lang'] = $xml_lang;
  9222. }
  9223. return $attr;
  9224. }
  9225. }
  9226. /**
  9227. * Class for handling width/height length attribute transformations to CSS
  9228. */
  9229. class HTMLPurifier_AttrTransform_Length extends HTMLPurifier_AttrTransform
  9230. {
  9231. protected $name;
  9232. protected $cssName;
  9233. public function __construct($name, $css_name = null) {
  9234. $this->name = $name;
  9235. $this->cssName = $css_name ? $css_name : $name;
  9236. }
  9237. public function transform($attr, $config, $context) {
  9238. if (!isset($attr[$this->name])) return $attr;
  9239. $length = $this->confiscateAttr($attr, $this->name);
  9240. if(ctype_digit($length)) $length .= 'px';
  9241. $this->prependCSS($attr, $this->cssName . ":$length;");
  9242. return $attr;
  9243. }
  9244. }
  9245. /**
  9246. * Pre-transform that changes deprecated name attribute to ID if necessary
  9247. */
  9248. class HTMLPurifier_AttrTransform_Name extends HTMLPurifier_AttrTransform
  9249. {
  9250. public function transform($attr, $config, $context) {
  9251. // Abort early if we're using relaxed definition of name
  9252. if ($config->get('HTML.Attr.Name.UseCDATA')) return $attr;
  9253. if (!isset($attr['name'])) return $attr;
  9254. $id = $this->confiscateAttr($attr, 'name');
  9255. if ( isset($attr['id'])) return $attr;
  9256. $attr['id'] = $id;
  9257. return $attr;
  9258. }
  9259. }
  9260. /**
  9261. * Post-transform that performs validation to the name attribute; if
  9262. * it is present with an equivalent id attribute, it is passed through;
  9263. * otherwise validation is performed.
  9264. */
  9265. class HTMLPurifier_AttrTransform_NameSync extends HTMLPurifier_AttrTransform
  9266. {
  9267. public function __construct() {
  9268. $this->idDef = new HTMLPurifier_AttrDef_HTML_ID();
  9269. }
  9270. public function transform($attr, $config, $context) {
  9271. if (!isset($attr['name'])) return $attr;
  9272. $name = $attr['name'];
  9273. if (isset($attr['id']) && $attr['id'] === $name) return $attr;
  9274. $result = $this->idDef->validate($name, $config, $context);
  9275. if ($result === false) unset($attr['name']);
  9276. else $attr['name'] = $result;
  9277. return $attr;
  9278. }
  9279. }
  9280. // must be called POST validation
  9281. /**
  9282. * Adds rel="nofollow" to all outbound links. This transform is
  9283. * only attached if Attr.Nofollow is TRUE.
  9284. */
  9285. class HTMLPurifier_AttrTransform_Nofollow extends HTMLPurifier_AttrTransform
  9286. {
  9287. private $parser;
  9288. public function __construct() {
  9289. $this->parser = new HTMLPurifier_URIParser();
  9290. }
  9291. public function transform($attr, $config, $context) {
  9292. if (!isset($attr['href'])) {
  9293. return $attr;
  9294. }
  9295. // XXX Kind of inefficient
  9296. $url = $this->parser->parse($attr['href']);
  9297. $scheme = $url->getSchemeObj($config, $context);
  9298. if ($scheme->browsable && !$url->isLocal($config, $context)) {
  9299. if (isset($attr['rel'])) {
  9300. $rels = explode(' ', $attr['rel']);
  9301. if (!in_array('nofollow', $rels)) {
  9302. $rels[] = 'nofollow';
  9303. }
  9304. $attr['rel'] = implode(' ', $rels);
  9305. } else {
  9306. $attr['rel'] = 'nofollow';
  9307. }
  9308. }
  9309. return $attr;
  9310. }
  9311. }
  9312. class HTMLPurifier_AttrTransform_SafeEmbed extends HTMLPurifier_AttrTransform
  9313. {
  9314. public $name = "SafeEmbed";
  9315. public function transform($attr, $config, $context) {
  9316. $attr['allowscriptaccess'] = 'never';
  9317. $attr['allownetworking'] = 'internal';
  9318. $attr['type'] = 'application/x-shockwave-flash';
  9319. return $attr;
  9320. }
  9321. }
  9322. /**
  9323. * Writes default type for all objects. Currently only supports flash.
  9324. */
  9325. class HTMLPurifier_AttrTransform_SafeObject extends HTMLPurifier_AttrTransform
  9326. {
  9327. public $name = "SafeObject";
  9328. function transform($attr, $config, $context) {
  9329. if (!isset($attr['type'])) $attr['type'] = 'application/x-shockwave-flash';
  9330. return $attr;
  9331. }
  9332. }
  9333. /**
  9334. * Validates name/value pairs in param tags to be used in safe objects. This
  9335. * will only allow name values it recognizes, and pre-fill certain attributes
  9336. * with required values.
  9337. *
  9338. * @note
  9339. * This class only supports Flash. In the future, Quicktime support
  9340. * may be added.
  9341. *
  9342. * @warning
  9343. * This class expects an injector to add the necessary parameters tags.
  9344. */
  9345. class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform
  9346. {
  9347. public $name = "SafeParam";
  9348. private $uri;
  9349. public function __construct() {
  9350. $this->uri = new HTMLPurifier_AttrDef_URI(true); // embedded
  9351. $this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent'));
  9352. }
  9353. public function transform($attr, $config, $context) {
  9354. // If we add support for other objects, we'll need to alter the
  9355. // transforms.
  9356. switch ($attr['name']) {
  9357. // application/x-shockwave-flash
  9358. // Keep this synchronized with Injector/SafeObject.php
  9359. case 'allowScriptAccess':
  9360. $attr['value'] = 'never';
  9361. break;
  9362. case 'allowNetworking':
  9363. $attr['value'] = 'internal';
  9364. break;
  9365. case 'allowFullScreen':
  9366. if ($config->get('HTML.FlashAllowFullScreen')) {
  9367. $attr['value'] = ($attr['value'] == 'true') ? 'true' : 'false';
  9368. } else {
  9369. $attr['value'] = 'false';
  9370. }
  9371. break;
  9372. case 'wmode':
  9373. $attr['value'] = $this->wmode->validate($attr['value'], $config, $context);
  9374. break;
  9375. case 'movie':
  9376. case 'src':
  9377. $attr['name'] = "movie";
  9378. $attr['value'] = $this->uri->validate($attr['value'], $config, $context);
  9379. break;
  9380. case 'flashvars':
  9381. // we're going to allow arbitrary inputs to the SWF, on
  9382. // the reasoning that it could only hack the SWF, not us.
  9383. break;
  9384. // add other cases to support other param name/value pairs
  9385. default:
  9386. $attr['name'] = $attr['value'] = null;
  9387. }
  9388. return $attr;
  9389. }
  9390. }
  9391. /**
  9392. * Implements required attribute stipulation for <script>
  9393. */
  9394. class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform
  9395. {
  9396. public function transform($attr, $config, $context) {
  9397. if (!isset($attr['type'])) {
  9398. $attr['type'] = 'text/javascript';
  9399. }
  9400. return $attr;
  9401. }
  9402. }
  9403. // must be called POST validation
  9404. /**
  9405. * Adds target="blank" to all outbound links. This transform is
  9406. * only attached if Attr.TargetBlank is TRUE. This works regardless
  9407. * of whether or not Attr.AllowedFrameTargets
  9408. */
  9409. class HTMLPurifier_AttrTransform_TargetBlank extends HTMLPurifier_AttrTransform
  9410. {
  9411. private $parser;
  9412. public function __construct() {
  9413. $this->parser = new HTMLPurifier_URIParser();
  9414. }
  9415. public function transform($attr, $config, $context) {
  9416. if (!isset($attr['href'])) {
  9417. return $attr;
  9418. }
  9419. // XXX Kind of inefficient
  9420. $url = $this->parser->parse($attr['href']);
  9421. $scheme = $url->getSchemeObj($config, $context);
  9422. if ($scheme->browsable && !$url->isBenign($config, $context)) {
  9423. $attr['target'] = '_blank';
  9424. }
  9425. return $attr;
  9426. }
  9427. }
  9428. /**
  9429. * Sets height/width defaults for <textarea>
  9430. */
  9431. class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform
  9432. {
  9433. public function transform($attr, $config, $context) {
  9434. // Calculated from Firefox
  9435. if (!isset($attr['cols'])) $attr['cols'] = '22';
  9436. if (!isset($attr['rows'])) $attr['rows'] = '3';
  9437. return $attr;
  9438. }
  9439. }
  9440. /**
  9441. * Definition that uses different definitions depending on context.
  9442. *
  9443. * The del and ins tags are notable because they allow different types of
  9444. * elements depending on whether or not they're in a block or inline context.
  9445. * Chameleon allows this behavior to happen by using two different
  9446. * definitions depending on context. While this somewhat generalized,
  9447. * it is specifically intended for those two tags.
  9448. */
  9449. class HTMLPurifier_ChildDef_Chameleon extends HTMLPurifier_ChildDef
  9450. {
  9451. /**
  9452. * Instance of the definition object to use when inline. Usually stricter.
  9453. */
  9454. public $inline;
  9455. /**
  9456. * Instance of the definition object to use when block.
  9457. */
  9458. public $block;
  9459. public $type = 'chameleon';
  9460. /**
  9461. * @param $inline List of elements to allow when inline.
  9462. * @param $block List of elements to allow when block.
  9463. */
  9464. public function __construct($inline, $block) {
  9465. $this->inline = new HTMLPurifier_ChildDef_Optional($inline);
  9466. $this->block = new HTMLPurifier_ChildDef_Optional($block);
  9467. $this->elements = $this->block->elements;
  9468. }
  9469. public function validateChildren($tokens_of_children, $config, $context) {
  9470. if ($context->get('IsInline') === false) {
  9471. return $this->block->validateChildren(
  9472. $tokens_of_children, $config, $context);
  9473. } else {
  9474. return $this->inline->validateChildren(
  9475. $tokens_of_children, $config, $context);
  9476. }
  9477. }
  9478. }
  9479. /**
  9480. * Custom validation class, accepts DTD child definitions
  9481. *
  9482. * @warning Currently this class is an all or nothing proposition, that is,
  9483. * it will only give a bool return value.
  9484. */
  9485. class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef
  9486. {
  9487. public $type = 'custom';
  9488. public $allow_empty = false;
  9489. /**
  9490. * Allowed child pattern as defined by the DTD
  9491. */
  9492. public $dtd_regex;
  9493. /**
  9494. * PCRE regex derived from $dtd_regex
  9495. * @private
  9496. */
  9497. private $_pcre_regex;
  9498. /**
  9499. * @param $dtd_regex Allowed child pattern from the DTD
  9500. */
  9501. public function __construct($dtd_regex) {
  9502. $this->dtd_regex = $dtd_regex;
  9503. $this->_compileRegex();
  9504. }
  9505. /**
  9506. * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex)
  9507. */
  9508. protected function _compileRegex() {
  9509. $raw = str_replace(' ', '', $this->dtd_regex);
  9510. if ($raw{0} != '(') {
  9511. $raw = "($raw)";
  9512. }
  9513. $el = '[#a-zA-Z0-9_.-]+';
  9514. $reg = $raw;
  9515. // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M
  9516. // DOING! Seriously: if there's problems, please report them.
  9517. // collect all elements into the $elements array
  9518. preg_match_all("/$el/", $reg, $matches);
  9519. foreach ($matches[0] as $match) {
  9520. $this->elements[$match] = true;
  9521. }
  9522. // setup all elements as parentheticals with leading commas
  9523. $reg = preg_replace("/$el/", '(,\\0)', $reg);
  9524. // remove commas when they were not solicited
  9525. $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg);
  9526. // remove all non-paranthetical commas: they are handled by first regex
  9527. $reg = preg_replace("/,\(/", '(', $reg);
  9528. $this->_pcre_regex = $reg;
  9529. }
  9530. public function validateChildren($tokens_of_children, $config, $context) {
  9531. $list_of_children = '';
  9532. $nesting = 0; // depth into the nest
  9533. foreach ($tokens_of_children as $token) {
  9534. if (!empty($token->is_whitespace)) continue;
  9535. $is_child = ($nesting == 0); // direct
  9536. if ($token instanceof HTMLPurifier_Token_Start) {
  9537. $nesting++;
  9538. } elseif ($token instanceof HTMLPurifier_Token_End) {
  9539. $nesting--;
  9540. }
  9541. if ($is_child) {
  9542. $list_of_children .= $token->name . ',';
  9543. }
  9544. }
  9545. // add leading comma to deal with stray comma declarations
  9546. $list_of_children = ',' . rtrim($list_of_children, ',');
  9547. $okay =
  9548. preg_match(
  9549. '/^,?'.$this->_pcre_regex.'$/',
  9550. $list_of_children
  9551. );
  9552. return (bool) $okay;
  9553. }
  9554. }
  9555. /**
  9556. * Definition that disallows all elements.
  9557. * @warning validateChildren() in this class is actually never called, because
  9558. * empty elements are corrected in HTMLPurifier_Strategy_MakeWellFormed
  9559. * before child definitions are parsed in earnest by
  9560. * HTMLPurifier_Strategy_FixNesting.
  9561. */
  9562. class HTMLPurifier_ChildDef_Empty extends HTMLPurifier_ChildDef
  9563. {
  9564. public $allow_empty = true;
  9565. public $type = 'empty';
  9566. public function __construct() {}
  9567. public function validateChildren($tokens_of_children, $config, $context) {
  9568. return array();
  9569. }
  9570. }
  9571. /**
  9572. * Definition for list containers ul and ol.
  9573. */
  9574. class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef
  9575. {
  9576. public $type = 'list';
  9577. // lying a little bit, so that we can handle ul and ol ourselves
  9578. // XXX: This whole business with 'wrap' is all a bit unsatisfactory
  9579. public $elements = array('li' => true, 'ul' => true, 'ol' => true);
  9580. public function validateChildren($tokens_of_children, $config, $context) {
  9581. // Flag for subclasses
  9582. $this->whitespace = false;
  9583. // if there are no tokens, delete parent node
  9584. if (empty($tokens_of_children)) return false;
  9585. // the new set of children
  9586. $result = array();
  9587. // current depth into the nest
  9588. $nesting = 0;
  9589. // a little sanity check to make sure it's not ALL whitespace
  9590. $all_whitespace = true;
  9591. $seen_li = false;
  9592. $need_close_li = false;
  9593. foreach ($tokens_of_children as $token) {
  9594. if (!empty($token->is_whitespace)) {
  9595. $result[] = $token;
  9596. continue;
  9597. }
  9598. $all_whitespace = false; // phew, we're not talking about whitespace
  9599. if ($nesting == 1 && $need_close_li) {
  9600. $result[] = new HTMLPurifier_Token_End('li');
  9601. $nesting--;
  9602. $need_close_li = false;
  9603. }
  9604. $is_child = ($nesting == 0);
  9605. if ($token instanceof HTMLPurifier_Token_Start) {
  9606. $nesting++;
  9607. } elseif ($token instanceof HTMLPurifier_Token_End) {
  9608. $nesting--;
  9609. }
  9610. if ($is_child) {
  9611. if ($token->name === 'li') {
  9612. // good
  9613. $seen_li = true;
  9614. } elseif ($token->name === 'ul' || $token->name === 'ol') {
  9615. // we want to tuck this into the previous li
  9616. $need_close_li = true;
  9617. $nesting++;
  9618. if (!$seen_li) {
  9619. // create a new li element
  9620. $result[] = new HTMLPurifier_Token_Start('li');
  9621. } else {
  9622. // backtrack until </li> found
  9623. while(true) {
  9624. $t = array_pop($result);
  9625. if ($t instanceof HTMLPurifier_Token_End) {
  9626. // XXX actually, these invariants could very plausibly be violated
  9627. // if we are doing silly things with modifying the set of allowed elements.
  9628. // FORTUNATELY, it doesn't make a difference, since the allowed
  9629. // elements are hard-coded here!
  9630. if ($t->name !== 'li') {
  9631. trigger_error("Only li present invariant violated in List ChildDef", E_USER_ERROR);
  9632. return false;
  9633. }
  9634. break;
  9635. } elseif ($t instanceof HTMLPurifier_Token_Empty) { // bleagh
  9636. if ($t->name !== 'li') {
  9637. trigger_error("Only li present invariant violated in List ChildDef", E_USER_ERROR);
  9638. return false;
  9639. }
  9640. // XXX this should have a helper for it...
  9641. $result[] = new HTMLPurifier_Token_Start('li', $t->attr, $t->line, $t->col, $t->armor);
  9642. break;
  9643. } else {
  9644. if (!$t->is_whitespace) {
  9645. trigger_error("Only whitespace present invariant violated in List ChildDef", E_USER_ERROR);
  9646. return false;
  9647. }
  9648. }
  9649. }
  9650. }
  9651. } else {
  9652. // start wrapping (this doesn't precisely mimic
  9653. // browser behavior, but what browsers do is kind of
  9654. // hard to mimic in a standards compliant way
  9655. // XXX Actually, this has no impact in practice,
  9656. // because this gets handled earlier. Arguably,
  9657. // we should rip out all of that processing
  9658. $result[] = new HTMLPurifier_Token_Start('li');
  9659. $nesting++;
  9660. $seen_li = true;
  9661. $need_close_li = true;
  9662. }
  9663. }
  9664. $result[] = $token;
  9665. }
  9666. if ($need_close_li) {
  9667. $result[] = new HTMLPurifier_Token_End('li');
  9668. }
  9669. if (empty($result)) return false;
  9670. if ($all_whitespace) {
  9671. return false;
  9672. }
  9673. if ($tokens_of_children == $result) return true;
  9674. return $result;
  9675. }
  9676. }
  9677. /**
  9678. * Definition that allows a set of elements, but disallows empty children.
  9679. */
  9680. class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef
  9681. {
  9682. /**
  9683. * Lookup table of allowed elements.
  9684. * @public
  9685. */
  9686. public $elements = array();
  9687. /**
  9688. * Whether or not the last passed node was all whitespace.
  9689. */
  9690. protected $whitespace = false;
  9691. /**
  9692. * @param $elements List of allowed element names (lowercase).
  9693. */
  9694. public function __construct($elements) {
  9695. if (is_string($elements)) {
  9696. $elements = str_replace(' ', '', $elements);
  9697. $elements = explode('|', $elements);
  9698. }
  9699. $keys = array_keys($elements);
  9700. if ($keys == array_keys($keys)) {
  9701. $elements = array_flip($elements);
  9702. foreach ($elements as $i => $x) {
  9703. $elements[$i] = true;
  9704. if (empty($i)) unset($elements[$i]); // remove blank
  9705. }
  9706. }
  9707. $this->elements = $elements;
  9708. }
  9709. public $allow_empty = false;
  9710. public $type = 'required';
  9711. public function validateChildren($tokens_of_children, $config, $context) {
  9712. // Flag for subclasses
  9713. $this->whitespace = false;
  9714. // if there are no tokens, delete parent node
  9715. if (empty($tokens_of_children)) return false;
  9716. // the new set of children
  9717. $result = array();
  9718. // current depth into the nest
  9719. $nesting = 0;
  9720. // whether or not we're deleting a node
  9721. $is_deleting = false;
  9722. // whether or not parsed character data is allowed
  9723. // this controls whether or not we silently drop a tag
  9724. // or generate escaped HTML from it
  9725. $pcdata_allowed = isset($this->elements['#PCDATA']);
  9726. // a little sanity check to make sure it's not ALL whitespace
  9727. $all_whitespace = true;
  9728. // some configuration
  9729. $escape_invalid_children = $config->get('Core.EscapeInvalidChildren');
  9730. // generator
  9731. $gen = new HTMLPurifier_Generator($config, $context);
  9732. foreach ($tokens_of_children as $token) {
  9733. if (!empty($token->is_whitespace)) {
  9734. $result[] = $token;
  9735. continue;
  9736. }
  9737. $all_whitespace = false; // phew, we're not talking about whitespace
  9738. $is_child = ($nesting == 0);
  9739. if ($token instanceof HTMLPurifier_Token_Start) {
  9740. $nesting++;
  9741. } elseif ($token instanceof HTMLPurifier_Token_End) {
  9742. $nesting--;
  9743. }
  9744. if ($is_child) {
  9745. $is_deleting = false;
  9746. if (!isset($this->elements[$token->name])) {
  9747. $is_deleting = true;
  9748. if ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text) {
  9749. $result[] = $token;
  9750. } elseif ($pcdata_allowed && $escape_invalid_children) {
  9751. $result[] = new HTMLPurifier_Token_Text(
  9752. $gen->generateFromToken($token)
  9753. );
  9754. }
  9755. continue;
  9756. }
  9757. }
  9758. if (!$is_deleting || ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text)) {
  9759. $result[] = $token;
  9760. } elseif ($pcdata_allowed && $escape_invalid_children) {
  9761. $result[] =
  9762. new HTMLPurifier_Token_Text(
  9763. $gen->generateFromToken($token)
  9764. );
  9765. } else {
  9766. // drop silently
  9767. }
  9768. }
  9769. if (empty($result)) return false;
  9770. if ($all_whitespace) {
  9771. $this->whitespace = true;
  9772. return false;
  9773. }
  9774. if ($tokens_of_children == $result) return true;
  9775. return $result;
  9776. }
  9777. }
  9778. /**
  9779. * Definition that allows a set of elements, and allows no children.
  9780. * @note This is a hack to reuse code from HTMLPurifier_ChildDef_Required,
  9781. * really, one shouldn't inherit from the other. Only altered behavior
  9782. * is to overload a returned false with an array. Thus, it will never
  9783. * return false.
  9784. */
  9785. class HTMLPurifier_ChildDef_Optional extends HTMLPurifier_ChildDef_Required
  9786. {
  9787. public $allow_empty = true;
  9788. public $type = 'optional';
  9789. public function validateChildren($tokens_of_children, $config, $context) {
  9790. $result = parent::validateChildren($tokens_of_children, $config, $context);
  9791. // we assume that $tokens_of_children is not modified
  9792. if ($result === false) {
  9793. if (empty($tokens_of_children)) return true;
  9794. elseif ($this->whitespace) return $tokens_of_children;
  9795. else return array();
  9796. }
  9797. return $result;
  9798. }
  9799. }
  9800. /**
  9801. * Takes the contents of blockquote when in strict and reformats for validation.
  9802. */
  9803. class HTMLPurifier_ChildDef_StrictBlockquote extends HTMLPurifier_ChildDef_Required
  9804. {
  9805. protected $real_elements;
  9806. protected $fake_elements;
  9807. public $allow_empty = true;
  9808. public $type = 'strictblockquote';
  9809. protected $init = false;
  9810. /**
  9811. * @note We don't want MakeWellFormed to auto-close inline elements since
  9812. * they might be allowed.
  9813. */
  9814. public function getAllowedElements($config) {
  9815. $this->init($config);
  9816. return $this->fake_elements;
  9817. }
  9818. public function validateChildren($tokens_of_children, $config, $context) {
  9819. $this->init($config);
  9820. // trick the parent class into thinking it allows more
  9821. $this->elements = $this->fake_elements;
  9822. $result = parent::validateChildren($tokens_of_children, $config, $context);
  9823. $this->elements = $this->real_elements;
  9824. if ($result === false) return array();
  9825. if ($result === true) $result = $tokens_of_children;
  9826. $def = $config->getHTMLDefinition();
  9827. $block_wrap_start = new HTMLPurifier_Token_Start($def->info_block_wrapper);
  9828. $block_wrap_end = new HTMLPurifier_Token_End( $def->info_block_wrapper);
  9829. $is_inline = false;
  9830. $depth = 0;
  9831. $ret = array();
  9832. // assuming that there are no comment tokens
  9833. foreach ($result as $i => $token) {
  9834. $token = $result[$i];
  9835. // ifs are nested for readability
  9836. if (!$is_inline) {
  9837. if (!$depth) {
  9838. if (
  9839. ($token instanceof HTMLPurifier_Token_Text && !$token->is_whitespace) ||
  9840. (!$token instanceof HTMLPurifier_Token_Text && !isset($this->elements[$token->name]))
  9841. ) {
  9842. $is_inline = true;
  9843. $ret[] = $block_wrap_start;
  9844. }
  9845. }
  9846. } else {
  9847. if (!$depth) {
  9848. // starting tokens have been inline text / empty
  9849. if ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) {
  9850. if (isset($this->elements[$token->name])) {
  9851. // ended
  9852. $ret[] = $block_wrap_end;
  9853. $is_inline = false;
  9854. }
  9855. }
  9856. }
  9857. }
  9858. $ret[] = $token;
  9859. if ($token instanceof HTMLPurifier_Token_Start) $depth++;
  9860. if ($token instanceof HTMLPurifier_Token_End) $depth--;
  9861. }
  9862. if ($is_inline) $ret[] = $block_wrap_end;
  9863. return $ret;
  9864. }
  9865. private function init($config) {
  9866. if (!$this->init) {
  9867. $def = $config->getHTMLDefinition();
  9868. // allow all inline elements
  9869. $this->real_elements = $this->elements;
  9870. $this->fake_elements = $def->info_content_sets['Flow'];
  9871. $this->fake_elements['#PCDATA'] = true;
  9872. $this->init = true;
  9873. }
  9874. }
  9875. }
  9876. /**
  9877. * Definition for tables. The general idea is to extract out all of the
  9878. * essential bits, and then reconstruct it later.
  9879. *
  9880. * This is a bit confusing, because the DTDs and the W3C
  9881. * validators seem to disagree on the appropriate definition. The
  9882. * DTD claims:
  9883. *
  9884. * (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+)
  9885. *
  9886. * But actually, the HTML4 spec then has this to say:
  9887. *
  9888. * The TBODY start tag is always required except when the table
  9889. * contains only one table body and no table head or foot sections.
  9890. * The TBODY end tag may always be safely omitted.
  9891. *
  9892. * So the DTD is kind of wrong. The validator is, unfortunately, kind
  9893. * of on crack.
  9894. *
  9895. * The definition changed again in XHTML1.1; and in my opinion, this
  9896. * formulation makes the most sense.
  9897. *
  9898. * caption?, ( col* | colgroup* ), (( thead?, tfoot?, tbody+ ) | ( tr+ ))
  9899. *
  9900. * Essentially, we have two modes: thead/tfoot/tbody mode, and tr mode.
  9901. * If we encounter a thead, tfoot or tbody, we are placed in the former
  9902. * mode, and we *must* wrap any stray tr segments with a tbody. But if
  9903. * we don't run into any of them, just have tr tags is OK.
  9904. */
  9905. class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
  9906. {
  9907. public $allow_empty = false;
  9908. public $type = 'table';
  9909. public $elements = array('tr' => true, 'tbody' => true, 'thead' => true,
  9910. 'tfoot' => true, 'caption' => true, 'colgroup' => true, 'col' => true);
  9911. public function __construct() {}
  9912. public function validateChildren($tokens_of_children, $config, $context) {
  9913. if (empty($tokens_of_children)) return false;
  9914. // this ensures that the loop gets run one last time before closing
  9915. // up. It's a little bit of a hack, but it works! Just make sure you
  9916. // get rid of the token later.
  9917. $tokens_of_children[] = false;
  9918. // only one of these elements is allowed in a table
  9919. $caption = false;
  9920. $thead = false;
  9921. $tfoot = false;
  9922. // as many of these as you want
  9923. $cols = array();
  9924. $content = array();
  9925. $nesting = 0; // current depth so we can determine nodes
  9926. $is_collecting = false; // are we globbing together tokens to package
  9927. // into one of the collectors?
  9928. $collection = array(); // collected nodes
  9929. $tag_index = 0; // the first node might be whitespace,
  9930. // so this tells us where the start tag is
  9931. $tbody_mode = false; // if true, then we need to wrap any stray
  9932. // <tr>s with a <tbody>.
  9933. foreach ($tokens_of_children as $token) {
  9934. $is_child = ($nesting == 0);
  9935. if ($token === false) {
  9936. // terminating sequence started
  9937. } elseif ($token instanceof HTMLPurifier_Token_Start) {
  9938. $nesting++;
  9939. } elseif ($token instanceof HTMLPurifier_Token_End) {
  9940. $nesting--;
  9941. }
  9942. // handle node collection
  9943. if ($is_collecting) {
  9944. if ($is_child) {
  9945. // okay, let's stash the tokens away
  9946. // first token tells us the type of the collection
  9947. switch ($collection[$tag_index]->name) {
  9948. case 'tbody':
  9949. $tbody_mode = true;
  9950. case 'tr':
  9951. $content[] = $collection;
  9952. break;
  9953. case 'caption':
  9954. if ($caption !== false) break;
  9955. $caption = $collection;
  9956. break;
  9957. case 'thead':
  9958. case 'tfoot':
  9959. $tbody_mode = true;
  9960. // XXX This breaks rendering properties with
  9961. // Firefox, which never floats a <thead> to
  9962. // the top. Ever. (Our scheme will float the
  9963. // first <thead> to the top.) So maybe
  9964. // <thead>s that are not first should be
  9965. // turned into <tbody>? Very tricky, indeed.
  9966. // access the appropriate variable, $thead or $tfoot
  9967. $var = $collection[$tag_index]->name;
  9968. if ($$var === false) {
  9969. $$var = $collection;
  9970. } else {
  9971. // Oops, there's a second one! What
  9972. // should we do? Current behavior is to
  9973. // transmutate the first and last entries into
  9974. // tbody tags, and then put into content.
  9975. // Maybe a better idea is to *attach
  9976. // it* to the existing thead or tfoot?
  9977. // We don't do this, because Firefox
  9978. // doesn't float an extra tfoot to the
  9979. // bottom like it does for the first one.
  9980. $collection[$tag_index]->name = 'tbody';
  9981. $collection[count($collection)-1]->name = 'tbody';
  9982. $content[] = $collection;
  9983. }
  9984. break;
  9985. case 'colgroup':
  9986. $cols[] = $collection;
  9987. break;
  9988. }
  9989. $collection = array();
  9990. $is_collecting = false;
  9991. $tag_index = 0;
  9992. } else {
  9993. // add the node to the collection
  9994. $collection[] = $token;
  9995. }
  9996. }
  9997. // terminate
  9998. if ($token === false) break;
  9999. if ($is_child) {
  10000. // determine what we're dealing with
  10001. if ($token->name == 'col') {
  10002. // the only empty tag in the possie, we can handle it
  10003. // immediately
  10004. $cols[] = array_merge($collection, array($token));
  10005. $collection = array();
  10006. $tag_index = 0;
  10007. continue;
  10008. }
  10009. switch($token->name) {
  10010. case 'caption':
  10011. case 'colgroup':
  10012. case 'thead':
  10013. case 'tfoot':
  10014. case 'tbody':
  10015. case 'tr':
  10016. $is_collecting = true;
  10017. $collection[] = $token;
  10018. continue;
  10019. default:
  10020. if (!empty($token->is_whitespace)) {
  10021. $collection[] = $token;
  10022. $tag_index++;
  10023. }
  10024. continue;
  10025. }
  10026. }
  10027. }
  10028. if (empty($content)) return false;
  10029. $ret = array();
  10030. if ($caption !== false) $ret = array_merge($ret, $caption);
  10031. if ($cols !== false) foreach ($cols as $token_array) $ret = array_merge($ret, $token_array);
  10032. if ($thead !== false) $ret = array_merge($ret, $thead);
  10033. if ($tfoot !== false) $ret = array_merge($ret, $tfoot);
  10034. if ($tbody_mode) {
  10035. // a little tricky, since the start of the collection may be
  10036. // whitespace
  10037. $inside_tbody = false;
  10038. foreach ($content as $token_array) {
  10039. // find the starting token
  10040. foreach ($token_array as $t) {
  10041. if ($t->name === 'tr' || $t->name === 'tbody') {
  10042. break;
  10043. }
  10044. } // iterator variable carries over
  10045. if ($t->name === 'tr') {
  10046. if ($inside_tbody) {
  10047. $ret = array_merge($ret, $token_array);
  10048. } else {
  10049. $ret[] = new HTMLPurifier_Token_Start('tbody');
  10050. $ret = array_merge($ret, $token_array);
  10051. $inside_tbody = true;
  10052. }
  10053. } elseif ($t->name === 'tbody') {
  10054. if ($inside_tbody) {
  10055. $ret[] = new HTMLPurifier_Token_End('tbody');
  10056. $inside_tbody = false;
  10057. $ret = array_merge($ret, $token_array);
  10058. } else {
  10059. $ret = array_merge($ret, $token_array);
  10060. }
  10061. } else {
  10062. trigger_error("tr/tbody in content invariant failed in Table ChildDef", E_USER_ERROR);
  10063. }
  10064. }
  10065. if ($inside_tbody) {
  10066. $ret[] = new HTMLPurifier_Token_End('tbody');
  10067. }
  10068. } else {
  10069. foreach ($content as $token_array) {
  10070. // invariant: everything in here is <tr>s
  10071. $ret = array_merge($ret, $token_array);
  10072. }
  10073. }
  10074. if (!empty($collection) && $is_collecting == false){
  10075. // grab the trailing space
  10076. $ret = array_merge($ret, $collection);
  10077. }
  10078. array_pop($tokens_of_children); // remove phantom token
  10079. return ($ret === $tokens_of_children) ? true : $ret;
  10080. }
  10081. }
  10082. class HTMLPurifier_DefinitionCache_Decorator extends HTMLPurifier_DefinitionCache
  10083. {
  10084. /**
  10085. * Cache object we are decorating
  10086. */
  10087. public $cache;
  10088. public function __construct() {}
  10089. /**
  10090. * Lazy decorator function
  10091. * @param $cache Reference to cache object to decorate
  10092. */
  10093. public function decorate(&$cache) {
  10094. $decorator = $this->copy();
  10095. // reference is necessary for mocks in PHP 4
  10096. $decorator->cache =& $cache;
  10097. $decorator->type = $cache->type;
  10098. return $decorator;
  10099. }
  10100. /**
  10101. * Cross-compatible clone substitute
  10102. */
  10103. public function copy() {
  10104. return new HTMLPurifier_DefinitionCache_Decorator();
  10105. }
  10106. public function add($def, $config) {
  10107. return $this->cache->add($def, $config);
  10108. }
  10109. public function set($def, $config) {
  10110. return $this->cache->set($def, $config);
  10111. }
  10112. public function replace($def, $config) {
  10113. return $this->cache->replace($def, $config);
  10114. }
  10115. public function get($config) {
  10116. return $this->cache->get($config);
  10117. }
  10118. public function remove($config) {
  10119. return $this->cache->remove($config);
  10120. }
  10121. public function flush($config) {
  10122. return $this->cache->flush($config);
  10123. }
  10124. public function cleanup($config) {
  10125. return $this->cache->cleanup($config);
  10126. }
  10127. }
  10128. /**
  10129. * Null cache object to use when no caching is on.
  10130. */
  10131. class HTMLPurifier_DefinitionCache_Null extends HTMLPurifier_DefinitionCache
  10132. {
  10133. public function add($def, $config) {
  10134. return false;
  10135. }
  10136. public function set($def, $config) {
  10137. return false;
  10138. }
  10139. public function replace($def, $config) {
  10140. return false;
  10141. }
  10142. public function remove($config) {
  10143. return false;
  10144. }
  10145. public function get($config) {
  10146. return false;
  10147. }
  10148. public function flush($config) {
  10149. return false;
  10150. }
  10151. public function cleanup($config) {
  10152. return false;
  10153. }
  10154. }
  10155. class HTMLPurifier_DefinitionCache_Serializer extends
  10156. HTMLPurifier_DefinitionCache
  10157. {
  10158. public function add($def, $config) {
  10159. if (!$this->checkDefType($def)) return;
  10160. $file = $this->generateFilePath($config);
  10161. if (file_exists($file)) return false;
  10162. if (!$this->_prepareDir($config)) return false;
  10163. return $this->_write($file, serialize($def), $config);
  10164. }
  10165. public function set($def, $config) {
  10166. if (!$this->checkDefType($def)) return;
  10167. $file = $this->generateFilePath($config);
  10168. if (!$this->_prepareDir($config)) return false;
  10169. return $this->_write($file, serialize($def), $config);
  10170. }
  10171. public function replace($def, $config) {
  10172. if (!$this->checkDefType($def)) return;
  10173. $file = $this->generateFilePath($config);
  10174. if (!file_exists($file)) return false;
  10175. if (!$this->_prepareDir($config)) return false;
  10176. return $this->_write($file, serialize($def), $config);
  10177. }
  10178. public function get($config) {
  10179. $file = $this->generateFilePath($config);
  10180. if (!file_exists($file)) return false;
  10181. return unserialize(file_get_contents($file));
  10182. }
  10183. public function remove($config) {
  10184. $file = $this->generateFilePath($config);
  10185. if (!file_exists($file)) return false;
  10186. return unlink($file);
  10187. }
  10188. public function flush($config) {
  10189. if (!$this->_prepareDir($config)) return false;
  10190. $dir = $this->generateDirectoryPath($config);
  10191. $dh = opendir($dir);
  10192. while (false !== ($filename = readdir($dh))) {
  10193. if (empty($filename)) continue;
  10194. if ($filename[0] === '.') continue;
  10195. unlink($dir . '/' . $filename);
  10196. }
  10197. }
  10198. public function cleanup($config) {
  10199. if (!$this->_prepareDir($config)) return false;
  10200. $dir = $this->generateDirectoryPath($config);
  10201. $dh = opendir($dir);
  10202. while (false !== ($filename = readdir($dh))) {
  10203. if (empty($filename)) continue;
  10204. if ($filename[0] === '.') continue;
  10205. $key = substr($filename, 0, strlen($filename) - 4);
  10206. if ($this->isOld($key, $config)) unlink($dir . '/' . $filename);
  10207. }
  10208. }
  10209. /**
  10210. * Generates the file path to the serial file corresponding to
  10211. * the configuration and definition name
  10212. * @todo Make protected
  10213. */
  10214. public function generateFilePath($config) {
  10215. $key = $this->generateKey($config);
  10216. return $this->generateDirectoryPath($config) . '/' . $key . '.ser';
  10217. }
  10218. /**
  10219. * Generates the path to the directory contain this cache's serial files
  10220. * @note No trailing slash
  10221. * @todo Make protected
  10222. */
  10223. public function generateDirectoryPath($config) {
  10224. $base = $this->generateBaseDirectoryPath($config);
  10225. return $base . '/' . $this->type;
  10226. }
  10227. /**
  10228. * Generates path to base directory that contains all definition type
  10229. * serials
  10230. * @todo Make protected
  10231. */
  10232. public function generateBaseDirectoryPath($config) {
  10233. $base = $config->get('Cache.SerializerPath');
  10234. $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base;
  10235. return $base;
  10236. }
  10237. /**
  10238. * Convenience wrapper function for file_put_contents
  10239. * @param $file File name to write to
  10240. * @param $data Data to write into file
  10241. * @param $config Config object
  10242. * @return Number of bytes written if success, or false if failure.
  10243. */
  10244. private function _write($file, $data, $config) {
  10245. $result = file_put_contents($file, $data);
  10246. if ($result !== false) {
  10247. // set permissions of the new file (no execute)
  10248. $chmod = $config->get('Cache.SerializerPermissions');
  10249. if (!$chmod) {
  10250. $chmod = 0644; // invalid config or simpletest
  10251. }
  10252. $chmod = $chmod & 0666;
  10253. chmod($file, $chmod);
  10254. }
  10255. return $result;
  10256. }
  10257. /**
  10258. * Prepares the directory that this type stores the serials in
  10259. * @param $config Config object
  10260. * @return True if successful
  10261. */
  10262. private function _prepareDir($config) {
  10263. $directory = $this->generateDirectoryPath($config);
  10264. $chmod = $config->get('Cache.SerializerPermissions');
  10265. if (!$chmod) {
  10266. $chmod = 0755; // invalid config or simpletest
  10267. }
  10268. if (!is_dir($directory)) {
  10269. $base = $this->generateBaseDirectoryPath($config);
  10270. if (!is_dir($base)) {
  10271. trigger_error('Base directory '.$base.' does not exist,
  10272. please create or change using %Cache.SerializerPath',
  10273. E_USER_WARNING);
  10274. return false;
  10275. } elseif (!$this->_testPermissions($base, $chmod)) {
  10276. return false;
  10277. }
  10278. $old = umask(0000);
  10279. mkdir($directory, $chmod);
  10280. umask($old);
  10281. } elseif (!$this->_testPermissions($directory, $chmod)) {
  10282. return false;
  10283. }
  10284. return true;
  10285. }
  10286. /**
  10287. * Tests permissions on a directory and throws out friendly
  10288. * error messages and attempts to chmod it itself if possible
  10289. * @param $dir Directory path
  10290. * @param $chmod Permissions
  10291. * @return True if directory writable
  10292. */
  10293. private function _testPermissions($dir, $chmod) {
  10294. // early abort, if it is writable, everything is hunky-dory
  10295. if (is_writable($dir)) return true;
  10296. if (!is_dir($dir)) {
  10297. // generally, you'll want to handle this beforehand
  10298. // so a more specific error message can be given
  10299. trigger_error('Directory '.$dir.' does not exist',
  10300. E_USER_WARNING);
  10301. return false;
  10302. }
  10303. if (function_exists('posix_getuid')) {
  10304. // POSIX system, we can give more specific advice
  10305. if (fileowner($dir) === posix_getuid()) {
  10306. // we can chmod it ourselves
  10307. $chmod = $chmod | 0700;
  10308. if (chmod($dir, $chmod)) return true;
  10309. } elseif (filegroup($dir) === posix_getgid()) {
  10310. $chmod = $chmod | 0070;
  10311. } else {
  10312. // PHP's probably running as nobody, so we'll
  10313. // need to give global permissions
  10314. $chmod = $chmod | 0777;
  10315. }
  10316. trigger_error('Directory '.$dir.' not writable, '.
  10317. 'please chmod to ' . decoct($chmod),
  10318. E_USER_WARNING);
  10319. } else {
  10320. // generic error message
  10321. trigger_error('Directory '.$dir.' not writable, '.
  10322. 'please alter file permissions',
  10323. E_USER_WARNING);
  10324. }
  10325. return false;
  10326. }
  10327. }
  10328. /**
  10329. * Definition cache decorator class that cleans up the cache
  10330. * whenever there is a cache miss.
  10331. */
  10332. class HTMLPurifier_DefinitionCache_Decorator_Cleanup extends
  10333. HTMLPurifier_DefinitionCache_Decorator
  10334. {
  10335. public $name = 'Cleanup';
  10336. public function copy() {
  10337. return new HTMLPurifier_DefinitionCache_Decorator_Cleanup();
  10338. }
  10339. public function add($def, $config) {
  10340. $status = parent::add($def, $config);
  10341. if (!$status) parent::cleanup($config);
  10342. return $status;
  10343. }
  10344. public function set($def, $config) {
  10345. $status = parent::set($def, $config);
  10346. if (!$status) parent::cleanup($config);
  10347. return $status;
  10348. }
  10349. public function replace($def, $config) {
  10350. $status = parent::replace($def, $config);
  10351. if (!$status) parent::cleanup($config);
  10352. return $status;
  10353. }
  10354. public function get($config) {
  10355. $ret = parent::get($config);
  10356. if (!$ret) parent::cleanup($config);
  10357. return $ret;
  10358. }
  10359. }
  10360. /**
  10361. * Definition cache decorator class that saves all cache retrievals
  10362. * to PHP's memory; good for unit tests or circumstances where
  10363. * there are lots of configuration objects floating around.
  10364. */
  10365. class HTMLPurifier_DefinitionCache_Decorator_Memory extends
  10366. HTMLPurifier_DefinitionCache_Decorator
  10367. {
  10368. protected $definitions;
  10369. public $name = 'Memory';
  10370. public function copy() {
  10371. return new HTMLPurifier_DefinitionCache_Decorator_Memory();
  10372. }
  10373. public function add($def, $config) {
  10374. $status = parent::add($def, $config);
  10375. if ($status) $this->definitions[$this->generateKey($config)] = $def;
  10376. return $status;
  10377. }
  10378. public function set($def, $config) {
  10379. $status = parent::set($def, $config);
  10380. if ($status) $this->definitions[$this->generateKey($config)] = $def;
  10381. return $status;
  10382. }
  10383. public function replace($def, $config) {
  10384. $status = parent::replace($def, $config);
  10385. if ($status) $this->definitions[$this->generateKey($config)] = $def;
  10386. return $status;
  10387. }
  10388. public function get($config) {
  10389. $key = $this->generateKey($config);
  10390. if (isset($this->definitions[$key])) return $this->definitions[$key];
  10391. $this->definitions[$key] = parent::get($config);
  10392. return $this->definitions[$key];
  10393. }
  10394. }
  10395. /**
  10396. * XHTML 1.1 Bi-directional Text Module, defines elements that
  10397. * declare directionality of content. Text Extension Module.
  10398. */
  10399. class HTMLPurifier_HTMLModule_Bdo extends HTMLPurifier_HTMLModule
  10400. {
  10401. public $name = 'Bdo';
  10402. public $attr_collections = array(
  10403. 'I18N' => array('dir' => false)
  10404. );
  10405. public function setup($config) {
  10406. $bdo = $this->addElement(
  10407. 'bdo', 'Inline', 'Inline', array('Core', 'Lang'),
  10408. array(
  10409. 'dir' => 'Enum#ltr,rtl', // required
  10410. // The Abstract Module specification has the attribute
  10411. // inclusions wrong for bdo: bdo allows Lang
  10412. )
  10413. );
  10414. $bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir();
  10415. $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl';
  10416. }
  10417. }
  10418. class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule
  10419. {
  10420. public $name = 'CommonAttributes';
  10421. public $attr_collections = array(
  10422. 'Core' => array(
  10423. 0 => array('Style'),
  10424. // 'xml:space' => false,
  10425. 'class' => 'Class',
  10426. 'id' => 'ID',
  10427. 'title' => 'CDATA',
  10428. ),
  10429. 'Lang' => array(),
  10430. 'I18N' => array(
  10431. 0 => array('Lang'), // proprietary, for xml:lang/lang
  10432. ),
  10433. 'Common' => array(
  10434. 0 => array('Core', 'I18N')
  10435. )
  10436. );
  10437. }
  10438. /**
  10439. * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
  10440. * Module.
  10441. */
  10442. class HTMLPurifier_HTMLModule_Edit extends HTMLPurifier_HTMLModule
  10443. {
  10444. public $name = 'Edit';
  10445. public function setup($config) {
  10446. $contents = 'Chameleon: #PCDATA | Inline ! #PCDATA | Flow';
  10447. $attr = array(
  10448. 'cite' => 'URI',
  10449. // 'datetime' => 'Datetime', // not implemented
  10450. );
  10451. $this->addElement('del', 'Inline', $contents, 'Common', $attr);
  10452. $this->addElement('ins', 'Inline', $contents, 'Common', $attr);
  10453. }
  10454. // HTML 4.01 specifies that ins/del must not contain block
  10455. // elements when used in an inline context, chameleon is
  10456. // a complicated workaround to acheive this effect
  10457. // Inline context ! Block context (exclamation mark is
  10458. // separator, see getChildDef for parsing)
  10459. public $defines_child_def = true;
  10460. public function getChildDef($def) {
  10461. if ($def->content_model_type != 'chameleon') return false;
  10462. $value = explode('!', $def->content_model);
  10463. return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]);
  10464. }
  10465. }
  10466. /**
  10467. * XHTML 1.1 Forms module, defines all form-related elements found in HTML 4.
  10468. */
  10469. class HTMLPurifier_HTMLModule_Forms extends HTMLPurifier_HTMLModule
  10470. {
  10471. public $name = 'Forms';
  10472. public $safe = false;
  10473. public $content_sets = array(
  10474. 'Block' => 'Form',
  10475. 'Inline' => 'Formctrl',
  10476. );
  10477. public function setup($config) {
  10478. $form = $this->addElement('form', 'Form',
  10479. 'Required: Heading | List | Block | fieldset', 'Common', array(
  10480. 'accept' => 'ContentTypes',
  10481. 'accept-charset' => 'Charsets',
  10482. 'action*' => 'URI',
  10483. 'method' => 'Enum#get,post',
  10484. // really ContentType, but these two are the only ones used today
  10485. 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data',
  10486. ));
  10487. $form->excludes = array('form' => true);
  10488. $input = $this->addElement('input', 'Formctrl', 'Empty', 'Common', array(
  10489. 'accept' => 'ContentTypes',
  10490. 'accesskey' => 'Character',
  10491. 'alt' => 'Text',
  10492. 'checked' => 'Bool#checked',
  10493. 'disabled' => 'Bool#disabled',
  10494. 'maxlength' => 'Number',
  10495. 'name' => 'CDATA',
  10496. 'readonly' => 'Bool#readonly',
  10497. 'size' => 'Number',
  10498. 'src' => 'URI#embedded',
  10499. 'tabindex' => 'Number',
  10500. 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image',
  10501. 'value' => 'CDATA',
  10502. ));
  10503. $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input();
  10504. $this->addElement('select', 'Formctrl', 'Required: optgroup | option', 'Common', array(
  10505. 'disabled' => 'Bool#disabled',
  10506. 'multiple' => 'Bool#multiple',
  10507. 'name' => 'CDATA',
  10508. 'size' => 'Number',
  10509. 'tabindex' => 'Number',
  10510. ));
  10511. $this->addElement('option', false, 'Optional: #PCDATA', 'Common', array(
  10512. 'disabled' => 'Bool#disabled',
  10513. 'label' => 'Text',
  10514. 'selected' => 'Bool#selected',
  10515. 'value' => 'CDATA',
  10516. ));
  10517. // It's illegal for there to be more than one selected, but not
  10518. // be multiple. Also, no selected means undefined behavior. This might
  10519. // be difficult to implement; perhaps an injector, or a context variable.
  10520. $textarea = $this->addElement('textarea', 'Formctrl', 'Optional: #PCDATA', 'Common', array(
  10521. 'accesskey' => 'Character',
  10522. 'cols*' => 'Number',
  10523. 'disabled' => 'Bool#disabled',
  10524. 'name' => 'CDATA',
  10525. 'readonly' => 'Bool#readonly',
  10526. 'rows*' => 'Number',
  10527. 'tabindex' => 'Number',
  10528. ));
  10529. $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea();
  10530. $button = $this->addElement('button', 'Formctrl', 'Optional: #PCDATA | Heading | List | Block | Inline', 'Common', array(
  10531. 'accesskey' => 'Character',
  10532. 'disabled' => 'Bool#disabled',
  10533. 'name' => 'CDATA',
  10534. 'tabindex' => 'Number',
  10535. 'type' => 'Enum#button,submit,reset',
  10536. 'value' => 'CDATA',
  10537. ));
  10538. // For exclusions, ideally we'd specify content sets, not literal elements
  10539. $button->excludes = $this->makeLookup(
  10540. 'form', 'fieldset', // Form
  10541. 'input', 'select', 'textarea', 'label', 'button', // Formctrl
  10542. 'a', // as per HTML 4.01 spec, this is omitted by modularization
  10543. 'isindex', 'iframe' // legacy items
  10544. );
  10545. // Extra exclusion: img usemap="" is not permitted within this element.
  10546. // We'll omit this for now, since we don't have any good way of
  10547. // indicating it yet.
  10548. // This is HIGHLY user-unfriendly; we need a custom child-def for this
  10549. $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common');
  10550. $label = $this->addElement('label', 'Formctrl', 'Optional: #PCDATA | Inline', 'Common', array(
  10551. 'accesskey' => 'Character',
  10552. // 'for' => 'IDREF', // IDREF not implemented, cannot allow
  10553. ));
  10554. $label->excludes = array('label' => true);
  10555. $this->addElement('legend', false, 'Optional: #PCDATA | Inline', 'Common', array(
  10556. 'accesskey' => 'Character',
  10557. ));
  10558. $this->addElement('optgroup', false, 'Required: option', 'Common', array(
  10559. 'disabled' => 'Bool#disabled',
  10560. 'label*' => 'Text',
  10561. ));
  10562. // Don't forget an injector for <isindex>. This one's a little complex
  10563. // because it maps to multiple elements.
  10564. }
  10565. }
  10566. /**
  10567. * XHTML 1.1 Hypertext Module, defines hypertext links. Core Module.
  10568. */
  10569. class HTMLPurifier_HTMLModule_Hypertext extends HTMLPurifier_HTMLModule
  10570. {
  10571. public $name = 'Hypertext';
  10572. public function setup($config) {
  10573. $a = $this->addElement(
  10574. 'a', 'Inline', 'Inline', 'Common',
  10575. array(
  10576. // 'accesskey' => 'Character',
  10577. // 'charset' => 'Charset',
  10578. 'href' => 'URI',
  10579. // 'hreflang' => 'LanguageCode',
  10580. 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'),
  10581. 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'),
  10582. // 'tabindex' => 'Number',
  10583. // 'type' => 'ContentType',
  10584. )
  10585. );
  10586. $a->formatting = true;
  10587. $a->excludes = array('a' => true);
  10588. }
  10589. }
  10590. /**
  10591. * XHTML 1.1 Iframe Module provides inline frames.
  10592. *
  10593. * @note This module is not considered safe unless an Iframe
  10594. * whitelisting mechanism is specified. Currently, the only
  10595. * such mechanism is %URL.SafeIframeRegexp
  10596. */
  10597. class HTMLPurifier_HTMLModule_Iframe extends HTMLPurifier_HTMLModule
  10598. {
  10599. public $name = 'Iframe';
  10600. public $safe = false;
  10601. public function setup($config) {
  10602. if ($config->get('HTML.SafeIframe')) {
  10603. $this->safe = true;
  10604. }
  10605. $this->addElement(
  10606. 'iframe', 'Inline', 'Flow', 'Common',
  10607. array(
  10608. 'src' => 'URI#embedded',
  10609. 'width' => 'Length',
  10610. 'height' => 'Length',
  10611. 'name' => 'ID',
  10612. 'scrolling' => 'Enum#yes,no,auto',
  10613. 'frameborder' => 'Enum#0,1',
  10614. 'longdesc' => 'URI',
  10615. 'marginheight' => 'Pixels',
  10616. 'marginwidth' => 'Pixels',
  10617. )
  10618. );
  10619. }
  10620. }
  10621. /**
  10622. * XHTML 1.1 Image Module provides basic image embedding.
  10623. * @note There is specialized code for removing empty images in
  10624. * HTMLPurifier_Strategy_RemoveForeignElements
  10625. */
  10626. class HTMLPurifier_HTMLModule_Image extends HTMLPurifier_HTMLModule
  10627. {
  10628. public $name = 'Image';
  10629. public function setup($config) {
  10630. $max = $config->get('HTML.MaxImgLength');
  10631. $img = $this->addElement(
  10632. 'img', 'Inline', 'Empty', 'Common',
  10633. array(
  10634. 'alt*' => 'Text',
  10635. // According to the spec, it's Length, but percents can
  10636. // be abused, so we allow only Pixels.
  10637. 'height' => 'Pixels#' . $max,
  10638. 'width' => 'Pixels#' . $max,
  10639. 'longdesc' => 'URI',
  10640. 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded
  10641. )
  10642. );
  10643. if ($max === null || $config->get('HTML.Trusted')) {
  10644. $img->attr['height'] =
  10645. $img->attr['width'] = 'Length';
  10646. }
  10647. // kind of strange, but splitting things up would be inefficient
  10648. $img->attr_transform_pre[] =
  10649. $img->attr_transform_post[] =
  10650. new HTMLPurifier_AttrTransform_ImgRequired();
  10651. }
  10652. }
  10653. /**
  10654. * XHTML 1.1 Legacy module defines elements that were previously
  10655. * deprecated.
  10656. *
  10657. * @note Not all legacy elements have been implemented yet, which
  10658. * is a bit of a reverse problem as compared to browsers! In
  10659. * addition, this legacy module may implement a bit more than
  10660. * mandated by XHTML 1.1.
  10661. *
  10662. * This module can be used in combination with TransformToStrict in order
  10663. * to transform as many deprecated elements as possible, but retain
  10664. * questionably deprecated elements that do not have good alternatives
  10665. * as well as transform elements that don't have an implementation.
  10666. * See docs/ref-strictness.txt for more details.
  10667. */
  10668. class HTMLPurifier_HTMLModule_Legacy extends HTMLPurifier_HTMLModule
  10669. {
  10670. public $name = 'Legacy';
  10671. public function setup($config) {
  10672. $this->addElement('basefont', 'Inline', 'Empty', false, array(
  10673. 'color' => 'Color',
  10674. 'face' => 'Text', // extremely broad, we should
  10675. 'size' => 'Text', // tighten it
  10676. 'id' => 'ID'
  10677. ));
  10678. $this->addElement('center', 'Block', 'Flow', 'Common');
  10679. $this->addElement('dir', 'Block', 'Required: li', 'Common', array(
  10680. 'compact' => 'Bool#compact'
  10681. ));
  10682. $this->addElement('font', 'Inline', 'Inline', array('Core', 'I18N'), array(
  10683. 'color' => 'Color',
  10684. 'face' => 'Text', // extremely broad, we should
  10685. 'size' => 'Text', // tighten it
  10686. ));
  10687. $this->addElement('menu', 'Block', 'Required: li', 'Common', array(
  10688. 'compact' => 'Bool#compact'
  10689. ));
  10690. $s = $this->addElement('s', 'Inline', 'Inline', 'Common');
  10691. $s->formatting = true;
  10692. $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common');
  10693. $strike->formatting = true;
  10694. $u = $this->addElement('u', 'Inline', 'Inline', 'Common');
  10695. $u->formatting = true;
  10696. // setup modifications to old elements
  10697. $align = 'Enum#left,right,center,justify';
  10698. $address = $this->addBlankElement('address');
  10699. $address->content_model = 'Inline | #PCDATA | p';
  10700. $address->content_model_type = 'optional';
  10701. $address->child = false;
  10702. $blockquote = $this->addBlankElement('blockquote');
  10703. $blockquote->content_model = 'Flow | #PCDATA';
  10704. $blockquote->content_model_type = 'optional';
  10705. $blockquote->child = false;
  10706. $br = $this->addBlankElement('br');
  10707. $br->attr['clear'] = 'Enum#left,all,right,none';
  10708. $caption = $this->addBlankElement('caption');
  10709. $caption->attr['align'] = 'Enum#top,bottom,left,right';
  10710. $div = $this->addBlankElement('div');
  10711. $div->attr['align'] = $align;
  10712. $dl = $this->addBlankElement('dl');
  10713. $dl->attr['compact'] = 'Bool#compact';
  10714. for ($i = 1; $i <= 6; $i++) {
  10715. $h = $this->addBlankElement("h$i");
  10716. $h->attr['align'] = $align;
  10717. }
  10718. $hr = $this->addBlankElement('hr');
  10719. $hr->attr['align'] = $align;
  10720. $hr->attr['noshade'] = 'Bool#noshade';
  10721. $hr->attr['size'] = 'Pixels';
  10722. $hr->attr['width'] = 'Length';
  10723. $img = $this->addBlankElement('img');
  10724. $img->attr['align'] = 'IAlign';
  10725. $img->attr['border'] = 'Pixels';
  10726. $img->attr['hspace'] = 'Pixels';
  10727. $img->attr['vspace'] = 'Pixels';
  10728. // figure out this integer business
  10729. $li = $this->addBlankElement('li');
  10730. $li->attr['value'] = new HTMLPurifier_AttrDef_Integer();
  10731. $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle';
  10732. $ol = $this->addBlankElement('ol');
  10733. $ol->attr['compact'] = 'Bool#compact';
  10734. $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer();
  10735. $ol->attr['type'] = 'Enum#s:1,i,I,a,A';
  10736. $p = $this->addBlankElement('p');
  10737. $p->attr['align'] = $align;
  10738. $pre = $this->addBlankElement('pre');
  10739. $pre->attr['width'] = 'Number';
  10740. // script omitted
  10741. $table = $this->addBlankElement('table');
  10742. $table->attr['align'] = 'Enum#left,center,right';
  10743. $table->attr['bgcolor'] = 'Color';
  10744. $tr = $this->addBlankElement('tr');
  10745. $tr->attr['bgcolor'] = 'Color';
  10746. $th = $this->addBlankElement('th');
  10747. $th->attr['bgcolor'] = 'Color';
  10748. $th->attr['height'] = 'Length';
  10749. $th->attr['nowrap'] = 'Bool#nowrap';
  10750. $th->attr['width'] = 'Length';
  10751. $td = $this->addBlankElement('td');
  10752. $td->attr['bgcolor'] = 'Color';
  10753. $td->attr['height'] = 'Length';
  10754. $td->attr['nowrap'] = 'Bool#nowrap';
  10755. $td->attr['width'] = 'Length';
  10756. $ul = $this->addBlankElement('ul');
  10757. $ul->attr['compact'] = 'Bool#compact';
  10758. $ul->attr['type'] = 'Enum#square,disc,circle';
  10759. // "safe" modifications to "unsafe" elements
  10760. // WARNING: If you want to add support for an unsafe, legacy
  10761. // attribute, make a new TrustedLegacy module with the trusted
  10762. // bit set appropriately
  10763. $form = $this->addBlankElement('form');
  10764. $form->content_model = 'Flow | #PCDATA';
  10765. $form->content_model_type = 'optional';
  10766. $form->attr['target'] = 'FrameTarget';
  10767. $input = $this->addBlankElement('input');
  10768. $input->attr['align'] = 'IAlign';
  10769. $legend = $this->addBlankElement('legend');
  10770. $legend->attr['align'] = 'LAlign';
  10771. }
  10772. }
  10773. /**
  10774. * XHTML 1.1 List Module, defines list-oriented elements. Core Module.
  10775. */
  10776. class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule
  10777. {
  10778. public $name = 'List';
  10779. // According to the abstract schema, the List content set is a fully formed
  10780. // one or more expr, but it invariably occurs in an optional declaration
  10781. // so we're not going to do that subtlety. It might cause trouble
  10782. // if a user defines "List" and expects that multiple lists are
  10783. // allowed to be specified, but then again, that's not very intuitive.
  10784. // Furthermore, the actual XML Schema may disagree. Regardless,
  10785. // we don't have support for such nested expressions without using
  10786. // the incredibly inefficient and draconic Custom ChildDef.
  10787. public $content_sets = array('Flow' => 'List');
  10788. public function setup($config) {
  10789. $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common');
  10790. $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common');
  10791. // XXX The wrap attribute is handled by MakeWellFormed. This is all
  10792. // quite unsatisfactory, because we generated this
  10793. // *specifically* for lists, and now a big chunk of the handling
  10794. // is done properly by the List ChildDef. So actually, we just
  10795. // want enough information to make autoclosing work properly,
  10796. // and then hand off the tricky stuff to the ChildDef.
  10797. $ol->wrap = 'li';
  10798. $ul->wrap = 'li';
  10799. $this->addElement('dl', 'List', 'Required: dt | dd', 'Common');
  10800. $this->addElement('li', false, 'Flow', 'Common');
  10801. $this->addElement('dd', false, 'Flow', 'Common');
  10802. $this->addElement('dt', false, 'Inline', 'Common');
  10803. }
  10804. }
  10805. class HTMLPurifier_HTMLModule_Name extends HTMLPurifier_HTMLModule
  10806. {
  10807. public $name = 'Name';
  10808. public function setup($config) {
  10809. $elements = array('a', 'applet', 'form', 'frame', 'iframe', 'img', 'map');
  10810. foreach ($elements as $name) {
  10811. $element = $this->addBlankElement($name);
  10812. $element->attr['name'] = 'CDATA';
  10813. if (!$config->get('HTML.Attr.Name.UseCDATA')) {
  10814. $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync();
  10815. }
  10816. }
  10817. }
  10818. }
  10819. /**
  10820. * Module adds the nofollow attribute transformation to a tags. It
  10821. * is enabled by HTML.Nofollow
  10822. */
  10823. class HTMLPurifier_HTMLModule_Nofollow extends HTMLPurifier_HTMLModule
  10824. {
  10825. public $name = 'Nofollow';
  10826. public function setup($config) {
  10827. $a = $this->addBlankElement('a');
  10828. $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow();
  10829. }
  10830. }
  10831. class HTMLPurifier_HTMLModule_NonXMLCommonAttributes extends HTMLPurifier_HTMLModule
  10832. {
  10833. public $name = 'NonXMLCommonAttributes';
  10834. public $attr_collections = array(
  10835. 'Lang' => array(
  10836. 'lang' => 'LanguageCode',
  10837. )
  10838. );
  10839. }
  10840. /**
  10841. * XHTML 1.1 Object Module, defines elements for generic object inclusion
  10842. * @warning Users will commonly use <embed> to cater to legacy browsers: this
  10843. * module does not allow this sort of behavior
  10844. */
  10845. class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule
  10846. {
  10847. public $name = 'Object';
  10848. public $safe = false;
  10849. public function setup($config) {
  10850. $this->addElement('object', 'Inline', 'Optional: #PCDATA | Flow | param', 'Common',
  10851. array(
  10852. 'archive' => 'URI',
  10853. 'classid' => 'URI',
  10854. 'codebase' => 'URI',
  10855. 'codetype' => 'Text',
  10856. 'data' => 'URI',
  10857. 'declare' => 'Bool#declare',
  10858. 'height' => 'Length',
  10859. 'name' => 'CDATA',
  10860. 'standby' => 'Text',
  10861. 'tabindex' => 'Number',
  10862. 'type' => 'ContentType',
  10863. 'width' => 'Length'
  10864. )
  10865. );
  10866. $this->addElement('param', false, 'Empty', false,
  10867. array(
  10868. 'id' => 'ID',
  10869. 'name*' => 'Text',
  10870. 'type' => 'Text',
  10871. 'value' => 'Text',
  10872. 'valuetype' => 'Enum#data,ref,object'
  10873. )
  10874. );
  10875. }
  10876. }
  10877. /**
  10878. * XHTML 1.1 Presentation Module, defines simple presentation-related
  10879. * markup. Text Extension Module.
  10880. * @note The official XML Schema and DTD specs further divide this into
  10881. * two modules:
  10882. * - Block Presentation (hr)
  10883. * - Inline Presentation (b, big, i, small, sub, sup, tt)
  10884. * We have chosen not to heed this distinction, as content_sets
  10885. * provides satisfactory disambiguation.
  10886. */
  10887. class HTMLPurifier_HTMLModule_Presentation extends HTMLPurifier_HTMLModule
  10888. {
  10889. public $name = 'Presentation';
  10890. public function setup($config) {
  10891. $this->addElement('hr', 'Block', 'Empty', 'Common');
  10892. $this->addElement('sub', 'Inline', 'Inline', 'Common');
  10893. $this->addElement('sup', 'Inline', 'Inline', 'Common');
  10894. $b = $this->addElement('b', 'Inline', 'Inline', 'Common');
  10895. $b->formatting = true;
  10896. $big = $this->addElement('big', 'Inline', 'Inline', 'Common');
  10897. $big->formatting = true;
  10898. $i = $this->addElement('i', 'Inline', 'Inline', 'Common');
  10899. $i->formatting = true;
  10900. $small = $this->addElement('small', 'Inline', 'Inline', 'Common');
  10901. $small->formatting = true;
  10902. $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common');
  10903. $tt->formatting = true;
  10904. }
  10905. }
  10906. /**
  10907. * Module defines proprietary tags and attributes in HTML.
  10908. * @warning If this module is enabled, standards-compliance is off!
  10909. */
  10910. class HTMLPurifier_HTMLModule_Proprietary extends HTMLPurifier_HTMLModule
  10911. {
  10912. public $name = 'Proprietary';
  10913. public function setup($config) {
  10914. $this->addElement('marquee', 'Inline', 'Flow', 'Common',
  10915. array(
  10916. 'direction' => 'Enum#left,right,up,down',
  10917. 'behavior' => 'Enum#alternate',
  10918. 'width' => 'Length',
  10919. 'height' => 'Length',
  10920. 'scrolldelay' => 'Number',
  10921. 'scrollamount' => 'Number',
  10922. 'loop' => 'Number',
  10923. 'bgcolor' => 'Color',
  10924. 'hspace' => 'Pixels',
  10925. 'vspace' => 'Pixels',
  10926. )
  10927. );
  10928. }
  10929. }
  10930. /**
  10931. * XHTML 1.1 Ruby Annotation Module, defines elements that indicate
  10932. * short runs of text alongside base text for annotation or pronounciation.
  10933. */
  10934. class HTMLPurifier_HTMLModule_Ruby extends HTMLPurifier_HTMLModule
  10935. {
  10936. public $name = 'Ruby';
  10937. public function setup($config) {
  10938. $this->addElement('ruby', 'Inline',
  10939. 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))',
  10940. 'Common');
  10941. $this->addElement('rbc', false, 'Required: rb', 'Common');
  10942. $this->addElement('rtc', false, 'Required: rt', 'Common');
  10943. $rb = $this->addElement('rb', false, 'Inline', 'Common');
  10944. $rb->excludes = array('ruby' => true);
  10945. $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number'));
  10946. $rt->excludes = array('ruby' => true);
  10947. $this->addElement('rp', false, 'Optional: #PCDATA', 'Common');
  10948. }
  10949. }
  10950. /**
  10951. * A "safe" embed module. See SafeObject. This is a proprietary element.
  10952. */
  10953. class HTMLPurifier_HTMLModule_SafeEmbed extends HTMLPurifier_HTMLModule
  10954. {
  10955. public $name = 'SafeEmbed';
  10956. public function setup($config) {
  10957. $max = $config->get('HTML.MaxImgLength');
  10958. $embed = $this->addElement(
  10959. 'embed', 'Inline', 'Empty', 'Common',
  10960. array(
  10961. 'src*' => 'URI#embedded',
  10962. 'type' => 'Enum#application/x-shockwave-flash',
  10963. 'width' => 'Pixels#' . $max,
  10964. 'height' => 'Pixels#' . $max,
  10965. 'allowscriptaccess' => 'Enum#never',
  10966. 'allownetworking' => 'Enum#internal',
  10967. 'flashvars' => 'Text',
  10968. 'wmode' => 'Enum#window,transparent,opaque',
  10969. 'name' => 'ID',
  10970. )
  10971. );
  10972. $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed();
  10973. }
  10974. }
  10975. /**
  10976. * A "safe" object module. In theory, objects permitted by this module will
  10977. * be safe, and untrusted users can be allowed to embed arbitrary flash objects
  10978. * (maybe other types too, but only Flash is supported as of right now).
  10979. * Highly experimental.
  10980. */
  10981. class HTMLPurifier_HTMLModule_SafeObject extends HTMLPurifier_HTMLModule
  10982. {
  10983. public $name = 'SafeObject';
  10984. public function setup($config) {
  10985. // These definitions are not intrinsically safe: the attribute transforms
  10986. // are a vital part of ensuring safety.
  10987. $max = $config->get('HTML.MaxImgLength');
  10988. $object = $this->addElement(
  10989. 'object',
  10990. 'Inline',
  10991. 'Optional: param | Flow | #PCDATA',
  10992. 'Common',
  10993. array(
  10994. // While technically not required by the spec, we're forcing
  10995. // it to this value.
  10996. 'type' => 'Enum#application/x-shockwave-flash',
  10997. 'width' => 'Pixels#' . $max,
  10998. 'height' => 'Pixels#' . $max,
  10999. 'data' => 'URI#embedded',
  11000. 'codebase' => new HTMLPurifier_AttrDef_Enum(array(
  11001. 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0')),
  11002. )
  11003. );
  11004. $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject();
  11005. $param = $this->addElement('param', false, 'Empty', false,
  11006. array(
  11007. 'id' => 'ID',
  11008. 'name*' => 'Text',
  11009. 'value' => 'Text'
  11010. )
  11011. );
  11012. $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam();
  11013. $this->info_injector[] = 'SafeObject';
  11014. }
  11015. }
  11016. /**
  11017. * A "safe" script module. No inline JS is allowed, and pointed to JS
  11018. * files must match whitelist.
  11019. */
  11020. class HTMLPurifier_HTMLModule_SafeScripting extends HTMLPurifier_HTMLModule
  11021. {
  11022. public $name = 'SafeScripting';
  11023. public function setup($config) {
  11024. // These definitions are not intrinsically safe: the attribute transforms
  11025. // are a vital part of ensuring safety.
  11026. $allowed = $config->get('HTML.SafeScripting');
  11027. $script = $this->addElement(
  11028. 'script',
  11029. 'Inline',
  11030. 'Empty',
  11031. null,
  11032. array(
  11033. // While technically not required by the spec, we're forcing
  11034. // it to this value.
  11035. 'type' => 'Enum#text/javascript',
  11036. 'src*' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed))
  11037. )
  11038. );
  11039. $script->attr_transform_pre[] =
  11040. $script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired();
  11041. }
  11042. }
  11043. /*
  11044. WARNING: THIS MODULE IS EXTREMELY DANGEROUS AS IT ENABLES INLINE SCRIPTING
  11045. INSIDE HTML PURIFIER DOCUMENTS. USE ONLY WITH TRUSTED USER INPUT!!!
  11046. */
  11047. /**
  11048. * XHTML 1.1 Scripting module, defines elements that are used to contain
  11049. * information pertaining to executable scripts or the lack of support
  11050. * for executable scripts.
  11051. * @note This module does not contain inline scripting elements
  11052. */
  11053. class HTMLPurifier_HTMLModule_Scripting extends HTMLPurifier_HTMLModule
  11054. {
  11055. public $name = 'Scripting';
  11056. public $elements = array('script', 'noscript');
  11057. public $content_sets = array('Block' => 'script | noscript', 'Inline' => 'script | noscript');
  11058. public $safe = false;
  11059. public function setup($config) {
  11060. // TODO: create custom child-definition for noscript that
  11061. // auto-wraps stray #PCDATA in a similar manner to
  11062. // blockquote's custom definition (we would use it but
  11063. // blockquote's contents are optional while noscript's contents
  11064. // are required)
  11065. // TODO: convert this to new syntax, main problem is getting
  11066. // both content sets working
  11067. // In theory, this could be safe, but I don't see any reason to
  11068. // allow it.
  11069. $this->info['noscript'] = new HTMLPurifier_ElementDef();
  11070. $this->info['noscript']->attr = array( 0 => array('Common') );
  11071. $this->info['noscript']->content_model = 'Heading | List | Block';
  11072. $this->info['noscript']->content_model_type = 'required';
  11073. $this->info['script'] = new HTMLPurifier_ElementDef();
  11074. $this->info['script']->attr = array(
  11075. 'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')),
  11076. 'src' => new HTMLPurifier_AttrDef_URI(true),
  11077. 'type' => new HTMLPurifier_AttrDef_Enum(array('text/javascript'))
  11078. );
  11079. $this->info['script']->content_model = '#PCDATA';
  11080. $this->info['script']->content_model_type = 'optional';
  11081. $this->info['script']->attr_transform_pre[] =
  11082. $this->info['script']->attr_transform_post[] =
  11083. new HTMLPurifier_AttrTransform_ScriptRequired();
  11084. }
  11085. }
  11086. /**
  11087. * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
  11088. * Module.
  11089. */
  11090. class HTMLPurifier_HTMLModule_StyleAttribute extends HTMLPurifier_HTMLModule
  11091. {
  11092. public $name = 'StyleAttribute';
  11093. public $attr_collections = array(
  11094. // The inclusion routine differs from the Abstract Modules but
  11095. // is in line with the DTD and XML Schemas.
  11096. 'Style' => array('style' => false), // see constructor
  11097. 'Core' => array(0 => array('Style'))
  11098. );
  11099. public function setup($config) {
  11100. $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS();
  11101. }
  11102. }
  11103. /**
  11104. * XHTML 1.1 Tables Module, fully defines accessible table elements.
  11105. */
  11106. class HTMLPurifier_HTMLModule_Tables extends HTMLPurifier_HTMLModule
  11107. {
  11108. public $name = 'Tables';
  11109. public function setup($config) {
  11110. $this->addElement('caption', false, 'Inline', 'Common');
  11111. $this->addElement('table', 'Block',
  11112. new HTMLPurifier_ChildDef_Table(), 'Common',
  11113. array(
  11114. 'border' => 'Pixels',
  11115. 'cellpadding' => 'Length',
  11116. 'cellspacing' => 'Length',
  11117. 'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border',
  11118. 'rules' => 'Enum#none,groups,rows,cols,all',
  11119. 'summary' => 'Text',
  11120. 'width' => 'Length'
  11121. )
  11122. );
  11123. // common attributes
  11124. $cell_align = array(
  11125. 'align' => 'Enum#left,center,right,justify,char',
  11126. 'charoff' => 'Length',
  11127. 'valign' => 'Enum#top,middle,bottom,baseline',
  11128. );
  11129. $cell_t = array_merge(
  11130. array(
  11131. 'abbr' => 'Text',
  11132. 'colspan' => 'Number',
  11133. 'rowspan' => 'Number',
  11134. // Apparently, as of HTML5 this attribute only applies
  11135. // to 'th' elements.
  11136. 'scope' => 'Enum#row,col,rowgroup,colgroup',
  11137. ),
  11138. $cell_align
  11139. );
  11140. $this->addElement('td', false, 'Flow', 'Common', $cell_t);
  11141. $this->addElement('th', false, 'Flow', 'Common', $cell_t);
  11142. $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align);
  11143. $cell_col = array_merge(
  11144. array(
  11145. 'span' => 'Number',
  11146. 'width' => 'MultiLength',
  11147. ),
  11148. $cell_align
  11149. );
  11150. $this->addElement('col', false, 'Empty', 'Common', $cell_col);
  11151. $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col);
  11152. $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align);
  11153. $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align);
  11154. $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align);
  11155. }
  11156. }
  11157. /**
  11158. * XHTML 1.1 Target Module, defines target attribute in link elements.
  11159. */
  11160. class HTMLPurifier_HTMLModule_Target extends HTMLPurifier_HTMLModule
  11161. {
  11162. public $name = 'Target';
  11163. public function setup($config) {
  11164. $elements = array('a');
  11165. foreach ($elements as $name) {
  11166. $e = $this->addBlankElement($name);
  11167. $e->attr = array(
  11168. 'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget()
  11169. );
  11170. }
  11171. }
  11172. }
  11173. /**
  11174. * Module adds the target=blank attribute transformation to a tags. It
  11175. * is enabled by HTML.TargetBlank
  11176. */
  11177. class HTMLPurifier_HTMLModule_TargetBlank extends HTMLPurifier_HTMLModule
  11178. {
  11179. public $name = 'TargetBlank';
  11180. public function setup($config) {
  11181. $a = $this->addBlankElement('a');
  11182. $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetBlank();
  11183. }
  11184. }
  11185. /**
  11186. * XHTML 1.1 Text Module, defines basic text containers. Core Module.
  11187. * @note In the normative XML Schema specification, this module
  11188. * is further abstracted into the following modules:
  11189. * - Block Phrasal (address, blockquote, pre, h1, h2, h3, h4, h5, h6)
  11190. * - Block Structural (div, p)
  11191. * - Inline Phrasal (abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var)
  11192. * - Inline Structural (br, span)
  11193. * This module, functionally, does not distinguish between these
  11194. * sub-modules, but the code is internally structured to reflect
  11195. * these distinctions.
  11196. */
  11197. class HTMLPurifier_HTMLModule_Text extends HTMLPurifier_HTMLModule
  11198. {
  11199. public $name = 'Text';
  11200. public $content_sets = array(
  11201. 'Flow' => 'Heading | Block | Inline'
  11202. );
  11203. public function setup($config) {
  11204. // Inline Phrasal -------------------------------------------------
  11205. $this->addElement('abbr', 'Inline', 'Inline', 'Common');
  11206. $this->addElement('acronym', 'Inline', 'Inline', 'Common');
  11207. $this->addElement('cite', 'Inline', 'Inline', 'Common');
  11208. $this->addElement('dfn', 'Inline', 'Inline', 'Common');
  11209. $this->addElement('kbd', 'Inline', 'Inline', 'Common');
  11210. $this->addElement('q', 'Inline', 'Inline', 'Common', array('cite' => 'URI'));
  11211. $this->addElement('samp', 'Inline', 'Inline', 'Common');
  11212. $this->addElement('var', 'Inline', 'Inline', 'Common');
  11213. $em = $this->addElement('em', 'Inline', 'Inline', 'Common');
  11214. $em->formatting = true;
  11215. $strong = $this->addElement('strong', 'Inline', 'Inline', 'Common');
  11216. $strong->formatting = true;
  11217. $code = $this->addElement('code', 'Inline', 'Inline', 'Common');
  11218. $code->formatting = true;
  11219. // Inline Structural ----------------------------------------------
  11220. $this->addElement('span', 'Inline', 'Inline', 'Common');
  11221. $this->addElement('br', 'Inline', 'Empty', 'Core');
  11222. // Block Phrasal --------------------------------------------------
  11223. $this->addElement('address', 'Block', 'Inline', 'Common');
  11224. $this->addElement('blockquote', 'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI') );
  11225. $pre = $this->addElement('pre', 'Block', 'Inline', 'Common');
  11226. $pre->excludes = $this->makeLookup(
  11227. 'img', 'big', 'small', 'object', 'applet', 'font', 'basefont' );
  11228. $this->addElement('h1', 'Heading', 'Inline', 'Common');
  11229. $this->addElement('h2', 'Heading', 'Inline', 'Common');
  11230. $this->addElement('h3', 'Heading', 'Inline', 'Common');
  11231. $this->addElement('h4', 'Heading', 'Inline', 'Common');
  11232. $this->addElement('h5', 'Heading', 'Inline', 'Common');
  11233. $this->addElement('h6', 'Heading', 'Inline', 'Common');
  11234. // Block Structural -----------------------------------------------
  11235. $p = $this->addElement('p', 'Block', 'Inline', 'Common');
  11236. $p->autoclose = array_flip(array("address", "blockquote", "center", "dir", "div", "dl", "fieldset", "ol", "p", "ul"));
  11237. $this->addElement('div', 'Block', 'Flow', 'Common');
  11238. }
  11239. }
  11240. /**
  11241. * Abstract class for a set of proprietary modules that clean up (tidy)
  11242. * poorly written HTML.
  11243. * @todo Figure out how to protect some of these methods/properties
  11244. */
  11245. class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule
  11246. {
  11247. /**
  11248. * List of supported levels. Index zero is a special case "no fixes"
  11249. * level.
  11250. */
  11251. public $levels = array(0 => 'none', 'light', 'medium', 'heavy');
  11252. /**
  11253. * Default level to place all fixes in. Disabled by default
  11254. */
  11255. public $defaultLevel = null;
  11256. /**
  11257. * Lists of fixes used by getFixesForLevel(). Format is:
  11258. * HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2');
  11259. */
  11260. public $fixesForLevel = array(
  11261. 'light' => array(),
  11262. 'medium' => array(),
  11263. 'heavy' => array()
  11264. );
  11265. /**
  11266. * Lazy load constructs the module by determining the necessary
  11267. * fixes to create and then delegating to the populate() function.
  11268. * @todo Wildcard matching and error reporting when an added or
  11269. * subtracted fix has no effect.
  11270. */
  11271. public function setup($config) {
  11272. // create fixes, initialize fixesForLevel
  11273. $fixes = $this->makeFixes();
  11274. $this->makeFixesForLevel($fixes);
  11275. // figure out which fixes to use
  11276. $level = $config->get('HTML.TidyLevel');
  11277. $fixes_lookup = $this->getFixesForLevel($level);
  11278. // get custom fix declarations: these need namespace processing
  11279. $add_fixes = $config->get('HTML.TidyAdd');
  11280. $remove_fixes = $config->get('HTML.TidyRemove');
  11281. foreach ($fixes as $name => $fix) {
  11282. // needs to be refactored a little to implement globbing
  11283. if (
  11284. isset($remove_fixes[$name]) ||
  11285. (!isset($add_fixes[$name]) && !isset($fixes_lookup[$name]))
  11286. ) {
  11287. unset($fixes[$name]);
  11288. }
  11289. }
  11290. // populate this module with necessary fixes
  11291. $this->populate($fixes);
  11292. }
  11293. /**
  11294. * Retrieves all fixes per a level, returning fixes for that specific
  11295. * level as well as all levels below it.
  11296. * @param $level String level identifier, see $levels for valid values
  11297. * @return Lookup up table of fixes
  11298. */
  11299. public function getFixesForLevel($level) {
  11300. if ($level == $this->levels[0]) {
  11301. return array();
  11302. }
  11303. $activated_levels = array();
  11304. for ($i = 1, $c = count($this->levels); $i < $c; $i++) {
  11305. $activated_levels[] = $this->levels[$i];
  11306. if ($this->levels[$i] == $level) break;
  11307. }
  11308. if ($i == $c) {
  11309. trigger_error(
  11310. 'Tidy level ' . htmlspecialchars($level) . ' not recognized',
  11311. E_USER_WARNING
  11312. );
  11313. return array();
  11314. }
  11315. $ret = array();
  11316. foreach ($activated_levels as $level) {
  11317. foreach ($this->fixesForLevel[$level] as $fix) {
  11318. $ret[$fix] = true;
  11319. }
  11320. }
  11321. return $ret;
  11322. }
  11323. /**
  11324. * Dynamically populates the $fixesForLevel member variable using
  11325. * the fixes array. It may be custom overloaded, used in conjunction
  11326. * with $defaultLevel, or not used at all.
  11327. */
  11328. public function makeFixesForLevel($fixes) {
  11329. if (!isset($this->defaultLevel)) return;
  11330. if (!isset($this->fixesForLevel[$this->defaultLevel])) {
  11331. trigger_error(
  11332. 'Default level ' . $this->defaultLevel . ' does not exist',
  11333. E_USER_ERROR
  11334. );
  11335. return;
  11336. }
  11337. $this->fixesForLevel[$this->defaultLevel] = array_keys($fixes);
  11338. }
  11339. /**
  11340. * Populates the module with transforms and other special-case code
  11341. * based on a list of fixes passed to it
  11342. * @param $lookup Lookup table of fixes to activate
  11343. */
  11344. public function populate($fixes) {
  11345. foreach ($fixes as $name => $fix) {
  11346. // determine what the fix is for
  11347. list($type, $params) = $this->getFixType($name);
  11348. switch ($type) {
  11349. case 'attr_transform_pre':
  11350. case 'attr_transform_post':
  11351. $attr = $params['attr'];
  11352. if (isset($params['element'])) {
  11353. $element = $params['element'];
  11354. if (empty($this->info[$element])) {
  11355. $e = $this->addBlankElement($element);
  11356. } else {
  11357. $e = $this->info[$element];
  11358. }
  11359. } else {
  11360. $type = "info_$type";
  11361. $e = $this;
  11362. }
  11363. // PHP does some weird parsing when I do
  11364. // $e->$type[$attr], so I have to assign a ref.
  11365. $f =& $e->$type;
  11366. $f[$attr] = $fix;
  11367. break;
  11368. case 'tag_transform':
  11369. $this->info_tag_transform[$params['element']] = $fix;
  11370. break;
  11371. case 'child':
  11372. case 'content_model_type':
  11373. $element = $params['element'];
  11374. if (empty($this->info[$element])) {
  11375. $e = $this->addBlankElement($element);
  11376. } else {
  11377. $e = $this->info[$element];
  11378. }
  11379. $e->$type = $fix;
  11380. break;
  11381. default:
  11382. trigger_error("Fix type $type not supported", E_USER_ERROR);
  11383. break;
  11384. }
  11385. }
  11386. }
  11387. /**
  11388. * Parses a fix name and determines what kind of fix it is, as well
  11389. * as other information defined by the fix
  11390. * @param $name String name of fix
  11391. * @return array(string $fix_type, array $fix_parameters)
  11392. * @note $fix_parameters is type dependant, see populate() for usage
  11393. * of these parameters
  11394. */
  11395. public function getFixType($name) {
  11396. // parse it
  11397. $property = $attr = null;
  11398. if (strpos($name, '#') !== false) list($name, $property) = explode('#', $name);
  11399. if (strpos($name, '@') !== false) list($name, $attr) = explode('@', $name);
  11400. // figure out the parameters
  11401. $params = array();
  11402. if ($name !== '') $params['element'] = $name;
  11403. if (!is_null($attr)) $params['attr'] = $attr;
  11404. // special case: attribute transform
  11405. if (!is_null($attr)) {
  11406. if (is_null($property)) $property = 'pre';
  11407. $type = 'attr_transform_' . $property;
  11408. return array($type, $params);
  11409. }
  11410. // special case: tag transform
  11411. if (is_null($property)) {
  11412. return array('tag_transform', $params);
  11413. }
  11414. return array($property, $params);
  11415. }
  11416. /**
  11417. * Defines all fixes the module will perform in a compact
  11418. * associative array of fix name to fix implementation.
  11419. */
  11420. public function makeFixes() {}
  11421. }
  11422. class HTMLPurifier_HTMLModule_XMLCommonAttributes extends HTMLPurifier_HTMLModule
  11423. {
  11424. public $name = 'XMLCommonAttributes';
  11425. public $attr_collections = array(
  11426. 'Lang' => array(
  11427. 'xml:lang' => 'LanguageCode',
  11428. )
  11429. );
  11430. }
  11431. /**
  11432. * Name is deprecated, but allowed in strict doctypes, so onl
  11433. */
  11434. class HTMLPurifier_HTMLModule_Tidy_Name extends HTMLPurifier_HTMLModule_Tidy
  11435. {
  11436. public $name = 'Tidy_Name';
  11437. public $defaultLevel = 'heavy';
  11438. public function makeFixes() {
  11439. $r = array();
  11440. // @name for img, a -----------------------------------------------
  11441. // Technically, it's allowed even on strict, so we allow authors to use
  11442. // it. However, it's deprecated in future versions of XHTML.
  11443. $r['img@name'] =
  11444. $r['a@name'] = new HTMLPurifier_AttrTransform_Name();
  11445. return $r;
  11446. }
  11447. }
  11448. class HTMLPurifier_HTMLModule_Tidy_Proprietary extends HTMLPurifier_HTMLModule_Tidy
  11449. {
  11450. public $name = 'Tidy_Proprietary';
  11451. public $defaultLevel = 'light';
  11452. public function makeFixes() {
  11453. $r = array();
  11454. $r['table@background'] = new HTMLPurifier_AttrTransform_Background();
  11455. $r['td@background'] = new HTMLPurifier_AttrTransform_Background();
  11456. $r['th@background'] = new HTMLPurifier_AttrTransform_Background();
  11457. $r['tr@background'] = new HTMLPurifier_AttrTransform_Background();
  11458. $r['thead@background'] = new HTMLPurifier_AttrTransform_Background();
  11459. $r['tfoot@background'] = new HTMLPurifier_AttrTransform_Background();
  11460. $r['tbody@background'] = new HTMLPurifier_AttrTransform_Background();
  11461. $r['table@height'] = new HTMLPurifier_AttrTransform_Length('height');
  11462. return $r;
  11463. }
  11464. }
  11465. class HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends HTMLPurifier_HTMLModule_Tidy
  11466. {
  11467. public function makeFixes() {
  11468. $r = array();
  11469. // == deprecated tag transforms ===================================
  11470. $r['font'] = new HTMLPurifier_TagTransform_Font();
  11471. $r['menu'] = new HTMLPurifier_TagTransform_Simple('ul');
  11472. $r['dir'] = new HTMLPurifier_TagTransform_Simple('ul');
  11473. $r['center'] = new HTMLPurifier_TagTransform_Simple('div', 'text-align:center;');
  11474. $r['u'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:underline;');
  11475. $r['s'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;');
  11476. $r['strike'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;');
  11477. // == deprecated attribute transforms =============================
  11478. $r['caption@align'] =
  11479. new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
  11480. // we're following IE's behavior, not Firefox's, due
  11481. // to the fact that no one supports caption-side:right,
  11482. // W3C included (with CSS 2.1). This is a slightly
  11483. // unreasonable attribute!
  11484. 'left' => 'text-align:left;',
  11485. 'right' => 'text-align:right;',
  11486. 'top' => 'caption-side:top;',
  11487. 'bottom' => 'caption-side:bottom;' // not supported by IE
  11488. ));
  11489. // @align for img -------------------------------------------------
  11490. $r['img@align'] =
  11491. new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
  11492. 'left' => 'float:left;',
  11493. 'right' => 'float:right;',
  11494. 'top' => 'vertical-align:top;',
  11495. 'middle' => 'vertical-align:middle;',
  11496. 'bottom' => 'vertical-align:baseline;',
  11497. ));
  11498. // @align for table -----------------------------------------------
  11499. $r['table@align'] =
  11500. new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
  11501. 'left' => 'float:left;',
  11502. 'center' => 'margin-left:auto;margin-right:auto;',
  11503. 'right' => 'float:right;'
  11504. ));
  11505. // @align for hr -----------------------------------------------
  11506. $r['hr@align'] =
  11507. new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
  11508. // we use both text-align and margin because these work
  11509. // for different browsers (IE and Firefox, respectively)
  11510. // and the melange makes for a pretty cross-compatible
  11511. // solution
  11512. 'left' => 'margin-left:0;margin-right:auto;text-align:left;',
  11513. 'center' => 'margin-left:auto;margin-right:auto;text-align:center;',
  11514. 'right' => 'margin-left:auto;margin-right:0;text-align:right;'
  11515. ));
  11516. // @align for h1, h2, h3, h4, h5, h6, p, div ----------------------
  11517. // {{{
  11518. $align_lookup = array();
  11519. $align_values = array('left', 'right', 'center', 'justify');
  11520. foreach ($align_values as $v) $align_lookup[$v] = "text-align:$v;";
  11521. // }}}
  11522. $r['h1@align'] =
  11523. $r['h2@align'] =
  11524. $r['h3@align'] =
  11525. $r['h4@align'] =
  11526. $r['h5@align'] =
  11527. $r['h6@align'] =
  11528. $r['p@align'] =
  11529. $r['div@align'] =
  11530. new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup);
  11531. // @bgcolor for table, tr, td, th ---------------------------------
  11532. $r['table@bgcolor'] =
  11533. $r['td@bgcolor'] =
  11534. $r['th@bgcolor'] =
  11535. new HTMLPurifier_AttrTransform_BgColor();
  11536. // @border for img ------------------------------------------------
  11537. $r['img@border'] = new HTMLPurifier_AttrTransform_Border();
  11538. // @clear for br --------------------------------------------------
  11539. $r['br@clear'] =
  11540. new HTMLPurifier_AttrTransform_EnumToCSS('clear', array(
  11541. 'left' => 'clear:left;',
  11542. 'right' => 'clear:right;',
  11543. 'all' => 'clear:both;',
  11544. 'none' => 'clear:none;',
  11545. ));
  11546. // @height for td, th ---------------------------------------------
  11547. $r['td@height'] =
  11548. $r['th@height'] =
  11549. new HTMLPurifier_AttrTransform_Length('height');
  11550. // @hspace for img ------------------------------------------------
  11551. $r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace');
  11552. // @noshade for hr ------------------------------------------------
  11553. // this transformation is not precise but often good enough.
  11554. // different browsers use different styles to designate noshade
  11555. $r['hr@noshade'] =
  11556. new HTMLPurifier_AttrTransform_BoolToCSS(
  11557. 'noshade',
  11558. 'color:#808080;background-color:#808080;border:0;'
  11559. );
  11560. // @nowrap for td, th ---------------------------------------------
  11561. $r['td@nowrap'] =
  11562. $r['th@nowrap'] =
  11563. new HTMLPurifier_AttrTransform_BoolToCSS(
  11564. 'nowrap',
  11565. 'white-space:nowrap;'
  11566. );
  11567. // @size for hr --------------------------------------------------
  11568. $r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size', 'height');
  11569. // @type for li, ol, ul -------------------------------------------
  11570. // {{{
  11571. $ul_types = array(
  11572. 'disc' => 'list-style-type:disc;',
  11573. 'square' => 'list-style-type:square;',
  11574. 'circle' => 'list-style-type:circle;'
  11575. );
  11576. $ol_types = array(
  11577. '1' => 'list-style-type:decimal;',
  11578. 'i' => 'list-style-type:lower-roman;',
  11579. 'I' => 'list-style-type:upper-roman;',
  11580. 'a' => 'list-style-type:lower-alpha;',
  11581. 'A' => 'list-style-type:upper-alpha;'
  11582. );
  11583. $li_types = $ul_types + $ol_types;
  11584. // }}}
  11585. $r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types);
  11586. $r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true);
  11587. $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true);
  11588. // @vspace for img ------------------------------------------------
  11589. $r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace');
  11590. // @width for hr, td, th ------------------------------------------
  11591. $r['td@width'] =
  11592. $r['th@width'] =
  11593. $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width');
  11594. return $r;
  11595. }
  11596. }
  11597. class HTMLPurifier_HTMLModule_Tidy_Strict extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
  11598. {
  11599. public $name = 'Tidy_Strict';
  11600. public $defaultLevel = 'light';
  11601. public function makeFixes() {
  11602. $r = parent::makeFixes();
  11603. $r['blockquote#content_model_type'] = 'strictblockquote';
  11604. return $r;
  11605. }
  11606. public $defines_child_def = true;
  11607. public function getChildDef($def) {
  11608. if ($def->content_model_type != 'strictblockquote') return parent::getChildDef($def);
  11609. return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model);
  11610. }
  11611. }
  11612. class HTMLPurifier_HTMLModule_Tidy_Transitional extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
  11613. {
  11614. public $name = 'Tidy_Transitional';
  11615. public $defaultLevel = 'heavy';
  11616. }
  11617. class HTMLPurifier_HTMLModule_Tidy_XHTML extends HTMLPurifier_HTMLModule_Tidy
  11618. {
  11619. public $name = 'Tidy_XHTML';
  11620. public $defaultLevel = 'medium';
  11621. public function makeFixes() {
  11622. $r = array();
  11623. $r['@lang'] = new HTMLPurifier_AttrTransform_Lang();
  11624. return $r;
  11625. }
  11626. }
  11627. /**
  11628. * Injector that auto paragraphs text in the root node based on
  11629. * double-spacing.
  11630. * @todo Ensure all states are unit tested, including variations as well.
  11631. * @todo Make a graph of the flow control for this Injector.
  11632. */
  11633. class HTMLPurifier_Injector_AutoParagraph extends HTMLPurifier_Injector
  11634. {
  11635. public $name = 'AutoParagraph';
  11636. public $needed = array('p');
  11637. private function _pStart() {
  11638. $par = new HTMLPurifier_Token_Start('p');
  11639. $par->armor['MakeWellFormed_TagClosedError'] = true;
  11640. return $par;
  11641. }
  11642. public function handleText(&$token) {
  11643. $text = $token->data;
  11644. // Does the current parent allow <p> tags?
  11645. if ($this->allowsElement('p')) {
  11646. if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) {
  11647. // Note that we have differing behavior when dealing with text
  11648. // in the anonymous root node, or a node inside the document.
  11649. // If the text as a double-newline, the treatment is the same;
  11650. // if it doesn't, see the next if-block if you're in the document.
  11651. $i = $nesting = null;
  11652. if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) {
  11653. // State 1.1: ... ^ (whitespace, then document end)
  11654. // ----
  11655. // This is a degenerate case
  11656. } else {
  11657. if (!$token->is_whitespace || $this->_isInline($current)) {
  11658. // State 1.2: PAR1
  11659. // ----
  11660. // State 1.3: PAR1\n\nPAR2
  11661. // ------------
  11662. // State 1.4: <div>PAR1\n\nPAR2 (see State 2)
  11663. // ------------
  11664. $token = array($this->_pStart());
  11665. $this->_splitText($text, $token);
  11666. } else {
  11667. // State 1.5: \n<hr />
  11668. // --
  11669. }
  11670. }
  11671. } else {
  11672. // State 2: <div>PAR1... (similar to 1.4)
  11673. // ----
  11674. // We're in an element that allows paragraph tags, but we're not
  11675. // sure if we're going to need them.
  11676. if ($this->_pLookAhead()) {
  11677. // State 2.1: <div>PAR1<b>PAR1\n\nPAR2
  11678. // ----
  11679. // Note: This will always be the first child, since any
  11680. // previous inline element would have triggered this very
  11681. // same routine, and found the double newline. One possible
  11682. // exception would be a comment.
  11683. $token = array($this->_pStart(), $token);
  11684. } else {
  11685. // State 2.2.1: <div>PAR1<div>
  11686. // ----
  11687. // State 2.2.2: <div>PAR1<b>PAR1</b></div>
  11688. // ----
  11689. }
  11690. }
  11691. // Is the current parent a <p> tag?
  11692. } elseif (
  11693. !empty($this->currentNesting) &&
  11694. $this->currentNesting[count($this->currentNesting)-1]->name == 'p'
  11695. ) {
  11696. // State 3.1: ...<p>PAR1
  11697. // ----
  11698. // State 3.2: ...<p>PAR1\n\nPAR2
  11699. // ------------
  11700. $token = array();
  11701. $this->_splitText($text, $token);
  11702. // Abort!
  11703. } else {
  11704. // State 4.1: ...<b>PAR1
  11705. // ----
  11706. // State 4.2: ...<b>PAR1\n\nPAR2
  11707. // ------------
  11708. }
  11709. }
  11710. public function handleElement(&$token) {
  11711. // We don't have to check if we're already in a <p> tag for block
  11712. // tokens, because the tag would have been autoclosed by MakeWellFormed.
  11713. if ($this->allowsElement('p')) {
  11714. if (!empty($this->currentNesting)) {
  11715. if ($this->_isInline($token)) {
  11716. // State 1: <div>...<b>
  11717. // ---
  11718. // Check if this token is adjacent to the parent token
  11719. // (seek backwards until token isn't whitespace)
  11720. $i = null;
  11721. $this->backward($i, $prev);
  11722. if (!$prev instanceof HTMLPurifier_Token_Start) {
  11723. // Token wasn't adjacent
  11724. if (
  11725. $prev instanceof HTMLPurifier_Token_Text &&
  11726. substr($prev->data, -2) === "\n\n"
  11727. ) {
  11728. // State 1.1.4: <div><p>PAR1</p>\n\n<b>
  11729. // ---
  11730. // Quite frankly, this should be handled by splitText
  11731. $token = array($this->_pStart(), $token);
  11732. } else {
  11733. // State 1.1.1: <div><p>PAR1</p><b>
  11734. // ---
  11735. // State 1.1.2: <div><br /><b>
  11736. // ---
  11737. // State 1.1.3: <div>PAR<b>
  11738. // ---
  11739. }
  11740. } else {
  11741. // State 1.2.1: <div><b>
  11742. // ---
  11743. // Lookahead to see if <p> is needed.
  11744. if ($this->_pLookAhead()) {
  11745. // State 1.3.1: <div><b>PAR1\n\nPAR2
  11746. // ---
  11747. $token = array($this->_pStart(), $token);
  11748. } else {
  11749. // State 1.3.2: <div><b>PAR1</b></div>
  11750. // ---
  11751. // State 1.3.3: <div><b>PAR1</b><div></div>\n\n</div>
  11752. // ---
  11753. }
  11754. }
  11755. } else {
  11756. // State 2.3: ...<div>
  11757. // -----
  11758. }
  11759. } else {
  11760. if ($this->_isInline($token)) {
  11761. // State 3.1: <b>
  11762. // ---
  11763. // This is where the {p} tag is inserted, not reflected in
  11764. // inputTokens yet, however.
  11765. $token = array($this->_pStart(), $token);
  11766. } else {
  11767. // State 3.2: <div>
  11768. // -----
  11769. }
  11770. $i = null;
  11771. if ($this->backward($i, $prev)) {
  11772. if (
  11773. !$prev instanceof HTMLPurifier_Token_Text
  11774. ) {
  11775. // State 3.1.1: ...</p>{p}<b>
  11776. // ---
  11777. // State 3.2.1: ...</p><div>
  11778. // -----
  11779. if (!is_array($token)) $token = array($token);
  11780. array_unshift($token, new HTMLPurifier_Token_Text("\n\n"));
  11781. } else {
  11782. // State 3.1.2: ...</p>\n\n{p}<b>
  11783. // ---
  11784. // State 3.2.2: ...</p>\n\n<div>
  11785. // -----
  11786. // Note: PAR<ELEM> cannot occur because PAR would have been
  11787. // wrapped in <p> tags.
  11788. }
  11789. }
  11790. }
  11791. } else {
  11792. // State 2.2: <ul><li>
  11793. // ----
  11794. // State 2.4: <p><b>
  11795. // ---
  11796. }
  11797. }
  11798. /**
  11799. * Splits up a text in paragraph tokens and appends them
  11800. * to the result stream that will replace the original
  11801. * @param $data String text data that will be processed
  11802. * into paragraphs
  11803. * @param $result Reference to array of tokens that the
  11804. * tags will be appended onto
  11805. * @param $config Instance of HTMLPurifier_Config
  11806. * @param $context Instance of HTMLPurifier_Context
  11807. */
  11808. private function _splitText($data, &$result) {
  11809. $raw_paragraphs = explode("\n\n", $data);
  11810. $paragraphs = array(); // without empty paragraphs
  11811. $needs_start = false;
  11812. $needs_end = false;
  11813. $c = count($raw_paragraphs);
  11814. if ($c == 1) {
  11815. // There were no double-newlines, abort quickly. In theory this
  11816. // should never happen.
  11817. $result[] = new HTMLPurifier_Token_Text($data);
  11818. return;
  11819. }
  11820. for ($i = 0; $i < $c; $i++) {
  11821. $par = $raw_paragraphs[$i];
  11822. if (trim($par) !== '') {
  11823. $paragraphs[] = $par;
  11824. } else {
  11825. if ($i == 0) {
  11826. // Double newline at the front
  11827. if (empty($result)) {
  11828. // The empty result indicates that the AutoParagraph
  11829. // injector did not add any start paragraph tokens.
  11830. // This means that we have been in a paragraph for
  11831. // a while, and the newline means we should start a new one.
  11832. $result[] = new HTMLPurifier_Token_End('p');
  11833. $result[] = new HTMLPurifier_Token_Text("\n\n");
  11834. // However, the start token should only be added if
  11835. // there is more processing to be done (i.e. there are
  11836. // real paragraphs in here). If there are none, the
  11837. // next start paragraph tag will be handled by the
  11838. // next call to the injector
  11839. $needs_start = true;
  11840. } else {
  11841. // We just started a new paragraph!
  11842. // Reinstate a double-newline for presentation's sake, since
  11843. // it was in the source code.
  11844. array_unshift($result, new HTMLPurifier_Token_Text("\n\n"));
  11845. }
  11846. } elseif ($i + 1 == $c) {
  11847. // Double newline at the end
  11848. // There should be a trailing </p> when we're finally done.
  11849. $needs_end = true;
  11850. }
  11851. }
  11852. }
  11853. // Check if this was just a giant blob of whitespace. Move this earlier,
  11854. // perhaps?
  11855. if (empty($paragraphs)) {
  11856. return;
  11857. }
  11858. // Add the start tag indicated by \n\n at the beginning of $data
  11859. if ($needs_start) {
  11860. $result[] = $this->_pStart();
  11861. }
  11862. // Append the paragraphs onto the result
  11863. foreach ($paragraphs as $par) {
  11864. $result[] = new HTMLPurifier_Token_Text($par);
  11865. $result[] = new HTMLPurifier_Token_End('p');
  11866. $result[] = new HTMLPurifier_Token_Text("\n\n");
  11867. $result[] = $this->_pStart();
  11868. }
  11869. // Remove trailing start token; Injector will handle this later if
  11870. // it was indeed needed. This prevents from needing to do a lookahead,
  11871. // at the cost of a lookbehind later.
  11872. array_pop($result);
  11873. // If there is no need for an end tag, remove all of it and let
  11874. // MakeWellFormed close it later.
  11875. if (!$needs_end) {
  11876. array_pop($result); // removes \n\n
  11877. array_pop($result); // removes </p>
  11878. }
  11879. }
  11880. /**
  11881. * Returns true if passed token is inline (and, ergo, allowed in
  11882. * paragraph tags)
  11883. */
  11884. private function _isInline($token) {
  11885. return isset($this->htmlDefinition->info['p']->child->elements[$token->name]);
  11886. }
  11887. /**
  11888. * Looks ahead in the token list and determines whether or not we need
  11889. * to insert a <p> tag.
  11890. */
  11891. private function _pLookAhead() {
  11892. $this->current($i, $current);
  11893. if ($current instanceof HTMLPurifier_Token_Start) $nesting = 1;
  11894. else $nesting = 0;
  11895. $ok = false;
  11896. while ($this->forwardUntilEndToken($i, $current, $nesting)) {
  11897. $result = $this->_checkNeedsP($current);
  11898. if ($result !== null) {
  11899. $ok = $result;
  11900. break;
  11901. }
  11902. }
  11903. return $ok;
  11904. }
  11905. /**
  11906. * Determines if a particular token requires an earlier inline token
  11907. * to get a paragraph. This should be used with _forwardUntilEndToken
  11908. */
  11909. private function _checkNeedsP($current) {
  11910. if ($current instanceof HTMLPurifier_Token_Start){
  11911. if (!$this->_isInline($current)) {
  11912. // <div>PAR1<div>
  11913. // ----
  11914. // Terminate early, since we hit a block element
  11915. return false;
  11916. }
  11917. } elseif ($current instanceof HTMLPurifier_Token_Text) {
  11918. if (strpos($current->data, "\n\n") !== false) {
  11919. // <div>PAR1<b>PAR1\n\nPAR2
  11920. // ----
  11921. return true;
  11922. } else {
  11923. // <div>PAR1<b>PAR1...
  11924. // ----
  11925. }
  11926. }
  11927. return null;
  11928. }
  11929. }
  11930. /**
  11931. * Injector that displays the URL of an anchor instead of linking to it, in addition to showing the text of the link.
  11932. */
  11933. class HTMLPurifier_Injector_DisplayLinkURI extends HTMLPurifier_Injector
  11934. {
  11935. public $name = 'DisplayLinkURI';
  11936. public $needed = array('a');
  11937. public function handleElement(&$token) {
  11938. }
  11939. public function handleEnd(&$token) {
  11940. if (isset($token->start->attr['href'])){
  11941. $url = $token->start->attr['href'];
  11942. unset($token->start->attr['href']);
  11943. $token = array($token, new HTMLPurifier_Token_Text(" ($url)"));
  11944. } else {
  11945. // nothing to display
  11946. }
  11947. }
  11948. }
  11949. /**
  11950. * Injector that converts http, https and ftp text URLs to actual links.
  11951. */
  11952. class HTMLPurifier_Injector_Linkify extends HTMLPurifier_Injector
  11953. {
  11954. public $name = 'Linkify';
  11955. public $needed = array('a' => array('href'));
  11956. public function handleText(&$token) {
  11957. if (!$this->allowsElement('a')) return;
  11958. if (strpos($token->data, '://') === false) {
  11959. // our really quick heuristic failed, abort
  11960. // this may not work so well if we want to match things like
  11961. // "google.com", but then again, most people don't
  11962. return;
  11963. }
  11964. // there is/are URL(s). Let's split the string:
  11965. // Note: this regex is extremely permissive
  11966. $bits = preg_split('#((?:https?|ftp)://[^\s\'"<>()]+)#S', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
  11967. $token = array();
  11968. // $i = index
  11969. // $c = count
  11970. // $l = is link
  11971. for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
  11972. if (!$l) {
  11973. if ($bits[$i] === '') continue;
  11974. $token[] = new HTMLPurifier_Token_Text($bits[$i]);
  11975. } else {
  11976. $token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i]));
  11977. $token[] = new HTMLPurifier_Token_Text($bits[$i]);
  11978. $token[] = new HTMLPurifier_Token_End('a');
  11979. }
  11980. }
  11981. }
  11982. }
  11983. /**
  11984. * Injector that converts configuration directive syntax %Namespace.Directive
  11985. * to links
  11986. */
  11987. class HTMLPurifier_Injector_PurifierLinkify extends HTMLPurifier_Injector
  11988. {
  11989. public $name = 'PurifierLinkify';
  11990. public $docURL;
  11991. public $needed = array('a' => array('href'));
  11992. public function prepare($config, $context) {
  11993. $this->docURL = $config->get('AutoFormat.PurifierLinkify.DocURL');
  11994. return parent::prepare($config, $context);
  11995. }
  11996. public function handleText(&$token) {
  11997. if (!$this->allowsElement('a')) return;
  11998. if (strpos($token->data, '%') === false) return;
  11999. $bits = preg_split('#%([a-z0-9]+\.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
  12000. $token = array();
  12001. // $i = index
  12002. // $c = count
  12003. // $l = is link
  12004. for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
  12005. if (!$l) {
  12006. if ($bits[$i] === '') continue;
  12007. $token[] = new HTMLPurifier_Token_Text($bits[$i]);
  12008. } else {
  12009. $token[] = new HTMLPurifier_Token_Start('a',
  12010. array('href' => str_replace('%s', $bits[$i], $this->docURL)));
  12011. $token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]);
  12012. $token[] = new HTMLPurifier_Token_End('a');
  12013. }
  12014. }
  12015. }
  12016. }
  12017. class HTMLPurifier_Injector_RemoveEmpty extends HTMLPurifier_Injector
  12018. {
  12019. private $context, $config, $attrValidator, $removeNbsp, $removeNbspExceptions;
  12020. // TODO: make me configurable
  12021. private $_exclude = array('colgroup' => 1, 'th' => 1, 'td' => 1, 'iframe' => 1);
  12022. public function prepare($config, $context) {
  12023. parent::prepare($config, $context);
  12024. $this->config = $config;
  12025. $this->context = $context;
  12026. $this->removeNbsp = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp');
  12027. $this->removeNbspExceptions = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions');
  12028. $this->attrValidator = new HTMLPurifier_AttrValidator();
  12029. }
  12030. public function handleElement(&$token) {
  12031. if (!$token instanceof HTMLPurifier_Token_Start) return;
  12032. $next = false;
  12033. for ($i = $this->inputIndex + 1, $c = count($this->inputTokens); $i < $c; $i++) {
  12034. $next = $this->inputTokens[$i];
  12035. if ($next instanceof HTMLPurifier_Token_Text) {
  12036. if ($next->is_whitespace) continue;
  12037. if ($this->removeNbsp && !isset($this->removeNbspExceptions[$token->name])) {
  12038. $plain = str_replace("\xC2\xA0", "", $next->data);
  12039. $isWsOrNbsp = $plain === '' || ctype_space($plain);
  12040. if ($isWsOrNbsp) continue;
  12041. }
  12042. }
  12043. break;
  12044. }
  12045. if (!$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) {
  12046. if (isset($this->_exclude[$token->name])) return;
  12047. $this->attrValidator->validateToken($token, $this->config, $this->context);
  12048. $token->armor['ValidateAttributes'] = true;
  12049. if (isset($token->attr['id']) || isset($token->attr['name'])) return;
  12050. $token = $i - $this->inputIndex + 1;
  12051. for ($b = $this->inputIndex - 1; $b > 0; $b--) {
  12052. $prev = $this->inputTokens[$b];
  12053. if ($prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) continue;
  12054. break;
  12055. }
  12056. // This is safe because we removed the token that triggered this.
  12057. $this->rewind($b - 1);
  12058. return;
  12059. }
  12060. }
  12061. }
  12062. /**
  12063. * Injector that removes spans with no attributes
  12064. */
  12065. class HTMLPurifier_Injector_RemoveSpansWithoutAttributes extends HTMLPurifier_Injector
  12066. {
  12067. public $name = 'RemoveSpansWithoutAttributes';
  12068. public $needed = array('span');
  12069. private $attrValidator;
  12070. /**
  12071. * Used by AttrValidator
  12072. */
  12073. private $config;
  12074. private $context;
  12075. public function prepare($config, $context) {
  12076. $this->attrValidator = new HTMLPurifier_AttrValidator();
  12077. $this->config = $config;
  12078. $this->context = $context;
  12079. return parent::prepare($config, $context);
  12080. }
  12081. public function handleElement(&$token) {
  12082. if ($token->name !== 'span' || !$token instanceof HTMLPurifier_Token_Start) {
  12083. return;
  12084. }
  12085. // We need to validate the attributes now since this doesn't normally
  12086. // happen until after MakeWellFormed. If all the attributes are removed
  12087. // the span needs to be removed too.
  12088. $this->attrValidator->validateToken($token, $this->config, $this->context);
  12089. $token->armor['ValidateAttributes'] = true;
  12090. if (!empty($token->attr)) {
  12091. return;
  12092. }
  12093. $nesting = 0;
  12094. $spanContentTokens = array();
  12095. while ($this->forwardUntilEndToken($i, $current, $nesting)) {}
  12096. if ($current instanceof HTMLPurifier_Token_End && $current->name === 'span') {
  12097. // Mark closing span tag for deletion
  12098. $current->markForDeletion = true;
  12099. // Delete open span tag
  12100. $token = false;
  12101. }
  12102. }
  12103. public function handleEnd(&$token) {
  12104. if ($token->markForDeletion) {
  12105. $token = false;
  12106. }
  12107. }
  12108. }
  12109. /**
  12110. * Adds important param elements to inside of object in order to make
  12111. * things safe.
  12112. */
  12113. class HTMLPurifier_Injector_SafeObject extends HTMLPurifier_Injector
  12114. {
  12115. public $name = 'SafeObject';
  12116. public $needed = array('object', 'param');
  12117. protected $objectStack = array();
  12118. protected $paramStack = array();
  12119. // Keep this synchronized with AttrTransform/SafeParam.php
  12120. protected $addParam = array(
  12121. 'allowScriptAccess' => 'never',
  12122. 'allowNetworking' => 'internal',
  12123. );
  12124. protected $allowedParam = array(
  12125. 'wmode' => true,
  12126. 'movie' => true,
  12127. 'flashvars' => true,
  12128. 'src' => true,
  12129. 'allowFullScreen' => true, // if omitted, assume to be 'false'
  12130. );
  12131. public function prepare($config, $context) {
  12132. parent::prepare($config, $context);
  12133. }
  12134. public function handleElement(&$token) {
  12135. if ($token->name == 'object') {
  12136. $this->objectStack[] = $token;
  12137. $this->paramStack[] = array();
  12138. $new = array($token);
  12139. foreach ($this->addParam as $name => $value) {
  12140. $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value));
  12141. }
  12142. $token = $new;
  12143. } elseif ($token->name == 'param') {
  12144. $nest = count($this->currentNesting) - 1;
  12145. if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') {
  12146. $i = count($this->objectStack) - 1;
  12147. if (!isset($token->attr['name'])) {
  12148. $token = false;
  12149. return;
  12150. }
  12151. $n = $token->attr['name'];
  12152. // We need this fix because YouTube doesn't supply a data
  12153. // attribute, which we need if a type is specified. This is
  12154. // *very* Flash specific.
  12155. if (!isset($this->objectStack[$i]->attr['data']) &&
  12156. ($token->attr['name'] == 'movie' || $token->attr['name'] == 'src')) {
  12157. $this->objectStack[$i]->attr['data'] = $token->attr['value'];
  12158. }
  12159. // Check if the parameter is the correct value but has not
  12160. // already been added
  12161. if (
  12162. !isset($this->paramStack[$i][$n]) &&
  12163. isset($this->addParam[$n]) &&
  12164. $token->attr['name'] === $this->addParam[$n]
  12165. ) {
  12166. // keep token, and add to param stack
  12167. $this->paramStack[$i][$n] = true;
  12168. } elseif (isset($this->allowedParam[$n])) {
  12169. // keep token, don't do anything to it
  12170. // (could possibly check for duplicates here)
  12171. } else {
  12172. $token = false;
  12173. }
  12174. } else {
  12175. // not directly inside an object, DENY!
  12176. $token = false;
  12177. }
  12178. }
  12179. }
  12180. public function handleEnd(&$token) {
  12181. // This is the WRONG way of handling the object and param stacks;
  12182. // we should be inserting them directly on the relevant object tokens
  12183. // so that the global stack handling handles it.
  12184. if ($token->name == 'object') {
  12185. array_pop($this->objectStack);
  12186. array_pop($this->paramStack);
  12187. }
  12188. }
  12189. }
  12190. /**
  12191. * Parser that uses PHP 5's DOM extension (part of the core).
  12192. *
  12193. * In PHP 5, the DOM XML extension was revamped into DOM and added to the core.
  12194. * It gives us a forgiving HTML parser, which we use to transform the HTML
  12195. * into a DOM, and then into the tokens. It is blazingly fast (for large
  12196. * documents, it performs twenty times faster than
  12197. * HTMLPurifier_Lexer_DirectLex,and is the default choice for PHP 5.
  12198. *
  12199. * @note Any empty elements will have empty tokens associated with them, even if
  12200. * this is prohibited by the spec. This is cannot be fixed until the spec
  12201. * comes into play.
  12202. *
  12203. * @note PHP's DOM extension does not actually parse any entities, we use
  12204. * our own function to do that.
  12205. *
  12206. * @warning DOM tends to drop whitespace, which may wreak havoc on indenting.
  12207. * If this is a huge problem, due to the fact that HTML is hand
  12208. * edited and you are unable to get a parser cache that caches the
  12209. * the output of HTML Purifier while keeping the original HTML lying
  12210. * around, you may want to run Tidy on the resulting output or use
  12211. * HTMLPurifier_DirectLex
  12212. */
  12213. class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
  12214. {
  12215. private $factory;
  12216. public function __construct() {
  12217. // setup the factory
  12218. parent::__construct();
  12219. $this->factory = new HTMLPurifier_TokenFactory();
  12220. }
  12221. public function tokenizeHTML($html, $config, $context) {
  12222. $html = $this->normalize($html, $config, $context);
  12223. // attempt to armor stray angled brackets that cannot possibly
  12224. // form tags and thus are probably being used as emoticons
  12225. if ($config->get('Core.AggressivelyFixLt')) {
  12226. $char = '[^a-z!\/]';
  12227. $comment = "/<!--(.*?)(-->|\z)/is";
  12228. $html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html);
  12229. do {
  12230. $old = $html;
  12231. $html = preg_replace("/<($char)/i", '&lt;\\1', $html);
  12232. } while ($html !== $old);
  12233. $html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments
  12234. }
  12235. // preprocess html, essential for UTF-8
  12236. $html = $this->wrapHTML($html, $config, $context);
  12237. $doc = new DOMDocument();
  12238. $doc->encoding = 'UTF-8'; // theoretically, the above has this covered
  12239. set_error_handler(array($this, 'muteErrorHandler'));
  12240. $doc->loadHTML($html);
  12241. restore_error_handler();
  12242. $tokens = array();
  12243. $this->tokenizeDOM(
  12244. $doc->getElementsByTagName('html')->item(0)-> // <html>
  12245. getElementsByTagName('body')->item(0)-> // <body>
  12246. getElementsByTagName('div')->item(0) // <div>
  12247. , $tokens);
  12248. return $tokens;
  12249. }
  12250. /**
  12251. * Iterative function that tokenizes a node, putting it into an accumulator.
  12252. * To iterate is human, to recurse divine - L. Peter Deutsch
  12253. * @param $node DOMNode to be tokenized.
  12254. * @param $tokens Array-list of already tokenized tokens.
  12255. * @returns Tokens of node appended to previously passed tokens.
  12256. */
  12257. protected function tokenizeDOM($node, &$tokens) {
  12258. $level = 0;
  12259. $nodes = array($level => array($node));
  12260. $closingNodes = array();
  12261. do {
  12262. while (!empty($nodes[$level])) {
  12263. $node = array_shift($nodes[$level]); // FIFO
  12264. $collect = $level > 0 ? true : false;
  12265. $needEndingTag = $this->createStartNode($node, $tokens, $collect);
  12266. if ($needEndingTag) {
  12267. $closingNodes[$level][] = $node;
  12268. }
  12269. if ($node->childNodes && $node->childNodes->length) {
  12270. $level++;
  12271. $nodes[$level] = array();
  12272. foreach ($node->childNodes as $childNode) {
  12273. array_push($nodes[$level], $childNode);
  12274. }
  12275. }
  12276. }
  12277. $level--;
  12278. if ($level && isset($closingNodes[$level])) {
  12279. while($node = array_pop($closingNodes[$level])) {
  12280. $this->createEndNode($node, $tokens);
  12281. }
  12282. }
  12283. } while ($level > 0);
  12284. }
  12285. /**
  12286. * @param $node DOMNode to be tokenized.
  12287. * @param $tokens Array-list of already tokenized tokens.
  12288. * @param $collect Says whether or start and close are collected, set to
  12289. * false at first recursion because it's the implicit DIV
  12290. * tag you're dealing with.
  12291. * @returns bool if the token needs an endtoken
  12292. */
  12293. protected function createStartNode($node, &$tokens, $collect) {
  12294. // intercept non element nodes. WE MUST catch all of them,
  12295. // but we're not getting the character reference nodes because
  12296. // those should have been preprocessed
  12297. if ($node->nodeType === XML_TEXT_NODE) {
  12298. $tokens[] = $this->factory->createText($node->data);
  12299. return false;
  12300. } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) {
  12301. // undo libxml's special treatment of <script> and <style> tags
  12302. $last = end($tokens);
  12303. $data = $node->data;
  12304. // (note $node->tagname is already normalized)
  12305. if ($last instanceof HTMLPurifier_Token_Start && ($last->name == 'script' || $last->name == 'style')) {
  12306. $new_data = trim($data);
  12307. if (substr($new_data, 0, 4) === '<!--') {
  12308. $data = substr($new_data, 4);
  12309. if (substr($data, -3) === '-->') {
  12310. $data = substr($data, 0, -3);
  12311. } else {
  12312. // Highly suspicious! Not sure what to do...
  12313. }
  12314. }
  12315. }
  12316. $tokens[] = $this->factory->createText($this->parseData($data));
  12317. return false;
  12318. } elseif ($node->nodeType === XML_COMMENT_NODE) {
  12319. // this is code is only invoked for comments in script/style in versions
  12320. // of libxml pre-2.6.28 (regular comments, of course, are still
  12321. // handled regularly)
  12322. $tokens[] = $this->factory->createComment($node->data);
  12323. return false;
  12324. } elseif (
  12325. // not-well tested: there may be other nodes we have to grab
  12326. $node->nodeType !== XML_ELEMENT_NODE
  12327. ) {
  12328. return false;
  12329. }
  12330. $attr = $node->hasAttributes() ? $this->transformAttrToAssoc($node->attributes) : array();
  12331. // We still have to make sure that the element actually IS empty
  12332. if (!$node->childNodes->length) {
  12333. if ($collect) {
  12334. $tokens[] = $this->factory->createEmpty($node->tagName, $attr);
  12335. }
  12336. return false;
  12337. } else {
  12338. if ($collect) {
  12339. $tokens[] = $this->factory->createStart(
  12340. $tag_name = $node->tagName, // somehow, it get's dropped
  12341. $attr
  12342. );
  12343. }
  12344. return true;
  12345. }
  12346. }
  12347. protected function createEndNode($node, &$tokens) {
  12348. $tokens[] = $this->factory->createEnd($node->tagName);
  12349. }
  12350. /**
  12351. * Converts a DOMNamedNodeMap of DOMAttr objects into an assoc array.
  12352. *
  12353. * @param $attribute_list DOMNamedNodeMap of DOMAttr objects.
  12354. * @returns Associative array of attributes.
  12355. */
  12356. protected function transformAttrToAssoc($node_map) {
  12357. // NamedNodeMap is documented very well, so we're using undocumented
  12358. // features, namely, the fact that it implements Iterator and
  12359. // has a ->length attribute
  12360. if ($node_map->length === 0) return array();
  12361. $array = array();
  12362. foreach ($node_map as $attr) {
  12363. $array[$attr->name] = $attr->value;
  12364. }
  12365. return $array;
  12366. }
  12367. /**
  12368. * An error handler that mutes all errors
  12369. */
  12370. public function muteErrorHandler($errno, $errstr) {}
  12371. /**
  12372. * Callback function for undoing escaping of stray angled brackets
  12373. * in comments
  12374. */
  12375. public function callbackUndoCommentSubst($matches) {
  12376. return '<!--' . strtr($matches[1], array('&amp;'=>'&','&lt;'=>'<')) . $matches[2];
  12377. }
  12378. /**
  12379. * Callback function that entity-izes ampersands in comments so that
  12380. * callbackUndoCommentSubst doesn't clobber them
  12381. */
  12382. public function callbackArmorCommentEntities($matches) {
  12383. return '<!--' . str_replace('&', '&amp;', $matches[1]) . $matches[2];
  12384. }
  12385. /**
  12386. * Wraps an HTML fragment in the necessary HTML
  12387. */
  12388. protected function wrapHTML($html, $config, $context) {
  12389. $def = $config->getDefinition('HTML');
  12390. $ret = '';
  12391. if (!empty($def->doctype->dtdPublic) || !empty($def->doctype->dtdSystem)) {
  12392. $ret .= '<!DOCTYPE html ';
  12393. if (!empty($def->doctype->dtdPublic)) $ret .= 'PUBLIC "' . $def->doctype->dtdPublic . '" ';
  12394. if (!empty($def->doctype->dtdSystem)) $ret .= '"' . $def->doctype->dtdSystem . '" ';
  12395. $ret .= '>';
  12396. }
  12397. $ret .= '<html><head>';
  12398. $ret .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
  12399. // No protection if $html contains a stray </div>!
  12400. $ret .= '</head><body><div>'.$html.'</div></body></html>';
  12401. return $ret;
  12402. }
  12403. }
  12404. /**
  12405. * Our in-house implementation of a parser.
  12406. *
  12407. * A pure PHP parser, DirectLex has absolutely no dependencies, making
  12408. * it a reasonably good default for PHP4. Written with efficiency in mind,
  12409. * it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it
  12410. * pales in comparison to HTMLPurifier_Lexer_DOMLex.
  12411. *
  12412. * @todo Reread XML spec and document differences.
  12413. */
  12414. class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
  12415. {
  12416. public $tracksLineNumbers = true;
  12417. /**
  12418. * Whitespace characters for str(c)spn.
  12419. */
  12420. protected $_whitespace = "\x20\x09\x0D\x0A";
  12421. /**
  12422. * Callback function for script CDATA fudge
  12423. * @param $matches, in form of array(opening tag, contents, closing tag)
  12424. */
  12425. protected function scriptCallback($matches) {
  12426. return $matches[1] . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8') . $matches[3];
  12427. }
  12428. public function tokenizeHTML($html, $config, $context) {
  12429. // special normalization for script tags without any armor
  12430. // our "armor" heurstic is a < sign any number of whitespaces after
  12431. // the first script tag
  12432. if ($config->get('HTML.Trusted')) {
  12433. $html = preg_replace_callback('#(<script[^>]*>)(\s*[^<].+?)(</script>)#si',
  12434. array($this, 'scriptCallback'), $html);
  12435. }
  12436. $html = $this->normalize($html, $config, $context);
  12437. $cursor = 0; // our location in the text
  12438. $inside_tag = false; // whether or not we're parsing the inside of a tag
  12439. $array = array(); // result array
  12440. // This is also treated to mean maintain *column* numbers too
  12441. $maintain_line_numbers = $config->get('Core.MaintainLineNumbers');
  12442. if ($maintain_line_numbers === null) {
  12443. // automatically determine line numbering by checking
  12444. // if error collection is on
  12445. $maintain_line_numbers = $config->get('Core.CollectErrors');
  12446. }
  12447. if ($maintain_line_numbers) {
  12448. $current_line = 1;
  12449. $current_col = 0;
  12450. $length = strlen($html);
  12451. } else {
  12452. $current_line = false;
  12453. $current_col = false;
  12454. $length = false;
  12455. }
  12456. $context->register('CurrentLine', $current_line);
  12457. $context->register('CurrentCol', $current_col);
  12458. $nl = "\n";
  12459. // how often to manually recalculate. This will ALWAYS be right,
  12460. // but it's pretty wasteful. Set to 0 to turn off
  12461. $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval');
  12462. $e = false;
  12463. if ($config->get('Core.CollectErrors')) {
  12464. $e =& $context->get('ErrorCollector');
  12465. }
  12466. // for testing synchronization
  12467. $loops = 0;
  12468. while(++$loops) {
  12469. // $cursor is either at the start of a token, or inside of
  12470. // a tag (i.e. there was a < immediately before it), as indicated
  12471. // by $inside_tag
  12472. if ($maintain_line_numbers) {
  12473. // $rcursor, however, is always at the start of a token.
  12474. $rcursor = $cursor - (int) $inside_tag;
  12475. // Column number is cheap, so we calculate it every round.
  12476. // We're interested at the *end* of the newline string, so
  12477. // we need to add strlen($nl) == 1 to $nl_pos before subtracting it
  12478. // from our "rcursor" position.
  12479. $nl_pos = strrpos($html, $nl, $rcursor - $length);
  12480. $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1);
  12481. // recalculate lines
  12482. if (
  12483. $synchronize_interval && // synchronization is on
  12484. $cursor > 0 && // cursor is further than zero
  12485. $loops % $synchronize_interval === 0 // time to synchronize!
  12486. ) {
  12487. $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
  12488. }
  12489. }
  12490. $position_next_lt = strpos($html, '<', $cursor);
  12491. $position_next_gt = strpos($html, '>', $cursor);
  12492. // triggers on "<b>asdf</b>" but not "asdf <b></b>"
  12493. // special case to set up context
  12494. if ($position_next_lt === $cursor) {
  12495. $inside_tag = true;
  12496. $cursor++;
  12497. }
  12498. if (!$inside_tag && $position_next_lt !== false) {
  12499. // We are not inside tag and there still is another tag to parse
  12500. $token = new
  12501. HTMLPurifier_Token_Text(
  12502. $this->parseData(
  12503. substr(
  12504. $html, $cursor, $position_next_lt - $cursor
  12505. )
  12506. )
  12507. );
  12508. if ($maintain_line_numbers) {
  12509. $token->rawPosition($current_line, $current_col);
  12510. $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor);
  12511. }
  12512. $array[] = $token;
  12513. $cursor = $position_next_lt + 1;
  12514. $inside_tag = true;
  12515. continue;
  12516. } elseif (!$inside_tag) {
  12517. // We are not inside tag but there are no more tags
  12518. // If we're already at the end, break
  12519. if ($cursor === strlen($html)) break;
  12520. // Create Text of rest of string
  12521. $token = new
  12522. HTMLPurifier_Token_Text(
  12523. $this->parseData(
  12524. substr(
  12525. $html, $cursor
  12526. )
  12527. )
  12528. );
  12529. if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
  12530. $array[] = $token;
  12531. break;
  12532. } elseif ($inside_tag && $position_next_gt !== false) {
  12533. // We are in tag and it is well formed
  12534. // Grab the internals of the tag
  12535. $strlen_segment = $position_next_gt - $cursor;
  12536. if ($strlen_segment < 1) {
  12537. // there's nothing to process!
  12538. $token = new HTMLPurifier_Token_Text('<');
  12539. $cursor++;
  12540. continue;
  12541. }
  12542. $segment = substr($html, $cursor, $strlen_segment);
  12543. if ($segment === false) {
  12544. // somehow, we attempted to access beyond the end of
  12545. // the string, defense-in-depth, reported by Nate Abele
  12546. break;
  12547. }
  12548. // Check if it's a comment
  12549. if (
  12550. substr($segment, 0, 3) === '!--'
  12551. ) {
  12552. // re-determine segment length, looking for -->
  12553. $position_comment_end = strpos($html, '-->', $cursor);
  12554. if ($position_comment_end === false) {
  12555. // uh oh, we have a comment that extends to
  12556. // infinity. Can't be helped: set comment
  12557. // end position to end of string
  12558. if ($e) $e->send(E_WARNING, 'Lexer: Unclosed comment');
  12559. $position_comment_end = strlen($html);
  12560. $end = true;
  12561. } else {
  12562. $end = false;
  12563. }
  12564. $strlen_segment = $position_comment_end - $cursor;
  12565. $segment = substr($html, $cursor, $strlen_segment);
  12566. $token = new
  12567. HTMLPurifier_Token_Comment(
  12568. substr(
  12569. $segment, 3, $strlen_segment - 3
  12570. )
  12571. );
  12572. if ($maintain_line_numbers) {
  12573. $token->rawPosition($current_line, $current_col);
  12574. $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment);
  12575. }
  12576. $array[] = $token;
  12577. $cursor = $end ? $position_comment_end : $position_comment_end + 3;
  12578. $inside_tag = false;
  12579. continue;
  12580. }
  12581. // Check if it's an end tag
  12582. $is_end_tag = (strpos($segment,'/') === 0);
  12583. if ($is_end_tag) {
  12584. $type = substr($segment, 1);
  12585. $token = new HTMLPurifier_Token_End($type);
  12586. if ($maintain_line_numbers) {
  12587. $token->rawPosition($current_line, $current_col);
  12588. $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
  12589. }
  12590. $array[] = $token;
  12591. $inside_tag = false;
  12592. $cursor = $position_next_gt + 1;
  12593. continue;
  12594. }
  12595. // Check leading character is alnum, if not, we may
  12596. // have accidently grabbed an emoticon. Translate into
  12597. // text and go our merry way
  12598. if (!ctype_alpha($segment[0])) {
  12599. // XML: $segment[0] !== '_' && $segment[0] !== ':'
  12600. if ($e) $e->send(E_NOTICE, 'Lexer: Unescaped lt');
  12601. $token = new HTMLPurifier_Token_Text('<');
  12602. if ($maintain_line_numbers) {
  12603. $token->rawPosition($current_line, $current_col);
  12604. $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
  12605. }
  12606. $array[] = $token;
  12607. $inside_tag = false;
  12608. continue;
  12609. }
  12610. // Check if it is explicitly self closing, if so, remove
  12611. // trailing slash. Remember, we could have a tag like <br>, so
  12612. // any later token processing scripts must convert improperly
  12613. // classified EmptyTags from StartTags.
  12614. $is_self_closing = (strrpos($segment,'/') === $strlen_segment-1);
  12615. if ($is_self_closing) {
  12616. $strlen_segment--;
  12617. $segment = substr($segment, 0, $strlen_segment);
  12618. }
  12619. // Check if there are any attributes
  12620. $position_first_space = strcspn($segment, $this->_whitespace);
  12621. if ($position_first_space >= $strlen_segment) {
  12622. if ($is_self_closing) {
  12623. $token = new HTMLPurifier_Token_Empty($segment);
  12624. } else {
  12625. $token = new HTMLPurifier_Token_Start($segment);
  12626. }
  12627. if ($maintain_line_numbers) {
  12628. $token->rawPosition($current_line, $current_col);
  12629. $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
  12630. }
  12631. $array[] = $token;
  12632. $inside_tag = false;
  12633. $cursor = $position_next_gt + 1;
  12634. continue;
  12635. }
  12636. // Grab out all the data
  12637. $type = substr($segment, 0, $position_first_space);
  12638. $attribute_string =
  12639. trim(
  12640. substr(
  12641. $segment, $position_first_space
  12642. )
  12643. );
  12644. if ($attribute_string) {
  12645. $attr = $this->parseAttributeString(
  12646. $attribute_string
  12647. , $config, $context
  12648. );
  12649. } else {
  12650. $attr = array();
  12651. }
  12652. if ($is_self_closing) {
  12653. $token = new HTMLPurifier_Token_Empty($type, $attr);
  12654. } else {
  12655. $token = new HTMLPurifier_Token_Start($type, $attr);
  12656. }
  12657. if ($maintain_line_numbers) {
  12658. $token->rawPosition($current_line, $current_col);
  12659. $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
  12660. }
  12661. $array[] = $token;
  12662. $cursor = $position_next_gt + 1;
  12663. $inside_tag = false;
  12664. continue;
  12665. } else {
  12666. // inside tag, but there's no ending > sign
  12667. if ($e) $e->send(E_WARNING, 'Lexer: Missing gt');
  12668. $token = new
  12669. HTMLPurifier_Token_Text(
  12670. '<' .
  12671. $this->parseData(
  12672. substr($html, $cursor)
  12673. )
  12674. );
  12675. if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
  12676. // no cursor scroll? Hmm...
  12677. $array[] = $token;
  12678. break;
  12679. }
  12680. break;
  12681. }
  12682. $context->destroy('CurrentLine');
  12683. $context->destroy('CurrentCol');
  12684. return $array;
  12685. }
  12686. /**
  12687. * PHP 5.0.x compatible substr_count that implements offset and length
  12688. */
  12689. protected function substrCount($haystack, $needle, $offset, $length) {
  12690. static $oldVersion;
  12691. if ($oldVersion === null) {
  12692. $oldVersion = version_compare(PHP_VERSION, '5.1', '<');
  12693. }
  12694. if ($oldVersion) {
  12695. $haystack = substr($haystack, $offset, $length);
  12696. return substr_count($haystack, $needle);
  12697. } else {
  12698. return substr_count($haystack, $needle, $offset, $length);
  12699. }
  12700. }
  12701. /**
  12702. * Takes the inside of an HTML tag and makes an assoc array of attributes.
  12703. *
  12704. * @param $string Inside of tag excluding name.
  12705. * @returns Assoc array of attributes.
  12706. */
  12707. public function parseAttributeString($string, $config, $context) {
  12708. $string = (string) $string; // quick typecast
  12709. if ($string == '') return array(); // no attributes
  12710. $e = false;
  12711. if ($config->get('Core.CollectErrors')) {
  12712. $e =& $context->get('ErrorCollector');
  12713. }
  12714. // let's see if we can abort as quickly as possible
  12715. // one equal sign, no spaces => one attribute
  12716. $num_equal = substr_count($string, '=');
  12717. $has_space = strpos($string, ' ');
  12718. if ($num_equal === 0 && !$has_space) {
  12719. // bool attribute
  12720. return array($string => $string);
  12721. } elseif ($num_equal === 1 && !$has_space) {
  12722. // only one attribute
  12723. list($key, $quoted_value) = explode('=', $string);
  12724. $quoted_value = trim($quoted_value);
  12725. if (!$key) {
  12726. if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
  12727. return array();
  12728. }
  12729. if (!$quoted_value) return array($key => '');
  12730. $first_char = @$quoted_value[0];
  12731. $last_char = @$quoted_value[strlen($quoted_value)-1];
  12732. $same_quote = ($first_char == $last_char);
  12733. $open_quote = ($first_char == '"' || $first_char == "'");
  12734. if ( $same_quote && $open_quote) {
  12735. // well behaved
  12736. $value = substr($quoted_value, 1, strlen($quoted_value) - 2);
  12737. } else {
  12738. // not well behaved
  12739. if ($open_quote) {
  12740. if ($e) $e->send(E_ERROR, 'Lexer: Missing end quote');
  12741. $value = substr($quoted_value, 1);
  12742. } else {
  12743. $value = $quoted_value;
  12744. }
  12745. }
  12746. if ($value === false) $value = '';
  12747. return array($key => $this->parseData($value));
  12748. }
  12749. // setup loop environment
  12750. $array = array(); // return assoc array of attributes
  12751. $cursor = 0; // current position in string (moves forward)
  12752. $size = strlen($string); // size of the string (stays the same)
  12753. // if we have unquoted attributes, the parser expects a terminating
  12754. // space, so let's guarantee that there's always a terminating space.
  12755. $string .= ' ';
  12756. while(true) {
  12757. if ($cursor >= $size) {
  12758. break;
  12759. }
  12760. $cursor += ($value = strspn($string, $this->_whitespace, $cursor));
  12761. // grab the key
  12762. $key_begin = $cursor; //we're currently at the start of the key
  12763. // scroll past all characters that are the key (not whitespace or =)
  12764. $cursor += strcspn($string, $this->_whitespace . '=', $cursor);
  12765. $key_end = $cursor; // now at the end of the key
  12766. $key = substr($string, $key_begin, $key_end - $key_begin);
  12767. if (!$key) {
  12768. if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
  12769. $cursor += strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop
  12770. continue; // empty key
  12771. }
  12772. // scroll past all whitespace
  12773. $cursor += strspn($string, $this->_whitespace, $cursor);
  12774. if ($cursor >= $size) {
  12775. $array[$key] = $key;
  12776. break;
  12777. }
  12778. // if the next character is an equal sign, we've got a regular
  12779. // pair, otherwise, it's a bool attribute
  12780. $first_char = @$string[$cursor];
  12781. if ($first_char == '=') {
  12782. // key="value"
  12783. $cursor++;
  12784. $cursor += strspn($string, $this->_whitespace, $cursor);
  12785. if ($cursor === false) {
  12786. $array[$key] = '';
  12787. break;
  12788. }
  12789. // we might be in front of a quote right now
  12790. $char = @$string[$cursor];
  12791. if ($char == '"' || $char == "'") {
  12792. // it's quoted, end bound is $char
  12793. $cursor++;
  12794. $value_begin = $cursor;
  12795. $cursor = strpos($string, $char, $cursor);
  12796. $value_end = $cursor;
  12797. } else {
  12798. // it's not quoted, end bound is whitespace
  12799. $value_begin = $cursor;
  12800. $cursor += strcspn($string, $this->_whitespace, $cursor);
  12801. $value_end = $cursor;
  12802. }
  12803. // we reached a premature end
  12804. if ($cursor === false) {
  12805. $cursor = $size;
  12806. $value_end = $cursor;
  12807. }
  12808. $value = substr($string, $value_begin, $value_end - $value_begin);
  12809. if ($value === false) $value = '';
  12810. $array[$key] = $this->parseData($value);
  12811. $cursor++;
  12812. } else {
  12813. // boolattr
  12814. if ($key !== '') {
  12815. $array[$key] = $key;
  12816. } else {
  12817. // purely theoretical
  12818. if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
  12819. }
  12820. }
  12821. }
  12822. return $array;
  12823. }
  12824. }
  12825. /**
  12826. * Composite strategy that runs multiple strategies on tokens.
  12827. */
  12828. abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy
  12829. {
  12830. /**
  12831. * List of strategies to run tokens through.
  12832. */
  12833. protected $strategies = array();
  12834. public function execute($tokens, $config, $context) {
  12835. foreach ($this->strategies as $strategy) {
  12836. $tokens = $strategy->execute($tokens, $config, $context);
  12837. }
  12838. return $tokens;
  12839. }
  12840. }
  12841. /**
  12842. * Core strategy composed of the big four strategies.
  12843. */
  12844. class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite
  12845. {
  12846. public function __construct() {
  12847. $this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements();
  12848. $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed();
  12849. $this->strategies[] = new HTMLPurifier_Strategy_FixNesting();
  12850. $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes();
  12851. }
  12852. }
  12853. /**
  12854. * Takes a well formed list of tokens and fixes their nesting.
  12855. *
  12856. * HTML elements dictate which elements are allowed to be their children,
  12857. * for example, you can't have a p tag in a span tag. Other elements have
  12858. * much more rigorous definitions: tables, for instance, require a specific
  12859. * order for their elements. There are also constraints not expressible by
  12860. * document type definitions, such as the chameleon nature of ins/del
  12861. * tags and global child exclusions.
  12862. *
  12863. * The first major objective of this strategy is to iterate through all the
  12864. * nodes (not tokens) of the list of tokens and determine whether or not
  12865. * their children conform to the element's definition. If they do not, the
  12866. * child definition may optionally supply an amended list of elements that
  12867. * is valid or require that the entire node be deleted (and the previous
  12868. * node rescanned).
  12869. *
  12870. * The second objective is to ensure that explicitly excluded elements of
  12871. * an element do not appear in its children. Code that accomplishes this
  12872. * task is pervasive through the strategy, though the two are distinct tasks
  12873. * and could, theoretically, be seperated (although it's not recommended).
  12874. *
  12875. * @note Whether or not unrecognized children are silently dropped or
  12876. * translated into text depends on the child definitions.
  12877. *
  12878. * @todo Enable nodes to be bubbled out of the structure.
  12879. *
  12880. * @warning This algorithm (though it may be hard to see) proceeds from
  12881. * a top-down fashion. Thus, parents are processed before
  12882. * children. This is easy to implement and has a nice effiency
  12883. * benefit, in that if a node is removed, we never waste any
  12884. * time processing it, but it also means that if a child
  12885. * changes in a non-encapsulated way (e.g. it is removed), we
  12886. * need to go back and reprocess the parent to see if those
  12887. * changes resulted in problems for the parent. See
  12888. * [BACKTRACK] for an example of this. In the current
  12889. * implementation, this backtracking can only be triggered when
  12890. * a node is removed and if that node was the sole node, the
  12891. * parent would need to be removed. As such, it is easy to see
  12892. * that backtracking only incurs constant overhead. If more
  12893. * sophisticated backtracking is implemented, care must be
  12894. * taken to avoid nontermination or exponential blowup.
  12895. */
  12896. class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
  12897. {
  12898. public function execute($tokens, $config, $context) {
  12899. //####################################################################//
  12900. // Pre-processing
  12901. // get a copy of the HTML definition
  12902. $definition = $config->getHTMLDefinition();
  12903. $excludes_enabled = !$config->get('Core.DisableExcludes');
  12904. // insert implicit "parent" node, will be removed at end.
  12905. // DEFINITION CALL
  12906. $parent_name = $definition->info_parent;
  12907. array_unshift($tokens, new HTMLPurifier_Token_Start($parent_name));
  12908. $tokens[] = new HTMLPurifier_Token_End($parent_name);
  12909. // setup the context variable 'IsInline', for chameleon processing
  12910. // is 'false' when we are not inline, 'true' when it must always
  12911. // be inline, and an integer when it is inline for a certain
  12912. // branch of the document tree
  12913. $is_inline = $definition->info_parent_def->descendants_are_inline;
  12914. $context->register('IsInline', $is_inline);
  12915. // setup error collector
  12916. $e =& $context->get('ErrorCollector', true);
  12917. //####################################################################//
  12918. // Loop initialization
  12919. // stack that contains the indexes of all parents,
  12920. // $stack[count($stack)-1] being the current parent
  12921. $stack = array();
  12922. // stack that contains all elements that are excluded
  12923. // it is organized by parent elements, similar to $stack,
  12924. // but it is only populated when an element with exclusions is
  12925. // processed, i.e. there won't be empty exclusions.
  12926. $exclude_stack = array();
  12927. // variable that contains the start token while we are processing
  12928. // nodes. This enables error reporting to do its job
  12929. $start_token = false;
  12930. $context->register('CurrentToken', $start_token);
  12931. //####################################################################//
  12932. // Loop
  12933. // iterate through all start nodes. Determining the start node
  12934. // is complicated so it has been omitted from the loop construct
  12935. for ($i = 0, $size = count($tokens) ; $i < $size; ) {
  12936. //################################################################//
  12937. // Gather information on children
  12938. // child token accumulator
  12939. $child_tokens = array();
  12940. // scroll to the end of this node, report number, and collect
  12941. // all children
  12942. for ($j = $i, $depth = 0; ; $j++) {
  12943. if ($tokens[$j] instanceof HTMLPurifier_Token_Start) {
  12944. $depth++;
  12945. // skip token assignment on first iteration, this is the
  12946. // token we currently are on
  12947. if ($depth == 1) continue;
  12948. } elseif ($tokens[$j] instanceof HTMLPurifier_Token_End) {
  12949. $depth--;
  12950. // skip token assignment on last iteration, this is the
  12951. // end token of the token we're currently on
  12952. if ($depth == 0) break;
  12953. }
  12954. $child_tokens[] = $tokens[$j];
  12955. }
  12956. // $i is index of start token
  12957. // $j is index of end token
  12958. $start_token = $tokens[$i]; // to make token available via CurrentToken
  12959. //################################################################//
  12960. // Gather information on parent
  12961. // calculate parent information
  12962. if ($count = count($stack)) {
  12963. $parent_index = $stack[$count-1];
  12964. $parent_name = $tokens[$parent_index]->name;
  12965. if ($parent_index == 0) {
  12966. $parent_def = $definition->info_parent_def;
  12967. } else {
  12968. $parent_def = $definition->info[$parent_name];
  12969. }
  12970. } else {
  12971. // processing as if the parent were the "root" node
  12972. // unknown info, it won't be used anyway, in the future,
  12973. // we may want to enforce one element only (this is
  12974. // necessary for HTML Purifier to clean entire documents
  12975. $parent_index = $parent_name = $parent_def = null;
  12976. }
  12977. // calculate context
  12978. if ($is_inline === false) {
  12979. // check if conditions make it inline
  12980. if (!empty($parent_def) && $parent_def->descendants_are_inline) {
  12981. $is_inline = $count - 1;
  12982. }
  12983. } else {
  12984. // check if we're out of inline
  12985. if ($count === $is_inline) {
  12986. $is_inline = false;
  12987. }
  12988. }
  12989. //################################################################//
  12990. // Determine whether element is explicitly excluded SGML-style
  12991. // determine whether or not element is excluded by checking all
  12992. // parent exclusions. The array should not be very large, two
  12993. // elements at most.
  12994. $excluded = false;
  12995. if (!empty($exclude_stack) && $excludes_enabled) {
  12996. foreach ($exclude_stack as $lookup) {
  12997. if (isset($lookup[$tokens[$i]->name])) {
  12998. $excluded = true;
  12999. // no need to continue processing
  13000. break;
  13001. }
  13002. }
  13003. }
  13004. //################################################################//
  13005. // Perform child validation
  13006. if ($excluded) {
  13007. // there is an exclusion, remove the entire node
  13008. $result = false;
  13009. $excludes = array(); // not used, but good to initialize anyway
  13010. } else {
  13011. // DEFINITION CALL
  13012. if ($i === 0) {
  13013. // special processing for the first node
  13014. $def = $definition->info_parent_def;
  13015. } else {
  13016. $def = $definition->info[$tokens[$i]->name];
  13017. }
  13018. if (!empty($def->child)) {
  13019. // have DTD child def validate children
  13020. $result = $def->child->validateChildren(
  13021. $child_tokens, $config, $context);
  13022. } else {
  13023. // weird, no child definition, get rid of everything
  13024. $result = false;
  13025. }
  13026. // determine whether or not this element has any exclusions
  13027. $excludes = $def->excludes;
  13028. }
  13029. // $result is now a bool or array
  13030. //################################################################//
  13031. // Process result by interpreting $result
  13032. if ($result === true || $child_tokens === $result) {
  13033. // leave the node as is
  13034. // register start token as a parental node start
  13035. $stack[] = $i;
  13036. // register exclusions if there are any
  13037. if (!empty($excludes)) $exclude_stack[] = $excludes;
  13038. // move cursor to next possible start node
  13039. $i++;
  13040. } elseif($result === false) {
  13041. // remove entire node
  13042. if ($e) {
  13043. if ($excluded) {
  13044. $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded');
  13045. } else {
  13046. $e->send(E_ERROR, 'Strategy_FixNesting: Node removed');
  13047. }
  13048. }
  13049. // calculate length of inner tokens and current tokens
  13050. $length = $j - $i + 1;
  13051. // perform removal
  13052. array_splice($tokens, $i, $length);
  13053. // update size
  13054. $size -= $length;
  13055. // there is no start token to register,
  13056. // current node is now the next possible start node
  13057. // unless it turns out that we need to do a double-check
  13058. // this is a rought heuristic that covers 100% of HTML's
  13059. // cases and 99% of all other cases. A child definition
  13060. // that would be tricked by this would be something like:
  13061. // ( | a b c) where it's all or nothing. Fortunately,
  13062. // our current implementation claims that that case would
  13063. // not allow empty, even if it did
  13064. if (!$parent_def->child->allow_empty) {
  13065. // we need to do a double-check [BACKTRACK]
  13066. $i = $parent_index;
  13067. array_pop($stack);
  13068. }
  13069. // PROJECTED OPTIMIZATION: Process all children elements before
  13070. // reprocessing parent node.
  13071. } else {
  13072. // replace node with $result
  13073. // calculate length of inner tokens
  13074. $length = $j - $i - 1;
  13075. if ($e) {
  13076. if (empty($result) && $length) {
  13077. $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed');
  13078. } else {
  13079. $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized');
  13080. }
  13081. }
  13082. // perform replacement
  13083. array_splice($tokens, $i + 1, $length, $result);
  13084. // update size
  13085. $size -= $length;
  13086. $size += count($result);
  13087. // register start token as a parental node start
  13088. $stack[] = $i;
  13089. // register exclusions if there are any
  13090. if (!empty($excludes)) $exclude_stack[] = $excludes;
  13091. // move cursor to next possible start node
  13092. $i++;
  13093. }
  13094. //################################################################//
  13095. // Scroll to next start node
  13096. // We assume, at this point, that $i is the index of the token
  13097. // that is the first possible new start point for a node.
  13098. // Test if the token indeed is a start tag, if not, move forward
  13099. // and test again.
  13100. $size = count($tokens);
  13101. while ($i < $size and !$tokens[$i] instanceof HTMLPurifier_Token_Start) {
  13102. if ($tokens[$i] instanceof HTMLPurifier_Token_End) {
  13103. // pop a token index off the stack if we ended a node
  13104. array_pop($stack);
  13105. // pop an exclusion lookup off exclusion stack if
  13106. // we ended node and that node had exclusions
  13107. if ($i == 0 || $i == $size - 1) {
  13108. // use specialized var if it's the super-parent
  13109. $s_excludes = $definition->info_parent_def->excludes;
  13110. } else {
  13111. $s_excludes = $definition->info[$tokens[$i]->name]->excludes;
  13112. }
  13113. if ($s_excludes) {
  13114. array_pop($exclude_stack);
  13115. }
  13116. }
  13117. $i++;
  13118. }
  13119. }
  13120. //####################################################################//
  13121. // Post-processing
  13122. // remove implicit parent tokens at the beginning and end
  13123. array_shift($tokens);
  13124. array_pop($tokens);
  13125. // remove context variables
  13126. $context->destroy('IsInline');
  13127. $context->destroy('CurrentToken');
  13128. //####################################################################//
  13129. // Return
  13130. return $tokens;
  13131. }
  13132. }
  13133. /**
  13134. * Takes tokens makes them well-formed (balance end tags, etc.)
  13135. *
  13136. * Specification of the armor attributes this strategy uses:
  13137. *
  13138. * - MakeWellFormed_TagClosedError: This armor field is used to
  13139. * suppress tag closed errors for certain tokens [TagClosedSuppress],
  13140. * in particular, if a tag was generated automatically by HTML
  13141. * Purifier, we may rely on our infrastructure to close it for us
  13142. * and shouldn't report an error to the user [TagClosedAuto].
  13143. */
  13144. class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
  13145. {
  13146. /**
  13147. * Array stream of tokens being processed.
  13148. */
  13149. protected $tokens;
  13150. /**
  13151. * Current index in $tokens.
  13152. */
  13153. protected $t;
  13154. /**
  13155. * Current nesting of elements.
  13156. */
  13157. protected $stack;
  13158. /**
  13159. * Injectors active in this stream processing.
  13160. */
  13161. protected $injectors;
  13162. /**
  13163. * Current instance of HTMLPurifier_Config.
  13164. */
  13165. protected $config;
  13166. /**
  13167. * Current instance of HTMLPurifier_Context.
  13168. */
  13169. protected $context;
  13170. public function execute($tokens, $config, $context) {
  13171. $definition = $config->getHTMLDefinition();
  13172. // local variables
  13173. $generator = new HTMLPurifier_Generator($config, $context);
  13174. $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
  13175. // used for autoclose early abortion
  13176. $global_parent_allowed_elements = array();
  13177. if (isset($definition->info[$definition->info_parent])) {
  13178. // may be unset under testing circumstances
  13179. $global_parent_allowed_elements = $definition->info[$definition->info_parent]->child->getAllowedElements($config);
  13180. }
  13181. $e = $context->get('ErrorCollector', true);
  13182. $t = false; // token index
  13183. $i = false; // injector index
  13184. $token = false; // the current token
  13185. $reprocess = false; // whether or not to reprocess the same token
  13186. $stack = array();
  13187. // member variables
  13188. $this->stack =& $stack;
  13189. $this->t =& $t;
  13190. $this->tokens =& $tokens;
  13191. $this->config = $config;
  13192. $this->context = $context;
  13193. // context variables
  13194. $context->register('CurrentNesting', $stack);
  13195. $context->register('InputIndex', $t);
  13196. $context->register('InputTokens', $tokens);
  13197. $context->register('CurrentToken', $token);
  13198. // -- begin INJECTOR --
  13199. $this->injectors = array();
  13200. $injectors = $config->getBatch('AutoFormat');
  13201. $def_injectors = $definition->info_injector;
  13202. $custom_injectors = $injectors['Custom'];
  13203. unset($injectors['Custom']); // special case
  13204. foreach ($injectors as $injector => $b) {
  13205. // XXX: Fix with a legitimate lookup table of enabled filters
  13206. if (strpos($injector, '.') !== false) continue;
  13207. $injector = "HTMLPurifier_Injector_$injector";
  13208. if (!$b) continue;
  13209. $this->injectors[] = new $injector;
  13210. }
  13211. foreach ($def_injectors as $injector) {
  13212. // assumed to be objects
  13213. $this->injectors[] = $injector;
  13214. }
  13215. foreach ($custom_injectors as $injector) {
  13216. if (!$injector) continue;
  13217. if (is_string($injector)) {
  13218. $injector = "HTMLPurifier_Injector_$injector";
  13219. $injector = new $injector;
  13220. }
  13221. $this->injectors[] = $injector;
  13222. }
  13223. // give the injectors references to the definition and context
  13224. // variables for performance reasons
  13225. foreach ($this->injectors as $ix => $injector) {
  13226. $error = $injector->prepare($config, $context);
  13227. if (!$error) continue;
  13228. array_splice($this->injectors, $ix, 1); // rm the injector
  13229. trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
  13230. }
  13231. // -- end INJECTOR --
  13232. // a note on reprocessing:
  13233. // In order to reduce code duplication, whenever some code needs
  13234. // to make HTML changes in order to make things "correct", the
  13235. // new HTML gets sent through the purifier, regardless of its
  13236. // status. This means that if we add a start token, because it
  13237. // was totally necessary, we don't have to update nesting; we just
  13238. // punt ($reprocess = true; continue;) and it does that for us.
  13239. // isset is in loop because $tokens size changes during loop exec
  13240. for (
  13241. $t = 0;
  13242. $t == 0 || isset($tokens[$t - 1]);
  13243. // only increment if we don't need to reprocess
  13244. $reprocess ? $reprocess = false : $t++
  13245. ) {
  13246. // check for a rewind
  13247. if (is_int($i) && $i >= 0) {
  13248. // possibility: disable rewinding if the current token has a
  13249. // rewind set on it already. This would offer protection from
  13250. // infinite loop, but might hinder some advanced rewinding.
  13251. $rewind_to = $this->injectors[$i]->getRewind();
  13252. if (is_int($rewind_to) && $rewind_to < $t) {
  13253. if ($rewind_to < 0) $rewind_to = 0;
  13254. while ($t > $rewind_to) {
  13255. $t--;
  13256. $prev = $tokens[$t];
  13257. // indicate that other injectors should not process this token,
  13258. // but we need to reprocess it
  13259. unset($prev->skip[$i]);
  13260. $prev->rewind = $i;
  13261. if ($prev instanceof HTMLPurifier_Token_Start) array_pop($this->stack);
  13262. elseif ($prev instanceof HTMLPurifier_Token_End) $this->stack[] = $prev->start;
  13263. }
  13264. }
  13265. $i = false;
  13266. }
  13267. // handle case of document end
  13268. if (!isset($tokens[$t])) {
  13269. // kill processing if stack is empty
  13270. if (empty($this->stack)) break;
  13271. // peek
  13272. $top_nesting = array_pop($this->stack);
  13273. $this->stack[] = $top_nesting;
  13274. // send error [TagClosedSuppress]
  13275. if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
  13276. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
  13277. }
  13278. // append, don't splice, since this is the end
  13279. $tokens[] = new HTMLPurifier_Token_End($top_nesting->name);
  13280. // punt!
  13281. $reprocess = true;
  13282. continue;
  13283. }
  13284. $token = $tokens[$t];
  13285. //echo '<br>'; printTokens($tokens, $t); printTokens($this->stack);
  13286. //flush();
  13287. // quick-check: if it's not a tag, no need to process
  13288. if (empty($token->is_tag)) {
  13289. if ($token instanceof HTMLPurifier_Token_Text) {
  13290. foreach ($this->injectors as $i => $injector) {
  13291. if (isset($token->skip[$i])) continue;
  13292. if ($token->rewind !== null && $token->rewind !== $i) continue;
  13293. $injector->handleText($token);
  13294. $this->processToken($token, $i);
  13295. $reprocess = true;
  13296. break;
  13297. }
  13298. }
  13299. // another possibility is a comment
  13300. continue;
  13301. }
  13302. if (isset($definition->info[$token->name])) {
  13303. $type = $definition->info[$token->name]->child->type;
  13304. } else {
  13305. $type = false; // Type is unknown, treat accordingly
  13306. }
  13307. // quick tag checks: anything that's *not* an end tag
  13308. $ok = false;
  13309. if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
  13310. // claims to be a start tag but is empty
  13311. $token = new HTMLPurifier_Token_Empty($token->name, $token->attr, $token->line, $token->col, $token->armor);
  13312. $ok = true;
  13313. } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
  13314. // claims to be empty but really is a start tag
  13315. $this->swap(new HTMLPurifier_Token_End($token->name));
  13316. $this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr, $token->line, $token->col, $token->armor));
  13317. // punt (since we had to modify the input stream in a non-trivial way)
  13318. $reprocess = true;
  13319. continue;
  13320. } elseif ($token instanceof HTMLPurifier_Token_Empty) {
  13321. // real empty token
  13322. $ok = true;
  13323. } elseif ($token instanceof HTMLPurifier_Token_Start) {
  13324. // start tag
  13325. // ...unless they also have to close their parent
  13326. if (!empty($this->stack)) {
  13327. // Performance note: you might think that it's rather
  13328. // inefficient, recalculating the autoclose information
  13329. // for every tag that a token closes (since when we
  13330. // do an autoclose, we push a new token into the
  13331. // stream and then /process/ that, before
  13332. // re-processing this token.) But this is
  13333. // necessary, because an injector can make an
  13334. // arbitrary transformations to the autoclosing
  13335. // tokens we introduce, so things may have changed
  13336. // in the meantime. Also, doing the inefficient thing is
  13337. // "easy" to reason about (for certain perverse definitions
  13338. // of "easy")
  13339. $parent = array_pop($this->stack);
  13340. $this->stack[] = $parent;
  13341. if (isset($definition->info[$parent->name])) {
  13342. $elements = $definition->info[$parent->name]->child->getAllowedElements($config);
  13343. $autoclose = !isset($elements[$token->name]);
  13344. } else {
  13345. $autoclose = false;
  13346. }
  13347. if ($autoclose && $definition->info[$token->name]->wrap) {
  13348. // Check if an element can be wrapped by another
  13349. // element to make it valid in a context (for
  13350. // example, <ul><ul> needs a <li> in between)
  13351. $wrapname = $definition->info[$token->name]->wrap;
  13352. $wrapdef = $definition->info[$wrapname];
  13353. $elements = $wrapdef->child->getAllowedElements($config);
  13354. $parent_elements = $definition->info[$parent->name]->child->getAllowedElements($config);
  13355. if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) {
  13356. $newtoken = new HTMLPurifier_Token_Start($wrapname);
  13357. $this->insertBefore($newtoken);
  13358. $reprocess = true;
  13359. continue;
  13360. }
  13361. }
  13362. $carryover = false;
  13363. if ($autoclose && $definition->info[$parent->name]->formatting) {
  13364. $carryover = true;
  13365. }
  13366. if ($autoclose) {
  13367. // check if this autoclose is doomed to fail
  13368. // (this rechecks $parent, which his harmless)
  13369. $autoclose_ok = isset($global_parent_allowed_elements[$token->name]);
  13370. if (!$autoclose_ok) {
  13371. foreach ($this->stack as $ancestor) {
  13372. $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config);
  13373. if (isset($elements[$token->name])) {
  13374. $autoclose_ok = true;
  13375. break;
  13376. }
  13377. if ($definition->info[$token->name]->wrap) {
  13378. $wrapname = $definition->info[$token->name]->wrap;
  13379. $wrapdef = $definition->info[$wrapname];
  13380. $wrap_elements = $wrapdef->child->getAllowedElements($config);
  13381. if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) {
  13382. $autoclose_ok = true;
  13383. break;
  13384. }
  13385. }
  13386. }
  13387. }
  13388. if ($autoclose_ok) {
  13389. // errors need to be updated
  13390. $new_token = new HTMLPurifier_Token_End($parent->name);
  13391. $new_token->start = $parent;
  13392. if ($carryover) {
  13393. $element = clone $parent;
  13394. // [TagClosedAuto]
  13395. $element->armor['MakeWellFormed_TagClosedError'] = true;
  13396. $element->carryover = true;
  13397. $this->processToken(array($new_token, $token, $element));
  13398. } else {
  13399. $this->insertBefore($new_token);
  13400. }
  13401. // [TagClosedSuppress]
  13402. if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
  13403. if (!$carryover) {
  13404. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
  13405. } else {
  13406. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
  13407. }
  13408. }
  13409. } else {
  13410. $this->remove();
  13411. }
  13412. $reprocess = true;
  13413. continue;
  13414. }
  13415. }
  13416. $ok = true;
  13417. }
  13418. if ($ok) {
  13419. foreach ($this->injectors as $i => $injector) {
  13420. if (isset($token->skip[$i])) continue;
  13421. if ($token->rewind !== null && $token->rewind !== $i) continue;
  13422. $injector->handleElement($token);
  13423. $this->processToken($token, $i);
  13424. $reprocess = true;
  13425. break;
  13426. }
  13427. if (!$reprocess) {
  13428. // ah, nothing interesting happened; do normal processing
  13429. $this->swap($token);
  13430. if ($token instanceof HTMLPurifier_Token_Start) {
  13431. $this->stack[] = $token;
  13432. } elseif ($token instanceof HTMLPurifier_Token_End) {
  13433. throw new HTMLPurifier_Exception('Improper handling of end tag in start code; possible error in MakeWellFormed');
  13434. }
  13435. }
  13436. continue;
  13437. }
  13438. // sanity check: we should be dealing with a closing tag
  13439. if (!$token instanceof HTMLPurifier_Token_End) {
  13440. throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
  13441. }
  13442. // make sure that we have something open
  13443. if (empty($this->stack)) {
  13444. if ($escape_invalid_tags) {
  13445. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
  13446. $this->swap(new HTMLPurifier_Token_Text(
  13447. $generator->generateFromToken($token)
  13448. ));
  13449. } else {
  13450. $this->remove();
  13451. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
  13452. }
  13453. $reprocess = true;
  13454. continue;
  13455. }
  13456. // first, check for the simplest case: everything closes neatly.
  13457. // Eventually, everything passes through here; if there are problems
  13458. // we modify the input stream accordingly and then punt, so that
  13459. // the tokens get processed again.
  13460. $current_parent = array_pop($this->stack);
  13461. if ($current_parent->name == $token->name) {
  13462. $token->start = $current_parent;
  13463. foreach ($this->injectors as $i => $injector) {
  13464. if (isset($token->skip[$i])) continue;
  13465. if ($token->rewind !== null && $token->rewind !== $i) continue;
  13466. $injector->handleEnd($token);
  13467. $this->processToken($token, $i);
  13468. $this->stack[] = $current_parent;
  13469. $reprocess = true;
  13470. break;
  13471. }
  13472. continue;
  13473. }
  13474. // okay, so we're trying to close the wrong tag
  13475. // undo the pop previous pop
  13476. $this->stack[] = $current_parent;
  13477. // scroll back the entire nest, trying to find our tag.
  13478. // (feature could be to specify how far you'd like to go)
  13479. $size = count($this->stack);
  13480. // -2 because -1 is the last element, but we already checked that
  13481. $skipped_tags = false;
  13482. for ($j = $size - 2; $j >= 0; $j--) {
  13483. if ($this->stack[$j]->name == $token->name) {
  13484. $skipped_tags = array_slice($this->stack, $j);
  13485. break;
  13486. }
  13487. }
  13488. // we didn't find the tag, so remove
  13489. if ($skipped_tags === false) {
  13490. if ($escape_invalid_tags) {
  13491. $this->swap(new HTMLPurifier_Token_Text(
  13492. $generator->generateFromToken($token)
  13493. ));
  13494. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
  13495. } else {
  13496. $this->remove();
  13497. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
  13498. }
  13499. $reprocess = true;
  13500. continue;
  13501. }
  13502. // do errors, in REVERSE $j order: a,b,c with </a></b></c>
  13503. $c = count($skipped_tags);
  13504. if ($e) {
  13505. for ($j = $c - 1; $j > 0; $j--) {
  13506. // notice we exclude $j == 0, i.e. the current ending tag, from
  13507. // the errors... [TagClosedSuppress]
  13508. if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
  13509. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
  13510. }
  13511. }
  13512. }
  13513. // insert tags, in FORWARD $j order: c,b,a with </a></b></c>
  13514. $replace = array($token);
  13515. for ($j = 1; $j < $c; $j++) {
  13516. // ...as well as from the insertions
  13517. $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name);
  13518. $new_token->start = $skipped_tags[$j];
  13519. array_unshift($replace, $new_token);
  13520. if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) {
  13521. // [TagClosedAuto]
  13522. $element = clone $skipped_tags[$j];
  13523. $element->carryover = true;
  13524. $element->armor['MakeWellFormed_TagClosedError'] = true;
  13525. $replace[] = $element;
  13526. }
  13527. }
  13528. $this->processToken($replace);
  13529. $reprocess = true;
  13530. continue;
  13531. }
  13532. $context->destroy('CurrentNesting');
  13533. $context->destroy('InputTokens');
  13534. $context->destroy('InputIndex');
  13535. $context->destroy('CurrentToken');
  13536. unset($this->injectors, $this->stack, $this->tokens, $this->t);
  13537. return $tokens;
  13538. }
  13539. /**
  13540. * Processes arbitrary token values for complicated substitution patterns.
  13541. * In general:
  13542. *
  13543. * If $token is an array, it is a list of tokens to substitute for the
  13544. * current token. These tokens then get individually processed. If there
  13545. * is a leading integer in the list, that integer determines how many
  13546. * tokens from the stream should be removed.
  13547. *
  13548. * If $token is a regular token, it is swapped with the current token.
  13549. *
  13550. * If $token is false, the current token is deleted.
  13551. *
  13552. * If $token is an integer, that number of tokens (with the first token
  13553. * being the current one) will be deleted.
  13554. *
  13555. * @param $token Token substitution value
  13556. * @param $injector Injector that performed the substitution; default is if
  13557. * this is not an injector related operation.
  13558. */
  13559. protected function processToken($token, $injector = -1) {
  13560. // normalize forms of token
  13561. if (is_object($token)) $token = array(1, $token);
  13562. if (is_int($token)) $token = array($token);
  13563. if ($token === false) $token = array(1);
  13564. if (!is_array($token)) throw new HTMLPurifier_Exception('Invalid token type from injector');
  13565. if (!is_int($token[0])) array_unshift($token, 1);
  13566. if ($token[0] === 0) throw new HTMLPurifier_Exception('Deleting zero tokens is not valid');
  13567. // $token is now an array with the following form:
  13568. // array(number nodes to delete, new node 1, new node 2, ...)
  13569. $delete = array_shift($token);
  13570. $old = array_splice($this->tokens, $this->t, $delete, $token);
  13571. if ($injector > -1) {
  13572. // determine appropriate skips
  13573. $oldskip = isset($old[0]) ? $old[0]->skip : array();
  13574. foreach ($token as $object) {
  13575. $object->skip = $oldskip;
  13576. $object->skip[$injector] = true;
  13577. }
  13578. }
  13579. }
  13580. /**
  13581. * Inserts a token before the current token. Cursor now points to
  13582. * this token. You must reprocess after this.
  13583. */
  13584. private function insertBefore($token) {
  13585. array_splice($this->tokens, $this->t, 0, array($token));
  13586. }
  13587. /**
  13588. * Removes current token. Cursor now points to new token occupying previously
  13589. * occupied space. You must reprocess after this.
  13590. */
  13591. private function remove() {
  13592. array_splice($this->tokens, $this->t, 1);
  13593. }
  13594. /**
  13595. * Swap current token with new token. Cursor points to new token (no
  13596. * change). You must reprocess after this.
  13597. */
  13598. private function swap($token) {
  13599. $this->tokens[$this->t] = $token;
  13600. }
  13601. }
  13602. /**
  13603. * Removes all unrecognized tags from the list of tokens.
  13604. *
  13605. * This strategy iterates through all the tokens and removes unrecognized
  13606. * tokens. If a token is not recognized but a TagTransform is defined for
  13607. * that element, the element will be transformed accordingly.
  13608. */
  13609. class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
  13610. {
  13611. public function execute($tokens, $config, $context) {
  13612. $definition = $config->getHTMLDefinition();
  13613. $generator = new HTMLPurifier_Generator($config, $context);
  13614. $result = array();
  13615. $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
  13616. $remove_invalid_img = $config->get('Core.RemoveInvalidImg');
  13617. // currently only used to determine if comments should be kept
  13618. $trusted = $config->get('HTML.Trusted');
  13619. $comment_lookup = $config->get('HTML.AllowedComments');
  13620. $comment_regexp = $config->get('HTML.AllowedCommentsRegexp');
  13621. $check_comments = $comment_lookup !== array() || $comment_regexp !== null;
  13622. $remove_script_contents = $config->get('Core.RemoveScriptContents');
  13623. $hidden_elements = $config->get('Core.HiddenElements');
  13624. // remove script contents compatibility
  13625. if ($remove_script_contents === true) {
  13626. $hidden_elements['script'] = true;
  13627. } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) {
  13628. unset($hidden_elements['script']);
  13629. }
  13630. $attr_validator = new HTMLPurifier_AttrValidator();
  13631. // removes tokens until it reaches a closing tag with its value
  13632. $remove_until = false;
  13633. // converts comments into text tokens when this is equal to a tag name
  13634. $textify_comments = false;
  13635. $token = false;
  13636. $context->register('CurrentToken', $token);
  13637. $e = false;
  13638. if ($config->get('Core.CollectErrors')) {
  13639. $e =& $context->get('ErrorCollector');
  13640. }
  13641. foreach($tokens as $token) {
  13642. if ($remove_until) {
  13643. if (empty($token->is_tag) || $token->name !== $remove_until) {
  13644. continue;
  13645. }
  13646. }
  13647. if (!empty( $token->is_tag )) {
  13648. // DEFINITION CALL
  13649. // before any processing, try to transform the element
  13650. if (
  13651. isset($definition->info_tag_transform[$token->name])
  13652. ) {
  13653. $original_name = $token->name;
  13654. // there is a transformation for this tag
  13655. // DEFINITION CALL
  13656. $token = $definition->
  13657. info_tag_transform[$token->name]->
  13658. transform($token, $config, $context);
  13659. if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name);
  13660. }
  13661. if (isset($definition->info[$token->name])) {
  13662. // mostly everything's good, but
  13663. // we need to make sure required attributes are in order
  13664. if (
  13665. ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) &&
  13666. $definition->info[$token->name]->required_attr &&
  13667. ($token->name != 'img' || $remove_invalid_img) // ensure config option still works
  13668. ) {
  13669. $attr_validator->validateToken($token, $config, $context);
  13670. $ok = true;
  13671. foreach ($definition->info[$token->name]->required_attr as $name) {
  13672. if (!isset($token->attr[$name])) {
  13673. $ok = false;
  13674. break;
  13675. }
  13676. }
  13677. if (!$ok) {
  13678. if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Missing required attribute', $name);
  13679. continue;
  13680. }
  13681. $token->armor['ValidateAttributes'] = true;
  13682. }
  13683. if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) {
  13684. $textify_comments = $token->name;
  13685. } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) {
  13686. $textify_comments = false;
  13687. }
  13688. } elseif ($escape_invalid_tags) {
  13689. // invalid tag, generate HTML representation and insert in
  13690. if ($e) $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text');
  13691. $token = new HTMLPurifier_Token_Text(
  13692. $generator->generateFromToken($token)
  13693. );
  13694. } else {
  13695. // check if we need to destroy all of the tag's children
  13696. // CAN BE GENERICIZED
  13697. if (isset($hidden_elements[$token->name])) {
  13698. if ($token instanceof HTMLPurifier_Token_Start) {
  13699. $remove_until = $token->name;
  13700. } elseif ($token instanceof HTMLPurifier_Token_Empty) {
  13701. // do nothing: we're still looking
  13702. } else {
  13703. $remove_until = false;
  13704. }
  13705. if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed');
  13706. } else {
  13707. if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed');
  13708. }
  13709. continue;
  13710. }
  13711. } elseif ($token instanceof HTMLPurifier_Token_Comment) {
  13712. // textify comments in script tags when they are allowed
  13713. if ($textify_comments !== false) {
  13714. $data = $token->data;
  13715. $token = new HTMLPurifier_Token_Text($data);
  13716. } elseif ($trusted || $check_comments) {
  13717. // always cleanup comments
  13718. $trailing_hyphen = false;
  13719. if ($e) {
  13720. // perform check whether or not there's a trailing hyphen
  13721. if (substr($token->data, -1) == '-') {
  13722. $trailing_hyphen = true;
  13723. }
  13724. }
  13725. $token->data = rtrim($token->data, '-');
  13726. $found_double_hyphen = false;
  13727. while (strpos($token->data, '--') !== false) {
  13728. $found_double_hyphen = true;
  13729. $token->data = str_replace('--', '-', $token->data);
  13730. }
  13731. if ($trusted || !empty($comment_lookup[trim($token->data)]) || ($comment_regexp !== NULL && preg_match($comment_regexp, trim($token->data)))) {
  13732. // OK good
  13733. if ($e) {
  13734. if ($trailing_hyphen) {
  13735. $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed');
  13736. }
  13737. if ($found_double_hyphen) {
  13738. $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed');
  13739. }
  13740. }
  13741. } else {
  13742. if ($e) {
  13743. $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed');
  13744. }
  13745. continue;
  13746. }
  13747. } else {
  13748. // strip comments
  13749. if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed');
  13750. continue;
  13751. }
  13752. } elseif ($token instanceof HTMLPurifier_Token_Text) {
  13753. } else {
  13754. continue;
  13755. }
  13756. $result[] = $token;
  13757. }
  13758. if ($remove_until && $e) {
  13759. // we removed tokens until the end, throw error
  13760. $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until);
  13761. }
  13762. $context->destroy('CurrentToken');
  13763. return $result;
  13764. }
  13765. }
  13766. /**
  13767. * Validate all attributes in the tokens.
  13768. */
  13769. class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
  13770. {
  13771. public function execute($tokens, $config, $context) {
  13772. // setup validator
  13773. $validator = new HTMLPurifier_AttrValidator();
  13774. $token = false;
  13775. $context->register('CurrentToken', $token);
  13776. foreach ($tokens as $key => $token) {
  13777. // only process tokens that have attributes,
  13778. // namely start and empty tags
  13779. if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) continue;
  13780. // skip tokens that are armored
  13781. if (!empty($token->armor['ValidateAttributes'])) continue;
  13782. // note that we have no facilities here for removing tokens
  13783. $validator->validateToken($token, $config, $context);
  13784. $tokens[$key] = $token; // for PHP 4
  13785. }
  13786. $context->destroy('CurrentToken');
  13787. return $tokens;
  13788. }
  13789. }
  13790. /**
  13791. * Transforms FONT tags to the proper form (SPAN with CSS styling)
  13792. *
  13793. * This transformation takes the three proprietary attributes of FONT and
  13794. * transforms them into their corresponding CSS attributes. These are color,
  13795. * face, and size.
  13796. *
  13797. * @note Size is an interesting case because it doesn't map cleanly to CSS.
  13798. * Thanks to
  13799. * http://style.cleverchimp.com/font_size_intervals/altintervals.html
  13800. * for reasonable mappings.
  13801. * @warning This doesn't work completely correctly; specifically, this
  13802. * TagTransform operates before well-formedness is enforced, so
  13803. * the "active formatting elements" algorithm doesn't get applied.
  13804. */
  13805. class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform
  13806. {
  13807. public $transform_to = 'span';
  13808. protected $_size_lookup = array(
  13809. '0' => 'xx-small',
  13810. '1' => 'xx-small',
  13811. '2' => 'small',
  13812. '3' => 'medium',
  13813. '4' => 'large',
  13814. '5' => 'x-large',
  13815. '6' => 'xx-large',
  13816. '7' => '300%',
  13817. '-1' => 'smaller',
  13818. '-2' => '60%',
  13819. '+1' => 'larger',
  13820. '+2' => '150%',
  13821. '+3' => '200%',
  13822. '+4' => '300%'
  13823. );
  13824. public function transform($tag, $config, $context) {
  13825. if ($tag instanceof HTMLPurifier_Token_End) {
  13826. $new_tag = clone $tag;
  13827. $new_tag->name = $this->transform_to;
  13828. return $new_tag;
  13829. }
  13830. $attr = $tag->attr;
  13831. $prepend_style = '';
  13832. // handle color transform
  13833. if (isset($attr['color'])) {
  13834. $prepend_style .= 'color:' . $attr['color'] . ';';
  13835. unset($attr['color']);
  13836. }
  13837. // handle face transform
  13838. if (isset($attr['face'])) {
  13839. $prepend_style .= 'font-family:' . $attr['face'] . ';';
  13840. unset($attr['face']);
  13841. }
  13842. // handle size transform
  13843. if (isset($attr['size'])) {
  13844. // normalize large numbers
  13845. if ($attr['size'] !== '') {
  13846. if ($attr['size']{0} == '+' || $attr['size']{0} == '-') {
  13847. $size = (int) $attr['size'];
  13848. if ($size < -2) $attr['size'] = '-2';
  13849. if ($size > 4) $attr['size'] = '+4';
  13850. } else {
  13851. $size = (int) $attr['size'];
  13852. if ($size > 7) $attr['size'] = '7';
  13853. }
  13854. }
  13855. if (isset($this->_size_lookup[$attr['size']])) {
  13856. $prepend_style .= 'font-size:' .
  13857. $this->_size_lookup[$attr['size']] . ';';
  13858. }
  13859. unset($attr['size']);
  13860. }
  13861. if ($prepend_style) {
  13862. $attr['style'] = isset($attr['style']) ?
  13863. $prepend_style . $attr['style'] :
  13864. $prepend_style;
  13865. }
  13866. $new_tag = clone $tag;
  13867. $new_tag->name = $this->transform_to;
  13868. $new_tag->attr = $attr;
  13869. return $new_tag;
  13870. }
  13871. }
  13872. /**
  13873. * Simple transformation, just change tag name to something else,
  13874. * and possibly add some styling. This will cover most of the deprecated
  13875. * tag cases.
  13876. */
  13877. class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform
  13878. {
  13879. protected $style;
  13880. /**
  13881. * @param $transform_to Tag name to transform to.
  13882. * @param $style CSS style to add to the tag
  13883. */
  13884. public function __construct($transform_to, $style = null) {
  13885. $this->transform_to = $transform_to;
  13886. $this->style = $style;
  13887. }
  13888. public function transform($tag, $config, $context) {
  13889. $new_tag = clone $tag;
  13890. $new_tag->name = $this->transform_to;
  13891. if (!is_null($this->style) &&
  13892. ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty)
  13893. ) {
  13894. $this->prependCSS($new_tag->attr, $this->style);
  13895. }
  13896. return $new_tag;
  13897. }
  13898. }
  13899. /**
  13900. * Concrete comment token class. Generally will be ignored.
  13901. */
  13902. class HTMLPurifier_Token_Comment extends HTMLPurifier_Token
  13903. {
  13904. public $data; /**< Character data within comment. */
  13905. public $is_whitespace = true;
  13906. /**
  13907. * Transparent constructor.
  13908. *
  13909. * @param $data String comment data.
  13910. */
  13911. public function __construct($data, $line = null, $col = null) {
  13912. $this->data = $data;
  13913. $this->line = $line;
  13914. $this->col = $col;
  13915. }
  13916. }
  13917. /**
  13918. * Abstract class of a tag token (start, end or empty), and its behavior.
  13919. */
  13920. class HTMLPurifier_Token_Tag extends HTMLPurifier_Token
  13921. {
  13922. /**
  13923. * Static bool marker that indicates the class is a tag.
  13924. *
  13925. * This allows us to check objects with <tt>!empty($obj->is_tag)</tt>
  13926. * without having to use a function call <tt>is_a()</tt>.
  13927. */
  13928. public $is_tag = true;
  13929. /**
  13930. * The lower-case name of the tag, like 'a', 'b' or 'blockquote'.
  13931. *
  13932. * @note Strictly speaking, XML tags are case sensitive, so we shouldn't
  13933. * be lower-casing them, but these tokens cater to HTML tags, which are
  13934. * insensitive.
  13935. */
  13936. public $name;
  13937. /**
  13938. * Associative array of the tag's attributes.
  13939. */
  13940. public $attr = array();
  13941. /**
  13942. * Non-overloaded constructor, which lower-cases passed tag name.
  13943. *
  13944. * @param $name String name.
  13945. * @param $attr Associative array of attributes.
  13946. */
  13947. public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) {
  13948. $this->name = ctype_lower($name) ? $name : strtolower($name);
  13949. foreach ($attr as $key => $value) {
  13950. // normalization only necessary when key is not lowercase
  13951. if (!ctype_lower($key)) {
  13952. $new_key = strtolower($key);
  13953. if (!isset($attr[$new_key])) {
  13954. $attr[$new_key] = $attr[$key];
  13955. }
  13956. if ($new_key !== $key) {
  13957. unset($attr[$key]);
  13958. }
  13959. }
  13960. }
  13961. $this->attr = $attr;
  13962. $this->line = $line;
  13963. $this->col = $col;
  13964. $this->armor = $armor;
  13965. }
  13966. }
  13967. /**
  13968. * Concrete empty token class.
  13969. */
  13970. class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag
  13971. {
  13972. }
  13973. /**
  13974. * Concrete end token class.
  13975. *
  13976. * @warning This class accepts attributes even though end tags cannot. This
  13977. * is for optimization reasons, as under normal circumstances, the Lexers
  13978. * do not pass attributes.
  13979. */
  13980. class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag
  13981. {
  13982. /**
  13983. * Token that started this node. Added by MakeWellFormed. Please
  13984. * do not edit this!
  13985. */
  13986. public $start;
  13987. }
  13988. /**
  13989. * Concrete start token class.
  13990. */
  13991. class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag
  13992. {
  13993. }
  13994. /**
  13995. * Concrete text token class.
  13996. *
  13997. * Text tokens comprise of regular parsed character data (PCDATA) and raw
  13998. * character data (from the CDATA sections). Internally, their
  13999. * data is parsed with all entities expanded. Surprisingly, the text token
  14000. * does have a "tag name" called #PCDATA, which is how the DTD represents it
  14001. * in permissible child nodes.
  14002. */
  14003. class HTMLPurifier_Token_Text extends HTMLPurifier_Token
  14004. {
  14005. public $name = '#PCDATA'; /**< PCDATA tag name compatible with DTD. */
  14006. public $data; /**< Parsed character data of text. */
  14007. public $is_whitespace; /**< Bool indicating if node is whitespace. */
  14008. /**
  14009. * Constructor, accepts data and determines if it is whitespace.
  14010. *
  14011. * @param $data String parsed character data.
  14012. */
  14013. public function __construct($data, $line = null, $col = null) {
  14014. $this->data = $data;
  14015. $this->is_whitespace = ctype_space($data);
  14016. $this->line = $line;
  14017. $this->col = $col;
  14018. }
  14019. }
  14020. class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter
  14021. {
  14022. public $name = 'DisableExternal';
  14023. protected $ourHostParts = false;
  14024. public function prepare($config) {
  14025. $our_host = $config->getDefinition('URI')->host;
  14026. if ($our_host !== null) $this->ourHostParts = array_reverse(explode('.', $our_host));
  14027. }
  14028. public function filter(&$uri, $config, $context) {
  14029. if (is_null($uri->host)) return true;
  14030. if ($this->ourHostParts === false) return false;
  14031. $host_parts = array_reverse(explode('.', $uri->host));
  14032. foreach ($this->ourHostParts as $i => $x) {
  14033. if (!isset($host_parts[$i])) return false;
  14034. if ($host_parts[$i] != $this->ourHostParts[$i]) return false;
  14035. }
  14036. return true;
  14037. }
  14038. }
  14039. class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal
  14040. {
  14041. public $name = 'DisableExternalResources';
  14042. public function filter(&$uri, $config, $context) {
  14043. if (!$context->get('EmbeddedURI', true)) return true;
  14044. return parent::filter($uri, $config, $context);
  14045. }
  14046. }
  14047. class HTMLPurifier_URIFilter_DisableResources extends HTMLPurifier_URIFilter
  14048. {
  14049. public $name = 'DisableResources';
  14050. public function filter(&$uri, $config, $context) {
  14051. return !$context->get('EmbeddedURI', true);
  14052. }
  14053. }
  14054. // It's not clear to me whether or not Punycode means that hostnames
  14055. // do not have canonical forms anymore. As far as I can tell, it's
  14056. // not a problem (punycoding should be identity when no Unicode
  14057. // points are involved), but I'm not 100% sure
  14058. class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
  14059. {
  14060. public $name = 'HostBlacklist';
  14061. protected $blacklist = array();
  14062. public function prepare($config) {
  14063. $this->blacklist = $config->get('URI.HostBlacklist');
  14064. return true;
  14065. }
  14066. public function filter(&$uri, $config, $context) {
  14067. foreach($this->blacklist as $blacklisted_host_fragment) {
  14068. if (strpos($uri->host, $blacklisted_host_fragment) !== false) {
  14069. return false;
  14070. }
  14071. }
  14072. return true;
  14073. }
  14074. }
  14075. // does not support network paths
  14076. class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
  14077. {
  14078. public $name = 'MakeAbsolute';
  14079. protected $base;
  14080. protected $basePathStack = array();
  14081. public function prepare($config) {
  14082. $def = $config->getDefinition('URI');
  14083. $this->base = $def->base;
  14084. if (is_null($this->base)) {
  14085. trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING);
  14086. return false;
  14087. }
  14088. $this->base->fragment = null; // fragment is invalid for base URI
  14089. $stack = explode('/', $this->base->path);
  14090. array_pop($stack); // discard last segment
  14091. $stack = $this->_collapseStack($stack); // do pre-parsing
  14092. $this->basePathStack = $stack;
  14093. return true;
  14094. }
  14095. public function filter(&$uri, $config, $context) {
  14096. if (is_null($this->base)) return true; // abort early
  14097. if (
  14098. $uri->path === '' && is_null($uri->scheme) &&
  14099. is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)
  14100. ) {
  14101. // reference to current document
  14102. $uri = clone $this->base;
  14103. return true;
  14104. }
  14105. if (!is_null($uri->scheme)) {
  14106. // absolute URI already: don't change
  14107. if (!is_null($uri->host)) return true;
  14108. $scheme_obj = $uri->getSchemeObj($config, $context);
  14109. if (!$scheme_obj) {
  14110. // scheme not recognized
  14111. return false;
  14112. }
  14113. if (!$scheme_obj->hierarchical) {
  14114. // non-hierarchal URI with explicit scheme, don't change
  14115. return true;
  14116. }
  14117. // special case: had a scheme but always is hierarchical and had no authority
  14118. }
  14119. if (!is_null($uri->host)) {
  14120. // network path, don't bother
  14121. return true;
  14122. }
  14123. if ($uri->path === '') {
  14124. $uri->path = $this->base->path;
  14125. } elseif ($uri->path[0] !== '/') {
  14126. // relative path, needs more complicated processing
  14127. $stack = explode('/', $uri->path);
  14128. $new_stack = array_merge($this->basePathStack, $stack);
  14129. if ($new_stack[0] !== '' && !is_null($this->base->host)) {
  14130. array_unshift($new_stack, '');
  14131. }
  14132. $new_stack = $this->_collapseStack($new_stack);
  14133. $uri->path = implode('/', $new_stack);
  14134. } else {
  14135. // absolute path, but still we should collapse
  14136. $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path)));
  14137. }
  14138. // re-combine
  14139. $uri->scheme = $this->base->scheme;
  14140. if (is_null($uri->userinfo)) $uri->userinfo = $this->base->userinfo;
  14141. if (is_null($uri->host)) $uri->host = $this->base->host;
  14142. if (is_null($uri->port)) $uri->port = $this->base->port;
  14143. return true;
  14144. }
  14145. /**
  14146. * Resolve dots and double-dots in a path stack
  14147. */
  14148. private function _collapseStack($stack) {
  14149. $result = array();
  14150. $is_folder = false;
  14151. for ($i = 0; isset($stack[$i]); $i++) {
  14152. $is_folder = false;
  14153. // absorb an internally duplicated slash
  14154. if ($stack[$i] == '' && $i && isset($stack[$i+1])) continue;
  14155. if ($stack[$i] == '..') {
  14156. if (!empty($result)) {
  14157. $segment = array_pop($result);
  14158. if ($segment === '' && empty($result)) {
  14159. // error case: attempted to back out too far:
  14160. // restore the leading slash
  14161. $result[] = '';
  14162. } elseif ($segment === '..') {
  14163. $result[] = '..'; // cannot remove .. with ..
  14164. }
  14165. } else {
  14166. // relative path, preserve the double-dots
  14167. $result[] = '..';
  14168. }
  14169. $is_folder = true;
  14170. continue;
  14171. }
  14172. if ($stack[$i] == '.') {
  14173. // silently absorb
  14174. $is_folder = true;
  14175. continue;
  14176. }
  14177. $result[] = $stack[$i];
  14178. }
  14179. if ($is_folder) $result[] = '';
  14180. return $result;
  14181. }
  14182. }
  14183. class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter
  14184. {
  14185. public $name = 'Munge';
  14186. public $post = true;
  14187. private $target, $parser, $doEmbed, $secretKey;
  14188. protected $replace = array();
  14189. public function prepare($config) {
  14190. $this->target = $config->get('URI.' . $this->name);
  14191. $this->parser = new HTMLPurifier_URIParser();
  14192. $this->doEmbed = $config->get('URI.MungeResources');
  14193. $this->secretKey = $config->get('URI.MungeSecretKey');
  14194. return true;
  14195. }
  14196. public function filter(&$uri, $config, $context) {
  14197. if ($context->get('EmbeddedURI', true) && !$this->doEmbed) return true;
  14198. $scheme_obj = $uri->getSchemeObj($config, $context);
  14199. if (!$scheme_obj) return true; // ignore unknown schemes, maybe another postfilter did it
  14200. if (!$scheme_obj->browsable) return true; // ignore non-browseable schemes, since we can't munge those in a reasonable way
  14201. if ($uri->isBenign($config, $context)) return true; // don't redirect if a benign URL
  14202. $this->makeReplace($uri, $config, $context);
  14203. $this->replace = array_map('rawurlencode', $this->replace);
  14204. $new_uri = strtr($this->target, $this->replace);
  14205. $new_uri = $this->parser->parse($new_uri);
  14206. // don't redirect if the target host is the same as the
  14207. // starting host
  14208. if ($uri->host === $new_uri->host) return true;
  14209. $uri = $new_uri; // overwrite
  14210. return true;
  14211. }
  14212. protected function makeReplace($uri, $config, $context) {
  14213. $string = $uri->toString();
  14214. // always available
  14215. $this->replace['%s'] = $string;
  14216. $this->replace['%r'] = $context->get('EmbeddedURI', true);
  14217. $token = $context->get('CurrentToken', true);
  14218. $this->replace['%n'] = $token ? $token->name : null;
  14219. $this->replace['%m'] = $context->get('CurrentAttr', true);
  14220. $this->replace['%p'] = $context->get('CurrentCSSProperty', true);
  14221. // not always available
  14222. if ($this->secretKey) $this->replace['%t'] = sha1($this->secretKey . ':' . $string);
  14223. }
  14224. }
  14225. /**
  14226. * Implements safety checks for safe iframes.
  14227. *
  14228. * @warning This filter is *critical* for ensuring that %HTML.SafeIframe
  14229. * works safely.
  14230. */
  14231. class HTMLPurifier_URIFilter_SafeIframe extends HTMLPurifier_URIFilter
  14232. {
  14233. public $name = 'SafeIframe';
  14234. public $always_load = true;
  14235. protected $regexp = NULL;
  14236. // XXX: The not so good bit about how this is all setup now is we
  14237. // can't check HTML.SafeIframe in the 'prepare' step: we have to
  14238. // defer till the actual filtering.
  14239. public function prepare($config) {
  14240. $this->regexp = $config->get('URI.SafeIframeRegexp');
  14241. return true;
  14242. }
  14243. public function filter(&$uri, $config, $context) {
  14244. // check if filter not applicable
  14245. if (!$config->get('HTML.SafeIframe')) return true;
  14246. // check if the filter should actually trigger
  14247. if (!$context->get('EmbeddedURI', true)) return true;
  14248. $token = $context->get('CurrentToken', true);
  14249. if (!($token && $token->name == 'iframe')) return true;
  14250. // check if we actually have some whitelists enabled
  14251. if ($this->regexp === null) return false;
  14252. // actually check the whitelists
  14253. return preg_match($this->regexp, $uri->toString());
  14254. }
  14255. }
  14256. /**
  14257. * Implements data: URI for base64 encoded images supported by GD.
  14258. */
  14259. class HTMLPurifier_URIScheme_data extends HTMLPurifier_URIScheme {
  14260. public $browsable = true;
  14261. public $allowed_types = array(
  14262. // you better write validation code for other types if you
  14263. // decide to allow them
  14264. 'image/jpeg' => true,
  14265. 'image/gif' => true,
  14266. 'image/png' => true,
  14267. );
  14268. // this is actually irrelevant since we only write out the path
  14269. // component
  14270. public $may_omit_host = true;
  14271. public function doValidate(&$uri, $config, $context) {
  14272. $result = explode(',', $uri->path, 2);
  14273. $is_base64 = false;
  14274. $charset = null;
  14275. $content_type = null;
  14276. if (count($result) == 2) {
  14277. list($metadata, $data) = $result;
  14278. // do some legwork on the metadata
  14279. $metas = explode(';', $metadata);
  14280. while(!empty($metas)) {
  14281. $cur = array_shift($metas);
  14282. if ($cur == 'base64') {
  14283. $is_base64 = true;
  14284. break;
  14285. }
  14286. if (substr($cur, 0, 8) == 'charset=') {
  14287. // doesn't match if there are arbitrary spaces, but
  14288. // whatever dude
  14289. if ($charset !== null) continue; // garbage
  14290. $charset = substr($cur, 8); // not used
  14291. } else {
  14292. if ($content_type !== null) continue; // garbage
  14293. $content_type = $cur;
  14294. }
  14295. }
  14296. } else {
  14297. $data = $result[0];
  14298. }
  14299. if ($content_type !== null && empty($this->allowed_types[$content_type])) {
  14300. return false;
  14301. }
  14302. if ($charset !== null) {
  14303. // error; we don't allow plaintext stuff
  14304. $charset = null;
  14305. }
  14306. $data = rawurldecode($data);
  14307. if ($is_base64) {
  14308. $raw_data = base64_decode($data);
  14309. } else {
  14310. $raw_data = $data;
  14311. }
  14312. // XXX probably want to refactor this into a general mechanism
  14313. // for filtering arbitrary content types
  14314. $file = tempnam("/tmp", "");
  14315. file_put_contents($file, $raw_data);
  14316. if (function_exists('exif_imagetype')) {
  14317. $image_code = exif_imagetype($file);
  14318. unlink($file);
  14319. } elseif (function_exists('getimagesize')) {
  14320. set_error_handler(array($this, 'muteErrorHandler'));
  14321. $info = getimagesize($file);
  14322. restore_error_handler();
  14323. unlink($file);
  14324. if ($info == false) return false;
  14325. $image_code = $info[2];
  14326. } else {
  14327. trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR);
  14328. }
  14329. $real_content_type = image_type_to_mime_type($image_code);
  14330. if ($real_content_type != $content_type) {
  14331. // we're nice guys; if the content type is something else we
  14332. // support, change it over
  14333. if (empty($this->allowed_types[$real_content_type])) return false;
  14334. $content_type = $real_content_type;
  14335. }
  14336. // ok, it's kosher, rewrite what we need
  14337. $uri->userinfo = null;
  14338. $uri->host = null;
  14339. $uri->port = null;
  14340. $uri->fragment = null;
  14341. $uri->query = null;
  14342. $uri->path = "$content_type;base64," . base64_encode($raw_data);
  14343. return true;
  14344. }
  14345. public function muteErrorHandler($errno, $errstr) {}
  14346. }
  14347. /**
  14348. * Validates file as defined by RFC 1630 and RFC 1738.
  14349. */
  14350. class HTMLPurifier_URIScheme_file extends HTMLPurifier_URIScheme {
  14351. // Generally file:// URLs are not accessible from most
  14352. // machines, so placing them as an img src is incorrect.
  14353. public $browsable = false;
  14354. // Basically the *only* URI scheme for which this is true, since
  14355. // accessing files on the local machine is very common. In fact,
  14356. // browsers on some operating systems don't understand the
  14357. // authority, though I hear it is used on Windows to refer to
  14358. // network shares.
  14359. public $may_omit_host = true;
  14360. public function doValidate(&$uri, $config, $context) {
  14361. // Authentication method is not supported
  14362. $uri->userinfo = null;
  14363. // file:// makes no provisions for accessing the resource
  14364. $uri->port = null;
  14365. // While it seems to work on Firefox, the querystring has
  14366. // no possible effect and is thus stripped.
  14367. $uri->query = null;
  14368. return true;
  14369. }
  14370. }
  14371. /**
  14372. * Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738.
  14373. */
  14374. class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme {
  14375. public $default_port = 21;
  14376. public $browsable = true; // usually
  14377. public $hierarchical = true;
  14378. public function doValidate(&$uri, $config, $context) {
  14379. $uri->query = null;
  14380. // typecode check
  14381. $semicolon_pos = strrpos($uri->path, ';'); // reverse
  14382. if ($semicolon_pos !== false) {
  14383. $type = substr($uri->path, $semicolon_pos + 1); // no semicolon
  14384. $uri->path = substr($uri->path, 0, $semicolon_pos);
  14385. $type_ret = '';
  14386. if (strpos($type, '=') !== false) {
  14387. // figure out whether or not the declaration is correct
  14388. list($key, $typecode) = explode('=', $type, 2);
  14389. if ($key !== 'type') {
  14390. // invalid key, tack it back on encoded
  14391. $uri->path .= '%3B' . $type;
  14392. } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') {
  14393. $type_ret = ";type=$typecode";
  14394. }
  14395. } else {
  14396. $uri->path .= '%3B' . $type;
  14397. }
  14398. $uri->path = str_replace(';', '%3B', $uri->path);
  14399. $uri->path .= $type_ret;
  14400. }
  14401. return true;
  14402. }
  14403. }
  14404. /**
  14405. * Validates http (HyperText Transfer Protocol) as defined by RFC 2616
  14406. */
  14407. class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme {
  14408. public $default_port = 80;
  14409. public $browsable = true;
  14410. public $hierarchical = true;
  14411. public function doValidate(&$uri, $config, $context) {
  14412. $uri->userinfo = null;
  14413. return true;
  14414. }
  14415. }
  14416. /**
  14417. * Validates https (Secure HTTP) according to http scheme.
  14418. */
  14419. class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http {
  14420. public $default_port = 443;
  14421. public $secure = true;
  14422. }
  14423. // VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the
  14424. // email is valid, but be careful!
  14425. /**
  14426. * Validates mailto (for E-mail) according to RFC 2368
  14427. * @todo Validate the email address
  14428. * @todo Filter allowed query parameters
  14429. */
  14430. class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme {
  14431. public $browsable = false;
  14432. public $may_omit_host = true;
  14433. public function doValidate(&$uri, $config, $context) {
  14434. $uri->userinfo = null;
  14435. $uri->host = null;
  14436. $uri->port = null;
  14437. // we need to validate path against RFC 2368's addr-spec
  14438. return true;
  14439. }
  14440. }
  14441. /**
  14442. * Validates news (Usenet) as defined by generic RFC 1738
  14443. */
  14444. class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme {
  14445. public $browsable = false;
  14446. public $may_omit_host = true;
  14447. public function doValidate(&$uri, $config, $context) {
  14448. $uri->userinfo = null;
  14449. $uri->host = null;
  14450. $uri->port = null;
  14451. $uri->query = null;
  14452. // typecode check needed on path
  14453. return true;
  14454. }
  14455. }
  14456. /**
  14457. * Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738
  14458. */
  14459. class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme {
  14460. public $default_port = 119;
  14461. public $browsable = false;
  14462. public function doValidate(&$uri, $config, $context) {
  14463. $uri->userinfo = null;
  14464. $uri->query = null;
  14465. return true;
  14466. }
  14467. }
  14468. /**
  14469. * Performs safe variable parsing based on types which can be used by
  14470. * users. This may not be able to represent all possible data inputs,
  14471. * however.
  14472. */
  14473. class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser
  14474. {
  14475. protected function parseImplementation($var, $type, $allow_null) {
  14476. if ($allow_null && $var === null) return null;
  14477. switch ($type) {
  14478. // Note: if code "breaks" from the switch, it triggers a generic
  14479. // exception to be thrown. Specific errors can be specifically
  14480. // done here.
  14481. case self::MIXED :
  14482. case self::ISTRING :
  14483. case self::STRING :
  14484. case self::TEXT :
  14485. case self::ITEXT :
  14486. return $var;
  14487. case self::INT :
  14488. if (is_string($var) && ctype_digit($var)) $var = (int) $var;
  14489. return $var;
  14490. case self::FLOAT :
  14491. if ((is_string($var) && is_numeric($var)) || is_int($var)) $var = (float) $var;
  14492. return $var;
  14493. case self::BOOL :
  14494. if (is_int($var) && ($var === 0 || $var === 1)) {
  14495. $var = (bool) $var;
  14496. } elseif (is_string($var)) {
  14497. if ($var == 'on' || $var == 'true' || $var == '1') {
  14498. $var = true;
  14499. } elseif ($var == 'off' || $var == 'false' || $var == '0') {
  14500. $var = false;
  14501. } else {
  14502. throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type");
  14503. }
  14504. }
  14505. return $var;
  14506. case self::ALIST :
  14507. case self::HASH :
  14508. case self::LOOKUP :
  14509. if (is_string($var)) {
  14510. // special case: technically, this is an array with
  14511. // a single empty string item, but having an empty
  14512. // array is more intuitive
  14513. if ($var == '') return array();
  14514. if (strpos($var, "\n") === false && strpos($var, "\r") === false) {
  14515. // simplistic string to array method that only works
  14516. // for simple lists of tag names or alphanumeric characters
  14517. $var = explode(',',$var);
  14518. } else {
  14519. $var = preg_split('/(,|[\n\r]+)/', $var);
  14520. }
  14521. // remove spaces
  14522. foreach ($var as $i => $j) $var[$i] = trim($j);
  14523. if ($type === self::HASH) {
  14524. // key:value,key2:value2
  14525. $nvar = array();
  14526. foreach ($var as $keypair) {
  14527. $c = explode(':', $keypair, 2);
  14528. if (!isset($c[1])) continue;
  14529. $nvar[trim($c[0])] = trim($c[1]);
  14530. }
  14531. $var = $nvar;
  14532. }
  14533. }
  14534. if (!is_array($var)) break;
  14535. $keys = array_keys($var);
  14536. if ($keys === array_keys($keys)) {
  14537. if ($type == self::ALIST) return $var;
  14538. elseif ($type == self::LOOKUP) {
  14539. $new = array();
  14540. foreach ($var as $key) {
  14541. $new[$key] = true;
  14542. }
  14543. return $new;
  14544. } else break;
  14545. }
  14546. if ($type === self::ALIST) {
  14547. trigger_error("Array list did not have consecutive integer indexes", E_USER_WARNING);
  14548. return array_values($var);
  14549. }
  14550. if ($type === self::LOOKUP) {
  14551. foreach ($var as $key => $value) {
  14552. if ($value !== true) {
  14553. trigger_error("Lookup array has non-true value at key '$key'; maybe your input array was not indexed numerically", E_USER_WARNING);
  14554. }
  14555. $var[$key] = true;
  14556. }
  14557. }
  14558. return $var;
  14559. default:
  14560. $this->errorInconsistent(__CLASS__, $type);
  14561. }
  14562. $this->errorGeneric($var, $type);
  14563. }
  14564. }
  14565. /**
  14566. * This variable parser uses PHP's internal code engine. Because it does
  14567. * this, it can represent all inputs; however, it is dangerous and cannot
  14568. * be used by users.
  14569. */
  14570. class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser
  14571. {
  14572. protected function parseImplementation($var, $type, $allow_null) {
  14573. return $this->evalExpression($var);
  14574. }
  14575. protected function evalExpression($expr) {
  14576. $var = null;
  14577. $result = eval("\$var = $expr;");
  14578. if ($result === false) {
  14579. throw new HTMLPurifier_VarParserException("Fatal error in evaluated code");
  14580. }
  14581. return $var;
  14582. }
  14583. }