summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--EVENTS.txt49
-rw-r--r--README19
-rw-r--r--actions/apidirectmessage.php2
-rw-r--r--actions/apifriendshipsdestroy.php9
-rw-r--r--actions/apigrouplist.php2
-rw-r--r--actions/apigrouplistall.php2
-rw-r--r--actions/apitimelinefavorites.php71
-rw-r--r--actions/apitimelinefriends.php76
-rw-r--r--actions/apitimelinegroup.php76
-rw-r--r--actions/apitimelinehome.php62
-rw-r--r--actions/apitimelinementions.php36
-rw-r--r--actions/apitimelinepublic.php29
-rw-r--r--actions/apitimelineretweetedtome.php2
-rw-r--r--actions/apitimelineretweetsofme.php38
-rw-r--r--actions/apitimelinetag.php53
-rw-r--r--actions/apitimelineuser.php62
-rw-r--r--actions/doc.php7
-rw-r--r--actions/favor.php2
-rw-r--r--actions/groupmembers.php4
-rw-r--r--actions/makeadmin.php3
-rw-r--r--actions/public.php10
-rw-r--r--actions/rsd.php226
-rw-r--r--actions/showgroup.php4
-rw-r--r--actions/showstream.php13
-rw-r--r--actions/twitapisearchatom.php4
-rw-r--r--actions/userauthorization.php21
-rw-r--r--classes/Avatar.php15
-rwxr-xr-xclasses/Conversation.php78
-rw-r--r--classes/Design.php15
-rw-r--r--classes/Fave.php44
-rw-r--r--classes/File.php15
-rw-r--r--classes/Memcached_DataObject.php2
-rw-r--r--classes/Nonce.php15
-rw-r--r--classes/Notice.php232
-rw-r--r--classes/Profile.php127
-rw-r--r--classes/Subscription.php154
-rw-r--r--classes/User.php19
-rw-r--r--classes/User_group.php37
-rw-r--r--classes/statusnet.ini10
-rw-r--r--classes/statusnet.links.ini7
-rw-r--r--db/statusnet.sql8
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier.auto.php11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier.autoload.php21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier.func.php23
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier.includes.php208
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier.kses.php30
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier.path.php11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier.php237
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier.safe-includes.php202
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrCollections.php128
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef.php87
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS.php87
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Background.php87
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php126
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Border.php43
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Color.php78
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Composite.php38
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php28
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Filter.php54
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Font.php149
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php90
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php40
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Length.php47
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php78
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Multiple.php58
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Number.php69
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Percentage.php40
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php38
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/URI.php56
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/Enum.php65
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Bool.php28
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Class.php34
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Color.php32
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/ID.php70
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Length.php41
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php53
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php41
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Nmtokens.php52
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Pixels.php48
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/Integer.php73
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/Lang.php73
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/Switch.php34
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/Text.php15
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI.php77
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Email.php17
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Host.php62
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/IPv4.php39
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/IPv6.php99
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform.php56
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Background.php23
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BdoDir.php19
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BgColor.php23
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BoolToCSS.php36
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Border.php18
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/EnumToCSS.php58
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgRequired.php42
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgSpace.php44
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Input.php40
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Lang.php28
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Length.php27
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Name.php21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/NameSync.php27
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeEmbed.php15
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeObject.php16
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeParam.php50
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ScriptRequired.php16
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Textarea.php18
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrTypes.php77
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/AttrValidator.php162
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Bootstrap.php98
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/CSSDefinition.php292
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ChildDef.php48
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ChildDef/Chameleon.php48
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ChildDef/Custom.php90
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ChildDef/Empty.php20
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ChildDef/Optional.php26
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ChildDef/Required.php117
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ChildDef/StrictBlockquote.php88
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ChildDef/Table.php142
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Config.php580
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema.php158
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php44
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php106
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Exception.php11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange.php42
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php77
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php37
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php180
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Validator.php206
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php66
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema.serbin0 -> 12999 bytes
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt8
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt19
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt8
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt10
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt16
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt8
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt5
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt31
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt15
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt46
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt8
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt18
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt16
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt10
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt13
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt18
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt28
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt17
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt15
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt10
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt7
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt13
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt19
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt10
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt34
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt16
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt29
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt16
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt74
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt22
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt19
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt18
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt20
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt18
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt23
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt33
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt16
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt20
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt8
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt24
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt8
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt8
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt10
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt13
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt25
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt7
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt15
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt17
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt10
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt13
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt19
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt13
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt83
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt17
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt30
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/info.ini3
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ContentSets.php155
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Context.php82
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Definition.php39
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DefinitionCache.php108
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator.php62
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php43
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Memory.php46
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in47
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Null.php39
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer.php172
-rwxr-xr-xextlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer/README3
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DefinitionCacheFactory.php91
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Doctype.php60
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/DoctypeRegistry.php103
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ElementDef.php176
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Encoder.php426
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/EntityLookup.php44
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/EntityLookup/entities.ser1
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/EntityParser.php144
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ErrorCollector.php209
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/ErrorStruct.php60
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Exception.php12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Filter.php46
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php135
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Filter/YouTube.php39
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Generator.php183
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLDefinition.php420
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule.php244
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Bdo.php31
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/CommonAttributes.php26
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Edit.php38
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Forms.php118
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Hypertext.php31
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Image.php40
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Legacy.php143
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/List.php35
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Name.php21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Object.php47
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Presentation.php36
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Proprietary.php33
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Ruby.php27
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/SafeEmbed.php33
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/SafeObject.php50
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Scripting.php54
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/StyleAttribute.php24
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tables.php66
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Target.php23
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Text.php71
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy.php207
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Name.php24
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Proprietary.php23
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Strict.php21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Transitional.php9
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/XHTML.php17
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php161
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModule/XMLCommonAttributes.php14
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/HTMLModuleManager.php403
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/IDAccumulator.php53
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Injector.php239
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Injector/AutoParagraph.php340
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Injector/DisplayLinkURI.php26
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Injector/Linkify.php46
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Injector/PurifierLinkify.php45
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Injector/RemoveEmpty.php51
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Injector/SafeObject.php87
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Language.php163
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Language/classes/en-x-test.php12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Language/messages/en-x-test.php11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Language/messages/en-x-testmini.php12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Language/messages/en.php62
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/LanguageFactory.php198
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Length.php115
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Lexer.php298
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Lexer/DOMLex.php213
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Lexer/DirectLex.php490
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Lexer/PEARSax3.php106
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Lexer/PH5P.php3906
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/PercentEncoder.php98
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Printer.php176
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Printer/CSSDefinition.php38
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.css10
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.js5
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.php368
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Printer/HTMLDefinition.php272
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/PropertyList.php86
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/PropertyListIterator.php32
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Strategy.php26
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Strategy/Composite.php25
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Strategy/Core.php18
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Strategy/FixNesting.php328
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Strategy/MakeWellFormed.php457
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Strategy/RemoveForeignElements.php171
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Strategy/ValidateAttributes.php39
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/StringHash.php39
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/StringHashParser.php110
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/TagTransform.php36
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/TagTransform/Font.php96
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/TagTransform/Simple.php35
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Token.php57
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Token/Comment.php22
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Token/Empty.php11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Token/End.php19
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Token/Start.php11
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Token/Tag.php56
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/Token/Text.php33
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/TokenFactory.php94
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URI.php173
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIDefinition.php93
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIFilter.php45
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIFilter/DisableExternal.php23
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIFilter/DisableExternalResources.php12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIFilter/HostBlacklist.php21
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIFilter/MakeAbsolute.php114
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIFilter/Munge.php58
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIParser.php70
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIScheme.php42
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIScheme/ftp.php43
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIScheme/http.php20
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIScheme/https.php12
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIScheme/mailto.php27
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIScheme/news.php22
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URIScheme/nntp.php20
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/URISchemeRegistry.php68
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/UnitConverter.php254
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/VarParser.php154
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/VarParser/Flexible.php96
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/VarParser/Native.php26
-rw-r--r--extlib/HTMLPurifier/HTMLPurifier/VarParserException.php11
-rw-r--r--extlib/HTMLPurifier/LICENSE504
-rw-r--r--js/jquery.form.js1292
-rw-r--r--js/jquery.js6209
-rw-r--r--js/jquery.min.js164
-rw-r--r--js/util.js268
-rw-r--r--lib/action.php8
-rw-r--r--lib/api.php26
-rw-r--r--lib/atom10entry.php105
-rw-r--r--lib/atom10feed.php305
-rw-r--r--lib/atomgroupnoticefeed.php67
-rw-r--r--lib/atomnoticefeed.php110
-rw-r--r--lib/atomusernoticefeed.php72
-rw-r--r--lib/cache.php4
-rw-r--r--lib/command.php29
-rw-r--r--lib/default.php15
-rw-r--r--lib/error.php10
-rw-r--r--lib/grouplist.php4
-rw-r--r--lib/groupsection.php3
-rw-r--r--lib/htmloutputter.php15
-rw-r--r--lib/httpclient.php5
-rw-r--r--lib/mysqlschema.php538
-rw-r--r--lib/noticelist.php41
-rw-r--r--lib/noticesection.php1
-rw-r--r--lib/pgsqlschema.php503
-rw-r--r--lib/profilelist.php4
-rw-r--r--lib/profilesection.php2
-rw-r--r--lib/right.php1
-rw-r--r--lib/router.php9
-rw-r--r--lib/schema.php58
-rw-r--r--lib/statusnet.php11
-rw-r--r--lib/subs.php144
-rw-r--r--lib/taguri.php96
-rw-r--r--lib/theme.php15
-rw-r--r--lib/userprofile.php9
-rw-r--r--lib/util.php261
-rw-r--r--plugins/FeedSub/FeedSubPlugin.php115
-rw-r--r--plugins/FeedSub/actions/feedsubcallback.php100
-rw-r--r--plugins/FeedSub/extlib/README9
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser.php351
-rw-r--r--plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php365
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php261
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/Exception.php42
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php214
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php62
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php277
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php276
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php151
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php116
-rw-r--r--plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php335
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php171
-rw-r--r--plugins/FeedSub/extlib/XML/Feed/Parser/Type.php467
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml28
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml20
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml45
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/delicious.feed177
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/flickr.feed184
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml7
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/hoder.xml102
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml13
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml47
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml30
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml15
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml103
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml62
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml67
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml42
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml226
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/samples/technorati.feed54
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc338
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc113
-rwxr-xr-xplugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc218
-rw-r--r--plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch14
-rw-r--r--plugins/FeedSub/feedinfo.php268
-rw-r--r--plugins/FeedSub/feedinfo.sql14
-rw-r--r--plugins/FeedSub/feedmunger.php238
-rw-r--r--plugins/FeedSub/tests/FeedMungerTest.php147
-rw-r--r--plugins/MemcachePlugin.php18
-rw-r--r--plugins/MobileProfile/MobileProfilePlugin.php10
-rw-r--r--plugins/MobileProfile/mp-screen.css5
-rw-r--r--plugins/OStatus/OStatusPlugin.php489
-rw-r--r--plugins/OStatus/README (renamed from plugins/FeedSub/README)0
-rw-r--r--plugins/OStatus/actions/feedsubsettings.php (renamed from plugins/FeedSub/actions/feedsubsettings.php)117
-rw-r--r--plugins/OStatus/actions/groupsalmon.php108
-rw-r--r--plugins/OStatus/actions/hostmeta.php42
-rw-r--r--plugins/OStatus/actions/ostatusinit.php168
-rw-r--r--plugins/OStatus/actions/ostatussub.php272
-rw-r--r--plugins/OStatus/actions/pushcallback.php122
-rw-r--r--plugins/OStatus/actions/pushhub.php202
-rw-r--r--plugins/OStatus/actions/usersalmon.php202
-rw-r--r--plugins/OStatus/actions/webfinger.php83
-rw-r--r--plugins/OStatus/classes/FeedSub.php452
-rw-r--r--plugins/OStatus/classes/HubSub.php305
-rw-r--r--plugins/OStatus/classes/Ostatus_profile.php951
-rw-r--r--plugins/OStatus/images/24px-Feed-icon.svg.png (renamed from plugins/FeedSub/images/24px-Feed-icon.svg.png)bin1204 -> 1204 bytes
-rw-r--r--plugins/OStatus/images/48px-Feed-icon.svg.png (renamed from plugins/FeedSub/images/48px-Feed-icon.svg.png)bin2434 -> 2434 bytes
-rw-r--r--plugins/OStatus/images/96px-Feed-icon.svg.png (renamed from plugins/FeedSub/images/96px-Feed-icon.svg.png)bin5440 -> 5440 bytes
-rw-r--r--plugins/OStatus/images/README (renamed from plugins/FeedSub/images/README)0
-rw-r--r--plugins/OStatus/js/ostatus.js60
-rw-r--r--plugins/OStatus/lib/activity.php716
-rw-r--r--plugins/OStatus/lib/feeddiscovery.php (renamed from plugins/FeedSub/feeddiscovery.php)84
-rw-r--r--plugins/OStatus/lib/hubdistribqueuehandler.php182
-rw-r--r--plugins/OStatus/lib/huboutqueuehandler.php60
-rw-r--r--plugins/OStatus/lib/hubverifyqueuehandler.php54
-rw-r--r--plugins/OStatus/lib/pushinputqueuehandler.php49
-rw-r--r--plugins/OStatus/lib/salmon.php67
-rw-r--r--plugins/OStatus/lib/salmonaction.php210
-rw-r--r--plugins/OStatus/lib/webfinger.php147
-rw-r--r--plugins/OStatus/lib/xrd.php183
-rw-r--r--plugins/OStatus/locale/OStatus.po (renamed from plugins/FeedSub/locale/FeedSub.po)0
-rw-r--r--plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po (renamed from plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po)0
-rw-r--r--plugins/OStatus/tests/ActivityParseTests.php209
-rw-r--r--plugins/OStatus/tests/FeedDiscoveryTest.php (renamed from plugins/FeedSub/tests/FeedDiscoveryTest.php)0
-rw-r--r--plugins/OStatus/tests/gettext-speedtest.php (renamed from plugins/FeedSub/tests/gettext-speedtest.php)0
-rw-r--r--plugins/OStatus/theme/base/css/ostatus.css30
-rw-r--r--plugins/OpenID/finishopenidlogin.php47
-rw-r--r--plugins/PostDebug/PostDebugPlugin.php150
-rw-r--r--plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php1
-rw-r--r--plugins/Realtime/realtimeupdate.js12
-rw-r--r--plugins/UserFlag/UserFlagPlugin.php19
-rw-r--r--plugins/UserFlag/clearflagform.php2
-rw-r--r--plugins/UserFlag/userflag.css4
-rwxr-xr-x[-rw-r--r--]scripts/setconfig.php16
-rw-r--r--tests/TagURITest.php36
-rw-r--r--theme/base/css/display.css54
-rw-r--r--theme/base/images/icons/icons-01.gifbin3650 -> 3870 bytes
-rw-r--r--theme/base/images/icons/twotone/green/against.gifbin0 -> 85 bytes
-rw-r--r--theme/base/images/icons/twotone/green/checkmark.gifbin0 -> 76 bytes
-rw-r--r--theme/base/images/icons/twotone/green/clear.gifbin0 -> 82 bytes
-rw-r--r--theme/base/images/icons/twotone/green/flag.gif (renamed from plugins/UserFlag/icon_flag.gif)bin80 -> 80 bytes
-rw-r--r--theme/base/logo.pngbin4988 -> 10107 bytes
-rw-r--r--theme/default/css/display.css48
-rw-r--r--theme/default/logo.pngbin6389 -> 10107 bytes
-rw-r--r--theme/default/mobilelogo.pngbin2238 -> 4017 bytes
-rw-r--r--theme/identica/css/display.css44
-rw-r--r--theme/identica/mobilelogo.pngbin2238 -> 4017 bytes
497 files changed, 38505 insertions, 9678 deletions
diff --git a/EVENTS.txt b/EVENTS.txt
index 6bf12bf13..d3c2fb7bf 100644
--- a/EVENTS.txt
+++ b/EVENTS.txt
@@ -355,6 +355,14 @@ EndShowHeadElements: Right before the </head> tag; put <script>s here if you nee
CheckSchema: chance to check the schema
+StartProfileRemoteSubscribe: Before showing the link to remote subscription
+- $userprofile: UserProfile widget
+- &$profile: the profile being shown
+
+EndProfileRemoteSubscribe: After showing the link to remote subscription
+- $userprofile: UserProfile widget
+- &$profile: the profile being shown
+
StartProfilePageProfileSection: Starting to show the section of the
profile page with the actual profile data;
hook to prevent showing the profile (e.g.)
@@ -714,3 +722,44 @@ StartRobotsTxt: Before outputting the robots.txt page
EndRobotsTxt: After the default robots.txt page (good place for customization)
- &$action: RobotstxtAction being shown
+StartGetProfileUri: When determining the canonical URI for a given profile
+- $profile: the current profile
+- &$uri: the URI
+
+EndGetProfileUri: After determining the canonical URI for a given profile
+- $profile: the current profile
+- &$uri: the URI
+
+StartFavorNotice: Saving a notice as a favorite
+- $profile: profile of the person faving (can be remote!)
+- $notice: notice being faved
+- &$fave: Favor object; null to start off with, but feel free to override.
+
+EndFavorNotice: After saving a notice as a favorite
+- $profile: profile of the person faving (can be remote!)
+- $notice: notice being faved
+
+StartDisfavorNotice: Saving a notice as a favorite
+- $profile: profile of the person faving (can be remote!)
+- $notice: notice being faved
+- &$result: result of the disfavoring (if you override)
+
+EndDisfavorNotice: After saving a notice as a favorite
+- $profile: profile of the person faving (can be remote!)
+- $notice: notice being faved
+
+StartFindMentions: start finding mentions in a block of text
+- $sender: sender profile
+- $text: plain text version of the notice
+- &$mentions: mentions found so far. Array of arrays; each array
+ has 'mentioned' (array of mentioned profiles), 'url' (url to link as),
+ 'title' (title of the link), 'position' (position of the text to
+ replace), 'text' (text to replace)
+
+EndFindMentions: end finding mentions in a block of text
+- $sender: sender profile
+- $text: plain text version of the notice
+- &$mentions: mentions found so far. Array of arrays; each array
+ has 'mentioned' (array of mentioned profiles), 'url' (url to link as),
+ 'title' (title of the link), 'position' (position of the text to
+ replace), 'text' (text to replace)
diff --git a/README b/README
index 9b4147645..75336eb83 100644
--- a/README
+++ b/README
@@ -1192,6 +1192,8 @@ server: If set, defines another server where avatars are stored in the
typically only make 2 connections to a single server at a
time <http://ur1.ca/6ih>, so this can parallelize the job.
Defaults to null.
+ssl: Whether to access avatars using HTTPS. Defaults to null, meaning
+ to guess based on site-wide SSL settings.
public
------
@@ -1221,6 +1223,19 @@ path: Path part of theme URLs, before the theme name. Relative to the
(using version numbers as the path) to make sure that all files are
reloaded by caching clients or proxies. Defaults to null,
which means to use the site path + '/theme'.
+ssl: Whether to use SSL for theme elements. Default is null, which means
+ guess based on site SSL settings.
+
+javascript
+----------
+
+server: You can speed up page loading by pointing the
+ theme file lookup to another server (virtual or real).
+ Defaults to NULL, meaning to use the site server.
+path: Path part of Javascript URLs. Defaults to null,
+ which means to use the site path + '/js/'.
+ssl: Whether to use SSL for JavaScript files. Default is null, which means
+ guess based on site SSL settings.
xmpp
----
@@ -1447,6 +1462,8 @@ server: server name to use when creating URLs for uploaded files.
a virtual server here can speed up Web performance.
path: URL path, relative to the server, to find files. Defaults to
main path + '/file/'.
+ssl: whether to use HTTPS for file URLs. Defaults to null, meaning to
+ guess based on other SSL settings.
filecommand: command to use for determining the type of a file. May be
skipped if fileinfo extension is installed. Defaults to
'/usr/bin/file'.
@@ -1506,6 +1523,8 @@ dir: directory to write backgrounds too. Default is '/background/'
subdir of install dir.
path: path to backgrounds. Default is sub-path of install path; note
that you may need to change this if you change site-path too.
+ssl: Whether or not to use HTTPS for background files. Defaults to
+ null, meaning to guess from site-wide SSL settings.
ping
----
diff --git a/actions/apidirectmessage.php b/actions/apidirectmessage.php
index 5fbc46518..5355acf82 100644
--- a/actions/apidirectmessage.php
+++ b/actions/apidirectmessage.php
@@ -79,7 +79,7 @@ class ApiDirectMessageAction extends ApiAuthAction
}
$server = common_root_url();
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
if ($this->arg('sent')) {
diff --git a/actions/apifriendshipsdestroy.php b/actions/apifriendshipsdestroy.php
index 91c6fd032..d48a57756 100644
--- a/actions/apifriendshipsdestroy.php
+++ b/actions/apifriendshipsdestroy.php
@@ -124,12 +124,9 @@ class ApiFriendshipsDestroyAction extends ApiAuthAction
return;
}
- $result = subs_unsubscribe_user($this->user, $this->other->nickname);
-
- if (is_string($result)) {
- $this->clientError($result, 403, $this->format);
- return;
- }
+ // throws an exception on error
+ Subscription::cancel($this->user->getProfile(),
+ $this->other->getProfile());
$this->initDocument($this->format);
$this->showProfile($this->other, $this->format);
diff --git a/actions/apigrouplist.php b/actions/apigrouplist.php
index 66b67a030..605b38232 100644
--- a/actions/apigrouplist.php
+++ b/actions/apigrouplist.php
@@ -93,7 +93,7 @@ class ApiGroupListAction extends ApiBareAuthAction
$sitename = common_config('site', 'name');
$title = sprintf(_("%s's groups"), $this->user->nickname);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:Groups";
$link = common_local_url(
'usergroups',
diff --git a/actions/apigrouplistall.php b/actions/apigrouplistall.php
index 1921c1f19..d2ef2978a 100644
--- a/actions/apigrouplistall.php
+++ b/actions/apigrouplistall.php
@@ -88,7 +88,7 @@ class ApiGroupListAllAction extends ApiPrivateAuthAction
$sitename = common_config('site', 'name');
$title = sprintf(_("%s groups"), $sitename);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:Groups";
$link = common_local_url('groups');
$subtitle = sprintf(_("groups on %s"), $sitename);
diff --git a/actions/apitimelinefavorites.php b/actions/apitimelinefavorites.php
index 1027d97d4..c89d02247 100644
--- a/actions/apitimelinefavorites.php
+++ b/actions/apitimelinefavorites.php
@@ -100,44 +100,81 @@ class ApiTimelineFavoritesAction extends ApiBareAuthAction
function showTimeline()
{
- $profile = $this->user->getProfile();
- $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
+ $profile = $this->user->getProfile();
+ $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
- $sitename = common_config('site', 'name');
- $title = sprintf(
+ $sitename = common_config('site', 'name');
+ $title = sprintf(
_('%1$s / Favorites from %2$s'),
$sitename,
$this->user->nickname
);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:Favorites:" . $this->user->id;
- $link = common_local_url(
- 'favorites',
- array('nickname' => $this->user->nickname)
- );
- $subtitle = sprintf(
+
+ $subtitle = sprintf(
_('%1$s updates favorited by %2$s / %2$s.'),
$sitename,
$profile->getBestName(),
$this->user->nickname
);
- $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+ $logo = !empty($avatar)
+ ? $avatar->displayUrl()
+ : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
switch($this->format) {
case 'xml':
$this->showXmlTimeline($this->notices);
break;
case 'rss':
- $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+ $link = common_local_url(
+ 'showfavorites',
+ array('nickname' => $this->user->nickname)
+ );
+ $this->showRssTimeline(
+ $this->notices,
+ $title,
+ $link,
+ $subtitle,
+ null,
+ $logo
+ );
break;
case 'atom':
- $selfuri = common_root_url() .
- ltrim($_SERVER['QUERY_STRING'], 'p=');
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link, $subtitle,
- null, $selfuri, $logo
+
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ $atom = new AtomNoticeFeed();
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
+
+ $atom->addLink(
+ common_local_url(
+ 'showfavorites',
+ array('nickname' => $this->user->nickname)
+ )
+ );
+
+ $id = $this->arg('id');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
+ }
+
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineFavorites', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
);
+
+ $atom->addEntryFromNotices($this->notices);
+
+ $this->raw($atom->getString());
+
break;
case 'json':
$this->showJsonTimeline($this->notices);
diff --git a/actions/apitimelinefriends.php b/actions/apitimelinefriends.php
index 4e3827bae..2db76857e 100644
--- a/actions/apitimelinefriends.php
+++ b/actions/apitimelinefriends.php
@@ -112,41 +112,73 @@ class ApiTimelineFriendsAction extends ApiBareAuthAction
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$sitename = common_config('site', 'name');
$title = sprintf(_("%s and friends"), $this->user->nickname);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:FriendsTimeline:" . $this->user->id;
- $link = common_local_url(
- 'all', array('nickname' => $this->user->nickname)
- );
- $subtitle = sprintf(
- _('Updates from %1$s and friends on %2$s!'),
- $this->user->nickname, $sitename
- );
- $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+
+ $subtitle = sprintf(
+ _('Updates from %1$s and friends on %2$s!'),
+ $this->user->nickname, $sitename
+ );
+
+ $logo = (!empty($avatar))
+ ? $avatar->displayUrl()
+ : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
switch($this->format) {
case 'xml':
$this->showXmlTimeline($this->notices);
break;
case 'rss':
- $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+
+ $link = common_local_url(
+ 'all', array(
+ 'nickname' => $this->user->nickname
+ )
+ );
+
+ $this->showRssTimeline(
+ $this->notices,
+ $title,
+ $link,
+ $subtitle,
+ null,
+ $logo
+ );
break;
case 'atom':
- $target_id = $this->arg('id');
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ $atom = new AtomNoticeFeed();
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
- if (isset($target_id)) {
- $selfuri = common_root_url() .
- 'api/statuses/friends_timeline/' .
- $target_id . '.atom';
- } else {
- $selfuri = common_root_url() .
- 'api/statuses/friends_timeline.atom';
+ $atom->addLink(
+ common_local_url(
+ 'all',
+ array('nickname' => $this->user->nickname)
+ )
+ );
+
+ $id = $this->arg('id');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
}
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link,
- $subtitle, null, $selfuri, $logo
- );
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineFriends', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
+ );
+
+ $atom->addEntryFromNotices($this->notices);
+
+ $this->raw($atom->getString());
+
break;
case 'json':
$this->showJsonTimeline($this->notices);
diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php
index af414c680..1d0c4afdd 100644
--- a/actions/apitimelinegroup.php
+++ b/actions/apitimelinegroup.php
@@ -107,41 +107,85 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
$sitename = common_config('site', 'name');
$avatar = $this->group->homepage_logo;
$title = sprintf(_("%s timeline"), $this->group->nickname);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:GroupTimeline:" . $this->group->id;
- $link = common_local_url(
- 'showgroup',
- array('nickname' => $this->group->nickname)
- );
+
$subtitle = sprintf(
_('Updates from %1$s on %2$s!'),
$this->group->nickname,
$sitename
);
- $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
+
+ $logo = ($avatar) ? $avatar : User_group::defaultLogo(AVATAR_PROFILE_SIZE);
switch($this->format) {
case 'xml':
$this->showXmlTimeline($this->notices);
break;
case 'rss':
- $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
- break;
- case 'atom':
- $selfuri = common_root_url() .
- 'api/statusnet/groups/timeline/' .
- $this->group->nickname . '.atom';
- $this->showAtomTimeline(
+ $this->showRssTimeline(
$this->notices,
$title,
- $id,
- $link,
+ $this->group->homeUrl(),
$subtitle,
null,
- $selfuri,
$logo
);
break;
+ case 'atom':
+
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ try {
+
+ // If this was called using an integer ID, i.e.: using the canonical
+ // URL for this group's feed, then pass the Group object into the feed,
+ // so the OStatus plugin, and possibly other plugins, can access it.
+ // Feels sorta hacky. -- Z
+
+ $atom = null;
+ $id = $this->arg('id');
+
+ if (strval(intval($id)) === strval($id)) {
+ $atom = new AtomGroupNoticeFeed($this->group);
+ } else {
+ $atom = new AtomGroupNoticeFeed();
+ }
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
+
+ $atom->addAuthorRaw($this->group->asAtomAuthor());
+ $atom->setActivitySubject($this->group->asActivitySubject());
+
+ $atom->addLink($this->group->homeUrl());
+
+ $id = $this->arg('id');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
+ }
+
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineGroup', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
+ );
+
+ $atom->addEntryFromNotices($this->notices);
+
+ $this->raw($atom->getString());
+
+ } catch (Atom10FeedException $e) {
+ $this->serverError(
+ 'Could not generate feed for group - ' . $e->getMessage()
+ );
+ return;
+ }
+
+ break;
case 'json':
$this->showJsonTimeline($this->notices);
break;
diff --git a/actions/apitimelinehome.php b/actions/apitimelinehome.php
index 828eae6cf..0c72f4020 100644
--- a/actions/apitimelinehome.php
+++ b/actions/apitimelinehome.php
@@ -113,41 +113,69 @@ class ApiTimelineHomeAction extends ApiBareAuthAction
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$sitename = common_config('site', 'name');
$title = sprintf(_("%s and friends"), $this->user->nickname);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:HomeTimeline:" . $this->user->id;
- $link = common_local_url(
- 'all', array('nickname' => $this->user->nickname)
- );
+
$subtitle = sprintf(
_('Updates from %1$s and friends on %2$s!'),
$this->user->nickname, $sitename
);
- $logo = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+
+ $logo = (!empty($avatar))
+ ? $avatar->displayUrl()
+ : Avatar::defaultImage(AVATAR_PROFILE_SIZE);
switch($this->format) {
case 'xml':
$this->showXmlTimeline($this->notices);
break;
case 'rss':
- $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
+ $link = common_local_url(
+ 'all',
+ array('nickname' => $this->user->nickname)
+ );
+ $this->showRssTimeline(
+ $this->notices,
+ $title,
+ $link,
+ $subtitle,
+ null,
+ $logo
+ );
break;
case 'atom':
- $target_id = $this->arg('id');
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ $atom = new AtomNoticeFeed();
- if (isset($target_id)) {
- $selfuri = common_root_url() .
- 'api/statuses/home_timeline/' .
- $target_id . '.atom';
- } else {
- $selfuri = common_root_url() .
- 'api/statuses/home_timeline.atom';
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
+
+ $atom->addLink(
+ common_local_url(
+ 'all',
+ array('nickname' => $this->user->nickname)
+ )
+ );
+
+ $id = $this->arg('id');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
}
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link,
- $subtitle, null, $selfuri, $logo
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineHome', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
);
+
+ $atom->addEntryFromNotices($this->notices);
+ $this->raw($atom->getString());
+
break;
case 'json':
$this->showJsonTimeline($this->notices);
diff --git a/actions/apitimelinementions.php b/actions/apitimelinementions.php
index 9dc2162cc..a39c63346 100644
--- a/actions/apitimelinementions.php
+++ b/actions/apitimelinementions.php
@@ -117,7 +117,7 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
_('%1$s / Updates mentioning %2$s'),
$sitename, $this->user->nickname
);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:Mentions:" . $this->user->id;
$link = common_local_url(
'replies',
@@ -137,12 +137,36 @@ class ApiTimelineMentionsAction extends ApiBareAuthAction
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $logo);
break;
case 'atom':
- $selfuri = common_root_url() .
- ltrim($_SERVER['QUERY_STRING'], 'p=');
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link, $subtitle,
- null, $selfuri, $logo
+
+ $atom = new AtomNoticeFeed();
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
+
+ $atom->addLink(
+ common_local_url(
+ 'replies',
+ array('nickname' => $this->user->nickname)
+ )
+ );
+
+ $id = $this->arg('id');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
+ }
+
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineMentions', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
);
+
+ $atom->addEntryFromNotices($this->notices);
+ $this->raw($atom->getString());
+
break;
case 'json':
$this->showJsonTimeline($this->notices);
diff --git a/actions/apitimelinepublic.php b/actions/apitimelinepublic.php
index 0fb0788e9..1ff0fd261 100644
--- a/actions/apitimelinepublic.php
+++ b/actions/apitimelinepublic.php
@@ -74,7 +74,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
parent::prepare($args);
$this->notices = $this->getNotices();
-
+
if ($this->since) {
throw new ServerException("since parameter is disabled for performance; use since_id", 403);
}
@@ -109,7 +109,7 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
$sitename = common_config('site', 'name');
$sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png');
$title = sprintf(_("%s public timeline"), $sitename);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:PublicTimeline";
$link = common_root_url();
$subtitle = sprintf(_("%s updates from everyone!"), $sitename);
@@ -122,11 +122,28 @@ class ApiTimelinePublicAction extends ApiPrivateAuthAction
$this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
break;
case 'atom':
- $selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link,
- $subtitle, null, $selfuri, $sitelogo
+
+ $atom = new AtomNoticeFeed();
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($sitelogo);
+ $atom->setUpdated('now');
+
+ $atom->addLink(common_local_url('public'));
+
+ $atom->addLink(
+ $this->getSelfUri(
+ 'ApiTimelinePublic', array('format' => 'atom')
+ ),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
);
+
+ $atom->addEntryFromNotices($this->notices);
+
+ $this->raw($atom->getString());
+
break;
case 'json':
$this->showJsonTimeline($this->notices);
diff --git a/actions/apitimelineretweetedtome.php b/actions/apitimelineretweetedtome.php
index e47bc30b8..73e35c86b 100644
--- a/actions/apitimelineretweetedtome.php
+++ b/actions/apitimelineretweetedtome.php
@@ -109,7 +109,7 @@ class ApiTimelineRetweetedToMeAction extends ApiAuthAction
$profile = $this->auth_user->getProfile();
$title = sprintf(_("Repeated to %s"), $this->auth_user->nickname);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:RepeatedToMe:" . $this->auth_user->id;
$link = common_local_url('all',
array('nickname' => $this->auth_user->nickname));
diff --git a/actions/apitimelineretweetsofme.php b/actions/apitimelineretweetsofme.php
index e4b09e9bd..c77912fd0 100644
--- a/actions/apitimelineretweetsofme.php
+++ b/actions/apitimelineretweetsofme.php
@@ -99,6 +99,8 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
$strm = $this->auth_user->repeatsOfMe($offset, $limit, $this->since_id, $this->max_id);
+ common_debug(var_export($strm, true));
+
switch ($this->format) {
case 'xml':
$this->showXmlTimeline($strm);
@@ -110,12 +112,40 @@ class ApiTimelineRetweetsOfMeAction extends ApiAuthAction
$profile = $this->auth_user->getProfile();
$title = sprintf(_("Repeats of %s"), $this->auth_user->nickname);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:RepeatsOfMe:" . $this->auth_user->id;
- $link = common_local_url('showstream',
- array('nickname' => $this->auth_user->nickname));
- $this->showAtomTimeline($strm, $title, $id, $link);
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ $atom = new AtomNoticeFeed();
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setUpdated('now');
+
+ $atom->addLink(
+ common_local_url(
+ 'showstream',
+ array('nickname' => $this->auth_user->nickname)
+ )
+ );
+
+ $id = $this->arg('id');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
+ }
+
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineRetweetsOfMe', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
+ );
+
+ $atom->addEntryFromNotices($strm);
+
+ $this->raw($atom->getString());
+
break;
default:
diff --git a/actions/apitimelinetag.php b/actions/apitimelinetag.php
index 1427d23b6..a29061fcc 100644
--- a/actions/apitimelinetag.php
+++ b/actions/apitimelinetag.php
@@ -100,16 +100,12 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
$sitename = common_config('site', 'name');
$sitelogo = (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png');
$title = sprintf(_("Notices tagged with %s"), $this->tag);
- $link = common_local_url(
- 'tag',
- array('tag' => $this->tag)
- );
$subtitle = sprintf(
_('Updates tagged with %1$s on %2$s!'),
$this->tag,
$sitename
);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:TagTimeline:".$tag;
switch($this->format) {
@@ -117,23 +113,52 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction
$this->showXmlTimeline($this->notices);
break;
case 'rss':
- $this->showRssTimeline($this->notices, $title, $link, $subtitle, null, $sitelogo);
- break;
- case 'atom':
- $selfuri = common_root_url() .
- 'api/statusnet/tags/timeline/' .
- $this->tag . '.atom';
- $this->showAtomTimeline(
+ $link = common_local_url(
+ 'tag',
+ array('tag' => $this->tag)
+ );
+ $this->showRssTimeline(
$this->notices,
$title,
- $id,
$link,
$subtitle,
null,
- $selfuri,
$sitelogo
);
break;
+ case 'atom':
+
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ $atom = new AtomNoticeFeed();
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
+
+ $atom->addLink(
+ common_local_url(
+ 'tag',
+ array('tag' => $this->tag)
+ )
+ );
+
+ $aargs = array('format' => 'atom');
+ if (!empty($this->tag)) {
+ $aargs['tag'] = $this->tag;
+ }
+
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineTag', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
+ );
+
+ $atom->addEntryFromNotices($this->notices);
+ $this->raw($atom->getString());
+
+ break;
case 'json':
$this->showJsonTimeline($this->notices);
break;
diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php
index 830b16941..3e849cc78 100644
--- a/actions/apitimelineuser.php
+++ b/actions/apitimelineuser.php
@@ -116,7 +116,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
$sitename = common_config('site', 'name');
$title = sprintf(_("%s timeline"), $this->user->nickname);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$id = "tag:$taguribase:UserTimeline:" . $this->user->id;
$link = common_local_url(
'showstream',
@@ -145,18 +145,60 @@ class ApiTimelineUserAction extends ApiBareAuthAction
);
break;
case 'atom':
- if (isset($apidata['api_arg'])) {
- $selfuri = common_root_url() .
- 'api/statuses/user_timeline/' .
- $apidata['api_arg'] . '.atom';
+
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ // If this was called using an integer ID, i.e.: using the canonical
+ // URL for this user's feed, then pass the User object into the feed,
+ // so the OStatus plugin, and possibly other plugins, can access it.
+ // Feels sorta hacky. -- Z
+
+ $atom = null;
+ $id = $this->arg('id');
+
+ if (strval(intval($id)) === strval($id)) {
+ $atom = new AtomUserNoticeFeed($this->user);
} else {
- $selfuri = common_root_url() .
- 'api/statuses/user_timeline.atom';
+ $atom = new AtomUserNoticeFeed();
+ }
+
+ $atom->setId($id);
+ $atom->setTitle($title);
+ $atom->setSubtitle($subtitle);
+ $atom->setLogo($logo);
+ $atom->setUpdated('now');
+
+ $atom->addLink(
+ common_local_url(
+ 'showstream',
+ array('nickname' => $this->user->nickname)
+ )
+ );
+
+ $id = $this->arg('id');
+ $aargs = array('format' => 'atom');
+ if (!empty($id)) {
+ $aargs['id'] = $id;
}
- $this->showAtomTimeline(
- $this->notices, $title, $id, $link,
- $subtitle, $suplink, $selfuri, $logo
+
+ $atom->addLink(
+ $this->getSelfUri('ApiTimelineUser', $aargs),
+ array('rel' => 'self', 'type' => 'application/atom+xml')
+ );
+
+ $atom->addLink(
+ $suplink,
+ array(
+ 'rel' => 'http://api.friendfeed.com/2008/03#sup',
+ 'type' => 'application/json'
+ )
);
+
+ $atom->addEntryFromNotices($this->notices);
+
+ #$this->raw($atom->getString());
+ print $atom->getString(); // temporary for output buffering
+
break;
case 'json':
$this->showJsonTimeline($this->notices);
diff --git a/actions/doc.php b/actions/doc.php
index eaf4b7df2..459f5f096 100644
--- a/actions/doc.php
+++ b/actions/doc.php
@@ -173,6 +173,10 @@ class DocAction extends Action
}
$local = glob(INSTALLDIR.'/local/doc-src/'.$this->title.'.*');
+ if ($local === false) {
+ // Some systems return false, others array(), if dir didn't exist.
+ $local = array();
+ }
if (count($local) || isset($localDef)) {
return $this->negotiateLanguage($local, $localDef);
@@ -183,6 +187,9 @@ class DocAction extends Action
}
$dist = glob(INSTALLDIR.'/doc-src/'.$this->title.'.*');
+ if ($dist === false) {
+ $dist = array();
+ }
if (count($dist) || isset($distDef)) {
return $this->negotiateLanguage($dist, $distDef);
diff --git a/actions/favor.php b/actions/favor.php
index 2aeb1da61..afca9768a 100644
--- a/actions/favor.php
+++ b/actions/favor.php
@@ -79,7 +79,7 @@ class FavorAction extends Action
$this->clientError(_('This notice is already a favorite!'));
return;
}
- $fave = Fave::addNew($user, $notice);
+ $fave = Fave::addNew($user->getProfile(), $notice);
if (!$fave) {
$this->serverError(_('Could not create favorite.'));
return;
diff --git a/actions/groupmembers.php b/actions/groupmembers.php
index 0f47c268d..f16e972a4 100644
--- a/actions/groupmembers.php
+++ b/actions/groupmembers.php
@@ -192,7 +192,9 @@ class GroupMemberListItem extends ProfileListItem
{
$user = common_current_user();
- if (!empty($user) && $user->id != $this->profile->id && $user->isAdmin($this->group) &&
+ if (!empty($user) &&
+ $user->id != $this->profile->id &&
+ ($user->isAdmin($this->group) || $user->hasRight(Right::MAKEGROUPADMIN)) &&
!$this->profile->isAdmin($this->group)) {
$this->out->elementStart('li', 'entity_make_admin');
$maf = new MakeAdminForm($this->out, $this->profile, $this->group,
diff --git a/actions/makeadmin.php b/actions/makeadmin.php
index 9ad7d6e7c..f19348648 100644
--- a/actions/makeadmin.php
+++ b/actions/makeadmin.php
@@ -87,7 +87,8 @@ class MakeadminAction extends Action
return false;
}
$user = common_current_user();
- if (!$user->isAdmin($this->group)) {
+ if (!$user->isAdmin($this->group) &&
+ !$user->hasRight(Right::MAKEGROUPADMIN)) {
$this->clientError(_('Only an admin can make another user an admin.'), 401);
return false;
}
diff --git a/actions/public.php b/actions/public.php
index 982dfde15..50278bfce 100644
--- a/actions/public.php
+++ b/actions/public.php
@@ -131,12 +131,20 @@ class PublicAction extends Action
return _('Public timeline');
}
}
-
+
function extraHead()
{
parent::extraHead();
$this->element('meta', array('http-equiv' => 'X-XRDS-Location',
'content' => common_local_url('publicxrds')));
+
+ $rsd = common_local_url('rsd');
+
+ // RSD, http://tales.phrasewise.com/rfc/rsd
+
+ $this->element('link', array('rel' => 'EditURI',
+ 'type' => 'application/rsd+xml',
+ 'href' => $rsd));
}
/**
diff --git a/actions/rsd.php b/actions/rsd.php
new file mode 100644
index 000000000..f88bf2e9a
--- /dev/null
+++ b/actions/rsd.php
@@ -0,0 +1,226 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008-2010, StatusNet, Inc.
+ *
+ * Really Simple Discovery (RSD) for API access
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category API
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * RSD action class
+ *
+ * Really Simple Discovery (RSD) is a simple (to a fault, maybe)
+ * discovery tool for blog APIs.
+ *
+ * http://tales.phrasewise.com/rfc/rsd
+ *
+ * Anil Dash suggested that RSD be used for services that implement
+ * the Twitter API:
+ *
+ * http://dashes.com/anil/2009/12/the-twitter-api-is-finished.html
+ *
+ * It's in use now for WordPress.com blogs:
+ *
+ * http://matt.wordpress.com/xmlrpc.php?rsd
+ *
+ * I (evan@status.net) have tried to stay faithful to the premise of
+ * RSD, while adding information useful to StatusNet client developers.
+ * In particular:
+ *
+ * - There is a link from each user's profile page to their personal
+ * RSD feed. A personal rsd.xml includes a 'blogID' element that is
+ * their username.
+ * - There is a link from the public root to '/rsd.xml', a public RSD
+ * feed. It's identical to the personal rsd except it doesn't include
+ * a blogId.
+ * - I've added a setting to the API to indicate that OAuth support is
+ * available.
+ *
+ * @category API
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ */
+
+class RsdAction extends Action
+{
+ /**
+ * Optional attribute for the personal rsd.xml file.
+ */
+
+ var $user = null;
+
+ /**
+ * Prepare the action for use.
+ *
+ * Check for a nickname; redirect if non-canonical; if
+ * not provided, assume public rsd.xml.
+ *
+ * @param array $args GET, POST, and URI arguments.
+ *
+ * @return boolean success flag
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ // optional argument
+
+ $nickname_arg = $this->arg('nickname');
+
+ if (empty($nickname_arg)) {
+ $this->user = null;
+ } else {
+ $nickname = common_canonical_nickname($nickname_arg);
+
+ // Permanent redirect on non-canonical nickname
+
+ if ($nickname_arg != $nickname) {
+ common_redirect(common_local_url('rsd',
+ array('nickname' => $nickname)),
+ 301);
+ return false;
+ }
+
+ $this->user = User::staticGet('nickname', $nickname);
+
+ if (empty($this->user)) {
+ $this->clientError(_('No such user.'), 404);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Action handler.
+ *
+ * Outputs the XML format for an RSD file. May include
+ * personal information if this is a personal file
+ * (based on whether $user attribute is set).
+ *
+ * @param array $args array of arguments
+ *
+ * @return nothing
+ */
+
+ function handle($args)
+ {
+ header('Content-Type: application/rsd+xml');
+
+ $this->startXML();
+
+ $rsdNS = 'http://archipelago.phrasewise.com/rsd';
+ $this->elementStart('rsd', array('version' => '1.0',
+ 'xmlns' => $rsdNS));
+ $this->elementStart('service');
+ $this->element('engineName', null, _('StatusNet'));
+ $this->element('engineLink', null, 'http://status.net/');
+ $this->elementStart('apis');
+ if (Event::handle('StartRsdListApis', array($this, $this->user))) {
+
+ $blogID = (empty($this->user)) ? '' : $this->user->nickname;
+ $apiAttrs = array('name' => 'Twitter',
+ 'preferred' => 'true',
+ 'apiLink' => $this->_apiRoot(),
+ 'blogID' => $blogID);
+
+ $this->elementStart('api', $apiAttrs);
+ $this->elementStart('settings');
+ $this->element('docs', null,
+ 'http://status.net/wiki/TwitterCompatibleAPI');
+ $this->element('setting', array('name' => 'OAuth'),
+ 'true');
+ $this->elementEnd('settings');
+ $this->elementEnd('api');
+ Event::handle('EndRsdListApis', array($this, $this->user));
+ }
+ $this->elementEnd('apis');
+ $this->elementEnd('service');
+ $this->elementEnd('rsd');
+
+ $this->endXML();
+
+ return true;
+ }
+
+ /**
+ * Returns last-modified date for use in caching
+ *
+ * Per-user rsd.xml is dated to last change of user
+ * (in case of nickname change); public has no date.
+ *
+ * @return string date of last change of this page
+ */
+
+ function lastModified()
+ {
+ if (!empty($this->user)) {
+ return $this->user->modified;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Flag to indicate if this action is read-only
+ *
+ * It is; it doesn't change the DB.
+ *
+ * @param array $args ignored
+ *
+ * @return boolean true
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
+ /**
+ * Return current site's API root
+ *
+ * Varies based on URL parameters, like if fancy URLs are
+ * turned on.
+ *
+ * @return string API root URI for this site
+ */
+
+ private function _apiRoot()
+ {
+ if (common_config('site', 'fancy')) {
+ return common_path('api/', true);
+ } else {
+ return common_path('index.php/api/', true);
+ }
+ }
+}
diff --git a/actions/showgroup.php b/actions/showgroup.php
index 8042a4951..eb1238902 100644
--- a/actions/showgroup.php
+++ b/actions/showgroup.php
@@ -330,13 +330,13 @@ class ShowgroupAction extends GroupDesignAction
new Feed(Feed::RSS2,
common_local_url('ApiTimelineGroup',
array('format' => 'rss',
- 'id' => $this->group->nickname)),
+ 'id' => $this->group->id)),
sprintf(_('Notice feed for %s group (RSS 2.0)'),
$this->group->nickname)),
new Feed(Feed::ATOM,
common_local_url('ApiTimelineGroup',
array('format' => 'atom',
- 'id' => $this->group->nickname)),
+ 'id' => $this->group->id)),
sprintf(_('Notice feed for %s group (Atom)'),
$this->group->nickname)),
new Feed(Feed::FOAF,
diff --git a/actions/showstream.php b/actions/showstream.php
index c52919386..f9407e35a 100644
--- a/actions/showstream.php
+++ b/actions/showstream.php
@@ -131,14 +131,14 @@ class ShowstreamAction extends ProfileAction
new Feed(Feed::RSS2,
common_local_url('ApiTimelineUser',
array(
- 'id' => $this->user->nickname,
+ 'id' => $this->user->id,
'format' => 'rss')),
sprintf(_('Notice feed for %s (RSS 2.0)'),
$this->user->nickname)),
new Feed(Feed::ATOM,
common_local_url('ApiTimelineUser',
array(
- 'id' => $this->user->nickname,
+ 'id' => $this->user->id,
'format' => 'atom')),
sprintf(_('Notice feed for %s (Atom)'),
$this->user->nickname)),
@@ -178,6 +178,15 @@ class ShowstreamAction extends ProfileAction
$this->element('link', array('rel' => 'microsummary',
'href' => common_local_url('microsummary',
array('nickname' => $this->profile->nickname))));
+
+ $rsd = common_local_url('rsd',
+ array('nickname' => $this->profile->nickname));
+
+ // RSD, http://tales.phrasewise.com/rfc/rsd
+ $this->element('link', array('rel' => 'EditURI',
+ 'type' => 'application/rsd+xml',
+ 'href' => $rsd));
+
}
function showProfile()
diff --git a/actions/twitapisearchatom.php b/actions/twitapisearchatom.php
index baed2a0c7..e389ddec8 100644
--- a/actions/twitapisearchatom.php
+++ b/actions/twitapisearchatom.php
@@ -245,7 +245,7 @@ class TwitapisearchatomAction extends ApiAction
'xmlns:twitter' => 'http://api.twitter.com/',
'xml:lang' => 'en-US')); // XXX Other locales ?
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$this->element('id', null, "tag:$taguribase:search/$server");
$site_uri = common_path(false);
@@ -329,7 +329,7 @@ class TwitapisearchatomAction extends ApiAction
$this->elementStart('entry');
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$this->element('id', null, "tag:$taguribase:$notice->id");
$this->element('published', null, common_date_w3dtf($notice->created));
diff --git a/actions/userauthorization.php b/actions/userauthorization.php
index 4321f1302..7f71c60db 100644
--- a/actions/userauthorization.php
+++ b/actions/userauthorization.php
@@ -127,10 +127,10 @@ class UserauthorizationAction extends Action
$location = $params->getLocation();
$avatar = $params->getAvatarURL();
- $this->elementStart('div', array('class' => 'profile'));
$this->elementStart('div', 'entity_profile vcard');
- $this->elementStart('a', array('href' => $profile,
- 'class' => 'url'));
+ $this->elementStart('dl', 'entity_depiction');
+ $this->element('dt', null, _('Photo'));
+ $this->elementStart('dd');
if ($avatar) {
$this->element('img', array('src' => $avatar,
'class' => 'photo avatar',
@@ -138,11 +138,19 @@ class UserauthorizationAction extends Action
'height' => AVATAR_PROFILE_SIZE,
'alt' => $nickname));
}
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_nickname');
+ $this->element('dt', null, _('Nickname'));
+ $this->elementStart('dd');
$hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname';
- $this->elementStart('span', $hasFN);
+ $this->elementStart('a', array('href' => $profile,
+ 'class' => 'url '.$hasFN));
$this->raw($nickname);
- $this->elementEnd('span');
$this->elementEnd('a');
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
if (!is_null($fullname)) {
$this->elementStart('dl', 'entity_fn');
@@ -214,7 +222,6 @@ class UserauthorizationAction extends Action
$this->elementEnd('li');
$this->elementEnd('ul');
$this->elementEnd('div');
- $this->elementEnd('div');
}
function sendAuthorization()
@@ -350,4 +357,4 @@ class UserauthorizationAction extends Action
}
}
}
-} \ No newline at end of file
+}
diff --git a/classes/Avatar.php b/classes/Avatar.php
index 91bde0f04..dbe2cd813 100644
--- a/classes/Avatar.php
+++ b/classes/Avatar.php
@@ -82,9 +82,20 @@ class Avatar extends Memcached_DataObject
$server = common_config('site', 'server');
}
- // XXX: protocol
+ $ssl = common_config('avatar', 'ssl');
+
+ if (is_null($ssl)) { // null -> guess
+ if (common_config('site', 'ssl') == 'always' &&
+ !common_config('avatar', 'server')) {
+ $ssl = true;
+ } else {
+ $ssl = false;
+ }
+ }
+
+ $protocol = ($ssl) ? 'https' : 'http';
- return 'http://'.$server.$path.$filename;
+ return $protocol.'://'.$server.$path.$filename;
}
function displayUrl()
diff --git a/classes/Conversation.php b/classes/Conversation.php
new file mode 100755
index 000000000..ea8bd87b5
--- /dev/null
+++ b/classes/Conversation.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Data class for Conversations
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Data
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+require_once INSTALLDIR . '/classes/Memcached_DataObject.php';
+
+class Conversation extends Memcached_DataObject
+{
+ ###START_AUTOCODE
+ /* the code below is auto generated do not remove the above tag */
+
+ public $__table = 'conversation'; // table name
+ public $id; // int(4) primary_key not_null
+ public $uri; // varchar(225) unique_key
+ public $created; // datetime not_null
+ public $modified; // timestamp not_null default_CURRENT_TIMESTAMP
+
+ /* Static get */
+ function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('conversation',$k,$v); }
+
+ /* the code above is auto generated do not remove the tag below */
+ ###END_AUTOCODE
+
+ /**
+ * Factory method for creating a new conversation
+ *
+ * @return Conversation the new conversation DO
+ */
+ static function create()
+ {
+ $conv = new Conversation();
+ $conv->created = common_sql_now();
+ $id = $conv->insert();
+
+ if (empty($id)) {
+ common_log_db_error($conv, 'INSERT', __FILE__);
+ return null;
+ }
+
+ $orig = clone($conv);
+ $orig->uri = common_local_url('conversation', array('id' => $id));
+ $result = $orig->update($conv);
+
+ if (empty($result)) {
+ common_log_db_error($conv, 'UPDATE', __FILE__);
+ return null;
+ }
+
+ return $conv;
+ }
+
+}
+
diff --git a/classes/Design.php b/classes/Design.php
index 4e7d7dfb2..ff44e0109 100644
--- a/classes/Design.php
+++ b/classes/Design.php
@@ -155,9 +155,20 @@ class Design extends Memcached_DataObject
$server = common_config('site', 'server');
}
- // XXX: protocol
+ $ssl = common_config('background', 'ssl');
+
+ if (is_null($ssl)) { // null -> guess
+ if (common_config('site', 'ssl') == 'always' &&
+ !common_config('background', 'server')) {
+ $ssl = true;
+ } else {
+ $ssl = false;
+ }
+ }
+
+ $protocol = ($ssl) ? 'https' : 'http';
- return 'http://'.$server.$path.$filename;
+ return $protocol.'://'.$server.$path.$filename;
}
function setDisposition($on, $off, $tile)
diff --git a/classes/Fave.php b/classes/Fave.php
index 8113c8e16..0b6eec2bc 100644
--- a/classes/Fave.php
+++ b/classes/Fave.php
@@ -21,17 +21,47 @@ class Fave extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
- static function addNew($user, $notice) {
- $fave = new Fave();
- $fave->user_id = $user->id;
- $fave->notice_id = $notice->id;
- if (!$fave->insert()) {
- common_log_db_error($fave, 'INSERT', __FILE__);
- return false;
+ static function addNew($profile, $notice) {
+
+ $fave = null;
+
+ if (Event::handle('StartFavorNotice', array($profile, $notice, &$fave))) {
+
+ $fave = new Fave();
+
+ $fave->user_id = $profile->id;
+ $fave->notice_id = $notice->id;
+
+ if (!$fave->insert()) {
+ common_log_db_error($fave, 'INSERT', __FILE__);
+ return false;
+ }
+
+ Event::handle('EndFavorNotice', array($profile, $notice));
}
+
return $fave;
}
+ function delete()
+ {
+ $profile = Profile::staticGet('id', $this->user_id);
+ $notice = Notice::staticGet('id', $this->notice_id);
+
+ $result = null;
+
+ if (Event::handle('StartDisfavorNotice', array($profile, $notice, &$result))) {
+
+ $result = parent::delete();
+
+ if ($result) {
+ Event::handle('EndDisfavorNotice', array($profile, $notice));
+ }
+ }
+
+ return $result;
+ }
+
function pkeyGet($kv)
{
return Memcached_DataObject::pkeyGet('Fave', $kv);
diff --git a/classes/File.php b/classes/File.php
index ee418a802..91b12d2e2 100644
--- a/classes/File.php
+++ b/classes/File.php
@@ -228,9 +228,20 @@ class File extends Memcached_DataObject
$server = common_config('site', 'server');
}
- // XXX: protocol
+ $ssl = common_config('attachments', 'ssl');
- return 'http://'.$server.$path.$filename;
+ if (is_null($ssl)) { // null -> guess
+ if (common_config('site', 'ssl') == 'always' &&
+ !common_config('attachments', 'server')) {
+ $ssl = true;
+ } else {
+ $ssl = false;
+ }
+ }
+
+ $protocol = ($ssl) ? 'https' : 'http';
+
+ return $protocol.'://'.$server.$path.$filename;
}
}
diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php
index 16c3d906c..40576dc71 100644
--- a/classes/Memcached_DataObject.php
+++ b/classes/Memcached_DataObject.php
@@ -314,7 +314,7 @@ class Memcached_DataObject extends Safe_DataObject
$cached[] = clone($inst);
}
$inst->free();
- $c->set($ckey, $cached, MEMCACHE_COMPRESSED, $expiry);
+ $c->set($ckey, $cached, Cache::COMPRESSED, $expiry);
return new ArrayWrapper($cached);
}
diff --git a/classes/Nonce.php b/classes/Nonce.php
index 486a65a3c..2f8ab00b5 100644
--- a/classes/Nonce.php
+++ b/classes/Nonce.php
@@ -22,4 +22,19 @@ class Nonce extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
+
+ /**
+ * Compatibility hack for PHP 5.3
+ *
+ * The statusnet.links.ini entry cannot be read because "," is no longer
+ * allowed in key names when read by parse_ini_file().
+ *
+ * @return array
+ * @access public
+ */
+ function links()
+ {
+ return array('consumer_key,token' => 'token:consumer_key,token');
+ }
+
}
diff --git a/classes/Notice.php b/classes/Notice.php
index f9f386357..6f1ef81fc 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -194,6 +194,7 @@ class Notice extends Memcached_DataObject
*/
static function saveNew($profile_id, $content, $source, $options=null) {
$defaults = array('uri' => null,
+ 'url' => null,
'reply_to' => null,
'repeat_of' => null);
@@ -256,9 +257,16 @@ class Notice extends Memcached_DataObject
}
$notice->content = $final;
- $notice->rendered = common_render_content($final, $notice);
+
+ if (!empty($rendered)) {
+ $notice->rendered = $rendered;
+ } else {
+ $notice->rendered = common_render_content($final, $notice);
+ }
+
$notice->source = $source;
$notice->uri = $uri;
+ $notice->url = $url;
// Handle repeat case
@@ -309,7 +317,8 @@ class Notice extends Memcached_DataObject
// the beginning of a new conversation.
if (empty($notice->conversation)) {
- $notice->conversation = $notice->id;
+ $conv = Conversation::create();
+ $notice->conversation = $conv->id;
$changed = true;
}
@@ -331,14 +340,15 @@ class Notice extends Memcached_DataObject
return $notice;
}
- function blowOnInsert()
+ function blowOnInsert($conversation = false)
{
self::blow('profile:notice_ids:%d', $this->profile_id);
self::blow('public');
- if ($this->conversation != $this->id) {
- self::blow('notice:conversation_ids:%d', $this->conversation);
- }
+ // XXX: Before we were blowing the casche only if the notice id
+ // was not the root of the conversation. What to do now?
+
+ self::blow('notice:conversation_ids:%d', $this->conversation);
if (!empty($this->repeat_of)) {
self::blow('notice:repeats:%d', $this->repeat_of);
@@ -679,7 +689,20 @@ class Notice extends Memcached_DataObject
{
$ni = $this->whoGets($groups, $recipients);
- Inbox::bulkInsert($this->id, array_keys($ni));
+ $ids = array_keys($ni);
+
+ // We remove the author (if they're a local user),
+ // since we'll have already done this in distribute()
+
+ $i = array_search($this->profile_id, $ids);
+
+ if ($i !== false) {
+ unset($ids[$i]);
+ }
+
+ // Bulk insert
+
+ Inbox::bulkInsert($this->id, $ids);
return;
}
@@ -783,7 +806,7 @@ class Notice extends Memcached_DataObject
$result = $gi->insert();
- if (!result) {
+ if (!$result) {
common_log_db_error($gi, 'INSERT', __FILE__);
throw new ServerException(_('Problem saving group inbox.'));
}
@@ -797,6 +820,7 @@ class Notice extends Memcached_DataObject
/**
* @return array of integer profile IDs
*/
+
function saveReplies()
{
// Don't save reply data for repeats
@@ -805,76 +829,44 @@ class Notice extends Memcached_DataObject
return array();
}
- // Alternative reply format
- $tname = false;
- if (preg_match('/^T ([A-Z0-9]{1,64}) /', $this->content, $match)) {
- $tname = $match[1];
- }
- // extract all @messages
- $cnt = preg_match_all('/(?:^|\s)@([a-z0-9]{1,64})/', $this->content, $match);
-
- $names = array();
-
- if ($cnt || $tname) {
- // XXX: is there another way to make an array copy?
- $names = ($tname) ? array_unique(array_merge(array(strtolower($tname)), $match[1])) : array_unique($match[1]);
- }
-
$sender = Profile::staticGet($this->profile_id);
+ $mentions = common_find_mentions($this->profile_id, $this->content);
+
$replied = array();
// store replied only for first @ (what user/notice what the reply directed,
// we assume first @ is it)
- for ($i=0; $i<count($names); $i++) {
- $nickname = $names[$i];
- $recipient = common_relative_profile($sender, $nickname, $this->created);
- if (empty($recipient)) {
- continue;
- }
- // Don't save replies from blocked profile to local user
- $recipient_user = User::staticGet('id', $recipient->id);
- if (!empty($recipient_user) && $recipient_user->hasBlocked($sender)) {
- continue;
- }
- $reply = new Reply();
- $reply->notice_id = $this->id;
- $reply->profile_id = $recipient->id;
- $id = $reply->insert();
- if (!$id) {
- $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
- common_log(LOG_ERR, 'DB error inserting reply: ' . $last_error->message);
- common_server_error(sprintf(_('DB error inserting reply: %s'), $last_error->message));
- return array();
- } else {
- $replied[$recipient->id] = 1;
- }
- }
+ foreach ($mentions as $mention) {
- // Hash format replies, too
- $cnt = preg_match_all('/(?:^|\s)@#([a-z0-9]{1,64})/', $this->content, $match);
- if ($cnt) {
- foreach ($match[1] as $tag) {
- $tagged = Profile_tag::getTagged($sender->id, $tag);
- foreach ($tagged as $t) {
- if (!$replied[$t->id]) {
- // Don't save replies from blocked profile to local user
- $t_user = User::staticGet('id', $t->id);
- if ($t_user && $t_user->hasBlocked($sender)) {
- continue;
- }
- $reply = new Reply();
- $reply->notice_id = $this->id;
- $reply->profile_id = $t->id;
- $id = $reply->insert();
- if (!$id) {
- common_log_db_error($reply, 'INSERT', __FILE__);
- return array();
- } else {
- $replied[$recipient->id] = 1;
- }
- }
+ foreach ($mention['mentioned'] as $mentioned) {
+
+ // skip if they're already covered
+
+ if (!empty($replied[$mentioned->id])) {
+ continue;
+ }
+
+ // Don't save replies from blocked profile to local user
+
+ $mentioned_user = User::staticGet('id', $mentioned->id);
+ if (!empty($mentioned_user) && $mentioned_user->hasBlocked($sender)) {
+ continue;
+ }
+
+ $reply = new Reply();
+
+ $reply->notice_id = $this->id;
+ $reply->profile_id = $mentioned->id;
+
+ $id = $reply->insert();
+
+ if (!$id) {
+ common_log_db_error($reply, 'INSERT', __FILE__);
+ throw new ServerException("Couldn't save reply for {$this->id}, {$mentioned->id}");
+ } else {
+ $replied[$mentioned->id] = 1;
}
}
}
@@ -917,7 +909,7 @@ class Notice extends Memcached_DataObject
/**
* Same calculation as saveGroups but without the saving
* @fixme merge the functions
- * @return array of Group objects
+ * @return array of Group_inbox objects
*/
function getGroups()
{
@@ -957,7 +949,10 @@ class Notice extends Memcached_DataObject
if ($namespace) {
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
- 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
+ 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
+ 'xmlns:georss' => 'http://www.georss.org/georss',
+ 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
+ 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
} else {
$attrs = array();
}
@@ -983,11 +978,6 @@ class Notice extends Memcached_DataObject
$xs->element('icon', null, $profile->avatarUrl(AVATAR_PROFILE_SIZE));
}
- $xs->elementStart('author');
- $xs->element('name', null, $profile->nickname);
- $xs->element('uri', null, $profile->profileurl);
- $xs->elementEnd('author');
-
if ($source) {
$xs->elementEnd('source');
}
@@ -995,7 +985,11 @@ class Notice extends Memcached_DataObject
$xs->element('title', null, $this->content);
$xs->element('summary', null, $this->content);
+ $xs->raw($profile->asAtomAuthor());
+ $xs->raw($profile->asActivityActor());
+
$xs->element('link', array('rel' => 'alternate',
+ 'type' => 'text/html',
'href' => $this->bestUrl()));
$xs->element('id', null, $this->uri);
@@ -1014,6 +1008,44 @@ class Notice extends Memcached_DataObject
}
}
+ if (!empty($this->conversation)) {
+
+ $conv = Conversation::staticGet('id', $this->conversation);
+
+ if (!empty($conv)) {
+ $xs->element(
+ 'link', array(
+ 'rel' => 'ostatus:conversation',
+ 'href' => $conv->uri
+ )
+ );
+ }
+ }
+
+ $reply_ids = $this->getReplies();
+
+ foreach ($reply_ids as $id) {
+ $profile = Profile::staticGet('id', $id);
+ if (!empty($profile)) {
+ $xs->element(
+ 'link', array(
+ 'rel' => 'ostatus:attention',
+ 'href' => $profile->getUri()
+ )
+ );
+ }
+ }
+
+ if (!empty($this->repeat_of)) {
+ $repeat = Notice::staticGet('id', $this->repeat_of);
+ if (!empty($repeat)) {
+ $xs->element(
+ 'ostatus:forward',
+ array('ref' => $repeat->uri, 'href' => $repeat->bestUrl())
+ );
+ }
+ }
+
$xs->element('content', array('type' => 'html'), $this->rendered);
$tag = new Notice_tag();
@@ -1041,9 +1073,7 @@ class Notice extends Memcached_DataObject
}
if (!empty($this->lat) && !empty($this->lon)) {
- $xs->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
$xs->element('georss:point', null, $this->lat . ' ' . $this->lon);
- $xs->elementEnd('geo');
}
$xs->elementEnd('entry');
@@ -1051,6 +1081,38 @@ class Notice extends Memcached_DataObject
return $xs->getString();
}
+ /**
+ * Returns an XML string fragment with a reference to a notice as an
+ * Activity Streams noun object with the given element type.
+ *
+ * Assumes that 'activity' namespace has been previously defined.
+ *
+ * @param string $element one of 'subject', 'object', 'target'
+ * @return string
+ */
+ function asActivityNoun($element)
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('activity:' . $element);
+ $xs->element('activity:object-type',
+ null,
+ 'http://activitystrea.ms/schema/1.0/note');
+ $xs->element('id',
+ null,
+ $this->uri);
+ $xs->element('content',
+ array('type' => 'text/html'),
+ $this->rendered);
+ $xs->element('link',
+ array('type' => 'text/html',
+ 'rel' => 'alternate',
+ 'href' => $this->bestUrl()));
+ $xs->elementEnd('activity:' . $element);
+
+ return $xs->getString();
+ }
+
function bestUrl()
{
if (!empty($this->url)) {
@@ -1176,6 +1238,10 @@ class Notice extends Memcached_DataObject
// Figure out who that is.
$sender = Profile::staticGet('id', $profile_id);
+ if (empty($sender)) {
+ return null;
+ }
+
$recipient = common_relative_profile($sender, $nickname, common_sql_now());
if (empty($recipient)) {
@@ -1444,6 +1510,14 @@ class Notice extends Memcached_DataObject
function distribute()
{
+ // We always insert for the author so they don't
+ // have to wait
+
+ $user = User::staticGet('id', $this->profile_id);
+ if (!empty($user)) {
+ Inbox::insertNotice($user->id, $this->id);
+ }
+
if (common_config('queue', 'inboxes')) {
// If there's a failure, we want to _force_
// distribution at this point.
diff --git a/classes/Profile.php b/classes/Profile.php
index 1076fb2cb..6b396c8c3 100644
--- a/classes/Profile.php
+++ b/classes/Profile.php
@@ -716,6 +716,7 @@ class Profile extends Memcached_DataObject
switch ($right)
{
case Right::DELETEOTHERSNOTICE:
+ case Right::MAKEGROUPADMIN:
case Right::SANDBOXUSER:
case Right::SILENCEUSER:
case Right::DELETEUSER:
@@ -753,4 +754,130 @@ class Profile extends Memcached_DataObject
return !empty($notice);
}
+
+ /**
+ * Returns an XML string fragment with limited profile information
+ * as an Atom <author> element.
+ *
+ * Assumes that Atom has been previously set up as the base namespace.
+ *
+ * @return string
+ */
+ function asAtomAuthor()
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('author');
+ $xs->element('name', null, $this->nickname);
+ $xs->element('uri', null, $this->getUri());
+ $xs->elementEnd('author');
+
+ return $xs->getString();
+ }
+
+ /**
+ * Returns an XML string fragment with profile information as an
+ * Activity Streams <activity:actor> element.
+ *
+ * Assumes that 'activity' namespace has been previously defined.
+ *
+ * @return string
+ */
+ function asActivityActor()
+ {
+ return $this->asActivityNoun('actor');
+ }
+
+ /**
+ * Returns an XML string fragment with profile information as an
+ * Activity Streams noun object with the given element type.
+ *
+ * Assumes that 'activity' namespace has been previously defined.
+ *
+ * @param string $element one of 'actor', 'subject', 'object', 'target'
+ * @return string
+ */
+ function asActivityNoun($element)
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('activity:' . $element);
+ $xs->element(
+ 'activity:object-type',
+ null,
+ 'http://activitystrea.ms/schema/1.0/person'
+ );
+ $xs->element(
+ 'id',
+ null,
+ $this->getUri()
+ );
+ $xs->element('title', null, $this->getBestName());
+
+ $avatar = $this->getAvatar(AVATAR_PROFILE_SIZE);
+
+ $xs->element(
+ 'link', array(
+ 'type' => empty($avatar) ? 'image/png' : $avatar->mediatype,
+ 'rel' => 'avatar',
+ 'href' => empty($avatar)
+ ? Avatar::defaultImage(AVATAR_PROFILE_SIZE)
+ : $avatar->displayUrl()
+ ),
+ ''
+ );
+
+ $xs->elementEnd('activity:' . $element);
+
+ return $xs->getString();
+ }
+
+ /**
+ * Returns the best URI for a profile. Plugins may override.
+ *
+ * @return string $uri
+ */
+ function getUri()
+ {
+ $uri = null;
+
+ // check for a local user first
+ $user = User::staticGet('id', $this->id);
+
+ if (!empty($user)) {
+ $uri = common_local_url(
+ 'userbyid',
+ array('id' => $user->id)
+ );
+ } else {
+
+ // give plugins a chance to set the URI
+ if (Event::handle('StartGetProfileUri', array($this, &$uri))) {
+
+ // return OMB profile if any
+ $remote = Remote_profile::staticGet('id', $this->id);
+
+ if (!empty($remote)) {
+ $uri = $remote->uri;
+ }
+
+ Event::handle('EndGetProfileUri', array($this, &$uri));
+ }
+ }
+
+ return $uri;
+ }
+
+ function hasBlocked($other)
+ {
+ $block = Profile_block::get($this->id, $other->id);
+
+ if (empty($block)) {
+ $result = false;
+ } else {
+ $result = true;
+ }
+
+ return $result;
+ }
}
diff --git a/classes/Subscription.php b/classes/Subscription.php
index faf1331cd..d6fb3fcbd 100644
--- a/classes/Subscription.php
+++ b/classes/Subscription.php
@@ -24,7 +24,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Subscription extends Memcached_DataObject
+class Subscription extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
@@ -34,8 +34,8 @@ class Subscription extends Memcached_DataObject
public $subscribed; // int(4) primary_key not_null
public $jabber; // tinyint(1) default_1
public $sms; // tinyint(1) default_1
- public $token; // varchar(255)
- public $secret; // varchar(255)
+ public $token; // varchar(255)
+ public $secret; // varchar(255)
public $created; // datetime() not_null
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
@@ -45,9 +45,155 @@ class Subscription extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
-
+
function pkeyGet($kv)
{
return Memcached_DataObject::pkeyGet('Subscription', $kv);
}
+
+ /**
+ * Make a new subscription
+ *
+ * @param Profile $subscriber party to receive new notices
+ * @param Profile $other party sending notices; publisher
+ *
+ * @return Subscription new subscription
+ */
+
+ static function start($subscriber, $other)
+ {
+ if (!$subscriber->hasRight(Right::SUBSCRIBE)) {
+ throw new Exception(_('You have been banned from subscribing.'));
+ }
+
+ if (self::exists($subscriber, $other)) {
+ throw new Exception(_('Already subscribed!'));
+ }
+
+ if ($other->hasBlocked($subscriber)) {
+ throw new Exception(_('User has blocked you.'));
+ }
+
+ if (Event::handle('StartSubscribe', array($subscriber, $other))) {
+
+ $sub = new Subscription();
+
+ $sub->subscriber = $subscriber->id;
+ $sub->subscribed = $other->id;
+ $sub->created = common_sql_now();
+
+ $result = $sub->insert();
+
+ if (!$result) {
+ common_log_db_error($sub, 'INSERT', __FILE__);
+ throw new Exception(_('Could not save subscription.'));
+ }
+
+ $sub->notify();
+
+ self::blow('user:notices_with_friends:%d', $subscriber->id);
+
+ $subscriber->blowSubscriptionsCount();
+ $other->blowSubscribersCount();
+
+ $otherUser = User::staticGet('id', $other->id);
+
+ if (!empty($otherUser) &&
+ $otherUser->autosubscribe &&
+ !self::exists($other, $subscriber) &&
+ !$subscriber->hasBlocked($other)) {
+
+ $auto = new Subscription();
+
+ $auto->subscriber = $subscriber->id;
+ $auto->subscribed = $other->id;
+ $auto->created = common_sql_now();
+
+ $result = $auto->insert();
+
+ if (!$result) {
+ common_log_db_error($auto, 'INSERT', __FILE__);
+ throw new Exception(_('Could not save subscription.'));
+ }
+
+ $auto->notify();
+ }
+
+ Event::handle('EndSubscribe', array($subscriber, $other));
+ }
+
+ return true;
+ }
+
+ function notify()
+ {
+ # XXX: add other notifications (Jabber, SMS) here
+ # XXX: queue this and handle it offline
+ # XXX: Whatever happens, do it in Twitter-like API, too
+
+ $this->notifyEmail();
+ }
+
+ function notifyEmail()
+ {
+ $subscribedUser = User::staticGet('id', $this->subscribed);
+
+ if (!empty($subscribedUser)) {
+
+ $subscriber = Profile::staticGet('id', $this->subscriber);
+
+ mail_subscribe_notify_profile($subscribedUser, $subscriber);
+ }
+ }
+
+ /**
+ * Cancel a subscription
+ *
+ */
+
+ function cancel($subscriber, $other)
+ {
+ if (!self::exists($subscriber, $other)) {
+ throw new Exception(_('Not subscribed!'));
+ }
+
+ // Don't allow deleting self subs
+
+ if ($subscriber->id == $other->id) {
+ throw new Exception(_('Couldn\'t delete self-subscription.'));
+ }
+
+ if (Event::handle('StartUnsubscribe', array($subscriber, $other))) {
+
+ $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id,
+ 'subscribed' => $other->id));
+
+ // note we checked for existence above
+
+ assert(!empty($sub));
+
+ $result = $sub->delete();
+
+ if (!$result) {
+ common_log_db_error($sub, 'DELETE', __FILE__);
+ throw new Exception(_('Couldn\'t delete subscription.'));
+ }
+
+ self::blow('user:notices_with_friends:%d', $subscriber->id);
+
+ $subscriber->blowSubscriptionsCount();
+ $other->blowSubscribersCount();
+
+ Event::handle('EndUnsubscribe', array($subscriber, $other));
+ }
+
+ return;
+ }
+
+ function exists($subscriber, $other)
+ {
+ $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id,
+ 'subscribed' => $other->id));
+ return (empty($sub)) ? false : true;
+ }
}
diff --git a/classes/User.php b/classes/User.php
index 72c3f39e9..10b1f4865 100644
--- a/classes/User.php
+++ b/classes/User.php
@@ -80,11 +80,7 @@ class User extends Memcached_DataObject
function isSubscribed($other)
{
- assert(!is_null($other));
- // XXX: cache results of this query
- $sub = Subscription::pkeyGet(array('subscriber' => $this->id,
- 'subscribed' => $other->id));
- return (is_null($sub)) ? false : true;
+ return Subscription::exists($this->getProfile(), $other);
}
// 'update' won't write key columns, so we have to do it ourselves.
@@ -167,17 +163,8 @@ class User extends Memcached_DataObject
function hasBlocked($other)
{
-
- $block = Profile_block::get($this->id, $other->id);
-
- if (is_null($block)) {
- $result = false;
- } else {
- $result = true;
- $block->free();
- }
-
- return $result;
+ $profile = $this->getProfile();
+ return $profile->hasBlocked($other);
}
/**
diff --git a/classes/User_group.php b/classes/User_group.php
index c86eadf8f..379e6b721 100644
--- a/classes/User_group.php
+++ b/classes/User_group.php
@@ -49,12 +49,12 @@ class User_group extends Memcached_DataObject
array('id' => $this->id));
}
- function getNotices($offset, $limit)
+ function getNotices($offset, $limit, $since_id=null, $max_id=null)
{
$ids = Notice::stream(array($this, '_streamDirect'),
array(),
'user_group:notice_ids:' . $this->id,
- $offset, $limit);
+ $offset, $limit, $since_id, $max_id);
return Notice::getStreamByIds($ids);
}
@@ -355,6 +355,39 @@ class User_group extends Memcached_DataObject
return $xs->getString();
}
+ function asAtomAuthor()
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('author');
+ $xs->element('name', null, $this->nickname);
+ $xs->element('uri', null, $this->permalink());
+ $xs->elementEnd('author');
+
+ return $xs->getString();
+ }
+
+ function asActivitySubject()
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('activity:subject');
+ $xs->element('activity:object', null, 'http://activitystrea.ms/schema/1.0/group');
+ $xs->element('id', null, $this->permalink());
+ $xs->element('title', null, $this->getBestName());
+ $xs->element(
+ 'link', array(
+ 'rel' => 'avatar',
+ 'href' => empty($this->homepage_logo)
+ ? User_group::defaultLogo(AVATAR_PROFILE_SIZE)
+ : $this->homepage_logo
+ )
+ );
+ $xs->elementEnd('activity:subject');
+
+ return $xs->getString();
+ }
+
static function register($fields) {
// MAGICALLY put fields into current scope
diff --git a/classes/statusnet.ini b/classes/statusnet.ini
index 5f8da7cf5..81c1b68b2 100644
--- a/classes/statusnet.ini
+++ b/classes/statusnet.ini
@@ -47,6 +47,16 @@ modified = 384
[consumer__keys]
consumer_key = K
+[conversation]
+id = 129
+uri = 2
+created = 142
+modified = 384
+
+[conversation__keys]
+id = N
+uri = U
+
[deleted_notice]
id = 129
profile_id = 129
diff --git a/classes/statusnet.links.ini b/classes/statusnet.links.ini
index 7f233e676..b9dd5af0c 100644
--- a/classes/statusnet.links.ini
+++ b/classes/statusnet.links.ini
@@ -19,8 +19,11 @@ profile_id = profile:id
[token]
consumer_key = consumer:consumer_key
-[nonce]
-consumer_key,token = token:consumer_key,token
+; Compatibility hack for PHP 5.3
+; This entry has been moved to the class definition, as commas are no longer
+; considered valid in keys, causing parse_ini_file() to reject the whole file.
+;[nonce]
+;consumer_key,token = token:consumer_key,token
[confirm_address]
user_id = user:id
diff --git a/db/statusnet.sql b/db/statusnet.sql
index 343464801..97117c80a 100644
--- a/db/statusnet.sql
+++ b/db/statusnet.sql
@@ -633,3 +633,11 @@ create table inbox (
constraint primary key (user_id)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table conversation (
+ id integer auto_increment primary key comment 'unique identifier',
+ uri varchar(225) unique comment 'URI of the conversation',
+ created datetime not null comment 'date this record was created',
+ modified timestamp comment 'date this record was modified'
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
diff --git a/extlib/HTMLPurifier/HTMLPurifier.auto.php b/extlib/HTMLPurifier/HTMLPurifier.auto.php
new file mode 100644
index 000000000..1960c399f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier.auto.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * This is a stub include that automatically configures the include path.
+ */
+
+set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path() );
+require_once 'HTMLPurifier/Bootstrap.php';
+require_once 'HTMLPurifier.autoload.php';
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier.autoload.php b/extlib/HTMLPurifier/HTMLPurifier.autoload.php
new file mode 100644
index 000000000..8d4017640
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier.autoload.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @file
+ * Convenience file that registers autoload handler for HTML Purifier.
+ */
+
+if (function_exists('spl_autoload_register') && function_exists('spl_autoload_unregister')) {
+ // We need unregister for our pre-registering functionality
+ HTMLPurifier_Bootstrap::registerAutoload();
+ if (function_exists('__autoload')) {
+ // Be polite and ensure that userland autoload gets retained
+ spl_autoload_register('__autoload');
+ }
+} elseif (!function_exists('__autoload')) {
+ function __autoload($class) {
+ return HTMLPurifier_Bootstrap::autoload($class);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier.func.php b/extlib/HTMLPurifier/HTMLPurifier.func.php
new file mode 100644
index 000000000..56a55b2fe
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier.func.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Defines a function wrapper for HTML Purifier for quick use.
+ * @note ''HTMLPurifier()'' is NOT the same as ''new HTMLPurifier()''
+ */
+
+/**
+ * Purify HTML.
+ * @param $html String HTML to purify
+ * @param $config Configuration to use, can be any value accepted by
+ * HTMLPurifier_Config::create()
+ */
+function HTMLPurifier($html, $config = null) {
+ static $purifier = false;
+ if (!$purifier) {
+ $purifier = new HTMLPurifier();
+ }
+ return $purifier->purify($html, $config);
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier.includes.php b/extlib/HTMLPurifier/HTMLPurifier.includes.php
new file mode 100644
index 000000000..7cfb97060
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier.includes.php
@@ -0,0 +1,208 @@
+<?php
+
+/**
+ * @file
+ * This file was auto-generated by generate-includes.php and includes all of
+ * the core files required by HTML Purifier. Use this if performance is a
+ * primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS
+ * FILE, changes will be overwritten the next time the script is run.
+ *
+ * @version 4.0.0
+ *
+ * @warning
+ * You must *not* include any other HTML Purifier files before this file,
+ * because 'require' not 'require_once' is used.
+ *
+ * @warning
+ * This file requires that the include path contains the HTML Purifier
+ * library directory; this is not auto-set.
+ */
+
+require 'HTMLPurifier.php';
+require 'HTMLPurifier/AttrCollections.php';
+require 'HTMLPurifier/AttrDef.php';
+require 'HTMLPurifier/AttrTransform.php';
+require 'HTMLPurifier/AttrTypes.php';
+require 'HTMLPurifier/AttrValidator.php';
+require 'HTMLPurifier/Bootstrap.php';
+require 'HTMLPurifier/Definition.php';
+require 'HTMLPurifier/CSSDefinition.php';
+require 'HTMLPurifier/ChildDef.php';
+require 'HTMLPurifier/Config.php';
+require 'HTMLPurifier/ConfigSchema.php';
+require 'HTMLPurifier/ContentSets.php';
+require 'HTMLPurifier/Context.php';
+require 'HTMLPurifier/DefinitionCache.php';
+require 'HTMLPurifier/DefinitionCacheFactory.php';
+require 'HTMLPurifier/Doctype.php';
+require 'HTMLPurifier/DoctypeRegistry.php';
+require 'HTMLPurifier/ElementDef.php';
+require 'HTMLPurifier/Encoder.php';
+require 'HTMLPurifier/EntityLookup.php';
+require 'HTMLPurifier/EntityParser.php';
+require 'HTMLPurifier/ErrorCollector.php';
+require 'HTMLPurifier/ErrorStruct.php';
+require 'HTMLPurifier/Exception.php';
+require 'HTMLPurifier/Filter.php';
+require 'HTMLPurifier/Generator.php';
+require 'HTMLPurifier/HTMLDefinition.php';
+require 'HTMLPurifier/HTMLModule.php';
+require 'HTMLPurifier/HTMLModuleManager.php';
+require 'HTMLPurifier/IDAccumulator.php';
+require 'HTMLPurifier/Injector.php';
+require 'HTMLPurifier/Language.php';
+require 'HTMLPurifier/LanguageFactory.php';
+require 'HTMLPurifier/Length.php';
+require 'HTMLPurifier/Lexer.php';
+require 'HTMLPurifier/PercentEncoder.php';
+require 'HTMLPurifier/PropertyList.php';
+require 'HTMLPurifier/PropertyListIterator.php';
+require 'HTMLPurifier/Strategy.php';
+require 'HTMLPurifier/StringHash.php';
+require 'HTMLPurifier/StringHashParser.php';
+require 'HTMLPurifier/TagTransform.php';
+require 'HTMLPurifier/Token.php';
+require 'HTMLPurifier/TokenFactory.php';
+require 'HTMLPurifier/URI.php';
+require 'HTMLPurifier/URIDefinition.php';
+require 'HTMLPurifier/URIFilter.php';
+require 'HTMLPurifier/URIParser.php';
+require 'HTMLPurifier/URIScheme.php';
+require 'HTMLPurifier/URISchemeRegistry.php';
+require 'HTMLPurifier/UnitConverter.php';
+require 'HTMLPurifier/VarParser.php';
+require 'HTMLPurifier/VarParserException.php';
+require 'HTMLPurifier/AttrDef/CSS.php';
+require 'HTMLPurifier/AttrDef/Enum.php';
+require 'HTMLPurifier/AttrDef/Integer.php';
+require 'HTMLPurifier/AttrDef/Lang.php';
+require 'HTMLPurifier/AttrDef/Switch.php';
+require 'HTMLPurifier/AttrDef/Text.php';
+require 'HTMLPurifier/AttrDef/URI.php';
+require 'HTMLPurifier/AttrDef/CSS/Number.php';
+require 'HTMLPurifier/AttrDef/CSS/AlphaValue.php';
+require 'HTMLPurifier/AttrDef/CSS/Background.php';
+require 'HTMLPurifier/AttrDef/CSS/BackgroundPosition.php';
+require 'HTMLPurifier/AttrDef/CSS/Border.php';
+require 'HTMLPurifier/AttrDef/CSS/Color.php';
+require 'HTMLPurifier/AttrDef/CSS/Composite.php';
+require 'HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php';
+require 'HTMLPurifier/AttrDef/CSS/Filter.php';
+require 'HTMLPurifier/AttrDef/CSS/Font.php';
+require 'HTMLPurifier/AttrDef/CSS/FontFamily.php';
+require 'HTMLPurifier/AttrDef/CSS/ImportantDecorator.php';
+require 'HTMLPurifier/AttrDef/CSS/Length.php';
+require 'HTMLPurifier/AttrDef/CSS/ListStyle.php';
+require 'HTMLPurifier/AttrDef/CSS/Multiple.php';
+require 'HTMLPurifier/AttrDef/CSS/Percentage.php';
+require 'HTMLPurifier/AttrDef/CSS/TextDecoration.php';
+require 'HTMLPurifier/AttrDef/CSS/URI.php';
+require 'HTMLPurifier/AttrDef/HTML/Bool.php';
+require 'HTMLPurifier/AttrDef/HTML/Nmtokens.php';
+require 'HTMLPurifier/AttrDef/HTML/Class.php';
+require 'HTMLPurifier/AttrDef/HTML/Color.php';
+require 'HTMLPurifier/AttrDef/HTML/FrameTarget.php';
+require 'HTMLPurifier/AttrDef/HTML/ID.php';
+require 'HTMLPurifier/AttrDef/HTML/Pixels.php';
+require 'HTMLPurifier/AttrDef/HTML/Length.php';
+require 'HTMLPurifier/AttrDef/HTML/LinkTypes.php';
+require 'HTMLPurifier/AttrDef/HTML/MultiLength.php';
+require 'HTMLPurifier/AttrDef/URI/Email.php';
+require 'HTMLPurifier/AttrDef/URI/Host.php';
+require 'HTMLPurifier/AttrDef/URI/IPv4.php';
+require 'HTMLPurifier/AttrDef/URI/IPv6.php';
+require 'HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php';
+require 'HTMLPurifier/AttrTransform/Background.php';
+require 'HTMLPurifier/AttrTransform/BdoDir.php';
+require 'HTMLPurifier/AttrTransform/BgColor.php';
+require 'HTMLPurifier/AttrTransform/BoolToCSS.php';
+require 'HTMLPurifier/AttrTransform/Border.php';
+require 'HTMLPurifier/AttrTransform/EnumToCSS.php';
+require 'HTMLPurifier/AttrTransform/ImgRequired.php';
+require 'HTMLPurifier/AttrTransform/ImgSpace.php';
+require 'HTMLPurifier/AttrTransform/Input.php';
+require 'HTMLPurifier/AttrTransform/Lang.php';
+require 'HTMLPurifier/AttrTransform/Length.php';
+require 'HTMLPurifier/AttrTransform/Name.php';
+require 'HTMLPurifier/AttrTransform/NameSync.php';
+require 'HTMLPurifier/AttrTransform/SafeEmbed.php';
+require 'HTMLPurifier/AttrTransform/SafeObject.php';
+require 'HTMLPurifier/AttrTransform/SafeParam.php';
+require 'HTMLPurifier/AttrTransform/ScriptRequired.php';
+require 'HTMLPurifier/AttrTransform/Textarea.php';
+require 'HTMLPurifier/ChildDef/Chameleon.php';
+require 'HTMLPurifier/ChildDef/Custom.php';
+require 'HTMLPurifier/ChildDef/Empty.php';
+require 'HTMLPurifier/ChildDef/Required.php';
+require 'HTMLPurifier/ChildDef/Optional.php';
+require 'HTMLPurifier/ChildDef/StrictBlockquote.php';
+require 'HTMLPurifier/ChildDef/Table.php';
+require 'HTMLPurifier/DefinitionCache/Decorator.php';
+require 'HTMLPurifier/DefinitionCache/Null.php';
+require 'HTMLPurifier/DefinitionCache/Serializer.php';
+require 'HTMLPurifier/DefinitionCache/Decorator/Cleanup.php';
+require 'HTMLPurifier/DefinitionCache/Decorator/Memory.php';
+require 'HTMLPurifier/HTMLModule/Bdo.php';
+require 'HTMLPurifier/HTMLModule/CommonAttributes.php';
+require 'HTMLPurifier/HTMLModule/Edit.php';
+require 'HTMLPurifier/HTMLModule/Forms.php';
+require 'HTMLPurifier/HTMLModule/Hypertext.php';
+require 'HTMLPurifier/HTMLModule/Image.php';
+require 'HTMLPurifier/HTMLModule/Legacy.php';
+require 'HTMLPurifier/HTMLModule/List.php';
+require 'HTMLPurifier/HTMLModule/Name.php';
+require 'HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php';
+require 'HTMLPurifier/HTMLModule/Object.php';
+require 'HTMLPurifier/HTMLModule/Presentation.php';
+require 'HTMLPurifier/HTMLModule/Proprietary.php';
+require 'HTMLPurifier/HTMLModule/Ruby.php';
+require 'HTMLPurifier/HTMLModule/SafeEmbed.php';
+require 'HTMLPurifier/HTMLModule/SafeObject.php';
+require 'HTMLPurifier/HTMLModule/Scripting.php';
+require 'HTMLPurifier/HTMLModule/StyleAttribute.php';
+require 'HTMLPurifier/HTMLModule/Tables.php';
+require 'HTMLPurifier/HTMLModule/Target.php';
+require 'HTMLPurifier/HTMLModule/Text.php';
+require 'HTMLPurifier/HTMLModule/Tidy.php';
+require 'HTMLPurifier/HTMLModule/XMLCommonAttributes.php';
+require 'HTMLPurifier/HTMLModule/Tidy/Name.php';
+require 'HTMLPurifier/HTMLModule/Tidy/Proprietary.php';
+require 'HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php';
+require 'HTMLPurifier/HTMLModule/Tidy/Strict.php';
+require 'HTMLPurifier/HTMLModule/Tidy/Transitional.php';
+require 'HTMLPurifier/HTMLModule/Tidy/XHTML.php';
+require 'HTMLPurifier/Injector/AutoParagraph.php';
+require 'HTMLPurifier/Injector/DisplayLinkURI.php';
+require 'HTMLPurifier/Injector/Linkify.php';
+require 'HTMLPurifier/Injector/PurifierLinkify.php';
+require 'HTMLPurifier/Injector/RemoveEmpty.php';
+require 'HTMLPurifier/Injector/SafeObject.php';
+require 'HTMLPurifier/Lexer/DOMLex.php';
+require 'HTMLPurifier/Lexer/DirectLex.php';
+require 'HTMLPurifier/Strategy/Composite.php';
+require 'HTMLPurifier/Strategy/Core.php';
+require 'HTMLPurifier/Strategy/FixNesting.php';
+require 'HTMLPurifier/Strategy/MakeWellFormed.php';
+require 'HTMLPurifier/Strategy/RemoveForeignElements.php';
+require 'HTMLPurifier/Strategy/ValidateAttributes.php';
+require 'HTMLPurifier/TagTransform/Font.php';
+require 'HTMLPurifier/TagTransform/Simple.php';
+require 'HTMLPurifier/Token/Comment.php';
+require 'HTMLPurifier/Token/Tag.php';
+require 'HTMLPurifier/Token/Empty.php';
+require 'HTMLPurifier/Token/End.php';
+require 'HTMLPurifier/Token/Start.php';
+require 'HTMLPurifier/Token/Text.php';
+require 'HTMLPurifier/URIFilter/DisableExternal.php';
+require 'HTMLPurifier/URIFilter/DisableExternalResources.php';
+require 'HTMLPurifier/URIFilter/HostBlacklist.php';
+require 'HTMLPurifier/URIFilter/MakeAbsolute.php';
+require 'HTMLPurifier/URIFilter/Munge.php';
+require 'HTMLPurifier/URIScheme/ftp.php';
+require 'HTMLPurifier/URIScheme/http.php';
+require 'HTMLPurifier/URIScheme/https.php';
+require 'HTMLPurifier/URIScheme/mailto.php';
+require 'HTMLPurifier/URIScheme/news.php';
+require 'HTMLPurifier/URIScheme/nntp.php';
+require 'HTMLPurifier/VarParser/Flexible.php';
+require 'HTMLPurifier/VarParser/Native.php';
diff --git a/extlib/HTMLPurifier/HTMLPurifier.kses.php b/extlib/HTMLPurifier/HTMLPurifier.kses.php
new file mode 100644
index 000000000..3143feb17
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier.kses.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Emulation layer for code that used kses(), substituting in HTML Purifier.
+ */
+
+require_once dirname(__FILE__) . '/HTMLPurifier.auto.php';
+
+function kses($string, $allowed_html, $allowed_protocols = null) {
+ $config = HTMLPurifier_Config::createDefault();
+ $allowed_elements = array();
+ $allowed_attributes = array();
+ foreach ($allowed_html as $element => $attributes) {
+ $allowed_elements[$element] = true;
+ foreach ($attributes as $attribute => $x) {
+ $allowed_attributes["$element.$attribute"] = true;
+ }
+ }
+ $config->set('HTML.AllowedElements', $allowed_elements);
+ $config->set('HTML.AllowedAttributes', $allowed_attributes);
+ $allowed_schemes = array();
+ if ($allowed_protocols !== null) {
+ $config->set('URI.AllowedSchemes', $allowed_protocols);
+ }
+ $purifier = new HTMLPurifier($config);
+ return $purifier->purify($string);
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier.path.php b/extlib/HTMLPurifier/HTMLPurifier.path.php
new file mode 100644
index 000000000..39b1b6531
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier.path.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * @file
+ * Convenience stub file that adds HTML Purifier's library file to the path
+ * without any other side-effects.
+ */
+
+set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path() );
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier.php b/extlib/HTMLPurifier/HTMLPurifier.php
new file mode 100644
index 000000000..e3fce9c2a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier.php
@@ -0,0 +1,237 @@
+<?php
+
+/*! @mainpage
+ *
+ * HTML Purifier is an HTML filter that will take an arbitrary snippet of
+ * HTML and rigorously test, validate and filter it into a version that
+ * is safe for output onto webpages. It achieves this by:
+ *
+ * -# Lexing (parsing into tokens) the document,
+ * -# Executing various strategies on the tokens:
+ * -# Removing all elements not in the whitelist,
+ * -# Making the tokens well-formed,
+ * -# Fixing the nesting of the nodes, and
+ * -# Validating attributes of the nodes; and
+ * -# Generating HTML from the purified tokens.
+ *
+ * However, most users will only need to interface with the HTMLPurifier
+ * and HTMLPurifier_Config.
+ */
+
+/*
+ HTML Purifier 4.0.0 - Standards Compliant HTML Filtering
+ Copyright (C) 2006-2008 Edward Z. Yang
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * Facade that coordinates HTML Purifier's subsystems in order to purify HTML.
+ *
+ * @note There are several points in which configuration can be specified
+ * for HTML Purifier. The precedence of these (from lowest to
+ * highest) is as follows:
+ * -# Instance: new HTMLPurifier($config)
+ * -# Invocation: purify($html, $config)
+ * These configurations are entirely independent of each other and
+ * are *not* merged (this behavior may change in the future).
+ *
+ * @todo We need an easier way to inject strategies using the configuration
+ * object.
+ */
+class HTMLPurifier
+{
+
+ /** Version of HTML Purifier */
+ public $version = '4.0.0';
+
+ /** Constant with version of HTML Purifier */
+ const VERSION = '4.0.0';
+
+ /** Global configuration object */
+ public $config;
+
+ /** Array of extra HTMLPurifier_Filter objects to run on HTML, for backwards compatibility */
+ private $filters = array();
+
+ /** Single instance of HTML Purifier */
+ private static $instance;
+
+ protected $strategy, $generator;
+
+ /**
+ * Resultant HTMLPurifier_Context of last run purification. Is an array
+ * of contexts if the last called method was purifyArray().
+ */
+ public $context;
+
+ /**
+ * Initializes the purifier.
+ * @param $config Optional HTMLPurifier_Config object for all instances of
+ * the purifier, if omitted, a default configuration is
+ * supplied (which can be overridden on a per-use basis).
+ * The parameter can also be any type that
+ * HTMLPurifier_Config::create() supports.
+ */
+ public function __construct($config = null) {
+
+ $this->config = HTMLPurifier_Config::create($config);
+
+ $this->strategy = new HTMLPurifier_Strategy_Core();
+
+ }
+
+ /**
+ * Adds a filter to process the output. First come first serve
+ * @param $filter HTMLPurifier_Filter object
+ */
+ public function addFilter($filter) {
+ trigger_error('HTMLPurifier->addFilter() is deprecated, use configuration directives in the Filter namespace or Filter.Custom', E_USER_WARNING);
+ $this->filters[] = $filter;
+ }
+
+ /**
+ * Filters an HTML snippet/document to be XSS-free and standards-compliant.
+ *
+ * @param $html String of HTML to purify
+ * @param $config HTMLPurifier_Config object for this operation, if omitted,
+ * defaults to the config object specified during this
+ * object's construction. The parameter can also be any type
+ * that HTMLPurifier_Config::create() supports.
+ * @return Purified HTML
+ */
+ public function purify($html, $config = null) {
+
+ // :TODO: make the config merge in, instead of replace
+ $config = $config ? HTMLPurifier_Config::create($config) : $this->config;
+
+ // implementation is partially environment dependant, partially
+ // configuration dependant
+ $lexer = HTMLPurifier_Lexer::create($config);
+
+ $context = new HTMLPurifier_Context();
+
+ // setup HTML generator
+ $this->generator = new HTMLPurifier_Generator($config, $context);
+ $context->register('Generator', $this->generator);
+
+ // set up global context variables
+ if ($config->get('Core.CollectErrors')) {
+ // may get moved out if other facilities use it
+ $language_factory = HTMLPurifier_LanguageFactory::instance();
+ $language = $language_factory->create($config, $context);
+ $context->register('Locale', $language);
+
+ $error_collector = new HTMLPurifier_ErrorCollector($context);
+ $context->register('ErrorCollector', $error_collector);
+ }
+
+ // setup id_accumulator context, necessary due to the fact that
+ // AttrValidator can be called from many places
+ $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
+ $context->register('IDAccumulator', $id_accumulator);
+
+ $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context);
+
+ // setup filters
+ $filter_flags = $config->getBatch('Filter');
+ $custom_filters = $filter_flags['Custom'];
+ unset($filter_flags['Custom']);
+ $filters = array();
+ foreach ($filter_flags as $filter => $flag) {
+ if (!$flag) continue;
+ if (strpos($filter, '.') !== false) continue;
+ $class = "HTMLPurifier_Filter_$filter";
+ $filters[] = new $class;
+ }
+ foreach ($custom_filters as $filter) {
+ // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat
+ $filters[] = $filter;
+ }
+ $filters = array_merge($filters, $this->filters);
+ // maybe prepare(), but later
+
+ for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) {
+ $html = $filters[$i]->preFilter($html, $config, $context);
+ }
+
+ // purified HTML
+ $html =
+ $this->generator->generateFromTokens(
+ // list of tokens
+ $this->strategy->execute(
+ // list of un-purified tokens
+ $lexer->tokenizeHTML(
+ // un-purified HTML
+ $html, $config, $context
+ ),
+ $config, $context
+ )
+ );
+
+ for ($i = $filter_size - 1; $i >= 0; $i--) {
+ $html = $filters[$i]->postFilter($html, $config, $context);
+ }
+
+ $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context);
+ $this->context =& $context;
+ return $html;
+ }
+
+ /**
+ * Filters an array of HTML snippets
+ * @param $config Optional HTMLPurifier_Config object for this operation.
+ * See HTMLPurifier::purify() for more details.
+ * @return Array of purified HTML
+ */
+ public function purifyArray($array_of_html, $config = null) {
+ $context_array = array();
+ foreach ($array_of_html as $key => $html) {
+ $array_of_html[$key] = $this->purify($html, $config);
+ $context_array[$key] = $this->context;
+ }
+ $this->context = $context_array;
+ return $array_of_html;
+ }
+
+ /**
+ * Singleton for enforcing just one HTML Purifier in your system
+ * @param $prototype Optional prototype HTMLPurifier instance to
+ * overload singleton with, or HTMLPurifier_Config
+ * instance to configure the generated version with.
+ */
+ public static function instance($prototype = null) {
+ if (!self::$instance || $prototype) {
+ if ($prototype instanceof HTMLPurifier) {
+ self::$instance = $prototype;
+ } elseif ($prototype) {
+ self::$instance = new HTMLPurifier($prototype);
+ } else {
+ self::$instance = new HTMLPurifier();
+ }
+ }
+ return self::$instance;
+ }
+
+ /**
+ * @note Backwards compatibility, see instance()
+ */
+ public static function getInstance($prototype = null) {
+ return HTMLPurifier::instance($prototype);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier.safe-includes.php b/extlib/HTMLPurifier/HTMLPurifier.safe-includes.php
new file mode 100644
index 000000000..cf2c1d617
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier.safe-includes.php
@@ -0,0 +1,202 @@
+<?php
+
+/**
+ * @file
+ * This file was auto-generated by generate-includes.php and includes all of
+ * the core files required by HTML Purifier. This is a convenience stub that
+ * includes all files using dirname(__FILE__) and require_once. PLEASE DO NOT
+ * EDIT THIS FILE, changes will be overwritten the next time the script is run.
+ *
+ * Changes to include_path are not necessary.
+ */
+
+$__dir = dirname(__FILE__);
+
+require_once $__dir . '/HTMLPurifier.php';
+require_once $__dir . '/HTMLPurifier/AttrCollections.php';
+require_once $__dir . '/HTMLPurifier/AttrDef.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform.php';
+require_once $__dir . '/HTMLPurifier/AttrTypes.php';
+require_once $__dir . '/HTMLPurifier/AttrValidator.php';
+require_once $__dir . '/HTMLPurifier/Bootstrap.php';
+require_once $__dir . '/HTMLPurifier/Definition.php';
+require_once $__dir . '/HTMLPurifier/CSSDefinition.php';
+require_once $__dir . '/HTMLPurifier/ChildDef.php';
+require_once $__dir . '/HTMLPurifier/Config.php';
+require_once $__dir . '/HTMLPurifier/ConfigSchema.php';
+require_once $__dir . '/HTMLPurifier/ContentSets.php';
+require_once $__dir . '/HTMLPurifier/Context.php';
+require_once $__dir . '/HTMLPurifier/DefinitionCache.php';
+require_once $__dir . '/HTMLPurifier/DefinitionCacheFactory.php';
+require_once $__dir . '/HTMLPurifier/Doctype.php';
+require_once $__dir . '/HTMLPurifier/DoctypeRegistry.php';
+require_once $__dir . '/HTMLPurifier/ElementDef.php';
+require_once $__dir . '/HTMLPurifier/Encoder.php';
+require_once $__dir . '/HTMLPurifier/EntityLookup.php';
+require_once $__dir . '/HTMLPurifier/EntityParser.php';
+require_once $__dir . '/HTMLPurifier/ErrorCollector.php';
+require_once $__dir . '/HTMLPurifier/ErrorStruct.php';
+require_once $__dir . '/HTMLPurifier/Exception.php';
+require_once $__dir . '/HTMLPurifier/Filter.php';
+require_once $__dir . '/HTMLPurifier/Generator.php';
+require_once $__dir . '/HTMLPurifier/HTMLDefinition.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule.php';
+require_once $__dir . '/HTMLPurifier/HTMLModuleManager.php';
+require_once $__dir . '/HTMLPurifier/IDAccumulator.php';
+require_once $__dir . '/HTMLPurifier/Injector.php';
+require_once $__dir . '/HTMLPurifier/Language.php';
+require_once $__dir . '/HTMLPurifier/LanguageFactory.php';
+require_once $__dir . '/HTMLPurifier/Length.php';
+require_once $__dir . '/HTMLPurifier/Lexer.php';
+require_once $__dir . '/HTMLPurifier/PercentEncoder.php';
+require_once $__dir . '/HTMLPurifier/PropertyList.php';
+require_once $__dir . '/HTMLPurifier/PropertyListIterator.php';
+require_once $__dir . '/HTMLPurifier/Strategy.php';
+require_once $__dir . '/HTMLPurifier/StringHash.php';
+require_once $__dir . '/HTMLPurifier/StringHashParser.php';
+require_once $__dir . '/HTMLPurifier/TagTransform.php';
+require_once $__dir . '/HTMLPurifier/Token.php';
+require_once $__dir . '/HTMLPurifier/TokenFactory.php';
+require_once $__dir . '/HTMLPurifier/URI.php';
+require_once $__dir . '/HTMLPurifier/URIDefinition.php';
+require_once $__dir . '/HTMLPurifier/URIFilter.php';
+require_once $__dir . '/HTMLPurifier/URIParser.php';
+require_once $__dir . '/HTMLPurifier/URIScheme.php';
+require_once $__dir . '/HTMLPurifier/URISchemeRegistry.php';
+require_once $__dir . '/HTMLPurifier/UnitConverter.php';
+require_once $__dir . '/HTMLPurifier/VarParser.php';
+require_once $__dir . '/HTMLPurifier/VarParserException.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/Enum.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/Integer.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/Lang.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/Switch.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/Text.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/URI.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Number.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/AlphaValue.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Background.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Border.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Color.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Composite.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Filter.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Font.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/FontFamily.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Length.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/ListStyle.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Multiple.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/Percentage.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/TextDecoration.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/CSS/URI.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Bool.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Nmtokens.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Class.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Color.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/FrameTarget.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/ID.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Pixels.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Length.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/LinkTypes.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/HTML/MultiLength.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/URI/Email.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/URI/Host.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/URI/IPv4.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/URI/IPv6.php';
+require_once $__dir . '/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/Background.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/BdoDir.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/BgColor.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/BoolToCSS.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/Border.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/EnumToCSS.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/ImgRequired.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/ImgSpace.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/Input.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/Lang.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/Length.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/Name.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/NameSync.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/SafeEmbed.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/SafeObject.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/SafeParam.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/ScriptRequired.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/Textarea.php';
+require_once $__dir . '/HTMLPurifier/ChildDef/Chameleon.php';
+require_once $__dir . '/HTMLPurifier/ChildDef/Custom.php';
+require_once $__dir . '/HTMLPurifier/ChildDef/Empty.php';
+require_once $__dir . '/HTMLPurifier/ChildDef/Required.php';
+require_once $__dir . '/HTMLPurifier/ChildDef/Optional.php';
+require_once $__dir . '/HTMLPurifier/ChildDef/StrictBlockquote.php';
+require_once $__dir . '/HTMLPurifier/ChildDef/Table.php';
+require_once $__dir . '/HTMLPurifier/DefinitionCache/Decorator.php';
+require_once $__dir . '/HTMLPurifier/DefinitionCache/Null.php';
+require_once $__dir . '/HTMLPurifier/DefinitionCache/Serializer.php';
+require_once $__dir . '/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php';
+require_once $__dir . '/HTMLPurifier/DefinitionCache/Decorator/Memory.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Bdo.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/CommonAttributes.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Edit.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Forms.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Hypertext.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Image.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Legacy.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/List.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Name.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Object.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Presentation.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Proprietary.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Ruby.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/SafeEmbed.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/SafeObject.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Scripting.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/StyleAttribute.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Tables.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Target.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Text.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/XMLCommonAttributes.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/Name.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/Proprietary.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/Strict.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/Transitional.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy/XHTML.php';
+require_once $__dir . '/HTMLPurifier/Injector/AutoParagraph.php';
+require_once $__dir . '/HTMLPurifier/Injector/DisplayLinkURI.php';
+require_once $__dir . '/HTMLPurifier/Injector/Linkify.php';
+require_once $__dir . '/HTMLPurifier/Injector/PurifierLinkify.php';
+require_once $__dir . '/HTMLPurifier/Injector/RemoveEmpty.php';
+require_once $__dir . '/HTMLPurifier/Injector/SafeObject.php';
+require_once $__dir . '/HTMLPurifier/Lexer/DOMLex.php';
+require_once $__dir . '/HTMLPurifier/Lexer/DirectLex.php';
+require_once $__dir . '/HTMLPurifier/Strategy/Composite.php';
+require_once $__dir . '/HTMLPurifier/Strategy/Core.php';
+require_once $__dir . '/HTMLPurifier/Strategy/FixNesting.php';
+require_once $__dir . '/HTMLPurifier/Strategy/MakeWellFormed.php';
+require_once $__dir . '/HTMLPurifier/Strategy/RemoveForeignElements.php';
+require_once $__dir . '/HTMLPurifier/Strategy/ValidateAttributes.php';
+require_once $__dir . '/HTMLPurifier/TagTransform/Font.php';
+require_once $__dir . '/HTMLPurifier/TagTransform/Simple.php';
+require_once $__dir . '/HTMLPurifier/Token/Comment.php';
+require_once $__dir . '/HTMLPurifier/Token/Tag.php';
+require_once $__dir . '/HTMLPurifier/Token/Empty.php';
+require_once $__dir . '/HTMLPurifier/Token/End.php';
+require_once $__dir . '/HTMLPurifier/Token/Start.php';
+require_once $__dir . '/HTMLPurifier/Token/Text.php';
+require_once $__dir . '/HTMLPurifier/URIFilter/DisableExternal.php';
+require_once $__dir . '/HTMLPurifier/URIFilter/DisableExternalResources.php';
+require_once $__dir . '/HTMLPurifier/URIFilter/HostBlacklist.php';
+require_once $__dir . '/HTMLPurifier/URIFilter/MakeAbsolute.php';
+require_once $__dir . '/HTMLPurifier/URIFilter/Munge.php';
+require_once $__dir . '/HTMLPurifier/URIScheme/ftp.php';
+require_once $__dir . '/HTMLPurifier/URIScheme/http.php';
+require_once $__dir . '/HTMLPurifier/URIScheme/https.php';
+require_once $__dir . '/HTMLPurifier/URIScheme/mailto.php';
+require_once $__dir . '/HTMLPurifier/URIScheme/news.php';
+require_once $__dir . '/HTMLPurifier/URIScheme/nntp.php';
+require_once $__dir . '/HTMLPurifier/VarParser/Flexible.php';
+require_once $__dir . '/HTMLPurifier/VarParser/Native.php';
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrCollections.php b/extlib/HTMLPurifier/HTMLPurifier/AttrCollections.php
new file mode 100644
index 000000000..555b86d04
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrCollections.php
@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * Defines common attribute collections that modules reference
+ */
+
+class HTMLPurifier_AttrCollections
+{
+
+ /**
+ * Associative array of attribute collections, indexed by name
+ */
+ public $info = array();
+
+ /**
+ * Performs all expansions on internal data for use by other inclusions
+ * It also collects all attribute collection extensions from
+ * modules
+ * @param $attr_types HTMLPurifier_AttrTypes instance
+ * @param $modules Hash array of HTMLPurifier_HTMLModule members
+ */
+ public function __construct($attr_types, $modules) {
+ // load extensions from the modules
+ foreach ($modules as $module) {
+ foreach ($module->attr_collections as $coll_i => $coll) {
+ if (!isset($this->info[$coll_i])) {
+ $this->info[$coll_i] = array();
+ }
+ foreach ($coll as $attr_i => $attr) {
+ if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) {
+ // merge in includes
+ $this->info[$coll_i][$attr_i] = array_merge(
+ $this->info[$coll_i][$attr_i], $attr);
+ continue;
+ }
+ $this->info[$coll_i][$attr_i] = $attr;
+ }
+ }
+ }
+ // perform internal expansions and inclusions
+ foreach ($this->info as $name => $attr) {
+ // merge attribute collections that include others
+ $this->performInclusions($this->info[$name]);
+ // replace string identifiers with actual attribute objects
+ $this->expandIdentifiers($this->info[$name], $attr_types);
+ }
+ }
+
+ /**
+ * Takes a reference to an attribute associative array and performs
+ * all inclusions specified by the zero index.
+ * @param &$attr Reference to attribute array
+ */
+ public function performInclusions(&$attr) {
+ if (!isset($attr[0])) return;
+ $merge = $attr[0];
+ $seen = array(); // recursion guard
+ // loop through all the inclusions
+ for ($i = 0; isset($merge[$i]); $i++) {
+ if (isset($seen[$merge[$i]])) continue;
+ $seen[$merge[$i]] = true;
+ // foreach attribute of the inclusion, copy it over
+ if (!isset($this->info[$merge[$i]])) continue;
+ foreach ($this->info[$merge[$i]] as $key => $value) {
+ if (isset($attr[$key])) continue; // also catches more inclusions
+ $attr[$key] = $value;
+ }
+ if (isset($this->info[$merge[$i]][0])) {
+ // recursion
+ $merge = array_merge($merge, $this->info[$merge[$i]][0]);
+ }
+ }
+ unset($attr[0]);
+ }
+
+ /**
+ * Expands all string identifiers in an attribute array by replacing
+ * them with the appropriate values inside HTMLPurifier_AttrTypes
+ * @param &$attr Reference to attribute array
+ * @param $attr_types HTMLPurifier_AttrTypes instance
+ */
+ public function expandIdentifiers(&$attr, $attr_types) {
+
+ // because foreach will process new elements we add, make sure we
+ // skip duplicates
+ $processed = array();
+
+ foreach ($attr as $def_i => $def) {
+ // skip inclusions
+ if ($def_i === 0) continue;
+
+ if (isset($processed[$def_i])) continue;
+
+ // determine whether or not attribute is required
+ if ($required = (strpos($def_i, '*') !== false)) {
+ // rename the definition
+ unset($attr[$def_i]);
+ $def_i = trim($def_i, '*');
+ $attr[$def_i] = $def;
+ }
+
+ $processed[$def_i] = true;
+
+ // if we've already got a literal object, move on
+ if (is_object($def)) {
+ // preserve previous required
+ $attr[$def_i]->required = ($required || $attr[$def_i]->required);
+ continue;
+ }
+
+ if ($def === false) {
+ unset($attr[$def_i]);
+ continue;
+ }
+
+ if ($t = $attr_types->get($def)) {
+ $attr[$def_i] = $t;
+ $attr[$def_i]->required = $required;
+ } else {
+ unset($attr[$def_i]);
+ }
+ }
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef.php
new file mode 100644
index 000000000..d32fa62d6
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * Base class for all validating attribute definitions.
+ *
+ * This family of classes forms the core for not only HTML attribute validation,
+ * but also any sort of string that needs to be validated or cleaned (which
+ * means CSS properties and composite definitions are defined here too).
+ * Besides defining (through code) what precisely makes the string valid,
+ * subclasses are also responsible for cleaning the code if possible.
+ */
+
+abstract class HTMLPurifier_AttrDef
+{
+
+ /**
+ * Tells us whether or not an HTML attribute is minimized. Has no
+ * meaning in other contexts.
+ */
+ public $minimized = false;
+
+ /**
+ * Tells us whether or not an HTML attribute is required. Has no
+ * meaning in other contexts
+ */
+ public $required = false;
+
+ /**
+ * Validates and cleans passed string according to a definition.
+ *
+ * @param $string String to be validated and cleaned.
+ * @param $config Mandatory HTMLPurifier_Config object.
+ * @param $context Mandatory HTMLPurifier_AttrContext object.
+ */
+ abstract public function validate($string, $config, $context);
+
+ /**
+ * Convenience method that parses a string as if it were CDATA.
+ *
+ * This method process a string in the manner specified at
+ * <http://www.w3.org/TR/html4/types.html#h-6.2> by removing
+ * leading and trailing whitespace, ignoring line feeds, and replacing
+ * carriage returns and tabs with spaces. While most useful for HTML
+ * attributes specified as CDATA, it can also be applied to most CSS
+ * values.
+ *
+ * @note This method is not entirely standards compliant, as trim() removes
+ * more types of whitespace than specified in the spec. In practice,
+ * this is rarely a problem, as those extra characters usually have
+ * already been removed by HTMLPurifier_Encoder.
+ *
+ * @warning This processing is inconsistent with XML's whitespace handling
+ * as specified by section 3.3.3 and referenced XHTML 1.0 section
+ * 4.7. However, note that we are NOT necessarily
+ * parsing XML, thus, this behavior may still be correct. We
+ * assume that newlines have been normalized.
+ */
+ public function parseCDATA($string) {
+ $string = trim($string);
+ $string = str_replace(array("\n", "\t", "\r"), ' ', $string);
+ return $string;
+ }
+
+ /**
+ * Factory method for creating this class from a string.
+ * @param $string String construction info
+ * @return Created AttrDef object corresponding to $string
+ */
+ public function make($string) {
+ // default implementation, return a flyweight of this object.
+ // If $string has an effect on the returned object (i.e. you
+ // need to overload this method), it is best
+ // to clone or instantiate new copies. (Instantiation is safer.)
+ return $this;
+ }
+
+ /**
+ * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work
+ * properly. THIS IS A HACK!
+ */
+ protected function mungeRgb($string) {
+ return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS.php
new file mode 100644
index 000000000..953e70675
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * Validates the HTML attribute style, otherwise known as CSS.
+ * @note We don't implement the whole CSS specification, so it might be
+ * difficult to reuse this component in the context of validating
+ * actual stylesheet declarations.
+ * @note If we were really serious about validating the CSS, we would
+ * tokenize the styles and then parse the tokens. Obviously, we
+ * are not doing that. Doing that could seriously harm performance,
+ * but would make these components a lot more viable for a CSS
+ * filtering solution.
+ */
+class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef
+{
+
+ public function validate($css, $config, $context) {
+
+ $css = $this->parseCDATA($css);
+
+ $definition = $config->getCSSDefinition();
+
+ // we're going to break the spec and explode by semicolons.
+ // This is because semicolon rarely appears in escaped form
+ // Doing this is generally flaky but fast
+ // IT MIGHT APPEAR IN URIs, see HTMLPurifier_AttrDef_CSSURI
+ // for details
+
+ $declarations = explode(';', $css);
+ $propvalues = array();
+
+ /**
+ * Name of the current CSS property being validated.
+ */
+ $property = false;
+ $context->register('CurrentCSSProperty', $property);
+
+ foreach ($declarations as $declaration) {
+ if (!$declaration) continue;
+ if (!strpos($declaration, ':')) continue;
+ list($property, $value) = explode(':', $declaration, 2);
+ $property = trim($property);
+ $value = trim($value);
+ $ok = false;
+ do {
+ if (isset($definition->info[$property])) {
+ $ok = true;
+ break;
+ }
+ if (ctype_lower($property)) break;
+ $property = strtolower($property);
+ if (isset($definition->info[$property])) {
+ $ok = true;
+ break;
+ }
+ } while(0);
+ if (!$ok) continue;
+ // inefficient call, since the validator will do this again
+ if (strtolower(trim($value)) !== 'inherit') {
+ // inherit works for everything (but only on the base property)
+ $result = $definition->info[$property]->validate(
+ $value, $config, $context );
+ } else {
+ $result = 'inherit';
+ }
+ if ($result === false) continue;
+ $propvalues[$property] = $result;
+ }
+
+ $context->destroy('CurrentCSSProperty');
+
+ // procedure does not write the new CSS simultaneously, so it's
+ // slightly inefficient, but it's the only way of getting rid of
+ // duplicates. Perhaps config to optimize it, but not now.
+
+ $new_declarations = '';
+ foreach ($propvalues as $prop => $value) {
+ $new_declarations .= "$prop:$value;";
+ }
+
+ return $new_declarations ? $new_declarations : false;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php
new file mode 100644
index 000000000..292c040d4
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php
@@ -0,0 +1,21 @@
+<?php
+
+class HTMLPurifier_AttrDef_CSS_AlphaValue extends HTMLPurifier_AttrDef_CSS_Number
+{
+
+ public function __construct() {
+ parent::__construct(false); // opacity is non-negative, but we will clamp it
+ }
+
+ public function validate($number, $config, $context) {
+ $result = parent::validate($number, $config, $context);
+ if ($result === false) return $result;
+ $float = (float) $result;
+ if ($float < 0.0) $result = '0';
+ if ($float > 1.0) $result = '1';
+ return $result;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Background.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Background.php
new file mode 100644
index 000000000..3a3d20cd6
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Background.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * Validates shorthand CSS property background.
+ * @warning Does not support url tokens that have internal spaces.
+ */
+class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Local copy of component validators.
+ * @note See HTMLPurifier_AttrDef_Font::$info for a similar impl.
+ */
+ protected $info;
+
+ public function __construct($config) {
+ $def = $config->getCSSDefinition();
+ $this->info['background-color'] = $def->info['background-color'];
+ $this->info['background-image'] = $def->info['background-image'];
+ $this->info['background-repeat'] = $def->info['background-repeat'];
+ $this->info['background-attachment'] = $def->info['background-attachment'];
+ $this->info['background-position'] = $def->info['background-position'];
+ }
+
+ public function validate($string, $config, $context) {
+
+ // regular pre-processing
+ $string = $this->parseCDATA($string);
+ if ($string === '') return false;
+
+ // munge rgb() decl if necessary
+ $string = $this->mungeRgb($string);
+
+ // assumes URI doesn't have spaces in it
+ $bits = explode(' ', strtolower($string)); // bits to process
+
+ $caught = array();
+ $caught['color'] = false;
+ $caught['image'] = false;
+ $caught['repeat'] = false;
+ $caught['attachment'] = false;
+ $caught['position'] = false;
+
+ $i = 0; // number of catches
+ $none = false;
+
+ foreach ($bits as $bit) {
+ if ($bit === '') continue;
+ foreach ($caught as $key => $status) {
+ if ($key != 'position') {
+ if ($status !== false) continue;
+ $r = $this->info['background-' . $key]->validate($bit, $config, $context);
+ } else {
+ $r = $bit;
+ }
+ if ($r === false) continue;
+ if ($key == 'position') {
+ if ($caught[$key] === false) $caught[$key] = '';
+ $caught[$key] .= $r . ' ';
+ } else {
+ $caught[$key] = $r;
+ }
+ $i++;
+ break;
+ }
+ }
+
+ if (!$i) return false;
+ if ($caught['position'] !== false) {
+ $caught['position'] = $this->info['background-position']->
+ validate($caught['position'], $config, $context);
+ }
+
+ $ret = array();
+ foreach ($caught as $value) {
+ if ($value === false) continue;
+ $ret[] = $value;
+ }
+
+ if (empty($ret)) return false;
+ return implode(' ', $ret);
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php
new file mode 100644
index 000000000..35df3985e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php
@@ -0,0 +1,126 @@
+<?php
+
+/* W3C says:
+ [ // adjective and number must be in correct order, even if
+ // you could switch them without introducing ambiguity.
+ // some browsers support that syntax
+ [
+ <percentage> | <length> | left | center | right
+ ]
+ [
+ <percentage> | <length> | top | center | bottom
+ ]?
+ ] |
+ [ // this signifies that the vertical and horizontal adjectives
+ // can be arbitrarily ordered, however, there can only be two,
+ // one of each, or none at all
+ [
+ left | center | right
+ ] ||
+ [
+ top | center | bottom
+ ]
+ ]
+ top, left = 0%
+ center, (none) = 50%
+ bottom, right = 100%
+*/
+
+/* QuirksMode says:
+ keyword + length/percentage must be ordered correctly, as per W3C
+
+ Internet Explorer and Opera, however, support arbitrary ordering. We
+ should fix it up.
+
+ Minor issue though, not strictly necessary.
+*/
+
+// control freaks may appreciate the ability to convert these to
+// percentages or something, but it's not necessary
+
+/**
+ * Validates the value of background-position.
+ */
+class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef
+{
+
+ protected $length;
+ protected $percentage;
+
+ public function __construct() {
+ $this->length = new HTMLPurifier_AttrDef_CSS_Length();
+ $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage();
+ }
+
+ public function validate($string, $config, $context) {
+ $string = $this->parseCDATA($string);
+ $bits = explode(' ', $string);
+
+ $keywords = array();
+ $keywords['h'] = false; // left, right
+ $keywords['v'] = false; // top, bottom
+ $keywords['c'] = false; // center
+ $measures = array();
+
+ $i = 0;
+
+ $lookup = array(
+ 'top' => 'v',
+ 'bottom' => 'v',
+ 'left' => 'h',
+ 'right' => 'h',
+ 'center' => 'c'
+ );
+
+ foreach ($bits as $bit) {
+ if ($bit === '') continue;
+
+ // test for keyword
+ $lbit = ctype_lower($bit) ? $bit : strtolower($bit);
+ if (isset($lookup[$lbit])) {
+ $status = $lookup[$lbit];
+ $keywords[$status] = $lbit;
+ $i++;
+ }
+
+ // test for length
+ $r = $this->length->validate($bit, $config, $context);
+ if ($r !== false) {
+ $measures[] = $r;
+ $i++;
+ }
+
+ // test for percentage
+ $r = $this->percentage->validate($bit, $config, $context);
+ if ($r !== false) {
+ $measures[] = $r;
+ $i++;
+ }
+
+ }
+
+ if (!$i) return false; // no valid values were caught
+
+
+ $ret = array();
+
+ // first keyword
+ if ($keywords['h']) $ret[] = $keywords['h'];
+ elseif (count($measures)) $ret[] = array_shift($measures);
+ elseif ($keywords['c']) {
+ $ret[] = $keywords['c'];
+ $keywords['c'] = false; // prevent re-use: center = center center
+ }
+
+ if ($keywords['v']) $ret[] = $keywords['v'];
+ elseif (count($measures)) $ret[] = array_shift($measures);
+ elseif ($keywords['c']) $ret[] = $keywords['c'];
+
+ if (empty($ret)) return false;
+ return implode(' ', $ret);
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Border.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Border.php
new file mode 100644
index 000000000..42a1d1b4a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Border.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Validates the border property as defined by CSS.
+ */
+class HTMLPurifier_AttrDef_CSS_Border extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Local copy of properties this property is shorthand for.
+ */
+ protected $info = array();
+
+ public function __construct($config) {
+ $def = $config->getCSSDefinition();
+ $this->info['border-width'] = $def->info['border-width'];
+ $this->info['border-style'] = $def->info['border-style'];
+ $this->info['border-top-color'] = $def->info['border-top-color'];
+ }
+
+ public function validate($string, $config, $context) {
+ $string = $this->parseCDATA($string);
+ $string = $this->mungeRgb($string);
+ $bits = explode(' ', $string);
+ $done = array(); // segments we've finished
+ $ret = ''; // return value
+ foreach ($bits as $bit) {
+ foreach ($this->info as $propname => $validator) {
+ if (isset($done[$propname])) continue;
+ $r = $validator->validate($bit, $config, $context);
+ if ($r !== false) {
+ $ret .= $r . ' ';
+ $done[$propname] = true;
+ break;
+ }
+ }
+ }
+ return rtrim($ret);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Color.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Color.php
new file mode 100644
index 000000000..07f95a671
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Color.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * Validates Color as defined by CSS.
+ */
+class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef
+{
+
+ public function validate($color, $config, $context) {
+
+ static $colors = null;
+ if ($colors === null) $colors = $config->get('Core.ColorKeywords');
+
+ $color = trim($color);
+ if ($color === '') return false;
+
+ $lower = strtolower($color);
+ if (isset($colors[$lower])) return $colors[$lower];
+
+ if (strpos($color, 'rgb(') !== false) {
+ // rgb literal handling
+ $length = strlen($color);
+ if (strpos($color, ')') !== $length - 1) return false;
+ $triad = substr($color, 4, $length - 4 - 1);
+ $parts = explode(',', $triad);
+ if (count($parts) !== 3) return false;
+ $type = false; // to ensure that they're all the same type
+ $new_parts = array();
+ foreach ($parts as $part) {
+ $part = trim($part);
+ if ($part === '') return false;
+ $length = strlen($part);
+ if ($part[$length - 1] === '%') {
+ // handle percents
+ if (!$type) {
+ $type = 'percentage';
+ } elseif ($type !== 'percentage') {
+ return false;
+ }
+ $num = (float) substr($part, 0, $length - 1);
+ if ($num < 0) $num = 0;
+ if ($num > 100) $num = 100;
+ $new_parts[] = "$num%";
+ } else {
+ // handle integers
+ if (!$type) {
+ $type = 'integer';
+ } elseif ($type !== 'integer') {
+ return false;
+ }
+ $num = (int) $part;
+ if ($num < 0) $num = 0;
+ if ($num > 255) $num = 255;
+ $new_parts[] = (string) $num;
+ }
+ }
+ $new_triad = implode(',', $new_parts);
+ $color = "rgb($new_triad)";
+ } else {
+ // hexadecimal handling
+ if ($color[0] === '#') {
+ $hex = substr($color, 1);
+ } else {
+ $hex = $color;
+ $color = '#' . $color;
+ }
+ $length = strlen($hex);
+ if ($length !== 3 && $length !== 6) return false;
+ if (!ctype_xdigit($hex)) return false;
+ }
+
+ return $color;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Composite.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Composite.php
new file mode 100644
index 000000000..de1289cba
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Composite.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * Allows multiple validators to attempt to validate attribute.
+ *
+ * Composite is just what it sounds like: a composite of many validators.
+ * This means that multiple HTMLPurifier_AttrDef objects will have a whack
+ * at the string. If one of them passes, that's what is returned. This is
+ * especially useful for CSS values, which often are a choice between
+ * an enumerated set of predefined values or a flexible data type.
+ */
+class HTMLPurifier_AttrDef_CSS_Composite extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * List of HTMLPurifier_AttrDef objects that may process strings
+ * @todo Make protected
+ */
+ public $defs;
+
+ /**
+ * @param $defs List of HTMLPurifier_AttrDef objects
+ */
+ public function __construct($defs) {
+ $this->defs = $defs;
+ }
+
+ public function validate($string, $config, $context) {
+ foreach ($this->defs as $i => $def) {
+ $result = $this->defs[$i]->validate($string, $config, $context);
+ if ($result !== false) return $result;
+ }
+ return false;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php
new file mode 100644
index 000000000..6599c5b2d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * Decorator which enables CSS properties to be disabled for specific elements.
+ */
+class HTMLPurifier_AttrDef_CSS_DenyElementDecorator extends HTMLPurifier_AttrDef
+{
+ public $def, $element;
+
+ /**
+ * @param $def Definition to wrap
+ * @param $element Element to deny
+ */
+ public function __construct($def, $element) {
+ $this->def = $def;
+ $this->element = $element;
+ }
+ /**
+ * Checks if CurrentToken is set and equal to $this->element
+ */
+ public function validate($string, $config, $context) {
+ $token = $context->get('CurrentToken', true);
+ if ($token && $token->name == $this->element) return false;
+ return $this->def->validate($string, $config, $context);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Filter.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Filter.php
new file mode 100644
index 000000000..147894b86
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Filter.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Microsoft's proprietary filter: CSS property
+ * @note Currently supports the alpha filter. In the future, this will
+ * probably need an extensible framework
+ */
+class HTMLPurifier_AttrDef_CSS_Filter extends HTMLPurifier_AttrDef
+{
+
+ protected $intValidator;
+
+ public function __construct() {
+ $this->intValidator = new HTMLPurifier_AttrDef_Integer();
+ }
+
+ public function validate($value, $config, $context) {
+ $value = $this->parseCDATA($value);
+ if ($value === 'none') return $value;
+ // if we looped this we could support multiple filters
+ $function_length = strcspn($value, '(');
+ $function = trim(substr($value, 0, $function_length));
+ if ($function !== 'alpha' &&
+ $function !== 'Alpha' &&
+ $function !== 'progid:DXImageTransform.Microsoft.Alpha'
+ ) return false;
+ $cursor = $function_length + 1;
+ $parameters_length = strcspn($value, ')', $cursor);
+ $parameters = substr($value, $cursor, $parameters_length);
+ $params = explode(',', $parameters);
+ $ret_params = array();
+ $lookup = array();
+ foreach ($params as $param) {
+ list($key, $value) = explode('=', $param);
+ $key = trim($key);
+ $value = trim($value);
+ if (isset($lookup[$key])) continue;
+ if ($key !== 'opacity') continue;
+ $value = $this->intValidator->validate($value, $config, $context);
+ if ($value === false) continue;
+ $int = (int) $value;
+ if ($int > 100) $value = '100';
+ if ($int < 0) $value = '0';
+ $ret_params[] = "$key=$value";
+ $lookup[$key] = true;
+ }
+ $ret_parameters = implode(',', $ret_params);
+ $ret_function = "$function($ret_parameters)";
+ return $ret_function;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Font.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Font.php
new file mode 100644
index 000000000..699ee0b70
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Font.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * Validates shorthand CSS property font.
+ */
+class HTMLPurifier_AttrDef_CSS_Font extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Local copy of component validators.
+ *
+ * @note If we moved specific CSS property definitions to their own
+ * classes instead of having them be assembled at run time by
+ * CSSDefinition, this wouldn't be necessary. We'd instantiate
+ * our own copies.
+ */
+ protected $info = array();
+
+ public function __construct($config) {
+ $def = $config->getCSSDefinition();
+ $this->info['font-style'] = $def->info['font-style'];
+ $this->info['font-variant'] = $def->info['font-variant'];
+ $this->info['font-weight'] = $def->info['font-weight'];
+ $this->info['font-size'] = $def->info['font-size'];
+ $this->info['line-height'] = $def->info['line-height'];
+ $this->info['font-family'] = $def->info['font-family'];
+ }
+
+ public function validate($string, $config, $context) {
+
+ static $system_fonts = array(
+ 'caption' => true,
+ 'icon' => true,
+ 'menu' => true,
+ 'message-box' => true,
+ 'small-caption' => true,
+ 'status-bar' => true
+ );
+
+ // regular pre-processing
+ $string = $this->parseCDATA($string);
+ if ($string === '') return false;
+
+ // check if it's one of the keywords
+ $lowercase_string = strtolower($string);
+ if (isset($system_fonts[$lowercase_string])) {
+ return $lowercase_string;
+ }
+
+ $bits = explode(' ', $string); // bits to process
+ $stage = 0; // this indicates what we're looking for
+ $caught = array(); // which stage 0 properties have we caught?
+ $stage_1 = array('font-style', 'font-variant', 'font-weight');
+ $final = ''; // output
+
+ for ($i = 0, $size = count($bits); $i < $size; $i++) {
+ if ($bits[$i] === '') continue;
+ switch ($stage) {
+
+ // attempting to catch font-style, font-variant or font-weight
+ case 0:
+ foreach ($stage_1 as $validator_name) {
+ if (isset($caught[$validator_name])) continue;
+ $r = $this->info[$validator_name]->validate(
+ $bits[$i], $config, $context);
+ if ($r !== false) {
+ $final .= $r . ' ';
+ $caught[$validator_name] = true;
+ break;
+ }
+ }
+ // all three caught, continue on
+ if (count($caught) >= 3) $stage = 1;
+ if ($r !== false) break;
+
+ // attempting to catch font-size and perhaps line-height
+ case 1:
+ $found_slash = false;
+ if (strpos($bits[$i], '/') !== false) {
+ list($font_size, $line_height) =
+ explode('/', $bits[$i]);
+ if ($line_height === '') {
+ // ooh, there's a space after the slash!
+ $line_height = false;
+ $found_slash = true;
+ }
+ } else {
+ $font_size = $bits[$i];
+ $line_height = false;
+ }
+ $r = $this->info['font-size']->validate(
+ $font_size, $config, $context);
+ if ($r !== false) {
+ $final .= $r;
+ // attempt to catch line-height
+ if ($line_height === false) {
+ // we need to scroll forward
+ for ($j = $i + 1; $j < $size; $j++) {
+ if ($bits[$j] === '') continue;
+ if ($bits[$j] === '/') {
+ if ($found_slash) {
+ return false;
+ } else {
+ $found_slash = true;
+ continue;
+ }
+ }
+ $line_height = $bits[$j];
+ break;
+ }
+ } else {
+ // slash already found
+ $found_slash = true;
+ $j = $i;
+ }
+ if ($found_slash) {
+ $i = $j;
+ $r = $this->info['line-height']->validate(
+ $line_height, $config, $context);
+ if ($r !== false) {
+ $final .= '/' . $r;
+ }
+ }
+ $final .= ' ';
+ $stage = 2;
+ break;
+ }
+ return false;
+
+ // attempting to catch font-family
+ case 2:
+ $font_family =
+ implode(' ', array_slice($bits, $i, $size - $i));
+ $r = $this->info['font-family']->validate(
+ $font_family, $config, $context);
+ if ($r !== false) {
+ $final .= $r . ' ';
+ // processing completed successfully
+ return rtrim($final);
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php
new file mode 100644
index 000000000..705ac893d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * Validates a font family list according to CSS spec
+ * @todo whitelisting allowed fonts would be nice
+ */
+class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
+{
+
+ public function validate($string, $config, $context) {
+ static $generic_names = array(
+ 'serif' => true,
+ 'sans-serif' => true,
+ 'monospace' => true,
+ 'fantasy' => true,
+ 'cursive' => true
+ );
+
+ // assume that no font names contain commas in them
+ $fonts = explode(',', $string);
+ $final = '';
+ foreach($fonts as $font) {
+ $font = trim($font);
+ if ($font === '') continue;
+ // match a generic name
+ if (isset($generic_names[$font])) {
+ $final .= $font . ', ';
+ continue;
+ }
+ // match a quoted name
+ if ($font[0] === '"' || $font[0] === "'") {
+ $length = strlen($font);
+ if ($length <= 2) continue;
+ $quote = $font[0];
+ if ($font[$length - 1] !== $quote) continue;
+ $font = substr($font, 1, $length - 2);
+
+ $new_font = '';
+ for ($i = 0, $c = strlen($font); $i < $c; $i++) {
+ if ($font[$i] === '\\') {
+ $i++;
+ if ($i >= $c) {
+ $new_font .= '\\';
+ break;
+ }
+ if (ctype_xdigit($font[$i])) {
+ $code = $font[$i];
+ for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) {
+ if (!ctype_xdigit($font[$i])) break;
+ $code .= $font[$i];
+ }
+ // We have to be extremely careful when adding
+ // new characters, to make sure we're not breaking
+ // the encoding.
+ $char = HTMLPurifier_Encoder::unichr(hexdec($code));
+ if (HTMLPurifier_Encoder::cleanUTF8($char) === '') continue;
+ $new_font .= $char;
+ if ($i < $c && trim($font[$i]) !== '') $i--;
+ continue;
+ }
+ if ($font[$i] === "\n") continue;
+ }
+ $new_font .= $font[$i];
+ }
+
+ $font = $new_font;
+ }
+ // $font is a pure representation of the font name
+
+ if (ctype_alnum($font) && $font !== '') {
+ // very simple font, allow it in unharmed
+ $final .= $font . ', ';
+ continue;
+ }
+
+ // complicated font, requires quoting
+
+ // armor single quotes and new lines
+ $font = str_replace("\\", "\\\\", $font);
+ $font = str_replace("'", "\\'", $font);
+ $final .= "'$font', ";
+ }
+ $final = rtrim($final, ', ');
+ if ($final === '') return false;
+ return $final;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php
new file mode 100644
index 000000000..4e6b35e5a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Decorator which enables !important to be used in CSS values.
+ */
+class HTMLPurifier_AttrDef_CSS_ImportantDecorator extends HTMLPurifier_AttrDef
+{
+ public $def, $allow;
+
+ /**
+ * @param $def Definition to wrap
+ * @param $allow Whether or not to allow !important
+ */
+ public function __construct($def, $allow = false) {
+ $this->def = $def;
+ $this->allow = $allow;
+ }
+ /**
+ * Intercepts and removes !important if necessary
+ */
+ public function validate($string, $config, $context) {
+ // test for ! and important tokens
+ $string = trim($string);
+ $is_important = false;
+ // :TODO: optimization: test directly for !important and ! important
+ if (strlen($string) >= 9 && substr($string, -9) === 'important') {
+ $temp = rtrim(substr($string, 0, -9));
+ // use a temp, because we might want to restore important
+ if (strlen($temp) >= 1 && substr($temp, -1) === '!') {
+ $string = rtrim(substr($temp, 0, -1));
+ $is_important = true;
+ }
+ }
+ $string = $this->def->validate($string, $config, $context);
+ if ($this->allow && $is_important) $string .= ' !important';
+ return $string;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Length.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Length.php
new file mode 100644
index 000000000..a07ec5813
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Length.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * Represents a Length as defined by CSS.
+ */
+class HTMLPurifier_AttrDef_CSS_Length extends HTMLPurifier_AttrDef
+{
+
+ protected $min, $max;
+
+ /**
+ * @param HTMLPurifier_Length $max Minimum length, or null for no bound. String is also acceptable.
+ * @param HTMLPurifier_Length $max Maximum length, or null for no bound. String is also acceptable.
+ */
+ public function __construct($min = null, $max = null) {
+ $this->min = $min !== null ? HTMLPurifier_Length::make($min) : null;
+ $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null;
+ }
+
+ public function validate($string, $config, $context) {
+ $string = $this->parseCDATA($string);
+
+ // Optimizations
+ if ($string === '') return false;
+ if ($string === '0') return '0';
+ if (strlen($string) === 1) return false;
+
+ $length = HTMLPurifier_Length::make($string);
+ if (!$length->isValid()) return false;
+
+ if ($this->min) {
+ $c = $length->compareTo($this->min);
+ if ($c === false) return false;
+ if ($c < 0) return false;
+ }
+ if ($this->max) {
+ $c = $length->compareTo($this->max);
+ if ($c === false) return false;
+ if ($c > 0) return false;
+ }
+
+ return $length->toString();
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php
new file mode 100644
index 000000000..4406868c0
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * Validates shorthand CSS property list-style.
+ * @warning Does not support url tokens that have internal spaces.
+ */
+class HTMLPurifier_AttrDef_CSS_ListStyle extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Local copy of component validators.
+ * @note See HTMLPurifier_AttrDef_CSS_Font::$info for a similar impl.
+ */
+ protected $info;
+
+ public function __construct($config) {
+ $def = $config->getCSSDefinition();
+ $this->info['list-style-type'] = $def->info['list-style-type'];
+ $this->info['list-style-position'] = $def->info['list-style-position'];
+ $this->info['list-style-image'] = $def->info['list-style-image'];
+ }
+
+ public function validate($string, $config, $context) {
+
+ // regular pre-processing
+ $string = $this->parseCDATA($string);
+ if ($string === '') return false;
+
+ // assumes URI doesn't have spaces in it
+ $bits = explode(' ', strtolower($string)); // bits to process
+
+ $caught = array();
+ $caught['type'] = false;
+ $caught['position'] = false;
+ $caught['image'] = false;
+
+ $i = 0; // number of catches
+ $none = false;
+
+ foreach ($bits as $bit) {
+ if ($i >= 3) return; // optimization bit
+ if ($bit === '') continue;
+ foreach ($caught as $key => $status) {
+ if ($status !== false) continue;
+ $r = $this->info['list-style-' . $key]->validate($bit, $config, $context);
+ if ($r === false) continue;
+ if ($r === 'none') {
+ if ($none) continue;
+ else $none = true;
+ if ($key == 'image') continue;
+ }
+ $caught[$key] = $r;
+ $i++;
+ break;
+ }
+ }
+
+ if (!$i) return false;
+
+ $ret = array();
+
+ // construct type
+ if ($caught['type']) $ret[] = $caught['type'];
+
+ // construct image
+ if ($caught['image']) $ret[] = $caught['image'];
+
+ // construct position
+ if ($caught['position']) $ret[] = $caught['position'];
+
+ if (empty($ret)) return false;
+ return implode(' ', $ret);
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Multiple.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Multiple.php
new file mode 100644
index 000000000..4d62a40d7
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Multiple.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * Framework class for strings that involve multiple values.
+ *
+ * Certain CSS properties such as border-width and margin allow multiple
+ * lengths to be specified. This class can take a vanilla border-width
+ * definition and multiply it, usually into a max of four.
+ *
+ * @note Even though the CSS specification isn't clear about it, inherit
+ * can only be used alone: it will never manifest as part of a multi
+ * shorthand declaration. Thus, this class does not allow inherit.
+ */
+class HTMLPurifier_AttrDef_CSS_Multiple extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Instance of component definition to defer validation to.
+ * @todo Make protected
+ */
+ public $single;
+
+ /**
+ * Max number of values allowed.
+ * @todo Make protected
+ */
+ public $max;
+
+ /**
+ * @param $single HTMLPurifier_AttrDef to multiply
+ * @param $max Max number of values allowed (usually four)
+ */
+ public function __construct($single, $max = 4) {
+ $this->single = $single;
+ $this->max = $max;
+ }
+
+ public function validate($string, $config, $context) {
+ $string = $this->parseCDATA($string);
+ if ($string === '') return false;
+ $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n
+ $length = count($parts);
+ $final = '';
+ for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) {
+ if (ctype_space($parts[$i])) continue;
+ $result = $this->single->validate($parts[$i], $config, $context);
+ if ($result !== false) {
+ $final .= $result . ' ';
+ $num++;
+ }
+ }
+ if ($final === '') return false;
+ return rtrim($final);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Number.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Number.php
new file mode 100644
index 000000000..3f99e12ec
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Number.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * Validates a number as defined by the CSS spec.
+ */
+class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Bool indicating whether or not only positive values allowed.
+ */
+ protected $non_negative = false;
+
+ /**
+ * @param $non_negative Bool indicating whether negatives are forbidden
+ */
+ public function __construct($non_negative = false) {
+ $this->non_negative = $non_negative;
+ }
+
+ /**
+ * @warning Some contexts do not pass $config, $context. These
+ * variables should not be used without checking HTMLPurifier_Length
+ */
+ public function validate($number, $config, $context) {
+
+ $number = $this->parseCDATA($number);
+
+ if ($number === '') return false;
+ if ($number === '0') return '0';
+
+ $sign = '';
+ switch ($number[0]) {
+ case '-':
+ if ($this->non_negative) return false;
+ $sign = '-';
+ case '+':
+ $number = substr($number, 1);
+ }
+
+ if (ctype_digit($number)) {
+ $number = ltrim($number, '0');
+ return $number ? $sign . $number : '0';
+ }
+
+ // Period is the only non-numeric character allowed
+ if (strpos($number, '.') === false) return false;
+
+ list($left, $right) = explode('.', $number, 2);
+
+ if ($left === '' && $right === '') return false;
+ if ($left !== '' && !ctype_digit($left)) return false;
+
+ $left = ltrim($left, '0');
+ $right = rtrim($right, '0');
+
+ if ($right === '') {
+ return $left ? $sign . $left : '0';
+ } elseif (!ctype_digit($right)) {
+ return false;
+ }
+
+ return $sign . $left . '.' . $right;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Percentage.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Percentage.php
new file mode 100644
index 000000000..c34b8fc3c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Percentage.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Validates a Percentage as defined by the CSS spec.
+ */
+class HTMLPurifier_AttrDef_CSS_Percentage extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Instance of HTMLPurifier_AttrDef_CSS_Number to defer number validation
+ */
+ protected $number_def;
+
+ /**
+ * @param Bool indicating whether to forbid negative values
+ */
+ public function __construct($non_negative = false) {
+ $this->number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative);
+ }
+
+ public function validate($string, $config, $context) {
+
+ $string = $this->parseCDATA($string);
+
+ if ($string === '') return false;
+ $length = strlen($string);
+ if ($length === 1) return false;
+ if ($string[$length - 1] !== '%') return false;
+
+ $number = substr($string, 0, $length - 1);
+ $number = $this->number_def->validate($number, $config, $context);
+
+ if ($number === false) return false;
+ return "$number%";
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php
new file mode 100644
index 000000000..772c922d8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * Validates the value for the CSS property text-decoration
+ * @note This class could be generalized into a version that acts sort of
+ * like Enum except you can compound the allowed values.
+ */
+class HTMLPurifier_AttrDef_CSS_TextDecoration extends HTMLPurifier_AttrDef
+{
+
+ public function validate($string, $config, $context) {
+
+ static $allowed_values = array(
+ 'line-through' => true,
+ 'overline' => true,
+ 'underline' => true,
+ );
+
+ $string = strtolower($this->parseCDATA($string));
+
+ if ($string === 'none') return $string;
+
+ $parts = explode(' ', $string);
+ $final = '';
+ foreach ($parts as $part) {
+ if (isset($allowed_values[$part])) {
+ $final .= $part . ' ';
+ }
+ }
+ $final = rtrim($final);
+ if ($final === '') return false;
+ return $final;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/URI.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/URI.php
new file mode 100644
index 000000000..435d7930b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/URI.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * Validates a URI in CSS syntax, which uses url('http://example.com')
+ * @note While theoretically speaking a URI in a CSS document could
+ * be non-embedded, as of CSS2 there is no such usage so we're
+ * generalizing it. This may need to be changed in the future.
+ * @warning Since HTMLPurifier_AttrDef_CSS blindly uses semicolons as
+ * the separator, you cannot put a literal semicolon in
+ * in the URI. Try percent encoding it, in that case.
+ */
+class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI
+{
+
+ public function __construct() {
+ parent::__construct(true); // always embedded
+ }
+
+ public function validate($uri_string, $config, $context) {
+ // parse the URI out of the string and then pass it onto
+ // the parent object
+
+ $uri_string = $this->parseCDATA($uri_string);
+ if (strpos($uri_string, 'url(') !== 0) return false;
+ $uri_string = substr($uri_string, 4);
+ $new_length = strlen($uri_string) - 1;
+ if ($uri_string[$new_length] != ')') return false;
+ $uri = trim(substr($uri_string, 0, $new_length));
+
+ if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) {
+ $quote = $uri[0];
+ $new_length = strlen($uri) - 1;
+ if ($uri[$new_length] !== $quote) return false;
+ $uri = substr($uri, 1, $new_length - 1);
+ }
+
+ $keys = array( '(', ')', ',', ' ', '"', "'");
+ $values = array('\\(', '\\)', '\\,', '\\ ', '\\"', "\\'");
+ $uri = str_replace($values, $keys, $uri);
+
+ $result = parent::validate($uri, $config, $context);
+
+ if ($result === false) return false;
+
+ // escape necessary characters according to CSS spec
+ // except for the comma, none of these should appear in the
+ // URI at all
+ $result = str_replace($keys, $values, $result);
+
+ return "url($result)";
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Enum.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Enum.php
new file mode 100644
index 000000000..5d603ebcc
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Enum.php
@@ -0,0 +1,65 @@
+<?php
+
+// Enum = Enumerated
+/**
+ * Validates a keyword against a list of valid values.
+ * @warning The case-insensitive compare of this function uses PHP's
+ * built-in strtolower and ctype_lower functions, which may
+ * cause problems with international comparisons
+ */
+class HTMLPurifier_AttrDef_Enum extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Lookup table of valid values.
+ * @todo Make protected
+ */
+ public $valid_values = array();
+
+ /**
+ * Bool indicating whether or not enumeration is case sensitive.
+ * @note In general this is always case insensitive.
+ */
+ protected $case_sensitive = false; // values according to W3C spec
+
+ /**
+ * @param $valid_values List of valid values
+ * @param $case_sensitive Bool indicating whether or not case sensitive
+ */
+ public function __construct(
+ $valid_values = array(), $case_sensitive = false
+ ) {
+ $this->valid_values = array_flip($valid_values);
+ $this->case_sensitive = $case_sensitive;
+ }
+
+ public function validate($string, $config, $context) {
+ $string = trim($string);
+ if (!$this->case_sensitive) {
+ // we may want to do full case-insensitive libraries
+ $string = ctype_lower($string) ? $string : strtolower($string);
+ }
+ $result = isset($this->valid_values[$string]);
+
+ return $result ? $string : false;
+ }
+
+ /**
+ * @param $string In form of comma-delimited list of case-insensitive
+ * valid values. Example: "foo,bar,baz". Prepend "s:" to make
+ * case sensitive
+ */
+ public function make($string) {
+ if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') {
+ $string = substr($string, 2);
+ $sensitive = true;
+ } else {
+ $sensitive = false;
+ }
+ $values = explode(',', $string);
+ return new HTMLPurifier_AttrDef_Enum($values, $sensitive);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Bool.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Bool.php
new file mode 100644
index 000000000..e06987eb8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Bool.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * Validates a boolean attribute
+ */
+class HTMLPurifier_AttrDef_HTML_Bool extends HTMLPurifier_AttrDef
+{
+
+ protected $name;
+ public $minimized = true;
+
+ public function __construct($name = false) {$this->name = $name;}
+
+ public function validate($string, $config, $context) {
+ if (empty($string)) return false;
+ return $this->name;
+ }
+
+ /**
+ * @param $string Name of attribute
+ */
+ public function make($string) {
+ return new HTMLPurifier_AttrDef_HTML_Bool($string);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Class.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Class.php
new file mode 100644
index 000000000..370068d97
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Class.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * Implements special behavior for class attribute (normally NMTOKENS)
+ */
+class HTMLPurifier_AttrDef_HTML_Class extends HTMLPurifier_AttrDef_HTML_Nmtokens
+{
+ protected function split($string, $config, $context) {
+ // really, this twiddle should be lazy loaded
+ $name = $config->getDefinition('HTML')->doctype->name;
+ if ($name == "XHTML 1.1" || $name == "XHTML 2.0") {
+ return parent::split($string, $config, $context);
+ } else {
+ return preg_split('/\s+/', $string);
+ }
+ }
+ protected function filter($tokens, $config, $context) {
+ $allowed = $config->get('Attr.AllowedClasses');
+ $forbidden = $config->get('Attr.ForbiddenClasses');
+ $ret = array();
+ foreach ($tokens as $token) {
+ if (
+ ($allowed === null || isset($allowed[$token])) &&
+ !isset($forbidden[$token]) &&
+ // We need this O(n) check because of PHP's array
+ // implementation that casts -0 to 0.
+ !in_array($token, $ret, true)
+ ) {
+ $ret[] = $token;
+ }
+ }
+ return $ret;
+ }
+}
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Color.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Color.php
new file mode 100644
index 000000000..d01e20454
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Color.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Validates a color according to the HTML spec.
+ */
+class HTMLPurifier_AttrDef_HTML_Color extends HTMLPurifier_AttrDef
+{
+
+ public function validate($string, $config, $context) {
+
+ static $colors = null;
+ if ($colors === null) $colors = $config->get('Core.ColorKeywords');
+
+ $string = trim($string);
+
+ if (empty($string)) return false;
+ if (isset($colors[$string])) return $colors[$string];
+ if ($string[0] === '#') $hex = substr($string, 1);
+ else $hex = $string;
+
+ $length = strlen($hex);
+ if ($length !== 3 && $length !== 6) return false;
+ if (!ctype_xdigit($hex)) return false;
+ if ($length === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
+
+ return "#$hex";
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php
new file mode 100644
index 000000000..ae6ea7c01
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * Special-case enum attribute definition that lazy loads allowed frame targets
+ */
+class HTMLPurifier_AttrDef_HTML_FrameTarget extends HTMLPurifier_AttrDef_Enum
+{
+
+ public $valid_values = false; // uninitialized value
+ protected $case_sensitive = false;
+
+ public function __construct() {}
+
+ public function validate($string, $config, $context) {
+ if ($this->valid_values === false) $this->valid_values = $config->get('Attr.AllowedFrameTargets');
+ return parent::validate($string, $config, $context);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/ID.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/ID.php
new file mode 100644
index 000000000..81d03762d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/ID.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * Validates the HTML attribute ID.
+ * @warning Even though this is the id processor, it
+ * will ignore the directive Attr:IDBlacklist, since it will only
+ * go according to the ID accumulator. Since the accumulator is
+ * automatically generated, it will have already absorbed the
+ * blacklist. If you're hacking around, make sure you use load()!
+ */
+
+class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef
+{
+
+ // ref functionality disabled, since we also have to verify
+ // whether or not the ID it refers to exists
+
+ public function validate($id, $config, $context) {
+
+ if (!$config->get('Attr.EnableID')) return false;
+
+ $id = trim($id); // trim it first
+
+ if ($id === '') return false;
+
+ $prefix = $config->get('Attr.IDPrefix');
+ if ($prefix !== '') {
+ $prefix .= $config->get('Attr.IDPrefixLocal');
+ // prevent re-appending the prefix
+ if (strpos($id, $prefix) !== 0) $id = $prefix . $id;
+ } elseif ($config->get('Attr.IDPrefixLocal') !== '') {
+ trigger_error('%Attr.IDPrefixLocal cannot be used unless '.
+ '%Attr.IDPrefix is set', E_USER_WARNING);
+ }
+
+ //if (!$this->ref) {
+ $id_accumulator =& $context->get('IDAccumulator');
+ if (isset($id_accumulator->ids[$id])) return false;
+ //}
+
+ // we purposely avoid using regex, hopefully this is faster
+
+ if (ctype_alpha($id)) {
+ $result = true;
+ } else {
+ if (!ctype_alpha(@$id[0])) return false;
+ $trim = trim( // primitive style of regexps, I suppose
+ $id,
+ 'A..Za..z0..9:-._'
+ );
+ $result = ($trim === '');
+ }
+
+ $regexp = $config->get('Attr.IDBlacklistRegexp');
+ if ($regexp && preg_match($regexp, $id)) {
+ return false;
+ }
+
+ if (/*!$this->ref && */$result) $id_accumulator->add($id);
+
+ // if no change was made to the ID, return the result
+ // else, return the new id if stripping whitespace made it
+ // valid, or return false.
+ return $result ? $id : false;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Length.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Length.php
new file mode 100644
index 000000000..a242f9c23
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Length.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Validates the HTML type length (not to be confused with CSS's length).
+ *
+ * This accepts integer pixels or percentages as lengths for certain
+ * HTML attributes.
+ */
+
+class HTMLPurifier_AttrDef_HTML_Length extends HTMLPurifier_AttrDef_HTML_Pixels
+{
+
+ public function validate($string, $config, $context) {
+
+ $string = trim($string);
+ if ($string === '') return false;
+
+ $parent_result = parent::validate($string, $config, $context);
+ if ($parent_result !== false) return $parent_result;
+
+ $length = strlen($string);
+ $last_char = $string[$length - 1];
+
+ if ($last_char !== '%') return false;
+
+ $points = substr($string, 0, $length - 1);
+
+ if (!is_numeric($points)) return false;
+
+ $points = (int) $points;
+
+ if ($points < 0) return '0%';
+ if ($points > 100) return '100%';
+
+ return ((string) $points) . '%';
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php
new file mode 100644
index 000000000..76d25ed08
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * Validates a rel/rev link attribute against a directive of allowed values
+ * @note We cannot use Enum because link types allow multiple
+ * values.
+ * @note Assumes link types are ASCII text
+ */
+class HTMLPurifier_AttrDef_HTML_LinkTypes extends HTMLPurifier_AttrDef
+{
+
+ /** Name config attribute to pull. */
+ protected $name;
+
+ public function __construct($name) {
+ $configLookup = array(
+ 'rel' => 'AllowedRel',
+ 'rev' => 'AllowedRev'
+ );
+ if (!isset($configLookup[$name])) {
+ trigger_error('Unrecognized attribute name for link '.
+ 'relationship.', E_USER_ERROR);
+ return;
+ }
+ $this->name = $configLookup[$name];
+ }
+
+ public function validate($string, $config, $context) {
+
+ $allowed = $config->get('Attr.' . $this->name);
+ if (empty($allowed)) return false;
+
+ $string = $this->parseCDATA($string);
+ $parts = explode(' ', $string);
+
+ // lookup to prevent duplicates
+ $ret_lookup = array();
+ foreach ($parts as $part) {
+ $part = strtolower(trim($part));
+ if (!isset($allowed[$part])) continue;
+ $ret_lookup[$part] = true;
+ }
+
+ if (empty($ret_lookup)) return false;
+ $string = implode(' ', array_keys($ret_lookup));
+
+ return $string;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php
new file mode 100644
index 000000000..c72fc76e4
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Validates a MultiLength as defined by the HTML spec.
+ *
+ * A multilength is either a integer (pixel count), a percentage, or
+ * a relative number.
+ */
+class HTMLPurifier_AttrDef_HTML_MultiLength extends HTMLPurifier_AttrDef_HTML_Length
+{
+
+ public function validate($string, $config, $context) {
+
+ $string = trim($string);
+ if ($string === '') return false;
+
+ $parent_result = parent::validate($string, $config, $context);
+ if ($parent_result !== false) return $parent_result;
+
+ $length = strlen($string);
+ $last_char = $string[$length - 1];
+
+ if ($last_char !== '*') return false;
+
+ $int = substr($string, 0, $length - 1);
+
+ if ($int == '') return '*';
+ if (!is_numeric($int)) return false;
+
+ $int = (int) $int;
+
+ if ($int < 0) return false;
+ if ($int == 0) return '0';
+ if ($int == 1) return '*';
+ return ((string) $int) . '*';
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Nmtokens.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Nmtokens.php
new file mode 100644
index 000000000..aa34120bd
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Nmtokens.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * Validates contents based on NMTOKENS attribute type.
+ */
+class HTMLPurifier_AttrDef_HTML_Nmtokens extends HTMLPurifier_AttrDef
+{
+
+ public function validate($string, $config, $context) {
+
+ $string = trim($string);
+
+ // early abort: '' and '0' (strings that convert to false) are invalid
+ if (!$string) return false;
+
+ $tokens = $this->split($string, $config, $context);
+ $tokens = $this->filter($tokens, $config, $context);
+ if (empty($tokens)) return false;
+ return implode(' ', $tokens);
+
+ }
+
+ /**
+ * Splits a space separated list of tokens into its constituent parts.
+ */
+ protected function split($string, $config, $context) {
+ // OPTIMIZABLE!
+ // do the preg_match, capture all subpatterns for reformulation
+
+ // we don't support U+00A1 and up codepoints or
+ // escaping because I don't know how to do that with regexps
+ // and plus it would complicate optimization efforts (you never
+ // see that anyway).
+ $pattern = '/(?:(?<=\s)|\A)'. // look behind for space or string start
+ '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)'.
+ '(?:(?=\s)|\z)/'; // look ahead for space or string end
+ preg_match_all($pattern, $string, $matches);
+ return $matches[1];
+ }
+
+ /**
+ * Template method for removing certain tokens based on arbitrary criteria.
+ * @note If we wanted to be really functional, we'd do an array_filter
+ * with a callback. But... we're not.
+ */
+ protected function filter($tokens, $config, $context) {
+ return $tokens;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Pixels.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Pixels.php
new file mode 100644
index 000000000..4cb2c1b85
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/Pixels.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * Validates an integer representation of pixels according to the HTML spec.
+ */
+class HTMLPurifier_AttrDef_HTML_Pixels extends HTMLPurifier_AttrDef
+{
+
+ protected $max;
+
+ public function __construct($max = null) {
+ $this->max = $max;
+ }
+
+ public function validate($string, $config, $context) {
+
+ $string = trim($string);
+ if ($string === '0') return $string;
+ if ($string === '') return false;
+ $length = strlen($string);
+ if (substr($string, $length - 2) == 'px') {
+ $string = substr($string, 0, $length - 2);
+ }
+ if (!is_numeric($string)) return false;
+ $int = (int) $string;
+
+ if ($int < 0) return '0';
+
+ // upper-bound value, extremely high values can
+ // crash operating systems, see <http://ha.ckers.org/imagecrash.html>
+ // WARNING, above link WILL crash you if you're using Windows
+
+ if ($this->max !== null && $int > $this->max) return (string) $this->max;
+
+ return (string) $int;
+
+ }
+
+ public function make($string) {
+ if ($string === '') $max = null;
+ else $max = (int) $string;
+ $class = get_class($this);
+ return new $class($max);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Integer.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Integer.php
new file mode 100644
index 000000000..d59738d2a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Integer.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * Validates an integer.
+ * @note While this class was modeled off the CSS definition, no currently
+ * allowed CSS uses this type. The properties that do are: widows,
+ * orphans, z-index, counter-increment, counter-reset. Some of the
+ * HTML attributes, however, find use for a non-negative version of this.
+ */
+class HTMLPurifier_AttrDef_Integer extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Bool indicating whether or not negative values are allowed
+ */
+ protected $negative = true;
+
+ /**
+ * Bool indicating whether or not zero is allowed
+ */
+ protected $zero = true;
+
+ /**
+ * Bool indicating whether or not positive values are allowed
+ */
+ protected $positive = true;
+
+ /**
+ * @param $negative Bool indicating whether or not negative values are allowed
+ * @param $zero Bool indicating whether or not zero is allowed
+ * @param $positive Bool indicating whether or not positive values are allowed
+ */
+ public function __construct(
+ $negative = true, $zero = true, $positive = true
+ ) {
+ $this->negative = $negative;
+ $this->zero = $zero;
+ $this->positive = $positive;
+ }
+
+ public function validate($integer, $config, $context) {
+
+ $integer = $this->parseCDATA($integer);
+ if ($integer === '') return false;
+
+ // we could possibly simply typecast it to integer, but there are
+ // certain fringe cases that must not return an integer.
+
+ // clip leading sign
+ if ( $this->negative && $integer[0] === '-' ) {
+ $digits = substr($integer, 1);
+ if ($digits === '0') $integer = '0'; // rm minus sign for zero
+ } elseif( $this->positive && $integer[0] === '+' ) {
+ $digits = $integer = substr($integer, 1); // rm unnecessary plus
+ } else {
+ $digits = $integer;
+ }
+
+ // test if it's numeric
+ if (!ctype_digit($digits)) return false;
+
+ // perform scope tests
+ if (!$this->zero && $integer == 0) return false;
+ if (!$this->positive && $integer > 0) return false;
+ if (!$this->negative && $integer < 0) return false;
+
+ return $integer;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Lang.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Lang.php
new file mode 100644
index 000000000..10e6da56d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Lang.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * Validates the HTML attribute lang, effectively a language code.
+ * @note Built according to RFC 3066, which obsoleted RFC 1766
+ */
+class HTMLPurifier_AttrDef_Lang extends HTMLPurifier_AttrDef
+{
+
+ public function validate($string, $config, $context) {
+
+ $string = trim($string);
+ if (!$string) return false;
+
+ $subtags = explode('-', $string);
+ $num_subtags = count($subtags);
+
+ if ($num_subtags == 0) return false; // sanity check
+
+ // process primary subtag : $subtags[0]
+ $length = strlen($subtags[0]);
+ switch ($length) {
+ case 0:
+ return false;
+ case 1:
+ if (! ($subtags[0] == 'x' || $subtags[0] == 'i') ) {
+ return false;
+ }
+ break;
+ case 2:
+ case 3:
+ if (! ctype_alpha($subtags[0]) ) {
+ return false;
+ } elseif (! ctype_lower($subtags[0]) ) {
+ $subtags[0] = strtolower($subtags[0]);
+ }
+ break;
+ default:
+ return false;
+ }
+
+ $new_string = $subtags[0];
+ if ($num_subtags == 1) return $new_string;
+
+ // process second subtag : $subtags[1]
+ $length = strlen($subtags[1]);
+ if ($length == 0 || ($length == 1 && $subtags[1] != 'x') || $length > 8 || !ctype_alnum($subtags[1])) {
+ return $new_string;
+ }
+ if (!ctype_lower($subtags[1])) $subtags[1] = strtolower($subtags[1]);
+
+ $new_string .= '-' . $subtags[1];
+ if ($num_subtags == 2) return $new_string;
+
+ // process all other subtags, index 2 and up
+ for ($i = 2; $i < $num_subtags; $i++) {
+ $length = strlen($subtags[$i]);
+ if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) {
+ return $new_string;
+ }
+ if (!ctype_lower($subtags[$i])) {
+ $subtags[$i] = strtolower($subtags[$i]);
+ }
+ $new_string .= '-' . $subtags[$i];
+ }
+
+ return $new_string;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Switch.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Switch.php
new file mode 100644
index 000000000..c9e3ed193
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Switch.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * Decorator that, depending on a token, switches between two definitions.
+ */
+class HTMLPurifier_AttrDef_Switch
+{
+
+ protected $tag;
+ protected $withTag, $withoutTag;
+
+ /**
+ * @param string $tag Tag name to switch upon
+ * @param HTMLPurifier_AttrDef $with_tag Call if token matches tag
+ * @param HTMLPurifier_AttrDef $without_tag Call if token doesn't match, or there is no token
+ */
+ public function __construct($tag, $with_tag, $without_tag) {
+ $this->tag = $tag;
+ $this->withTag = $with_tag;
+ $this->withoutTag = $without_tag;
+ }
+
+ public function validate($string, $config, $context) {
+ $token = $context->get('CurrentToken', true);
+ if (!$token || $token->name !== $this->tag) {
+ return $this->withoutTag->validate($string, $config, $context);
+ } else {
+ return $this->withTag->validate($string, $config, $context);
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Text.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Text.php
new file mode 100644
index 000000000..c6216cc53
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/Text.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * Validates arbitrary text according to the HTML spec.
+ */
+class HTMLPurifier_AttrDef_Text extends HTMLPurifier_AttrDef
+{
+
+ public function validate($string, $config, $context) {
+ return $this->parseCDATA($string);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI.php
new file mode 100644
index 000000000..01a6d83e9
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * Validates a URI as defined by RFC 3986.
+ * @note Scheme-specific mechanics deferred to HTMLPurifier_URIScheme
+ */
+class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef
+{
+
+ protected $parser;
+ protected $embedsResource;
+
+ /**
+ * @param $embeds_resource_resource Does the URI here result in an extra HTTP request?
+ */
+ public function __construct($embeds_resource = false) {
+ $this->parser = new HTMLPurifier_URIParser();
+ $this->embedsResource = (bool) $embeds_resource;
+ }
+
+ public function make($string) {
+ $embeds = (bool) $string;
+ return new HTMLPurifier_AttrDef_URI($embeds);
+ }
+
+ public function validate($uri, $config, $context) {
+
+ if ($config->get('URI.Disable')) return false;
+
+ $uri = $this->parseCDATA($uri);
+
+ // parse the URI
+ $uri = $this->parser->parse($uri);
+ if ($uri === false) return false;
+
+ // add embedded flag to context for validators
+ $context->register('EmbeddedURI', $this->embedsResource);
+
+ $ok = false;
+ do {
+
+ // generic validation
+ $result = $uri->validate($config, $context);
+ if (!$result) break;
+
+ // chained filtering
+ $uri_def = $config->getDefinition('URI');
+ $result = $uri_def->filter($uri, $config, $context);
+ if (!$result) break;
+
+ // scheme-specific validation
+ $scheme_obj = $uri->getSchemeObj($config, $context);
+ if (!$scheme_obj) break;
+ if ($this->embedsResource && !$scheme_obj->browsable) break;
+ $result = $scheme_obj->validate($uri, $config, $context);
+ if (!$result) break;
+
+ // Post chained filtering
+ $result = $uri_def->postFilter($uri, $config, $context);
+ if (!$result) break;
+
+ // survived gauntlet
+ $ok = true;
+
+ } while (false);
+
+ $context->destroy('EmbeddedURI');
+ if (!$ok) return false;
+
+ // back to string
+ return $uri->toString();
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Email.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Email.php
new file mode 100644
index 000000000..bfee9d166
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Email.php
@@ -0,0 +1,17 @@
+<?php
+
+abstract class HTMLPurifier_AttrDef_URI_Email extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Unpacks a mailbox into its display-name and address
+ */
+ function unpack($string) {
+ // needs to be implemented
+ }
+
+}
+
+// sub-implementations
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php
new file mode 100644
index 000000000..94c715ab4
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * Primitive email validation class based on the regexp found at
+ * http://www.regular-expressions.info/email.html
+ */
+class HTMLPurifier_AttrDef_URI_Email_SimpleCheck extends HTMLPurifier_AttrDef_URI_Email
+{
+
+ public function validate($string, $config, $context) {
+ // no support for named mailboxes i.e. "Bob <bob@example.com>"
+ // that needs more percent encoding to be done
+ if ($string == '') return false;
+ $string = trim($string);
+ $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string);
+ return $result ? $string : false;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Host.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Host.php
new file mode 100644
index 000000000..2156c10c6
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Host.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Validates a host according to the IPv4, IPv6 and DNS (future) specifications.
+ */
+class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * Instance of HTMLPurifier_AttrDef_URI_IPv4 sub-validator
+ */
+ protected $ipv4;
+
+ /**
+ * Instance of HTMLPurifier_AttrDef_URI_IPv6 sub-validator
+ */
+ protected $ipv6;
+
+ public function __construct() {
+ $this->ipv4 = new HTMLPurifier_AttrDef_URI_IPv4();
+ $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6();
+ }
+
+ public function validate($string, $config, $context) {
+ $length = strlen($string);
+ if ($string === '') return '';
+ if ($length > 1 && $string[0] === '[' && $string[$length-1] === ']') {
+ //IPv6
+ $ip = substr($string, 1, $length - 2);
+ $valid = $this->ipv6->validate($ip, $config, $context);
+ if ($valid === false) return false;
+ return '['. $valid . ']';
+ }
+
+ // need to do checks on unusual encodings too
+ $ipv4 = $this->ipv4->validate($string, $config, $context);
+ if ($ipv4 !== false) return $ipv4;
+
+ // A regular domain name.
+
+ // This breaks I18N domain names, but we don't have proper IRI support,
+ // so force users to insert Punycode. If there's complaining we'll
+ // try to fix things into an international friendly form.
+
+ // The productions describing this are:
+ $a = '[a-z]'; // alpha
+ $an = '[a-z0-9]'; // alphanum
+ $and = '[a-z0-9-]'; // alphanum | "-"
+ // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+ $domainlabel = "$an($and*$an)?";
+ // toplabel = alpha | alpha *( alphanum | "-" ) alphanum
+ $toplabel = "$a($and*$an)?";
+ // hostname = *( domainlabel "." ) toplabel [ "." ]
+ $match = preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string);
+ if (!$match) return false;
+
+ return $string;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/IPv4.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/IPv4.php
new file mode 100644
index 000000000..ec4cf591b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/IPv4.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * Validates an IPv4 address
+ * @author Feyd @ forums.devnetwork.net (public domain)
+ */
+class HTMLPurifier_AttrDef_URI_IPv4 extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * IPv4 regex, protected so that IPv6 can reuse it
+ */
+ protected $ip4;
+
+ public function validate($aIP, $config, $context) {
+
+ if (!$this->ip4) $this->_loadRegex();
+
+ if (preg_match('#^' . $this->ip4 . '$#s', $aIP))
+ {
+ return $aIP;
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Lazy load function to prevent regex from being stuffed in
+ * cache.
+ */
+ protected function _loadRegex() {
+ $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255
+ $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})";
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/IPv6.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/IPv6.php
new file mode 100644
index 000000000..9454e9be5
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/IPv6.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * Validates an IPv6 address.
+ * @author Feyd @ forums.devnetwork.net (public domain)
+ * @note This function requires brackets to have been removed from address
+ * in URI.
+ */
+class HTMLPurifier_AttrDef_URI_IPv6 extends HTMLPurifier_AttrDef_URI_IPv4
+{
+
+ public function validate($aIP, $config, $context) {
+
+ if (!$this->ip4) $this->_loadRegex();
+
+ $original = $aIP;
+
+ $hex = '[0-9a-fA-F]';
+ $blk = '(?:' . $hex . '{1,4})';
+ $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128
+
+ // prefix check
+ if (strpos($aIP, '/') !== false)
+ {
+ if (preg_match('#' . $pre . '$#s', $aIP, $find))
+ {
+ $aIP = substr($aIP, 0, 0-strlen($find[0]));
+ unset($find);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ // IPv4-compatiblity check
+ if (preg_match('#(?<=:'.')' . $this->ip4 . '$#s', $aIP, $find))
+ {
+ $aIP = substr($aIP, 0, 0-strlen($find[0]));
+ $ip = explode('.', $find[0]);
+ $ip = array_map('dechex', $ip);
+ $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3];
+ unset($find, $ip);
+ }
+
+ // compression check
+ $aIP = explode('::', $aIP);
+ $c = count($aIP);
+ if ($c > 2)
+ {
+ return false;
+ }
+ elseif ($c == 2)
+ {
+ list($first, $second) = $aIP;
+ $first = explode(':', $first);
+ $second = explode(':', $second);
+
+ if (count($first) + count($second) > 8)
+ {
+ return false;
+ }
+
+ while(count($first) < 8)
+ {
+ array_push($first, '0');
+ }
+
+ array_splice($first, 8 - count($second), 8, $second);
+ $aIP = $first;
+ unset($first,$second);
+ }
+ else
+ {
+ $aIP = explode(':', $aIP[0]);
+ }
+ $c = count($aIP);
+
+ if ($c != 8)
+ {
+ return false;
+ }
+
+ // All the pieces should be 16-bit hex strings. Are they?
+ foreach ($aIP as $piece)
+ {
+ if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece)))
+ {
+ return false;
+ }
+ }
+
+ return $original;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform.php
new file mode 100644
index 000000000..e61d3e01b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * Processes an entire attribute array for corrections needing multiple values.
+ *
+ * Occasionally, a certain attribute will need to be removed and popped onto
+ * another value. Instead of creating a complex return syntax for
+ * HTMLPurifier_AttrDef, we just pass the whole attribute array to a
+ * specialized object and have that do the special work. That is the
+ * family of HTMLPurifier_AttrTransform.
+ *
+ * An attribute transformation can be assigned to run before or after
+ * HTMLPurifier_AttrDef validation. See HTMLPurifier_HTMLDefinition for
+ * more details.
+ */
+
+abstract class HTMLPurifier_AttrTransform
+{
+
+ /**
+ * Abstract: makes changes to the attributes dependent on multiple values.
+ *
+ * @param $attr Assoc array of attributes, usually from
+ * HTMLPurifier_Token_Tag::$attr
+ * @param $config Mandatory HTMLPurifier_Config object.
+ * @param $context Mandatory HTMLPurifier_Context object
+ * @returns Processed attribute array.
+ */
+ abstract public function transform($attr, $config, $context);
+
+ /**
+ * Prepends CSS properties to the style attribute, creating the
+ * attribute if it doesn't exist.
+ * @param $attr Attribute array to process (passed by reference)
+ * @param $css CSS to prepend
+ */
+ public function prependCSS(&$attr, $css) {
+ $attr['style'] = isset($attr['style']) ? $attr['style'] : '';
+ $attr['style'] = $css . $attr['style'];
+ }
+
+ /**
+ * Retrieves and removes an attribute
+ * @param $attr Attribute array to process (passed by reference)
+ * @param $key Key of attribute to confiscate
+ */
+ public function confiscateAttr(&$attr, $key) {
+ if (!isset($attr[$key])) return null;
+ $value = $attr[$key];
+ unset($attr[$key]);
+ return $value;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Background.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Background.php
new file mode 100644
index 000000000..0e1ff24a3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Background.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * Pre-transform that changes proprietary background attribute to CSS.
+ */
+class HTMLPurifier_AttrTransform_Background extends HTMLPurifier_AttrTransform {
+
+ public function transform($attr, $config, $context) {
+
+ if (!isset($attr['background'])) return $attr;
+
+ $background = $this->confiscateAttr($attr, 'background');
+ // some validation should happen here
+
+ $this->prependCSS($attr, "background-image:url($background);");
+
+ return $attr;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BdoDir.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BdoDir.php
new file mode 100644
index 000000000..4d1a05665
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BdoDir.php
@@ -0,0 +1,19 @@
+<?php
+
+// this MUST be placed in post, as it assumes that any value in dir is valid
+
+/**
+ * Post-trasnform that ensures that bdo tags have the dir attribute set.
+ */
+class HTMLPurifier_AttrTransform_BdoDir extends HTMLPurifier_AttrTransform
+{
+
+ public function transform($attr, $config, $context) {
+ if (isset($attr['dir'])) return $attr;
+ $attr['dir'] = $config->get('Attr.DefaultTextDir');
+ return $attr;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BgColor.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BgColor.php
new file mode 100644
index 000000000..ad3916bb9
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BgColor.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * Pre-transform that changes deprecated bgcolor attribute to CSS.
+ */
+class HTMLPurifier_AttrTransform_BgColor extends HTMLPurifier_AttrTransform {
+
+ public function transform($attr, $config, $context) {
+
+ if (!isset($attr['bgcolor'])) return $attr;
+
+ $bgcolor = $this->confiscateAttr($attr, 'bgcolor');
+ // some validation should happen here
+
+ $this->prependCSS($attr, "background-color:$bgcolor;");
+
+ return $attr;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BoolToCSS.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BoolToCSS.php
new file mode 100644
index 000000000..51159b671
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/BoolToCSS.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * Pre-transform that changes converts a boolean attribute to fixed CSS
+ */
+class HTMLPurifier_AttrTransform_BoolToCSS extends HTMLPurifier_AttrTransform {
+
+ /**
+ * Name of boolean attribute that is trigger
+ */
+ protected $attr;
+
+ /**
+ * CSS declarations to add to style, needs trailing semicolon
+ */
+ protected $css;
+
+ /**
+ * @param $attr string attribute name to convert from
+ * @param $css string CSS declarations to add to style (needs semicolon)
+ */
+ public function __construct($attr, $css) {
+ $this->attr = $attr;
+ $this->css = $css;
+ }
+
+ public function transform($attr, $config, $context) {
+ if (!isset($attr[$this->attr])) return $attr;
+ unset($attr[$this->attr]);
+ $this->prependCSS($attr, $this->css);
+ return $attr;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Border.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Border.php
new file mode 100644
index 000000000..476b0b079
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Border.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Pre-transform that changes deprecated border attribute to CSS.
+ */
+class HTMLPurifier_AttrTransform_Border extends HTMLPurifier_AttrTransform {
+
+ public function transform($attr, $config, $context) {
+ if (!isset($attr['border'])) return $attr;
+ $border_width = $this->confiscateAttr($attr, 'border');
+ // some validation should happen here
+ $this->prependCSS($attr, "border:{$border_width}px solid;");
+ return $attr;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/EnumToCSS.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/EnumToCSS.php
new file mode 100644
index 000000000..2a5b4514a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/EnumToCSS.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * Generic pre-transform that converts an attribute with a fixed number of
+ * values (enumerated) to CSS.
+ */
+class HTMLPurifier_AttrTransform_EnumToCSS extends HTMLPurifier_AttrTransform {
+
+ /**
+ * Name of attribute to transform from
+ */
+ protected $attr;
+
+ /**
+ * Lookup array of attribute values to CSS
+ */
+ protected $enumToCSS = array();
+
+ /**
+ * Case sensitivity of the matching
+ * @warning Currently can only be guaranteed to work with ASCII
+ * values.
+ */
+ protected $caseSensitive = false;
+
+ /**
+ * @param $attr String attribute name to transform from
+ * @param $enumToCSS Lookup array of attribute values to CSS
+ * @param $case_sensitive Boolean case sensitivity indicator, default false
+ */
+ public function __construct($attr, $enum_to_css, $case_sensitive = false) {
+ $this->attr = $attr;
+ $this->enumToCSS = $enum_to_css;
+ $this->caseSensitive = (bool) $case_sensitive;
+ }
+
+ public function transform($attr, $config, $context) {
+
+ if (!isset($attr[$this->attr])) return $attr;
+
+ $value = trim($attr[$this->attr]);
+ unset($attr[$this->attr]);
+
+ if (!$this->caseSensitive) $value = strtolower($value);
+
+ if (!isset($this->enumToCSS[$value])) {
+ return $attr;
+ }
+
+ $this->prependCSS($attr, $this->enumToCSS[$value]);
+
+ return $attr;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgRequired.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgRequired.php
new file mode 100644
index 000000000..a219479a0
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgRequired.php
@@ -0,0 +1,42 @@
+<?php
+
+// must be called POST validation
+
+/**
+ * Transform that supplies default values for the src and alt attributes
+ * in img tags, as well as prevents the img tag from being removed
+ * because of a missing alt tag. This needs to be registered as both
+ * a pre and post attribute transform.
+ */
+class HTMLPurifier_AttrTransform_ImgRequired extends HTMLPurifier_AttrTransform
+{
+
+ public function transform($attr, $config, $context) {
+
+ $src = true;
+ if (!isset($attr['src'])) {
+ if ($config->get('Core.RemoveInvalidImg')) return $attr;
+ $attr['src'] = $config->get('Attr.DefaultInvalidImage');
+ $src = false;
+ }
+
+ if (!isset($attr['alt'])) {
+ if ($src) {
+ $alt = $config->get('Attr.DefaultImageAlt');
+ if ($alt === null) {
+ $attr['alt'] = basename($attr['src']);
+ } else {
+ $attr['alt'] = $alt;
+ }
+ } else {
+ $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt');
+ }
+ }
+
+ return $attr;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgSpace.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgSpace.php
new file mode 100644
index 000000000..fd84c10c3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgSpace.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * Pre-transform that changes deprecated hspace and vspace attributes to CSS
+ */
+class HTMLPurifier_AttrTransform_ImgSpace extends HTMLPurifier_AttrTransform {
+
+ protected $attr;
+ protected $css = array(
+ 'hspace' => array('left', 'right'),
+ 'vspace' => array('top', 'bottom')
+ );
+
+ public function __construct($attr) {
+ $this->attr = $attr;
+ if (!isset($this->css[$attr])) {
+ trigger_error(htmlspecialchars($attr) . ' is not valid space attribute');
+ }
+ }
+
+ public function transform($attr, $config, $context) {
+
+ if (!isset($attr[$this->attr])) return $attr;
+
+ $width = $this->confiscateAttr($attr, $this->attr);
+ // some validation could happen here
+
+ if (!isset($this->css[$this->attr])) return $attr;
+
+ $style = '';
+ foreach ($this->css[$this->attr] as $suffix) {
+ $property = "margin-$suffix";
+ $style .= "$property:{$width}px;";
+ }
+
+ $this->prependCSS($attr, $style);
+
+ return $attr;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Input.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Input.php
new file mode 100644
index 000000000..16829552d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Input.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Performs miscellaneous cross attribute validation and filtering for
+ * input elements. This is meant to be a post-transform.
+ */
+class HTMLPurifier_AttrTransform_Input extends HTMLPurifier_AttrTransform {
+
+ protected $pixels;
+
+ public function __construct() {
+ $this->pixels = new HTMLPurifier_AttrDef_HTML_Pixels();
+ }
+
+ public function transform($attr, $config, $context) {
+ if (!isset($attr['type'])) $t = 'text';
+ else $t = strtolower($attr['type']);
+ if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') {
+ unset($attr['checked']);
+ }
+ if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') {
+ unset($attr['maxlength']);
+ }
+ if (isset($attr['size']) && $t !== 'text' && $t !== 'password') {
+ $result = $this->pixels->validate($attr['size'], $config, $context);
+ if ($result === false) unset($attr['size']);
+ else $attr['size'] = $result;
+ }
+ if (isset($attr['src']) && $t !== 'image') {
+ unset($attr['src']);
+ }
+ if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) {
+ $attr['value'] = '';
+ }
+ return $attr;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Lang.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Lang.php
new file mode 100644
index 000000000..5869e7f82
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Lang.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * Post-transform that copies lang's value to xml:lang (and vice-versa)
+ * @note Theoretically speaking, this could be a pre-transform, but putting
+ * post is more efficient.
+ */
+class HTMLPurifier_AttrTransform_Lang extends HTMLPurifier_AttrTransform
+{
+
+ public function transform($attr, $config, $context) {
+
+ $lang = isset($attr['lang']) ? $attr['lang'] : false;
+ $xml_lang = isset($attr['xml:lang']) ? $attr['xml:lang'] : false;
+
+ if ($lang !== false && $xml_lang === false) {
+ $attr['xml:lang'] = $lang;
+ } elseif ($xml_lang !== false) {
+ $attr['lang'] = $xml_lang;
+ }
+
+ return $attr;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Length.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Length.php
new file mode 100644
index 000000000..ea2f30473
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Length.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * Class for handling width/height length attribute transformations to CSS
+ */
+class HTMLPurifier_AttrTransform_Length extends HTMLPurifier_AttrTransform
+{
+
+ protected $name;
+ protected $cssName;
+
+ public function __construct($name, $css_name = null) {
+ $this->name = $name;
+ $this->cssName = $css_name ? $css_name : $name;
+ }
+
+ public function transform($attr, $config, $context) {
+ if (!isset($attr[$this->name])) return $attr;
+ $length = $this->confiscateAttr($attr, $this->name);
+ if(ctype_digit($length)) $length .= 'px';
+ $this->prependCSS($attr, $this->cssName . ":$length;");
+ return $attr;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Name.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Name.php
new file mode 100644
index 000000000..15315bc73
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Name.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * Pre-transform that changes deprecated name attribute to ID if necessary
+ */
+class HTMLPurifier_AttrTransform_Name extends HTMLPurifier_AttrTransform
+{
+
+ public function transform($attr, $config, $context) {
+ // Abort early if we're using relaxed definition of name
+ if ($config->get('HTML.Attr.Name.UseCDATA')) return $attr;
+ if (!isset($attr['name'])) return $attr;
+ $id = $this->confiscateAttr($attr, 'name');
+ if ( isset($attr['id'])) return $attr;
+ $attr['id'] = $id;
+ return $attr;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/NameSync.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/NameSync.php
new file mode 100644
index 000000000..a95638c14
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/NameSync.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * Post-transform that performs validation to the name attribute; if
+ * it is present with an equivalent id attribute, it is passed through;
+ * otherwise validation is performed.
+ */
+class HTMLPurifier_AttrTransform_NameSync extends HTMLPurifier_AttrTransform
+{
+
+ public function __construct() {
+ $this->idDef = new HTMLPurifier_AttrDef_HTML_ID();
+ }
+
+ public function transform($attr, $config, $context) {
+ if (!isset($attr['name'])) return $attr;
+ $name = $attr['name'];
+ if (isset($attr['id']) && $attr['id'] === $name) return $attr;
+ $result = $this->idDef->validate($name, $config, $context);
+ if ($result === false) unset($attr['name']);
+ else $attr['name'] = $result;
+ return $attr;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeEmbed.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeEmbed.php
new file mode 100644
index 000000000..4da449981
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeEmbed.php
@@ -0,0 +1,15 @@
+<?php
+
+class HTMLPurifier_AttrTransform_SafeEmbed extends HTMLPurifier_AttrTransform
+{
+ public $name = "SafeEmbed";
+
+ public function transform($attr, $config, $context) {
+ $attr['allowscriptaccess'] = 'never';
+ $attr['allownetworking'] = 'internal';
+ $attr['type'] = 'application/x-shockwave-flash';
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeObject.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeObject.php
new file mode 100644
index 000000000..1ed74898b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeObject.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * Writes default type for all objects. Currently only supports flash.
+ */
+class HTMLPurifier_AttrTransform_SafeObject extends HTMLPurifier_AttrTransform
+{
+ public $name = "SafeObject";
+
+ function transform($attr, $config, $context) {
+ if (!isset($attr['type'])) $attr['type'] = 'application/x-shockwave-flash';
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeParam.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeParam.php
new file mode 100644
index 000000000..94e8052a9
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/SafeParam.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * Validates name/value pairs in param tags to be used in safe objects. This
+ * will only allow name values it recognizes, and pre-fill certain attributes
+ * with required values.
+ *
+ * @note
+ * This class only supports Flash. In the future, Quicktime support
+ * may be added.
+ *
+ * @warning
+ * This class expects an injector to add the necessary parameters tags.
+ */
+class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform
+{
+ public $name = "SafeParam";
+ private $uri;
+
+ public function __construct() {
+ $this->uri = new HTMLPurifier_AttrDef_URI(true); // embedded
+ }
+
+ public function transform($attr, $config, $context) {
+ // If we add support for other objects, we'll need to alter the
+ // transforms.
+ switch ($attr['name']) {
+ // application/x-shockwave-flash
+ // Keep this synchronized with Injector/SafeObject.php
+ case 'allowScriptAccess':
+ $attr['value'] = 'never';
+ break;
+ case 'allowNetworking':
+ $attr['value'] = 'internal';
+ break;
+ case 'wmode':
+ $attr['value'] = 'window';
+ break;
+ case 'movie':
+ $attr['value'] = $this->uri->validate($attr['value'], $config, $context);
+ break;
+ // add other cases to support other param name/value pairs
+ default:
+ $attr['name'] = $attr['value'] = null;
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ScriptRequired.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ScriptRequired.php
new file mode 100644
index 000000000..4499050a2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ScriptRequired.php
@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * Implements required attribute stipulation for <script>
+ */
+class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform
+{
+ public function transform($attr, $config, $context) {
+ if (!isset($attr['type'])) {
+ $attr['type'] = 'text/javascript';
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Textarea.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Textarea.php
new file mode 100644
index 000000000..81ac3488b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/Textarea.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Sets height/width defaults for <textarea>
+ */
+class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform
+{
+
+ public function transform($attr, $config, $context) {
+ // Calculated from Firefox
+ if (!isset($attr['cols'])) $attr['cols'] = '22';
+ if (!isset($attr['rows'])) $attr['rows'] = '3';
+ return $attr;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTypes.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTypes.php
new file mode 100644
index 000000000..fc2ea4e58
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTypes.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * Provides lookup array of attribute types to HTMLPurifier_AttrDef objects
+ */
+class HTMLPurifier_AttrTypes
+{
+ /**
+ * Lookup array of attribute string identifiers to concrete implementations
+ */
+ protected $info = array();
+
+ /**
+ * Constructs the info array, supplying default implementations for attribute
+ * types.
+ */
+ public function __construct() {
+ // pseudo-types, must be instantiated via shorthand
+ $this->info['Enum'] = new HTMLPurifier_AttrDef_Enum();
+ $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool();
+
+ $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID();
+ $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length();
+ $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength();
+ $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens();
+ $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels();
+ $this->info['Text'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['URI'] = new HTMLPurifier_AttrDef_URI();
+ $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang();
+ $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color();
+
+ // unimplemented aliases
+ $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['Character'] = new HTMLPurifier_AttrDef_Text();
+
+ // "proprietary" types
+ $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class();
+
+ // number is really a positive integer (one or more digits)
+ // FIXME: ^^ not always, see start and value of list items
+ $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true);
+ }
+
+ /**
+ * Retrieves a type
+ * @param $type String type name
+ * @return Object AttrDef for type
+ */
+ public function get($type) {
+
+ // determine if there is any extra info tacked on
+ if (strpos($type, '#') !== false) list($type, $string) = explode('#', $type, 2);
+ else $string = '';
+
+ if (!isset($this->info[$type])) {
+ trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR);
+ return;
+ }
+
+ return $this->info[$type]->make($string);
+
+ }
+
+ /**
+ * Sets a new implementation for a type
+ * @param $type String type name
+ * @param $impl Object AttrDef for type
+ */
+ public function set($type, $impl) {
+ $this->info[$type] = $impl;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrValidator.php b/extlib/HTMLPurifier/HTMLPurifier/AttrValidator.php
new file mode 100644
index 000000000..829a0f8f2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/AttrValidator.php
@@ -0,0 +1,162 @@
+<?php
+
+/**
+ * Validates the attributes of a token. Doesn't manage required attributes
+ * very well. The only reason we factored this out was because RemoveForeignElements
+ * also needed it besides ValidateAttributes.
+ */
+class HTMLPurifier_AttrValidator
+{
+
+ /**
+ * Validates the attributes of a token, returning a modified token
+ * that has valid tokens
+ * @param $token Reference to token to validate. We require a reference
+ * because the operation this class performs on the token are
+ * not atomic, so the context CurrentToken to be updated
+ * throughout
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ */
+ public function validateToken(&$token, &$config, $context) {
+
+ $definition = $config->getHTMLDefinition();
+ $e =& $context->get('ErrorCollector', true);
+
+ // initialize IDAccumulator if necessary
+ $ok =& $context->get('IDAccumulator', true);
+ if (!$ok) {
+ $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
+ $context->register('IDAccumulator', $id_accumulator);
+ }
+
+ // initialize CurrentToken if necessary
+ $current_token =& $context->get('CurrentToken', true);
+ if (!$current_token) $context->register('CurrentToken', $token);
+
+ if (
+ !$token instanceof HTMLPurifier_Token_Start &&
+ !$token instanceof HTMLPurifier_Token_Empty
+ ) return $token;
+
+ // create alias to global definition array, see also $defs
+ // DEFINITION CALL
+ $d_defs = $definition->info_global_attr;
+
+ // don't update token until the very end, to ensure an atomic update
+ $attr = $token->attr;
+
+ // do global transformations (pre)
+ // nothing currently utilizes this
+ foreach ($definition->info_attr_transform_pre as $transform) {
+ $attr = $transform->transform($o = $attr, $config, $context);
+ if ($e) {
+ if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
+ }
+ }
+
+ // do local transformations only applicable to this element (pre)
+ // ex. <p align="right"> to <p style="text-align:right;">
+ foreach ($definition->info[$token->name]->attr_transform_pre as $transform) {
+ $attr = $transform->transform($o = $attr, $config, $context);
+ if ($e) {
+ if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
+ }
+ }
+
+ // create alias to this element's attribute definition array, see
+ // also $d_defs (global attribute definition array)
+ // DEFINITION CALL
+ $defs = $definition->info[$token->name]->attr;
+
+ $attr_key = false;
+ $context->register('CurrentAttr', $attr_key);
+
+ // iterate through all the attribute keypairs
+ // Watch out for name collisions: $key has previously been used
+ foreach ($attr as $attr_key => $value) {
+
+ // call the definition
+ if ( isset($defs[$attr_key]) ) {
+ // there is a local definition defined
+ if ($defs[$attr_key] === false) {
+ // We've explicitly been told not to allow this element.
+ // This is usually when there's a global definition
+ // that must be overridden.
+ // Theoretically speaking, we could have a
+ // AttrDef_DenyAll, but this is faster!
+ $result = false;
+ } else {
+ // validate according to the element's definition
+ $result = $defs[$attr_key]->validate(
+ $value, $config, $context
+ );
+ }
+ } elseif ( isset($d_defs[$attr_key]) ) {
+ // there is a global definition defined, validate according
+ // to the global definition
+ $result = $d_defs[$attr_key]->validate(
+ $value, $config, $context
+ );
+ } else {
+ // system never heard of the attribute? DELETE!
+ $result = false;
+ }
+
+ // put the results into effect
+ if ($result === false || $result === null) {
+ // this is a generic error message that should replaced
+ // with more specific ones when possible
+ if ($e) $e->send(E_ERROR, 'AttrValidator: Attribute removed');
+
+ // remove the attribute
+ unset($attr[$attr_key]);
+ } elseif (is_string($result)) {
+ // generally, if a substitution is happening, there
+ // was some sort of implicit correction going on. We'll
+ // delegate it to the attribute classes to say exactly what.
+
+ // simple substitution
+ $attr[$attr_key] = $result;
+ } else {
+ // nothing happens
+ }
+
+ // we'd also want slightly more complicated substitution
+ // involving an array as the return value,
+ // although we're not sure how colliding attributes would
+ // resolve (certain ones would be completely overriden,
+ // others would prepend themselves).
+ }
+
+ $context->destroy('CurrentAttr');
+
+ // post transforms
+
+ // global (error reporting untested)
+ foreach ($definition->info_attr_transform_post as $transform) {
+ $attr = $transform->transform($o = $attr, $config, $context);
+ if ($e) {
+ if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
+ }
+ }
+
+ // local (error reporting untested)
+ foreach ($definition->info[$token->name]->attr_transform_post as $transform) {
+ $attr = $transform->transform($o = $attr, $config, $context);
+ if ($e) {
+ if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
+ }
+ }
+
+ $token->attr = $attr;
+
+ // destroy CurrentToken if we made it ourselves
+ if (!$current_token) $context->destroy('CurrentToken');
+
+ }
+
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Bootstrap.php b/extlib/HTMLPurifier/HTMLPurifier/Bootstrap.php
new file mode 100644
index 000000000..559f61a23
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Bootstrap.php
@@ -0,0 +1,98 @@
+<?php
+
+// constants are slow, so we use as few as possible
+if (!defined('HTMLPURIFIER_PREFIX')) {
+ define('HTMLPURIFIER_PREFIX', realpath(dirname(__FILE__) . '/..'));
+}
+
+// accomodations for versions earlier than 5.0.2
+// borrowed from PHP_Compat, LGPL licensed, by Aidan Lister <aidan@php.net>
+if (!defined('PHP_EOL')) {
+ switch (strtoupper(substr(PHP_OS, 0, 3))) {
+ case 'WIN':
+ define('PHP_EOL', "\r\n");
+ break;
+ case 'DAR':
+ define('PHP_EOL', "\r");
+ break;
+ default:
+ define('PHP_EOL', "\n");
+ }
+}
+
+/**
+ * Bootstrap class that contains meta-functionality for HTML Purifier such as
+ * the autoload function.
+ *
+ * @note
+ * This class may be used without any other files from HTML Purifier.
+ */
+class HTMLPurifier_Bootstrap
+{
+
+ /**
+ * Autoload function for HTML Purifier
+ * @param $class Class to load
+ */
+ public static function autoload($class) {
+ $file = HTMLPurifier_Bootstrap::getPath($class);
+ if (!$file) return false;
+ require HTMLPURIFIER_PREFIX . '/' . $file;
+ return true;
+ }
+
+ /**
+ * Returns the path for a specific class.
+ */
+ public static function getPath($class) {
+ if (strncmp('HTMLPurifier', $class, 12) !== 0) return false;
+ // Custom implementations
+ if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) {
+ $code = str_replace('_', '-', substr($class, 22));
+ $file = 'HTMLPurifier/Language/classes/' . $code . '.php';
+ } else {
+ $file = str_replace('_', '/', $class) . '.php';
+ }
+ if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) return false;
+ return $file;
+ }
+
+ /**
+ * "Pre-registers" our autoloader on the SPL stack.
+ */
+ public static function registerAutoload() {
+ $autoload = array('HTMLPurifier_Bootstrap', 'autoload');
+ if ( ($funcs = spl_autoload_functions()) === false ) {
+ spl_autoload_register($autoload);
+ } elseif (function_exists('spl_autoload_unregister')) {
+ $compat = version_compare(PHP_VERSION, '5.1.2', '<=') &&
+ version_compare(PHP_VERSION, '5.1.0', '>=');
+ foreach ($funcs as $func) {
+ if (is_array($func)) {
+ // :TRICKY: There are some compatibility issues and some
+ // places where we need to error out
+ $reflector = new ReflectionMethod($func[0], $func[1]);
+ if (!$reflector->isStatic()) {
+ throw new Exception('
+ HTML Purifier autoloader registrar is not compatible
+ with non-static object methods due to PHP Bug #44144;
+ Please do not use HTMLPurifier.autoload.php (or any
+ file that includes this file); instead, place the code:
+ spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\'))
+ after your own autoloaders.
+ ');
+ }
+ // Suprisingly, spl_autoload_register supports the
+ // Class::staticMethod callback format, although call_user_func doesn't
+ if ($compat) $func = implode('::', $func);
+ }
+ spl_autoload_unregister($func);
+ }
+ spl_autoload_register($autoload);
+ foreach ($funcs as $func) spl_autoload_register($func);
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/CSSDefinition.php b/extlib/HTMLPurifier/HTMLPurifier/CSSDefinition.php
new file mode 100644
index 000000000..6a2e6f56d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/CSSDefinition.php
@@ -0,0 +1,292 @@
+<?php
+
+/**
+ * Defines allowed CSS attributes and what their values are.
+ * @see HTMLPurifier_HTMLDefinition
+ */
+class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
+{
+
+ public $type = 'CSS';
+
+ /**
+ * Assoc array of attribute name to definition object.
+ */
+ public $info = array();
+
+ /**
+ * Constructs the info array. The meat of this class.
+ */
+ protected function doSetup($config) {
+
+ $this->info['text-align'] = new HTMLPurifier_AttrDef_Enum(
+ array('left', 'right', 'center', 'justify'), false);
+
+ $border_style =
+ $this->info['border-bottom-style'] =
+ $this->info['border-right-style'] =
+ $this->info['border-left-style'] =
+ $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum(
+ array('none', 'hidden', 'dotted', 'dashed', 'solid', 'double',
+ 'groove', 'ridge', 'inset', 'outset'), false);
+
+ $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style);
+
+ $this->info['clear'] = new HTMLPurifier_AttrDef_Enum(
+ array('none', 'left', 'right', 'both'), false);
+ $this->info['float'] = new HTMLPurifier_AttrDef_Enum(
+ array('none', 'left', 'right'), false);
+ $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum(
+ array('normal', 'italic', 'oblique'), false);
+ $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum(
+ array('normal', 'small-caps'), false);
+
+ $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite(
+ array(
+ new HTMLPurifier_AttrDef_Enum(array('none')),
+ new HTMLPurifier_AttrDef_CSS_URI()
+ )
+ );
+
+ $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum(
+ array('inside', 'outside'), false);
+ $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum(
+ array('disc', 'circle', 'square', 'decimal', 'lower-roman',
+ 'upper-roman', 'lower-alpha', 'upper-alpha', 'none'), false);
+ $this->info['list-style-image'] = $uri_or_none;
+
+ $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config);
+
+ $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum(
+ array('capitalize', 'uppercase', 'lowercase', 'none'), false);
+ $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color();
+
+ $this->info['background-image'] = $uri_or_none;
+ $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum(
+ array('repeat', 'repeat-x', 'repeat-y', 'no-repeat')
+ );
+ $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum(
+ array('scroll', 'fixed')
+ );
+ $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition();
+
+ $border_color =
+ $this->info['border-top-color'] =
+ $this->info['border-bottom-color'] =
+ $this->info['border-left-color'] =
+ $this->info['border-right-color'] =
+ $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_Enum(array('transparent')),
+ new HTMLPurifier_AttrDef_CSS_Color()
+ ));
+
+ $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config);
+
+ $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color);
+
+ $border_width =
+ $this->info['border-top-width'] =
+ $this->info['border-bottom-width'] =
+ $this->info['border-left-width'] =
+ $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')),
+ new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative
+ ));
+
+ $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width);
+
+ $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_Enum(array('normal')),
+ new HTMLPurifier_AttrDef_CSS_Length()
+ ));
+
+ $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_Enum(array('normal')),
+ new HTMLPurifier_AttrDef_CSS_Length()
+ ));
+
+ $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_Enum(array('xx-small', 'x-small',
+ 'small', 'medium', 'large', 'x-large', 'xx-large',
+ 'larger', 'smaller')),
+ new HTMLPurifier_AttrDef_CSS_Percentage(),
+ new HTMLPurifier_AttrDef_CSS_Length()
+ ));
+
+ $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_Enum(array('normal')),
+ new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives
+ new HTMLPurifier_AttrDef_CSS_Length('0'),
+ new HTMLPurifier_AttrDef_CSS_Percentage(true)
+ ));
+
+ $margin =
+ $this->info['margin-top'] =
+ $this->info['margin-bottom'] =
+ $this->info['margin-left'] =
+ $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_CSS_Length(),
+ new HTMLPurifier_AttrDef_CSS_Percentage(),
+ new HTMLPurifier_AttrDef_Enum(array('auto'))
+ ));
+
+ $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin);
+
+ // non-negative
+ $padding =
+ $this->info['padding-top'] =
+ $this->info['padding-bottom'] =
+ $this->info['padding-left'] =
+ $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_CSS_Length('0'),
+ new HTMLPurifier_AttrDef_CSS_Percentage(true)
+ ));
+
+ $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding);
+
+ $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_CSS_Length(),
+ new HTMLPurifier_AttrDef_CSS_Percentage()
+ ));
+
+ $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_CSS_Length('0'),
+ new HTMLPurifier_AttrDef_CSS_Percentage(true),
+ new HTMLPurifier_AttrDef_Enum(array('auto'))
+ ));
+ $max = $config->get('CSS.MaxImgLength');
+
+ $this->info['width'] =
+ $this->info['height'] =
+ $max === null ?
+ $trusted_wh :
+ new HTMLPurifier_AttrDef_Switch('img',
+ // For img tags:
+ new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_CSS_Length('0', $max),
+ new HTMLPurifier_AttrDef_Enum(array('auto'))
+ )),
+ // For everyone else:
+ $trusted_wh
+ );
+
+ $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration();
+
+ $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily();
+
+ // this could use specialized code
+ $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum(
+ array('normal', 'bold', 'bolder', 'lighter', '100', '200', '300',
+ '400', '500', '600', '700', '800', '900'), false);
+
+ // MUST be called after other font properties, as it references
+ // a CSSDefinition object
+ $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config);
+
+ // same here
+ $this->info['border'] =
+ $this->info['border-bottom'] =
+ $this->info['border-top'] =
+ $this->info['border-left'] =
+ $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config);
+
+ $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum(array(
+ 'collapse', 'separate'));
+
+ $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum(array(
+ 'top', 'bottom'));
+
+ $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum(array(
+ 'auto', 'fixed'));
+
+ $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite(array(
+ new HTMLPurifier_AttrDef_Enum(array('baseline', 'sub', 'super',
+ 'top', 'text-top', 'middle', 'bottom', 'text-bottom')),
+ new HTMLPurifier_AttrDef_CSS_Length(),
+ new HTMLPurifier_AttrDef_CSS_Percentage()
+ ));
+
+ $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2);
+
+ // partial support
+ $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum(array('nowrap'));
+
+ if ($config->get('CSS.Proprietary')) {
+ $this->doSetupProprietary($config);
+ }
+
+ if ($config->get('CSS.AllowTricky')) {
+ $this->doSetupTricky($config);
+ }
+
+ $allow_important = $config->get('CSS.AllowImportant');
+ // wrap all attr-defs with decorator that handles !important
+ foreach ($this->info as $k => $v) {
+ $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important);
+ }
+
+ $this->setupConfigStuff($config);
+ }
+
+ protected function doSetupProprietary($config) {
+ // Internet Explorer only scrollbar colors
+ $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+
+ // technically not proprietary, but CSS3, and no one supports it
+ $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
+ $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
+ $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
+
+ // only opacity, for now
+ $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter();
+
+ }
+
+ protected function doSetupTricky($config) {
+ $this->info['display'] = new HTMLPurifier_AttrDef_Enum(array(
+ 'inline', 'block', 'list-item', 'run-in', 'compact',
+ 'marker', 'table', 'inline-table', 'table-row-group',
+ 'table-header-group', 'table-footer-group', 'table-row',
+ 'table-column-group', 'table-column', 'table-cell', 'table-caption', 'none'
+ ));
+ $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum(array(
+ 'visible', 'hidden', 'collapse'
+ ));
+ $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll'));
+ }
+
+
+ /**
+ * Performs extra config-based processing. Based off of
+ * HTMLPurifier_HTMLDefinition.
+ * @todo Refactor duplicate elements into common class (probably using
+ * composition, not inheritance).
+ */
+ protected function setupConfigStuff($config) {
+
+ // setup allowed elements
+ $support = "(for information on implementing this, see the ".
+ "support forums) ";
+ $allowed_attributes = $config->get('CSS.AllowedProperties');
+ if ($allowed_attributes !== null) {
+ foreach ($this->info as $name => $d) {
+ if(!isset($allowed_attributes[$name])) unset($this->info[$name]);
+ unset($allowed_attributes[$name]);
+ }
+ // emit errors
+ foreach ($allowed_attributes as $name => $d) {
+ // :TODO: Is this htmlspecialchars() call really necessary?
+ $name = htmlspecialchars($name);
+ trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING);
+ }
+ }
+
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef.php
new file mode 100644
index 000000000..c5d5216da
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * Defines allowed child nodes and validates tokens against it.
+ */
+abstract class HTMLPurifier_ChildDef
+{
+ /**
+ * Type of child definition, usually right-most part of class name lowercase.
+ * Used occasionally in terms of context.
+ */
+ public $type;
+
+ /**
+ * Bool that indicates whether or not an empty array of children is okay
+ *
+ * This is necessary for redundant checking when changes affecting
+ * a child node may cause a parent node to now be disallowed.
+ */
+ public $allow_empty;
+
+ /**
+ * Lookup array of all elements that this definition could possibly allow
+ */
+ public $elements = array();
+
+ /**
+ * Get lookup of tag names that should not close this element automatically.
+ * All other elements will do so.
+ */
+ public function getAllowedElements($config) {
+ return $this->elements;
+ }
+
+ /**
+ * Validates nodes according to definition and returns modification.
+ *
+ * @param $tokens_of_children Array of HTMLPurifier_Token
+ * @param $config HTMLPurifier_Config object
+ * @param $context HTMLPurifier_Context object
+ * @return bool true to leave nodes as is
+ * @return bool false to remove parent node
+ * @return array of replacement child tokens
+ */
+ abstract public function validateChildren($tokens_of_children, $config, $context);
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Chameleon.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Chameleon.php
new file mode 100644
index 000000000..15c364ee3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Chameleon.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * Definition that uses different definitions depending on context.
+ *
+ * The del and ins tags are notable because they allow different types of
+ * elements depending on whether or not they're in a block or inline context.
+ * Chameleon allows this behavior to happen by using two different
+ * definitions depending on context. While this somewhat generalized,
+ * it is specifically intended for those two tags.
+ */
+class HTMLPurifier_ChildDef_Chameleon extends HTMLPurifier_ChildDef
+{
+
+ /**
+ * Instance of the definition object to use when inline. Usually stricter.
+ */
+ public $inline;
+
+ /**
+ * Instance of the definition object to use when block.
+ */
+ public $block;
+
+ public $type = 'chameleon';
+
+ /**
+ * @param $inline List of elements to allow when inline.
+ * @param $block List of elements to allow when block.
+ */
+ public function __construct($inline, $block) {
+ $this->inline = new HTMLPurifier_ChildDef_Optional($inline);
+ $this->block = new HTMLPurifier_ChildDef_Optional($block);
+ $this->elements = $this->block->elements;
+ }
+
+ public function validateChildren($tokens_of_children, $config, $context) {
+ if ($context->get('IsInline') === false) {
+ return $this->block->validateChildren(
+ $tokens_of_children, $config, $context);
+ } else {
+ return $this->inline->validateChildren(
+ $tokens_of_children, $config, $context);
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Custom.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Custom.php
new file mode 100644
index 000000000..b68047b4b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Custom.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * Custom validation class, accepts DTD child definitions
+ *
+ * @warning Currently this class is an all or nothing proposition, that is,
+ * it will only give a bool return value.
+ */
+class HTMLPurifier_ChildDef_Custom extends HTMLPurifier_ChildDef
+{
+ public $type = 'custom';
+ public $allow_empty = false;
+ /**
+ * Allowed child pattern as defined by the DTD
+ */
+ public $dtd_regex;
+ /**
+ * PCRE regex derived from $dtd_regex
+ * @private
+ */
+ private $_pcre_regex;
+ /**
+ * @param $dtd_regex Allowed child pattern from the DTD
+ */
+ public function __construct($dtd_regex) {
+ $this->dtd_regex = $dtd_regex;
+ $this->_compileRegex();
+ }
+ /**
+ * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex)
+ */
+ protected function _compileRegex() {
+ $raw = str_replace(' ', '', $this->dtd_regex);
+ if ($raw{0} != '(') {
+ $raw = "($raw)";
+ }
+ $el = '[#a-zA-Z0-9_.-]+';
+ $reg = $raw;
+
+ // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M
+ // DOING! Seriously: if there's problems, please report them.
+
+ // collect all elements into the $elements array
+ preg_match_all("/$el/", $reg, $matches);
+ foreach ($matches[0] as $match) {
+ $this->elements[$match] = true;
+ }
+
+ // setup all elements as parentheticals with leading commas
+ $reg = preg_replace("/$el/", '(,\\0)', $reg);
+
+ // remove commas when they were not solicited
+ $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg);
+
+ // remove all non-paranthetical commas: they are handled by first regex
+ $reg = preg_replace("/,\(/", '(', $reg);
+
+ $this->_pcre_regex = $reg;
+ }
+ public function validateChildren($tokens_of_children, $config, $context) {
+ $list_of_children = '';
+ $nesting = 0; // depth into the nest
+ foreach ($tokens_of_children as $token) {
+ if (!empty($token->is_whitespace)) continue;
+
+ $is_child = ($nesting == 0); // direct
+
+ if ($token instanceof HTMLPurifier_Token_Start) {
+ $nesting++;
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ $nesting--;
+ }
+
+ if ($is_child) {
+ $list_of_children .= $token->name . ',';
+ }
+ }
+ // add leading comma to deal with stray comma declarations
+ $list_of_children = ',' . rtrim($list_of_children, ',');
+ $okay =
+ preg_match(
+ '/^,?'.$this->_pcre_regex.'$/',
+ $list_of_children
+ );
+
+ return (bool) $okay;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Empty.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Empty.php
new file mode 100644
index 000000000..13171f665
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Empty.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * Definition that disallows all elements.
+ * @warning validateChildren() in this class is actually never called, because
+ * empty elements are corrected in HTMLPurifier_Strategy_MakeWellFormed
+ * before child definitions are parsed in earnest by
+ * HTMLPurifier_Strategy_FixNesting.
+ */
+class HTMLPurifier_ChildDef_Empty extends HTMLPurifier_ChildDef
+{
+ public $allow_empty = true;
+ public $type = 'empty';
+ public function __construct() {}
+ public function validateChildren($tokens_of_children, $config, $context) {
+ return array();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Optional.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Optional.php
new file mode 100644
index 000000000..32bcb9898
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Optional.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * Definition that allows a set of elements, and allows no children.
+ * @note This is a hack to reuse code from HTMLPurifier_ChildDef_Required,
+ * really, one shouldn't inherit from the other. Only altered behavior
+ * is to overload a returned false with an array. Thus, it will never
+ * return false.
+ */
+class HTMLPurifier_ChildDef_Optional extends HTMLPurifier_ChildDef_Required
+{
+ public $allow_empty = true;
+ public $type = 'optional';
+ public function validateChildren($tokens_of_children, $config, $context) {
+ $result = parent::validateChildren($tokens_of_children, $config, $context);
+ // we assume that $tokens_of_children is not modified
+ if ($result === false) {
+ if (empty($tokens_of_children)) return true;
+ elseif ($this->whitespace) return $tokens_of_children;
+ else return array();
+ }
+ return $result;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Required.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Required.php
new file mode 100644
index 000000000..4889f249b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Required.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * Definition that allows a set of elements, but disallows empty children.
+ */
+class HTMLPurifier_ChildDef_Required extends HTMLPurifier_ChildDef
+{
+ /**
+ * Lookup table of allowed elements.
+ * @public
+ */
+ public $elements = array();
+ /**
+ * Whether or not the last passed node was all whitespace.
+ */
+ protected $whitespace = false;
+ /**
+ * @param $elements List of allowed element names (lowercase).
+ */
+ public function __construct($elements) {
+ if (is_string($elements)) {
+ $elements = str_replace(' ', '', $elements);
+ $elements = explode('|', $elements);
+ }
+ $keys = array_keys($elements);
+ if ($keys == array_keys($keys)) {
+ $elements = array_flip($elements);
+ foreach ($elements as $i => $x) {
+ $elements[$i] = true;
+ if (empty($i)) unset($elements[$i]); // remove blank
+ }
+ }
+ $this->elements = $elements;
+ }
+ public $allow_empty = false;
+ public $type = 'required';
+ public function validateChildren($tokens_of_children, $config, $context) {
+ // Flag for subclasses
+ $this->whitespace = false;
+
+ // if there are no tokens, delete parent node
+ if (empty($tokens_of_children)) return false;
+
+ // the new set of children
+ $result = array();
+
+ // current depth into the nest
+ $nesting = 0;
+
+ // whether or not we're deleting a node
+ $is_deleting = false;
+
+ // whether or not parsed character data is allowed
+ // this controls whether or not we silently drop a tag
+ // or generate escaped HTML from it
+ $pcdata_allowed = isset($this->elements['#PCDATA']);
+
+ // a little sanity check to make sure it's not ALL whitespace
+ $all_whitespace = true;
+
+ // some configuration
+ $escape_invalid_children = $config->get('Core.EscapeInvalidChildren');
+
+ // generator
+ $gen = new HTMLPurifier_Generator($config, $context);
+
+ foreach ($tokens_of_children as $token) {
+ if (!empty($token->is_whitespace)) {
+ $result[] = $token;
+ continue;
+ }
+ $all_whitespace = false; // phew, we're not talking about whitespace
+
+ $is_child = ($nesting == 0);
+
+ if ($token instanceof HTMLPurifier_Token_Start) {
+ $nesting++;
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ $nesting--;
+ }
+
+ if ($is_child) {
+ $is_deleting = false;
+ if (!isset($this->elements[$token->name])) {
+ $is_deleting = true;
+ if ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text) {
+ $result[] = $token;
+ } elseif ($pcdata_allowed && $escape_invalid_children) {
+ $result[] = new HTMLPurifier_Token_Text(
+ $gen->generateFromToken($token)
+ );
+ }
+ continue;
+ }
+ }
+ if (!$is_deleting || ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text)) {
+ $result[] = $token;
+ } elseif ($pcdata_allowed && $escape_invalid_children) {
+ $result[] =
+ new HTMLPurifier_Token_Text(
+ $gen->generateFromToken($token)
+ );
+ } else {
+ // drop silently
+ }
+ }
+ if (empty($result)) return false;
+ if ($all_whitespace) {
+ $this->whitespace = true;
+ return false;
+ }
+ if ($tokens_of_children == $result) return true;
+ return $result;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/StrictBlockquote.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/StrictBlockquote.php
new file mode 100644
index 000000000..dfae8a6e5
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/StrictBlockquote.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * Takes the contents of blockquote when in strict and reformats for validation.
+ */
+class HTMLPurifier_ChildDef_StrictBlockquote extends HTMLPurifier_ChildDef_Required
+{
+ protected $real_elements;
+ protected $fake_elements;
+ public $allow_empty = true;
+ public $type = 'strictblockquote';
+ protected $init = false;
+
+ /**
+ * @note We don't want MakeWellFormed to auto-close inline elements since
+ * they might be allowed.
+ */
+ public function getAllowedElements($config) {
+ $this->init($config);
+ return $this->fake_elements;
+ }
+
+ public function validateChildren($tokens_of_children, $config, $context) {
+
+ $this->init($config);
+
+ // trick the parent class into thinking it allows more
+ $this->elements = $this->fake_elements;
+ $result = parent::validateChildren($tokens_of_children, $config, $context);
+ $this->elements = $this->real_elements;
+
+ if ($result === false) return array();
+ if ($result === true) $result = $tokens_of_children;
+
+ $def = $config->getHTMLDefinition();
+ $block_wrap_start = new HTMLPurifier_Token_Start($def->info_block_wrapper);
+ $block_wrap_end = new HTMLPurifier_Token_End( $def->info_block_wrapper);
+ $is_inline = false;
+ $depth = 0;
+ $ret = array();
+
+ // assuming that there are no comment tokens
+ foreach ($result as $i => $token) {
+ $token = $result[$i];
+ // ifs are nested for readability
+ if (!$is_inline) {
+ if (!$depth) {
+ if (
+ ($token instanceof HTMLPurifier_Token_Text && !$token->is_whitespace) ||
+ (!$token instanceof HTMLPurifier_Token_Text && !isset($this->elements[$token->name]))
+ ) {
+ $is_inline = true;
+ $ret[] = $block_wrap_start;
+ }
+ }
+ } else {
+ if (!$depth) {
+ // starting tokens have been inline text / empty
+ if ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) {
+ if (isset($this->elements[$token->name])) {
+ // ended
+ $ret[] = $block_wrap_end;
+ $is_inline = false;
+ }
+ }
+ }
+ }
+ $ret[] = $token;
+ if ($token instanceof HTMLPurifier_Token_Start) $depth++;
+ if ($token instanceof HTMLPurifier_Token_End) $depth--;
+ }
+ if ($is_inline) $ret[] = $block_wrap_end;
+ return $ret;
+ }
+
+ private function init($config) {
+ if (!$this->init) {
+ $def = $config->getHTMLDefinition();
+ // allow all inline elements
+ $this->real_elements = $this->elements;
+ $this->fake_elements = $def->info_content_sets['Flow'];
+ $this->fake_elements['#PCDATA'] = true;
+ $this->init = true;
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Table.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Table.php
new file mode 100644
index 000000000..34f0227dd
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Table.php
@@ -0,0 +1,142 @@
+<?php
+
+/**
+ * Definition for tables
+ */
+class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
+{
+ public $allow_empty = false;
+ public $type = 'table';
+ public $elements = array('tr' => true, 'tbody' => true, 'thead' => true,
+ 'tfoot' => true, 'caption' => true, 'colgroup' => true, 'col' => true);
+ public function __construct() {}
+ public function validateChildren($tokens_of_children, $config, $context) {
+ if (empty($tokens_of_children)) return false;
+
+ // this ensures that the loop gets run one last time before closing
+ // up. It's a little bit of a hack, but it works! Just make sure you
+ // get rid of the token later.
+ $tokens_of_children[] = false;
+
+ // only one of these elements is allowed in a table
+ $caption = false;
+ $thead = false;
+ $tfoot = false;
+
+ // as many of these as you want
+ $cols = array();
+ $content = array();
+
+ $nesting = 0; // current depth so we can determine nodes
+ $is_collecting = false; // are we globbing together tokens to package
+ // into one of the collectors?
+ $collection = array(); // collected nodes
+ $tag_index = 0; // the first node might be whitespace,
+ // so this tells us where the start tag is
+
+ foreach ($tokens_of_children as $token) {
+ $is_child = ($nesting == 0);
+
+ if ($token === false) {
+ // terminating sequence started
+ } elseif ($token instanceof HTMLPurifier_Token_Start) {
+ $nesting++;
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ $nesting--;
+ }
+
+ // handle node collection
+ if ($is_collecting) {
+ if ($is_child) {
+ // okay, let's stash the tokens away
+ // first token tells us the type of the collection
+ switch ($collection[$tag_index]->name) {
+ case 'tr':
+ case 'tbody':
+ $content[] = $collection;
+ break;
+ case 'caption':
+ if ($caption !== false) break;
+ $caption = $collection;
+ break;
+ case 'thead':
+ case 'tfoot':
+ // access the appropriate variable, $thead or $tfoot
+ $var = $collection[$tag_index]->name;
+ if ($$var === false) {
+ $$var = $collection;
+ } else {
+ // transmutate the first and less entries into
+ // tbody tags, and then put into content
+ $collection[$tag_index]->name = 'tbody';
+ $collection[count($collection)-1]->name = 'tbody';
+ $content[] = $collection;
+ }
+ break;
+ case 'colgroup':
+ $cols[] = $collection;
+ break;
+ }
+ $collection = array();
+ $is_collecting = false;
+ $tag_index = 0;
+ } else {
+ // add the node to the collection
+ $collection[] = $token;
+ }
+ }
+
+ // terminate
+ if ($token === false) break;
+
+ if ($is_child) {
+ // determine what we're dealing with
+ if ($token->name == 'col') {
+ // the only empty tag in the possie, we can handle it
+ // immediately
+ $cols[] = array_merge($collection, array($token));
+ $collection = array();
+ $tag_index = 0;
+ continue;
+ }
+ switch($token->name) {
+ case 'caption':
+ case 'colgroup':
+ case 'thead':
+ case 'tfoot':
+ case 'tbody':
+ case 'tr':
+ $is_collecting = true;
+ $collection[] = $token;
+ continue;
+ default:
+ if (!empty($token->is_whitespace)) {
+ $collection[] = $token;
+ $tag_index++;
+ }
+ continue;
+ }
+ }
+ }
+
+ if (empty($content)) return false;
+
+ $ret = array();
+ if ($caption !== false) $ret = array_merge($ret, $caption);
+ if ($cols !== false) foreach ($cols as $token_array) $ret = array_merge($ret, $token_array);
+ if ($thead !== false) $ret = array_merge($ret, $thead);
+ if ($tfoot !== false) $ret = array_merge($ret, $tfoot);
+ foreach ($content as $token_array) $ret = array_merge($ret, $token_array);
+ if (!empty($collection) && $is_collecting == false){
+ // grab the trailing space
+ $ret = array_merge($ret, $collection);
+ }
+
+ array_pop($tokens_of_children); // remove phantom token
+
+ return ($ret === $tokens_of_children) ? true : $ret;
+
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Config.php b/extlib/HTMLPurifier/HTMLPurifier/Config.php
new file mode 100644
index 000000000..a01706043
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Config.php
@@ -0,0 +1,580 @@
+<?php
+
+/**
+ * Configuration object that triggers customizable behavior.
+ *
+ * @warning This class is strongly defined: that means that the class
+ * will fail if an undefined directive is retrieved or set.
+ *
+ * @note Many classes that could (although many times don't) use the
+ * configuration object make it a mandatory parameter. This is
+ * because a configuration object should always be forwarded,
+ * otherwise, you run the risk of missing a parameter and then
+ * being stumped when a configuration directive doesn't work.
+ *
+ * @todo Reconsider some of the public member variables
+ */
+class HTMLPurifier_Config
+{
+
+ /**
+ * HTML Purifier's version
+ */
+ public $version = '4.0.0';
+
+ /**
+ * Bool indicator whether or not to automatically finalize
+ * the object if a read operation is done
+ */
+ public $autoFinalize = true;
+
+ // protected member variables
+
+ /**
+ * Namespace indexed array of serials for specific namespaces (see
+ * getSerial() for more info).
+ */
+ protected $serials = array();
+
+ /**
+ * Serial for entire configuration object
+ */
+ protected $serial;
+
+ /**
+ * Parser for variables
+ */
+ protected $parser;
+
+ /**
+ * Reference HTMLPurifier_ConfigSchema for value checking
+ * @note This is public for introspective purposes. Please don't
+ * abuse!
+ */
+ public $def;
+
+ /**
+ * Indexed array of definitions
+ */
+ protected $definitions;
+
+ /**
+ * Bool indicator whether or not config is finalized
+ */
+ protected $finalized = false;
+
+ /**
+ * Property list containing configuration directives.
+ */
+ protected $plist;
+
+ /**
+ * Whether or not a set is taking place due to an
+ * alias lookup.
+ */
+ private $aliasMode;
+
+ /**
+ * Set to false if you do not want line and file numbers in errors
+ * (useful when unit testing)
+ */
+ public $chatty = true;
+
+ /**
+ * Current lock; only gets to this namespace are allowed.
+ */
+ private $lock;
+
+ /**
+ * @param $definition HTMLPurifier_ConfigSchema that defines what directives
+ * are allowed.
+ */
+ public function __construct($definition, $parent = null) {
+ $parent = $parent ? $parent : $definition->defaultPlist;
+ $this->plist = new HTMLPurifier_PropertyList($parent);
+ $this->def = $definition; // keep a copy around for checking
+ $this->parser = new HTMLPurifier_VarParser_Flexible();
+ }
+
+ /**
+ * Convenience constructor that creates a config object based on a mixed var
+ * @param mixed $config Variable that defines the state of the config
+ * object. Can be: a HTMLPurifier_Config() object,
+ * an array of directives based on loadArray(),
+ * or a string filename of an ini file.
+ * @param HTMLPurifier_ConfigSchema Schema object
+ * @return Configured HTMLPurifier_Config object
+ */
+ public static function create($config, $schema = null) {
+ if ($config instanceof HTMLPurifier_Config) {
+ // pass-through
+ return $config;
+ }
+ if (!$schema) {
+ $ret = HTMLPurifier_Config::createDefault();
+ } else {
+ $ret = new HTMLPurifier_Config($schema);
+ }
+ if (is_string($config)) $ret->loadIni($config);
+ elseif (is_array($config)) $ret->loadArray($config);
+ return $ret;
+ }
+
+ /**
+ * Creates a new config object that inherits from a previous one.
+ * @param HTMLPurifier_Config $config Configuration object to inherit
+ * from.
+ * @return HTMLPurifier_Config object with $config as its parent.
+ */
+ public static function inherit(HTMLPurifier_Config $config) {
+ return new HTMLPurifier_Config($config->def, $config->plist);
+ }
+
+ /**
+ * Convenience constructor that creates a default configuration object.
+ * @return Default HTMLPurifier_Config object.
+ */
+ public static function createDefault() {
+ $definition = HTMLPurifier_ConfigSchema::instance();
+ $config = new HTMLPurifier_Config($definition);
+ return $config;
+ }
+
+ /**
+ * Retreives a value from the configuration.
+ * @param $key String key
+ */
+ public function get($key, $a = null) {
+ if ($a !== null) {
+ $this->triggerError("Using deprecated API: use \$config->get('$key.$a') instead", E_USER_WARNING);
+ $key = "$key.$a";
+ }
+ if (!$this->finalized) $this->autoFinalize();
+ if (!isset($this->def->info[$key])) {
+ // can't add % due to SimpleTest bug
+ $this->triggerError('Cannot retrieve value of undefined directive ' . htmlspecialchars($key),
+ E_USER_WARNING);
+ return;
+ }
+ if (isset($this->def->info[$key]->isAlias)) {
+ $d = $this->def->info[$key];
+ $this->triggerError('Cannot get value from aliased directive, use real name ' . $d->key,
+ E_USER_ERROR);
+ return;
+ }
+ if ($this->lock) {
+ list($ns) = explode('.', $key);
+ if ($ns !== $this->lock) {
+ $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);
+ return;
+ }
+ }
+ return $this->plist->get($key);
+ }
+
+ /**
+ * Retreives an array of directives to values from a given namespace
+ * @param $namespace String namespace
+ */
+ public function getBatch($namespace) {
+ if (!$this->finalized) $this->autoFinalize();
+ $full = $this->getAll();
+ if (!isset($full[$namespace])) {
+ $this->triggerError('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace),
+ E_USER_WARNING);
+ return;
+ }
+ return $full[$namespace];
+ }
+
+ /**
+ * Returns a md5 signature of a segment of the configuration object
+ * that uniquely identifies that particular configuration
+ * @note Revision is handled specially and is removed from the batch
+ * before processing!
+ * @param $namespace Namespace to get serial for
+ */
+ public function getBatchSerial($namespace) {
+ if (empty($this->serials[$namespace])) {
+ $batch = $this->getBatch($namespace);
+ unset($batch['DefinitionRev']);
+ $this->serials[$namespace] = md5(serialize($batch));
+ }
+ return $this->serials[$namespace];
+ }
+
+ /**
+ * Returns a md5 signature for the entire configuration object
+ * that uniquely identifies that particular configuration
+ */
+ public function getSerial() {
+ if (empty($this->serial)) {
+ $this->serial = md5(serialize($this->getAll()));
+ }
+ return $this->serial;
+ }
+
+ /**
+ * Retrieves all directives, organized by namespace
+ * @warning This is a pretty inefficient function, avoid if you can
+ */
+ public function getAll() {
+ if (!$this->finalized) $this->autoFinalize();
+ $ret = array();
+ foreach ($this->plist->squash() as $name => $value) {
+ list($ns, $key) = explode('.', $name, 2);
+ $ret[$ns][$key] = $value;
+ }
+ return $ret;
+ }
+
+ /**
+ * Sets a value to configuration.
+ * @param $key String key
+ * @param $value Mixed value
+ */
+ public function set($key, $value, $a = null) {
+ if (strpos($key, '.') === false) {
+ $namespace = $key;
+ $directive = $value;
+ $value = $a;
+ $key = "$key.$directive";
+ $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE);
+ } else {
+ list($namespace) = explode('.', $key);
+ }
+ if ($this->isFinalized('Cannot set directive after finalization')) return;
+ if (!isset($this->def->info[$key])) {
+ $this->triggerError('Cannot set undefined directive ' . htmlspecialchars($key) . ' to value',
+ E_USER_WARNING);
+ return;
+ }
+ $def = $this->def->info[$key];
+
+ if (isset($def->isAlias)) {
+ if ($this->aliasMode) {
+ $this->triggerError('Double-aliases not allowed, please fix '.
+ 'ConfigSchema bug with' . $key, E_USER_ERROR);
+ return;
+ }
+ $this->aliasMode = true;
+ $this->set($def->key, $value);
+ $this->aliasMode = false;
+ $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE);
+ return;
+ }
+
+ // Raw type might be negative when using the fully optimized form
+ // of stdclass, which indicates allow_null == true
+ $rtype = is_int($def) ? $def : $def->type;
+ if ($rtype < 0) {
+ $type = -$rtype;
+ $allow_null = true;
+ } else {
+ $type = $rtype;
+ $allow_null = isset($def->allow_null);
+ }
+
+ try {
+ $value = $this->parser->parse($value, $type, $allow_null);
+ } catch (HTMLPurifier_VarParserException $e) {
+ $this->triggerError('Value for ' . $key . ' is of invalid type, should be ' . HTMLPurifier_VarParser::getTypeName($type), E_USER_WARNING);
+ return;
+ }
+ if (is_string($value) && is_object($def)) {
+ // resolve value alias if defined
+ if (isset($def->aliases[$value])) {
+ $value = $def->aliases[$value];
+ }
+ // check to see if the value is allowed
+ if (isset($def->allowed) && !isset($def->allowed[$value])) {
+ $this->triggerError('Value not supported, valid values are: ' .
+ $this->_listify($def->allowed), E_USER_WARNING);
+ return;
+ }
+ }
+ $this->plist->set($key, $value);
+
+ // reset definitions if the directives they depend on changed
+ // this is a very costly process, so it's discouraged
+ // with finalization
+ if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') {
+ $this->definitions[$namespace] = null;
+ }
+
+ $this->serials[$namespace] = false;
+ }
+
+ /**
+ * Convenience function for error reporting
+ */
+ private function _listify($lookup) {
+ $list = array();
+ foreach ($lookup as $name => $b) $list[] = $name;
+ return implode(', ', $list);
+ }
+
+ /**
+ * Retrieves object reference to the HTML definition.
+ * @param $raw Return a copy that has not been setup yet. Must be
+ * called before it's been setup, otherwise won't work.
+ */
+ public function getHTMLDefinition($raw = false) {
+ return $this->getDefinition('HTML', $raw);
+ }
+
+ /**
+ * Retrieves object reference to the CSS definition
+ * @param $raw Return a copy that has not been setup yet. Must be
+ * called before it's been setup, otherwise won't work.
+ */
+ public function getCSSDefinition($raw = false) {
+ return $this->getDefinition('CSS', $raw);
+ }
+
+ /**
+ * Retrieves a definition
+ * @param $type Type of definition: HTML, CSS, etc
+ * @param $raw Whether or not definition should be returned raw
+ */
+ public function getDefinition($type, $raw = false) {
+ if (!$this->finalized) $this->autoFinalize();
+ // temporarily suspend locks, so we can handle recursive definition calls
+ $lock = $this->lock;
+ $this->lock = null;
+ $factory = HTMLPurifier_DefinitionCacheFactory::instance();
+ $cache = $factory->create($type, $this);
+ $this->lock = $lock;
+ if (!$raw) {
+ // see if we can quickly supply a definition
+ if (!empty($this->definitions[$type])) {
+ if (!$this->definitions[$type]->setup) {
+ $this->definitions[$type]->setup($this);
+ $cache->set($this->definitions[$type], $this);
+ }
+ return $this->definitions[$type];
+ }
+ // memory check missed, try cache
+ $this->definitions[$type] = $cache->get($this);
+ if ($this->definitions[$type]) {
+ // definition in cache, return it
+ return $this->definitions[$type];
+ }
+ } elseif (
+ !empty($this->definitions[$type]) &&
+ !$this->definitions[$type]->setup
+ ) {
+ // raw requested, raw in memory, quick return
+ return $this->definitions[$type];
+ }
+ // quick checks failed, let's create the object
+ if ($type == 'HTML') {
+ $this->definitions[$type] = new HTMLPurifier_HTMLDefinition();
+ } elseif ($type == 'CSS') {
+ $this->definitions[$type] = new HTMLPurifier_CSSDefinition();
+ } elseif ($type == 'URI') {
+ $this->definitions[$type] = new HTMLPurifier_URIDefinition();
+ } else {
+ throw new HTMLPurifier_Exception("Definition of $type type not supported");
+ }
+ // quick abort if raw
+ if ($raw) {
+ if (is_null($this->get($type . '.DefinitionID'))) {
+ // fatally error out if definition ID not set
+ throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID");
+ }
+ return $this->definitions[$type];
+ }
+ // set it up
+ $this->lock = $type;
+ $this->definitions[$type]->setup($this);
+ $this->lock = null;
+ // save in cache
+ $cache->set($this->definitions[$type], $this);
+ return $this->definitions[$type];
+ }
+
+ /**
+ * Loads configuration values from an array with the following structure:
+ * Namespace.Directive => Value
+ * @param $config_array Configuration associative array
+ */
+ public function loadArray($config_array) {
+ if ($this->isFinalized('Cannot load directives after finalization')) return;
+ foreach ($config_array as $key => $value) {
+ $key = str_replace('_', '.', $key);
+ if (strpos($key, '.') !== false) {
+ $this->set($key, $value);
+ } else {
+ $namespace = $key;
+ $namespace_values = $value;
+ foreach ($namespace_values as $directive => $value) {
+ $this->set($namespace .'.'. $directive, $value);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a list of array(namespace, directive) for all directives
+ * that are allowed in a web-form context as per an allowed
+ * namespaces/directives list.
+ * @param $allowed List of allowed namespaces/directives
+ */
+ public static function getAllowedDirectivesForForm($allowed, $schema = null) {
+ if (!$schema) {
+ $schema = HTMLPurifier_ConfigSchema::instance();
+ }
+ if ($allowed !== true) {
+ if (is_string($allowed)) $allowed = array($allowed);
+ $allowed_ns = array();
+ $allowed_directives = array();
+ $blacklisted_directives = array();
+ foreach ($allowed as $ns_or_directive) {
+ if (strpos($ns_or_directive, '.') !== false) {
+ // directive
+ if ($ns_or_directive[0] == '-') {
+ $blacklisted_directives[substr($ns_or_directive, 1)] = true;
+ } else {
+ $allowed_directives[$ns_or_directive] = true;
+ }
+ } else {
+ // namespace
+ $allowed_ns[$ns_or_directive] = true;
+ }
+ }
+ }
+ $ret = array();
+ foreach ($schema->info as $key => $def) {
+ list($ns, $directive) = explode('.', $key, 2);
+ if ($allowed !== true) {
+ if (isset($blacklisted_directives["$ns.$directive"])) continue;
+ if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) continue;
+ }
+ if (isset($def->isAlias)) continue;
+ if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') continue;
+ $ret[] = array($ns, $directive);
+ }
+ return $ret;
+ }
+
+ /**
+ * Loads configuration values from $_GET/$_POST that were posted
+ * via ConfigForm
+ * @param $array $_GET or $_POST array to import
+ * @param $index Index/name that the config variables are in
+ * @param $allowed List of allowed namespaces/directives
+ * @param $mq_fix Boolean whether or not to enable magic quotes fix
+ * @param $schema Instance of HTMLPurifier_ConfigSchema to use, if not global copy
+ */
+ public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
+ $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema);
+ $config = HTMLPurifier_Config::create($ret, $schema);
+ return $config;
+ }
+
+ /**
+ * Merges in configuration values from $_GET/$_POST to object. NOT STATIC.
+ * @note Same parameters as loadArrayFromForm
+ */
+ public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) {
+ $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def);
+ $this->loadArray($ret);
+ }
+
+ /**
+ * Prepares an array from a form into something usable for the more
+ * strict parts of HTMLPurifier_Config
+ */
+ public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) {
+ if ($index !== false) $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array();
+ $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc();
+
+ $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema);
+ $ret = array();
+ foreach ($allowed as $key) {
+ list($ns, $directive) = $key;
+ $skey = "$ns.$directive";
+ if (!empty($array["Null_$skey"])) {
+ $ret[$ns][$directive] = null;
+ continue;
+ }
+ if (!isset($array[$skey])) continue;
+ $value = $mq ? stripslashes($array[$skey]) : $array[$skey];
+ $ret[$ns][$directive] = $value;
+ }
+ return $ret;
+ }
+
+ /**
+ * Loads configuration values from an ini file
+ * @param $filename Name of ini file
+ */
+ public function loadIni($filename) {
+ if ($this->isFinalized('Cannot load directives after finalization')) return;
+ $array = parse_ini_file($filename, true);
+ $this->loadArray($array);
+ }
+
+ /**
+ * Checks whether or not the configuration object is finalized.
+ * @param $error String error message, or false for no error
+ */
+ public function isFinalized($error = false) {
+ if ($this->finalized && $error) {
+ $this->triggerError($error, E_USER_ERROR);
+ }
+ return $this->finalized;
+ }
+
+ /**
+ * Finalizes configuration only if auto finalize is on and not
+ * already finalized
+ */
+ public function autoFinalize() {
+ if ($this->autoFinalize) {
+ $this->finalize();
+ } else {
+ $this->plist->squash(true);
+ }
+ }
+
+ /**
+ * Finalizes a configuration object, prohibiting further change
+ */
+ public function finalize() {
+ $this->finalized = true;
+ unset($this->parser);
+ }
+
+ /**
+ * Produces a nicely formatted error message by supplying the
+ * stack frame information from two levels up and OUTSIDE of
+ * HTMLPurifier_Config.
+ */
+ protected function triggerError($msg, $no) {
+ // determine previous stack frame
+ $backtrace = debug_backtrace();
+ if ($this->chatty && isset($backtrace[1])) {
+ $frame = $backtrace[1];
+ $extra = " on line {$frame['line']} in file {$frame['file']}";
+ } else {
+ $extra = '';
+ }
+ trigger_error($msg . $extra, $no);
+ }
+
+ /**
+ * Returns a serialized form of the configuration object that can
+ * be reconstituted.
+ */
+ public function serialize() {
+ $this->getDefinition('HTML');
+ $this->getDefinition('CSS');
+ $this->getDefinition('URI');
+ return serialize($this);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema.php
new file mode 100644
index 000000000..67be5c71f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema.php
@@ -0,0 +1,158 @@
+<?php
+
+/**
+ * Configuration definition, defines directives and their defaults.
+ */
+class HTMLPurifier_ConfigSchema {
+
+ /**
+ * Defaults of the directives and namespaces.
+ * @note This shares the exact same structure as HTMLPurifier_Config::$conf
+ */
+ public $defaults = array();
+
+ /**
+ * The default property list. Do not edit this property list.
+ */
+ public $defaultPlist;
+
+ /**
+ * Definition of the directives. The structure of this is:
+ *
+ * array(
+ * 'Namespace' => array(
+ * 'Directive' => new stdclass(),
+ * )
+ * )
+ *
+ * The stdclass may have the following properties:
+ *
+ * - If isAlias isn't set:
+ * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions
+ * - allow_null: If set, this directive allows null values
+ * - aliases: If set, an associative array of value aliases to real values
+ * - allowed: If set, a lookup array of allowed (string) values
+ * - If isAlias is set:
+ * - namespace: Namespace this directive aliases to
+ * - name: Directive name this directive aliases to
+ *
+ * In certain degenerate cases, stdclass will actually be an integer. In
+ * that case, the value is equivalent to an stdclass with the type
+ * property set to the integer. If the integer is negative, type is
+ * equal to the absolute value of integer, and allow_null is true.
+ *
+ * This class is friendly with HTMLPurifier_Config. If you need introspection
+ * about the schema, you're better of using the ConfigSchema_Interchange,
+ * which uses more memory but has much richer information.
+ */
+ public $info = array();
+
+ /**
+ * Application-wide singleton
+ */
+ static protected $singleton;
+
+ public function __construct() {
+ $this->defaultPlist = new HTMLPurifier_PropertyList();
+ }
+
+ /**
+ * Unserializes the default ConfigSchema.
+ */
+ public static function makeFromSerial() {
+ return unserialize(file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'));
+ }
+
+ /**
+ * Retrieves an instance of the application-wide configuration definition.
+ */
+ public static function instance($prototype = null) {
+ if ($prototype !== null) {
+ HTMLPurifier_ConfigSchema::$singleton = $prototype;
+ } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) {
+ HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial();
+ }
+ return HTMLPurifier_ConfigSchema::$singleton;
+ }
+
+ /**
+ * Defines a directive for configuration
+ * @warning Will fail of directive's namespace is defined.
+ * @warning This method's signature is slightly different from the legacy
+ * define() static method! Beware!
+ * @param $namespace Namespace the directive is in
+ * @param $name Key of directive
+ * @param $default Default value of directive
+ * @param $type Allowed type of the directive. See
+ * HTMLPurifier_DirectiveDef::$type for allowed values
+ * @param $allow_null Whether or not to allow null values
+ */
+ public function add($key, $default, $type, $allow_null) {
+ $obj = new stdclass();
+ $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type];
+ if ($allow_null) $obj->allow_null = true;
+ $this->info[$key] = $obj;
+ $this->defaults[$key] = $default;
+ $this->defaultPlist->set($key, $default);
+ }
+
+ /**
+ * Defines a directive value alias.
+ *
+ * Directive value aliases are convenient for developers because it lets
+ * them set a directive to several values and get the same result.
+ * @param $namespace Directive's namespace
+ * @param $name Name of Directive
+ * @param $aliases Hash of aliased values to the real alias
+ */
+ public function addValueAliases($key, $aliases) {
+ if (!isset($this->info[$key]->aliases)) {
+ $this->info[$key]->aliases = array();
+ }
+ foreach ($aliases as $alias => $real) {
+ $this->info[$key]->aliases[$alias] = $real;
+ }
+ }
+
+ /**
+ * Defines a set of allowed values for a directive.
+ * @warning This is slightly different from the corresponding static
+ * method definition.
+ * @param $namespace Namespace of directive
+ * @param $name Name of directive
+ * @param $allowed Lookup array of allowed values
+ */
+ public function addAllowedValues($key, $allowed) {
+ $this->info[$key]->allowed = $allowed;
+ }
+
+ /**
+ * Defines a directive alias for backwards compatibility
+ * @param $namespace
+ * @param $name Directive that will be aliased
+ * @param $new_namespace
+ * @param $new_name Directive that the alias will be to
+ */
+ public function addAlias($key, $new_key) {
+ $obj = new stdclass;
+ $obj->key = $new_key;
+ $obj->isAlias = true;
+ $this->info[$key] = $obj;
+ }
+
+ /**
+ * Replaces any stdclass that only has the type property with type integer.
+ */
+ public function postProcess() {
+ foreach ($this->info as $key => $v) {
+ if (count((array) $v) == 1) {
+ $this->info[$key] = $v->type;
+ } elseif (count((array) $v) == 2 && isset($v->allow_null)) {
+ $this->info[$key] = -$v->type;
+ }
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php
new file mode 100644
index 000000000..c05668a70
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * Converts HTMLPurifier_ConfigSchema_Interchange to our runtime
+ * representation used to perform checks on user configuration.
+ */
+class HTMLPurifier_ConfigSchema_Builder_ConfigSchema
+{
+
+ public function build($interchange) {
+ $schema = new HTMLPurifier_ConfigSchema();
+ foreach ($interchange->directives as $d) {
+ $schema->add(
+ $d->id->key,
+ $d->default,
+ $d->type,
+ $d->typeAllowsNull
+ );
+ if ($d->allowed !== null) {
+ $schema->addAllowedValues(
+ $d->id->key,
+ $d->allowed
+ );
+ }
+ foreach ($d->aliases as $alias) {
+ $schema->addAlias(
+ $alias->key,
+ $d->id->key
+ );
+ }
+ if ($d->valueAliases !== null) {
+ $schema->addValueAliases(
+ $d->id->key,
+ $d->valueAliases
+ );
+ }
+ }
+ $schema->postProcess();
+ return $schema;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php
new file mode 100644
index 000000000..244561a37
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * Converts HTMLPurifier_ConfigSchema_Interchange to an XML format,
+ * which can be further processed to generate documentation.
+ */
+class HTMLPurifier_ConfigSchema_Builder_Xml extends XMLWriter
+{
+
+ protected $interchange;
+ private $namespace;
+
+ protected function writeHTMLDiv($html) {
+ $this->startElement('div');
+
+ $purifier = HTMLPurifier::getInstance();
+ $html = $purifier->purify($html);
+ $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
+ $this->writeRaw($html);
+
+ $this->endElement(); // div
+ }
+
+ protected function export($var) {
+ if ($var === array()) return 'array()';
+ return var_export($var, true);
+ }
+
+ public function build($interchange) {
+ // global access, only use as last resort
+ $this->interchange = $interchange;
+
+ $this->setIndent(true);
+ $this->startDocument('1.0', 'UTF-8');
+ $this->startElement('configdoc');
+ $this->writeElement('title', $interchange->name);
+
+ foreach ($interchange->directives as $directive) {
+ $this->buildDirective($directive);
+ }
+
+ if ($this->namespace) $this->endElement(); // namespace
+
+ $this->endElement(); // configdoc
+ $this->flush();
+ }
+
+ public function buildDirective($directive) {
+
+ // Kludge, although I suppose having a notion of a "root namespace"
+ // certainly makes things look nicer when documentation is built.
+ // Depends on things being sorted.
+ if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) {
+ if ($this->namespace) $this->endElement(); // namespace
+ $this->namespace = $directive->id->getRootNamespace();
+ $this->startElement('namespace');
+ $this->writeAttribute('id', $this->namespace);
+ $this->writeElement('name', $this->namespace);
+ }
+
+ $this->startElement('directive');
+ $this->writeAttribute('id', $directive->id->toString());
+
+ $this->writeElement('name', $directive->id->getDirective());
+
+ $this->startElement('aliases');
+ foreach ($directive->aliases as $alias) $this->writeElement('alias', $alias->toString());
+ $this->endElement(); // aliases
+
+ $this->startElement('constraints');
+ if ($directive->version) $this->writeElement('version', $directive->version);
+ $this->startElement('type');
+ if ($directive->typeAllowsNull) $this->writeAttribute('allow-null', 'yes');
+ $this->text($directive->type);
+ $this->endElement(); // type
+ if ($directive->allowed) {
+ $this->startElement('allowed');
+ foreach ($directive->allowed as $value => $x) $this->writeElement('value', $value);
+ $this->endElement(); // allowed
+ }
+ $this->writeElement('default', $this->export($directive->default));
+ $this->writeAttribute('xml:space', 'preserve');
+ if ($directive->external) {
+ $this->startElement('external');
+ foreach ($directive->external as $project) $this->writeElement('project', $project);
+ $this->endElement();
+ }
+ $this->endElement(); // constraints
+
+ if ($directive->deprecatedVersion) {
+ $this->startElement('deprecated');
+ $this->writeElement('version', $directive->deprecatedVersion);
+ $this->writeElement('use', $directive->deprecatedUse->toString());
+ $this->endElement(); // deprecated
+ }
+
+ $this->startElement('description');
+ $this->writeHTMLDiv($directive->description);
+ $this->endElement(); // description
+
+ $this->endElement(); // directive
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Exception.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Exception.php
new file mode 100644
index 000000000..2671516c5
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Exception.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * Exceptions related to configuration schema
+ */
+class HTMLPurifier_ConfigSchema_Exception extends HTMLPurifier_Exception
+{
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange.php
new file mode 100644
index 000000000..91a5aa730
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * Generic schema interchange format that can be converted to a runtime
+ * representation (HTMLPurifier_ConfigSchema) or HTML documentation. Members
+ * are completely validated.
+ */
+class HTMLPurifier_ConfigSchema_Interchange
+{
+
+ /**
+ * Name of the application this schema is describing.
+ */
+ public $name;
+
+ /**
+ * Array of Directive ID => array(directive info)
+ */
+ public $directives = array();
+
+ /**
+ * Adds a directive array to $directives
+ */
+ public function addDirective($directive) {
+ if (isset($this->directives[$i = $directive->id->toString()])) {
+ throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'");
+ }
+ $this->directives[$i] = $directive;
+ }
+
+ /**
+ * Convenience function to perform standard validation. Throws exception
+ * on failed validation.
+ */
+ public function validate() {
+ $validator = new HTMLPurifier_ConfigSchema_Validator();
+ return $validator->validate($this);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php
new file mode 100644
index 000000000..ac8be0d97
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * Interchange component class describing configuration directives.
+ */
+class HTMLPurifier_ConfigSchema_Interchange_Directive
+{
+
+ /**
+ * ID of directive, instance of HTMLPurifier_ConfigSchema_Interchange_Id.
+ */
+ public $id;
+
+ /**
+ * String type, e.g. 'integer' or 'istring'.
+ */
+ public $type;
+
+ /**
+ * Default value, e.g. 3 or 'DefaultVal'.
+ */
+ public $default;
+
+ /**
+ * HTML description.
+ */
+ public $description;
+
+ /**
+ * Boolean whether or not null is allowed as a value.
+ */
+ public $typeAllowsNull = false;
+
+ /**
+ * Lookup table of allowed scalar values, e.g. array('allowed' => true).
+ * Null if all values are allowed.
+ */
+ public $allowed;
+
+ /**
+ * List of aliases for the directive,
+ * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))).
+ */
+ public $aliases = array();
+
+ /**
+ * Hash of value aliases, e.g. array('alt' => 'real'). Null if value
+ * aliasing is disabled (necessary for non-scalar types).
+ */
+ public $valueAliases;
+
+ /**
+ * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'.
+ * Null if the directive has always existed.
+ */
+ public $version;
+
+ /**
+ * ID of directive that supercedes this old directive, is an instance
+ * of HTMLPurifier_ConfigSchema_Interchange_Id. Null if not deprecated.
+ */
+ public $deprecatedUse;
+
+ /**
+ * Version of HTML Purifier this directive was deprecated. Null if not
+ * deprecated.
+ */
+ public $deprecatedVersion;
+
+ /**
+ * List of external projects this directive depends on, e.g. array('CSSTidy').
+ */
+ public $external = array();
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php
new file mode 100644
index 000000000..b9b3c6f5c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * Represents a directive ID in the interchange format.
+ */
+class HTMLPurifier_ConfigSchema_Interchange_Id
+{
+
+ public $key;
+
+ public function __construct($key) {
+ $this->key = $key;
+ }
+
+ /**
+ * @warning This is NOT magic, to ensure that people don't abuse SPL and
+ * cause problems for PHP 5.0 support.
+ */
+ public function toString() {
+ return $this->key;
+ }
+
+ public function getRootNamespace() {
+ return substr($this->key, 0, strpos($this->key, "."));
+ }
+
+ public function getDirective() {
+ return substr($this->key, strpos($this->key, ".") + 1);
+ }
+
+ public static function make($id) {
+ return new HTMLPurifier_ConfigSchema_Interchange_Id($id);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php
new file mode 100644
index 000000000..785b72ce8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php
@@ -0,0 +1,180 @@
+<?php
+
+class HTMLPurifier_ConfigSchema_InterchangeBuilder
+{
+
+ /**
+ * Used for processing DEFAULT, nothing else.
+ */
+ protected $varParser;
+
+ public function __construct($varParser = null) {
+ $this->varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native();
+ }
+
+ public static function buildFromDirectory($dir = null) {
+ $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder();
+ $interchange = new HTMLPurifier_ConfigSchema_Interchange();
+ return $builder->buildDir($interchange, $dir);
+ }
+
+ public function buildDir($interchange, $dir = null) {
+ if (!$dir) $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema';
+ if (file_exists($dir . '/info.ini')) {
+ $info = parse_ini_file($dir . '/info.ini');
+ $interchange->name = $info['name'];
+ }
+
+ $files = array();
+ $dh = opendir($dir);
+ while (false !== ($file = readdir($dh))) {
+ if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') {
+ continue;
+ }
+ $files[] = $file;
+ }
+ closedir($dh);
+
+ sort($files);
+ foreach ($files as $file) {
+ $this->buildFile($interchange, $dir . '/' . $file);
+ }
+
+ return $interchange;
+ }
+
+ public function buildFile($interchange, $file) {
+ $parser = new HTMLPurifier_StringHashParser();
+ $this->build(
+ $interchange,
+ new HTMLPurifier_StringHash( $parser->parseFile($file) )
+ );
+ }
+
+ /**
+ * Builds an interchange object based on a hash.
+ * @param $interchange HTMLPurifier_ConfigSchema_Interchange object to build
+ * @param $hash HTMLPurifier_ConfigSchema_StringHash source data
+ */
+ public function build($interchange, $hash) {
+ if (!$hash instanceof HTMLPurifier_StringHash) {
+ $hash = new HTMLPurifier_StringHash($hash);
+ }
+ if (!isset($hash['ID'])) {
+ throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID');
+ }
+ if (strpos($hash['ID'], '.') === false) {
+ if (count($hash) == 2 && isset($hash['DESCRIPTION'])) {
+ $hash->offsetGet('DESCRIPTION'); // prevent complaining
+ } else {
+ throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace');
+ }
+ } else {
+ $this->buildDirective($interchange, $hash);
+ }
+ $this->_findUnused($hash);
+ }
+
+ public function buildDirective($interchange, $hash) {
+ $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive();
+
+ // These are required elements:
+ $directive->id = $this->id($hash->offsetGet('ID'));
+ $id = $directive->id->toString(); // convenience
+
+ if (isset($hash['TYPE'])) {
+ $type = explode('/', $hash->offsetGet('TYPE'));
+ if (isset($type[1])) $directive->typeAllowsNull = true;
+ $directive->type = $type[0];
+ } else {
+ throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined");
+ }
+
+ if (isset($hash['DEFAULT'])) {
+ try {
+ $directive->default = $this->varParser->parse($hash->offsetGet('DEFAULT'), $directive->type, $directive->typeAllowsNull);
+ } catch (HTMLPurifier_VarParserException $e) {
+ throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'");
+ }
+ }
+
+ if (isset($hash['DESCRIPTION'])) {
+ $directive->description = $hash->offsetGet('DESCRIPTION');
+ }
+
+ if (isset($hash['ALLOWED'])) {
+ $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED')));
+ }
+
+ if (isset($hash['VALUE-ALIASES'])) {
+ $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES'));
+ }
+
+ if (isset($hash['ALIASES'])) {
+ $raw_aliases = trim($hash->offsetGet('ALIASES'));
+ $aliases = preg_split('/\s*,\s*/', $raw_aliases);
+ foreach ($aliases as $alias) {
+ $directive->aliases[] = $this->id($alias);
+ }
+ }
+
+ if (isset($hash['VERSION'])) {
+ $directive->version = $hash->offsetGet('VERSION');
+ }
+
+ if (isset($hash['DEPRECATED-USE'])) {
+ $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE'));
+ }
+
+ if (isset($hash['DEPRECATED-VERSION'])) {
+ $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION');
+ }
+
+ if (isset($hash['EXTERNAL'])) {
+ $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL')));
+ }
+
+ $interchange->addDirective($directive);
+ }
+
+ /**
+ * Evaluates an array PHP code string without array() wrapper
+ */
+ protected function evalArray($contents) {
+ return eval('return array('. $contents .');');
+ }
+
+ /**
+ * Converts an array list into a lookup array.
+ */
+ protected function lookup($array) {
+ $ret = array();
+ foreach ($array as $val) $ret[$val] = true;
+ return $ret;
+ }
+
+ /**
+ * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id
+ * object based on a string Id.
+ */
+ protected function id($id) {
+ return HTMLPurifier_ConfigSchema_Interchange_Id::make($id);
+ }
+
+ /**
+ * Triggers errors for any unused keys passed in the hash; such keys
+ * may indicate typos, missing values, etc.
+ * @param $hash Instance of ConfigSchema_StringHash to check.
+ */
+ protected function _findUnused($hash) {
+ $accessed = $hash->getAccessed();
+ foreach ($hash as $k => $v) {
+ if (!isset($accessed[$k])) {
+ trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE);
+ }
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Validator.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Validator.php
new file mode 100644
index 000000000..f374f6a02
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/Validator.php
@@ -0,0 +1,206 @@
+<?php
+
+/**
+ * Performs validations on HTMLPurifier_ConfigSchema_Interchange
+ *
+ * @note If you see '// handled by InterchangeBuilder', that means a
+ * design decision in that class would prevent this validation from
+ * ever being necessary. We have them anyway, however, for
+ * redundancy.
+ */
+class HTMLPurifier_ConfigSchema_Validator
+{
+
+ /**
+ * Easy to access global objects.
+ */
+ protected $interchange, $aliases;
+
+ /**
+ * Context-stack to provide easy to read error messages.
+ */
+ protected $context = array();
+
+ /**
+ * HTMLPurifier_VarParser to test default's type.
+ */
+ protected $parser;
+
+ public function __construct() {
+ $this->parser = new HTMLPurifier_VarParser();
+ }
+
+ /**
+ * Validates a fully-formed interchange object. Throws an
+ * HTMLPurifier_ConfigSchema_Exception if there's a problem.
+ */
+ public function validate($interchange) {
+ $this->interchange = $interchange;
+ $this->aliases = array();
+ // PHP is a bit lax with integer <=> string conversions in
+ // arrays, so we don't use the identical !== comparison
+ foreach ($interchange->directives as $i => $directive) {
+ $id = $directive->id->toString();
+ if ($i != $id) $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'");
+ $this->validateDirective($directive);
+ }
+ return true;
+ }
+
+ /**
+ * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object.
+ */
+ public function validateId($id) {
+ $id_string = $id->toString();
+ $this->context[] = "id '$id_string'";
+ if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) {
+ // handled by InterchangeBuilder
+ $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id');
+ }
+ // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.)
+ // we probably should check that it has at least one namespace
+ $this->with($id, 'key')
+ ->assertNotEmpty()
+ ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder
+ array_pop($this->context);
+ }
+
+ /**
+ * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object.
+ */
+ public function validateDirective($d) {
+ $id = $d->id->toString();
+ $this->context[] = "directive '$id'";
+ $this->validateId($d->id);
+
+ $this->with($d, 'description')
+ ->assertNotEmpty();
+
+ // BEGIN - handled by InterchangeBuilder
+ $this->with($d, 'type')
+ ->assertNotEmpty();
+ $this->with($d, 'typeAllowsNull')
+ ->assertIsBool();
+ try {
+ // This also tests validity of $d->type
+ $this->parser->parse($d->default, $d->type, $d->typeAllowsNull);
+ } catch (HTMLPurifier_VarParserException $e) {
+ $this->error('default', 'had error: ' . $e->getMessage());
+ }
+ // END - handled by InterchangeBuilder
+
+ if (!is_null($d->allowed) || !empty($d->valueAliases)) {
+ // allowed and valueAliases require that we be dealing with
+ // strings, so check for that early.
+ $d_int = HTMLPurifier_VarParser::$types[$d->type];
+ if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) {
+ $this->error('type', 'must be a string type when used with allowed or value aliases');
+ }
+ }
+
+ $this->validateDirectiveAllowed($d);
+ $this->validateDirectiveValueAliases($d);
+ $this->validateDirectiveAliases($d);
+
+ array_pop($this->context);
+ }
+
+ /**
+ * Extra validation if $allowed member variable of
+ * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
+ */
+ public function validateDirectiveAllowed($d) {
+ if (is_null($d->allowed)) return;
+ $this->with($d, 'allowed')
+ ->assertNotEmpty()
+ ->assertIsLookup(); // handled by InterchangeBuilder
+ if (is_string($d->default) && !isset($d->allowed[$d->default])) {
+ $this->error('default', 'must be an allowed value');
+ }
+ $this->context[] = 'allowed';
+ foreach ($d->allowed as $val => $x) {
+ if (!is_string($val)) $this->error("value $val", 'must be a string');
+ }
+ array_pop($this->context);
+ }
+
+ /**
+ * Extra validation if $valueAliases member variable of
+ * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
+ */
+ public function validateDirectiveValueAliases($d) {
+ if (is_null($d->valueAliases)) return;
+ $this->with($d, 'valueAliases')
+ ->assertIsArray(); // handled by InterchangeBuilder
+ $this->context[] = 'valueAliases';
+ foreach ($d->valueAliases as $alias => $real) {
+ if (!is_string($alias)) $this->error("alias $alias", 'must be a string');
+ if (!is_string($real)) $this->error("alias target $real from alias '$alias'", 'must be a string');
+ if ($alias === $real) {
+ $this->error("alias '$alias'", "must not be an alias to itself");
+ }
+ }
+ if (!is_null($d->allowed)) {
+ foreach ($d->valueAliases as $alias => $real) {
+ if (isset($d->allowed[$alias])) {
+ $this->error("alias '$alias'", 'must not be an allowed value');
+ } elseif (!isset($d->allowed[$real])) {
+ $this->error("alias '$alias'", 'must be an alias to an allowed value');
+ }
+ }
+ }
+ array_pop($this->context);
+ }
+
+ /**
+ * Extra validation if $aliases member variable of
+ * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
+ */
+ public function validateDirectiveAliases($d) {
+ $this->with($d, 'aliases')
+ ->assertIsArray(); // handled by InterchangeBuilder
+ $this->context[] = 'aliases';
+ foreach ($d->aliases as $alias) {
+ $this->validateId($alias);
+ $s = $alias->toString();
+ if (isset($this->interchange->directives[$s])) {
+ $this->error("alias '$s'", 'collides with another directive');
+ }
+ if (isset($this->aliases[$s])) {
+ $other_directive = $this->aliases[$s];
+ $this->error("alias '$s'", "collides with alias for directive '$other_directive'");
+ }
+ $this->aliases[$s] = $d->id->toString();
+ }
+ array_pop($this->context);
+ }
+
+ // protected helper functions
+
+ /**
+ * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom
+ * for validating simple member variables of objects.
+ */
+ protected function with($obj, $member) {
+ return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member);
+ }
+
+ /**
+ * Emits an error, providing helpful context.
+ */
+ protected function error($target, $msg) {
+ if ($target !== false) $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext();
+ else $prefix = ucfirst($this->getFormattedContext());
+ throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg));
+ }
+
+ /**
+ * Returns a formatted context string.
+ */
+ protected function getFormattedContext() {
+ return implode(' in ', array_reverse($this->context));
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php
new file mode 100644
index 000000000..b95aea18c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * Fluent interface for validating the contents of member variables.
+ * This should be immutable. See HTMLPurifier_ConfigSchema_Validator for
+ * use-cases. We name this an 'atom' because it's ONLY for validations that
+ * are independent and usually scalar.
+ */
+class HTMLPurifier_ConfigSchema_ValidatorAtom
+{
+
+ protected $context, $obj, $member, $contents;
+
+ public function __construct($context, $obj, $member) {
+ $this->context = $context;
+ $this->obj = $obj;
+ $this->member = $member;
+ $this->contents =& $obj->$member;
+ }
+
+ public function assertIsString() {
+ if (!is_string($this->contents)) $this->error('must be a string');
+ return $this;
+ }
+
+ public function assertIsBool() {
+ if (!is_bool($this->contents)) $this->error('must be a boolean');
+ return $this;
+ }
+
+ public function assertIsArray() {
+ if (!is_array($this->contents)) $this->error('must be an array');
+ return $this;
+ }
+
+ public function assertNotNull() {
+ if ($this->contents === null) $this->error('must not be null');
+ return $this;
+ }
+
+ public function assertAlnum() {
+ $this->assertIsString();
+ if (!ctype_alnum($this->contents)) $this->error('must be alphanumeric');
+ return $this;
+ }
+
+ public function assertNotEmpty() {
+ if (empty($this->contents)) $this->error('must not be empty');
+ return $this;
+ }
+
+ public function assertIsLookup() {
+ $this->assertIsArray();
+ foreach ($this->contents as $v) {
+ if ($v !== true) $this->error('must be a lookup array');
+ }
+ return $this;
+ }
+
+ protected function error($msg) {
+ throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema.ser b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema.ser
new file mode 100644
index 000000000..bbf12f9c3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema.ser
Binary files differ
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt
new file mode 100644
index 000000000..0517fed0a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt
@@ -0,0 +1,8 @@
+Attr.AllowedClasses
+TYPE: lookup/null
+VERSION: 4.0.0
+DEFAULT: null
+--DESCRIPTION--
+List of allowed class values in the class attribute. By default, this is null,
+which means all classes are allowed.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt
new file mode 100644
index 000000000..249edd647
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt
@@ -0,0 +1,12 @@
+Attr.AllowedFrameTargets
+TYPE: lookup
+DEFAULT: array()
+--DESCRIPTION--
+Lookup table of all allowed link frame targets. Some commonly used link
+targets include _blank, _self, _parent and _top. Values should be
+lowercase, as validation will be done in a case-sensitive manner despite
+W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute
+so this directive will have no effect in that doctype. XHTML 1.1 does not
+enable the Target module by default, you will have to manually enable it
+(see the module documentation for more details.)
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt
new file mode 100644
index 000000000..9a8fa6a2e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt
@@ -0,0 +1,9 @@
+Attr.AllowedRel
+TYPE: lookup
+VERSION: 1.6.0
+DEFAULT: array()
+--DESCRIPTION--
+List of allowed forward document relationships in the rel attribute. Common
+values may be nofollow or print. By default, this is empty, meaning that no
+document relationships are allowed.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt
new file mode 100644
index 000000000..b01788348
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt
@@ -0,0 +1,9 @@
+Attr.AllowedRev
+TYPE: lookup
+VERSION: 1.6.0
+DEFAULT: array()
+--DESCRIPTION--
+List of allowed reverse document relationships in the rev attribute. This
+attribute is a bit of an edge-case; if you don't know what it is for, stay
+away.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt
new file mode 100644
index 000000000..e774b823b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt
@@ -0,0 +1,19 @@
+Attr.ClassUseCDATA
+TYPE: bool/null
+DEFAULT: null
+VERSION: 4.0.0
+--DESCRIPTION--
+If null, class will auto-detect the doctype and, if matching XHTML 1.1 or
+XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise,
+it will use a relaxed CDATA definition. If true, the relaxed CDATA definition
+is forced; if false, the NMTOKENS definition is forced. To get behavior
+of HTML Purifier prior to 4.0.0, set this directive to false.
+
+Some rational behind the auto-detection:
+in previous versions of HTML Purifier, it was assumed that the form of
+class was NMTOKENS, as specified by the XHTML Modularization (representing
+XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however
+specify class as CDATA. HTML 5 effectively defines it as CDATA, but
+with the additional constraint that each name should be unique (this is not
+explicitly outlined in previous specifications).
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt
new file mode 100644
index 000000000..533165e17
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt
@@ -0,0 +1,11 @@
+Attr.DefaultImageAlt
+TYPE: string/null
+DEFAULT: null
+VERSION: 3.2.0
+--DESCRIPTION--
+This is the content of the alt tag of an image if the user had not
+previously specified an alt attribute. This applies to all images without
+a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which
+only applies to invalid images, and overrides in the case of an invalid image.
+Default behavior with null is to use the basename of the src tag for the alt.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt
new file mode 100644
index 000000000..9eb7e3846
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt
@@ -0,0 +1,9 @@
+Attr.DefaultInvalidImage
+TYPE: string
+DEFAULT: ''
+--DESCRIPTION--
+This is the default image an img tag will be pointed to if it does not have
+a valid src attribute. In future versions, we may allow the image tag to
+be removed completely, but due to design issues, this is not possible right
+now.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt
new file mode 100644
index 000000000..2f17bf477
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt
@@ -0,0 +1,8 @@
+Attr.DefaultInvalidImageAlt
+TYPE: string
+DEFAULT: 'Invalid image'
+--DESCRIPTION--
+This is the content of the alt tag of an invalid image if the user had not
+previously specified an alt attribute. It has no effect when the image is
+valid but there was no alt attribute present.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt
new file mode 100644
index 000000000..52654b53a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt
@@ -0,0 +1,10 @@
+Attr.DefaultTextDir
+TYPE: string
+DEFAULT: 'ltr'
+--DESCRIPTION--
+Defines the default text direction (ltr or rtl) of the document being
+parsed. This generally is the same as the value of the dir attribute in
+HTML, or ltr if that is not specified.
+--ALLOWED--
+'ltr', 'rtl'
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt
new file mode 100644
index 000000000..6440d2103
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt
@@ -0,0 +1,16 @@
+Attr.EnableID
+TYPE: bool
+DEFAULT: false
+VERSION: 1.2.0
+--DESCRIPTION--
+Allows the ID attribute in HTML. This is disabled by default due to the
+fact that without proper configuration user input can easily break the
+validation of a webpage by specifying an ID that is already on the
+surrounding HTML. If you don't mind throwing caution to the wind, enable
+this directive, but I strongly recommend you also consider blacklisting IDs
+you use (%Attr.IDBlacklist) or prefixing all user supplied IDs
+(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of
+pre-1.2.0 versions.
+--ALIASES--
+HTML.EnableAttrID
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt
new file mode 100644
index 000000000..f31d226f5
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt
@@ -0,0 +1,8 @@
+Attr.ForbiddenClasses
+TYPE: lookup
+VERSION: 4.0.0
+DEFAULT: array()
+--DESCRIPTION--
+List of forbidden class values in the class attribute. By default, this is
+empty, which means that no classes are forbidden. See also %Attr.AllowedClasses.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt
new file mode 100644
index 000000000..5f2b5e3d2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt
@@ -0,0 +1,5 @@
+Attr.IDBlacklist
+TYPE: list
+DEFAULT: array()
+DESCRIPTION: Array of IDs not allowed in the document.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt
new file mode 100644
index 000000000..6f5824586
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt
@@ -0,0 +1,9 @@
+Attr.IDBlacklistRegexp
+TYPE: string/null
+VERSION: 1.6.0
+DEFAULT: NULL
+--DESCRIPTION--
+PCRE regular expression to be matched against all IDs. If the expression is
+matches, the ID is rejected. Use this with care: may cause significant
+degradation. ID matching is done after all other validation.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt
new file mode 100644
index 000000000..cc49d43fd
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt
@@ -0,0 +1,12 @@
+Attr.IDPrefix
+TYPE: string
+VERSION: 1.2.0
+DEFAULT: ''
+--DESCRIPTION--
+String to prefix to IDs. If you have no idea what IDs your pages may use,
+you may opt to simply add a prefix to all user-submitted ID attributes so
+that they are still usable, but will not conflict with core page IDs.
+Example: setting the directive to 'user_' will result in a user submitted
+'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true
+before using this.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt
new file mode 100644
index 000000000..2c5924a7a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt
@@ -0,0 +1,14 @@
+Attr.IDPrefixLocal
+TYPE: string
+VERSION: 1.2.0
+DEFAULT: ''
+--DESCRIPTION--
+Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you
+need to allow multiple sets of user content on web page, you may need to
+have a seperate prefix that changes with each iteration. This way,
+seperately submitted user content displayed on the same page doesn't
+clobber each other. Ideal values are unique identifiers for the content it
+represents (i.e. the id of the row in the database). Be sure to add a
+seperator (like an underscore) at the end. Warning: this directive will
+not work unless %Attr.IDPrefix is set to a non-empty value!
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt
new file mode 100644
index 000000000..d5caa1bb9
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt
@@ -0,0 +1,31 @@
+AutoFormat.AutoParagraph
+TYPE: bool
+VERSION: 2.0.1
+DEFAULT: false
+--DESCRIPTION--
+
+<p>
+ This directive turns on auto-paragraphing, where double newlines are
+ converted in to paragraphs whenever possible. Auto-paragraphing:
+</p>
+<ul>
+ <li>Always applies to inline elements or text in the root node,</li>
+ <li>Applies to inline elements or text with double newlines in nodes
+ that allow paragraph tags,</li>
+ <li>Applies to double newlines in paragraph tags</li>
+</ul>
+<p>
+ <code>p</code> tags must be allowed for this directive to take effect.
+ We do not use <code>br</code> tags for paragraphing, as that is
+ semantically incorrect.
+</p>
+<p>
+ To prevent auto-paragraphing as a content-producer, refrain from using
+ double-newlines except to specify a new paragraph or in contexts where
+ it has special meaning (whitespace usually has no meaning except in
+ tags like <code>pre</code>, so this should not be difficult.) To prevent
+ the paragraphing of inline text adjacent to block elements, wrap them
+ in <code>div</code> tags (the behavior is slightly different outside of
+ the root node.)
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt
new file mode 100644
index 000000000..2a476481a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt
@@ -0,0 +1,12 @@
+AutoFormat.Custom
+TYPE: list
+VERSION: 2.0.1
+DEFAULT: array()
+--DESCRIPTION--
+
+<p>
+ This directive can be used to add custom auto-format injectors.
+ Specify an array of injector names (class name minus the prefix)
+ or concrete implementations. Injector class must exist.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt
new file mode 100644
index 000000000..663064a34
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt
@@ -0,0 +1,11 @@
+AutoFormat.DisplayLinkURI
+TYPE: bool
+VERSION: 3.2.0
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ This directive turns on the in-text display of URIs in &lt;a&gt; tags, and disables
+ those links. For example, <a href="http://example.com">example</a> becomes
+ example (<a>http://example.com</a>).
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt
new file mode 100644
index 000000000..3a48ba960
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt
@@ -0,0 +1,12 @@
+AutoFormat.Linkify
+TYPE: bool
+VERSION: 2.0.1
+DEFAULT: false
+--DESCRIPTION--
+
+<p>
+ This directive turns on linkification, auto-linking http, ftp and
+ https URLs. <code>a</code> tags with the <code>href</code> attribute
+ must be allowed.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt
new file mode 100644
index 000000000..db58b1346
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt
@@ -0,0 +1,12 @@
+AutoFormat.PurifierLinkify.DocURL
+TYPE: string
+VERSION: 2.0.1
+DEFAULT: '#%s'
+ALIASES: AutoFormatParam.PurifierLinkifyDocURL
+--DESCRIPTION--
+<p>
+ Location of configuration documentation to link to, let %s substitute
+ into the configuration's namespace and directive names sans the percent
+ sign.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt
new file mode 100644
index 000000000..7996488be
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt
@@ -0,0 +1,12 @@
+AutoFormat.PurifierLinkify
+TYPE: bool
+VERSION: 2.0.1
+DEFAULT: false
+--DESCRIPTION--
+
+<p>
+ Internal auto-formatter that converts configuration directives in
+ syntax <a>%Namespace.Directive</a> to links. <code>a</code> tags
+ with the <code>href</code> attribute must be allowed.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt
new file mode 100644
index 000000000..35c393b4e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt
@@ -0,0 +1,11 @@
+AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions
+TYPE: lookup
+VERSION: 4.0.0
+DEFAULT: array('td' => true, 'th' => true)
+--DESCRIPTION--
+<p>
+ When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp
+ are enabled, this directive defines what HTML elements should not be
+ removede if they have only a non-breaking space in them.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt
new file mode 100644
index 000000000..ca17eb1dc
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt
@@ -0,0 +1,15 @@
+AutoFormat.RemoveEmpty.RemoveNbsp
+TYPE: bool
+VERSION: 4.0.0
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ When enabled, HTML Purifier will treat any elements that contain only
+ non-breaking spaces as well as regular whitespace as empty, and remove
+ them when %AutoForamt.RemoveEmpty is enabled.
+</p>
+<p>
+ See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements
+ that don't have this behavior applied to them.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt
new file mode 100644
index 000000000..34657ba47
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt
@@ -0,0 +1,46 @@
+AutoFormat.RemoveEmpty
+TYPE: bool
+VERSION: 3.2.0
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ When enabled, HTML Purifier will attempt to remove empty elements that
+ contribute no semantic information to the document. The following types
+ of nodes will be removed:
+</p>
+<ul><li>
+ Tags with no attributes and no content, and that are not empty
+ elements (remove <code>&lt;a&gt;&lt;/a&gt;</code> but not
+ <code>&lt;br /&gt;</code>), and
+ </li>
+ <li>
+ Tags with no content, except for:<ul>
+ <li>The <code>colgroup</code> element, or</li>
+ <li>
+ Elements with the <code>id</code> or <code>name</code> attribute,
+ when those attributes are permitted on those elements.
+ </li>
+ </ul></li>
+</ul>
+<p>
+ Please be very careful when using this functionality; while it may not
+ seem that empty elements contain useful information, they can alter the
+ layout of a document given appropriate styling. This directive is most
+ useful when you are processing machine-generated HTML, please avoid using
+ it on regular user HTML.
+</p>
+<p>
+ Elements that contain only whitespace will be treated as empty. Non-breaking
+ spaces, however, do not count as whitespace. See
+ %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior.
+</p>
+<p>
+ This algorithm is not perfect; you may still notice some empty tags,
+ particularly if a node had elements, but those elements were later removed
+ because they were not permitted in that context, or tags that, after
+ being auto-closed by another tag, where empty. This is for safety reasons
+ to prevent clever code from breaking validation. The general rule of thumb:
+ if a tag looked empty on the way in, it will get removed; if HTML Purifier
+ made it empty, it will stay.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt
new file mode 100644
index 000000000..b324608f7
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt
@@ -0,0 +1,8 @@
+CSS.AllowImportant
+TYPE: bool
+DEFAULT: false
+VERSION: 3.1.0
+--DESCRIPTION--
+This parameter determines whether or not !important cascade modifiers should
+be allowed in user CSS. If false, !important will stripped.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt
new file mode 100644
index 000000000..748be0eec
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt
@@ -0,0 +1,11 @@
+CSS.AllowTricky
+TYPE: bool
+DEFAULT: false
+VERSION: 3.1.0
+--DESCRIPTION--
+This parameter determines whether or not to allow "tricky" CSS properties and
+values. Tricky CSS properties/values can drastically modify page layout or
+be used for deceptive practices but do not directly constitute a security risk.
+For example, <code>display:none;</code> is considered a tricky property that
+will only be allowed if this directive is set to true.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt
new file mode 100644
index 000000000..460112ebe
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt
@@ -0,0 +1,18 @@
+CSS.AllowedProperties
+TYPE: lookup/null
+VERSION: 3.1.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ If HTML Purifier's style attributes set is unsatisfactory for your needs,
+ you can overload it with your own list of tags to allow. Note that this
+ method is subtractive: it does its job by taking away from HTML Purifier
+ usual feature set, so you cannot add an attribute that HTML Purifier never
+ supported in the first place.
+</p>
+<p>
+ <strong>Warning:</strong> If another directive conflicts with the
+ elements here, <em>that</em> directive will win and override.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt
new file mode 100644
index 000000000..5cb7dda3b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt
@@ -0,0 +1,11 @@
+CSS.DefinitionRev
+TYPE: int
+VERSION: 2.0.0
+DEFAULT: 1
+--DESCRIPTION--
+
+<p>
+ Revision identifier for your custom definition. See
+ %HTML.DefinitionRev for details.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt
new file mode 100644
index 000000000..7a3291470
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt
@@ -0,0 +1,16 @@
+CSS.MaxImgLength
+TYPE: string/null
+DEFAULT: '1200px'
+VERSION: 3.1.1
+--DESCRIPTION--
+<p>
+ This parameter sets the maximum allowed length on <code>img</code> tags,
+ effectively the <code>width</code> and <code>height</code> properties.
+ Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is
+ in place to prevent imagecrash attacks, disable with null at your own risk.
+ This directive is similar to %HTML.MaxImgLength, and both should be
+ concurrently edited, although there are
+ subtle differences in the input format (the CSS max is a number with
+ a unit).
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt
new file mode 100644
index 000000000..148eedb8b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt
@@ -0,0 +1,10 @@
+CSS.Proprietary
+TYPE: bool
+VERSION: 3.0.0
+DEFAULT: false
+--DESCRIPTION--
+
+<p>
+ Whether or not to allow safe, proprietary CSS values.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt
new file mode 100644
index 000000000..c486724c8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt
@@ -0,0 +1,14 @@
+Cache.DefinitionImpl
+TYPE: string/null
+VERSION: 2.0.0
+DEFAULT: 'Serializer'
+--DESCRIPTION--
+
+This directive defines which method to use when caching definitions,
+the complex data-type that makes HTML Purifier tick. Set to null
+to disable caching (not recommended, as you will see a definite
+performance degradation).
+
+--ALIASES--
+Core.DefinitionCache
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt
new file mode 100644
index 000000000..54036507d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt
@@ -0,0 +1,13 @@
+Cache.SerializerPath
+TYPE: string/null
+VERSION: 2.0.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ Absolute path with no trailing slash to store serialized definitions in.
+ Default is within the
+ HTML Purifier library inside DefinitionCache/Serializer. This
+ path must be writable by the webserver.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt
new file mode 100644
index 000000000..568cbf3b3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt
@@ -0,0 +1,18 @@
+Core.AggressivelyFixLt
+TYPE: bool
+VERSION: 2.1.0
+DEFAULT: true
+--DESCRIPTION--
+<p>
+ This directive enables aggressive pre-filter fixes HTML Purifier can
+ perform in order to ensure that open angled-brackets do not get killed
+ during parsing stage. Enabling this will result in two preg_replace_callback
+ calls and at least two preg_replace calls for every HTML document parsed;
+ if your users make very well-formed HTML, you can set this directive false.
+ This has no effect when DirectLex is used.
+</p>
+<p>
+ <strong>Notice:</strong> This directive's default turned from false to true
+ in HTML Purifier 3.2.0.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt
new file mode 100644
index 000000000..d7317911f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt
@@ -0,0 +1,12 @@
+Core.CollectErrors
+TYPE: bool
+VERSION: 2.0.0
+DEFAULT: false
+--DESCRIPTION--
+
+Whether or not to collect errors found while filtering the document. This
+is a useful way to give feedback to your users. <strong>Warning:</strong>
+Currently this feature is very patchy and experimental, with lots of
+possible error messages not yet implemented. It will not cause any
+problems, but it may not help your users either.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt
new file mode 100644
index 000000000..08b381d34
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt
@@ -0,0 +1,28 @@
+Core.ColorKeywords
+TYPE: hash
+VERSION: 2.0.0
+--DEFAULT--
+array (
+ 'maroon' => '#800000',
+ 'red' => '#FF0000',
+ 'orange' => '#FFA500',
+ 'yellow' => '#FFFF00',
+ 'olive' => '#808000',
+ 'purple' => '#800080',
+ 'fuchsia' => '#FF00FF',
+ 'white' => '#FFFFFF',
+ 'lime' => '#00FF00',
+ 'green' => '#008000',
+ 'navy' => '#000080',
+ 'blue' => '#0000FF',
+ 'aqua' => '#00FFFF',
+ 'teal' => '#008080',
+ 'black' => '#000000',
+ 'silver' => '#C0C0C0',
+ 'gray' => '#808080',
+)
+--DESCRIPTION--
+
+Lookup array of color names to six digit hexadecimal number corresponding
+to color, with preceding hash mark. Used when parsing colors.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt
new file mode 100644
index 000000000..64b114fce
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt
@@ -0,0 +1,14 @@
+Core.ConvertDocumentToFragment
+TYPE: bool
+DEFAULT: true
+--DESCRIPTION--
+
+This parameter determines whether or not the filter should convert
+input that is a full document with html and body tags to a fragment
+of just the contents of a body tag. This parameter is simply something
+HTML Purifier can do during an edge-case: for most inputs, this
+processing is not necessary.
+
+--ALIASES--
+Core.AcceptFullDocuments
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt
new file mode 100644
index 000000000..36f16e07e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt
@@ -0,0 +1,17 @@
+Core.DirectLexLineNumberSyncInterval
+TYPE: int
+VERSION: 2.0.0
+DEFAULT: 0
+--DESCRIPTION--
+
+<p>
+ Specifies the number of tokens the DirectLex line number tracking
+ implementations should process before attempting to resyncronize the
+ current line count by manually counting all previous new-lines. When
+ at 0, this functionality is disabled. Lower values will decrease
+ performance, and this is only strictly necessary if the counting
+ algorithm is buggy (in which case you should report it as a bug).
+ This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is
+ not being used.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt
new file mode 100644
index 000000000..8bfb47c3a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt
@@ -0,0 +1,15 @@
+Core.Encoding
+TYPE: istring
+DEFAULT: 'utf-8'
+--DESCRIPTION--
+If for some reason you are unable to convert all webpages to UTF-8, you can
+use this directive as a stop-gap compatibility change to let HTML Purifier
+deal with non UTF-8 input. This technique has notable deficiencies:
+absolutely no characters outside of the selected character encoding will be
+preserved, not even the ones that have been ampersand escaped (this is due
+to a UTF-8 specific <em>feature</em> that automatically resolves all
+entities), making it pretty useless for anything except the most I18N-blind
+applications, although %Core.EscapeNonASCIICharacters offers fixes this
+trouble with another tradeoff. This directive only accepts ISO-8859-1 if
+iconv is not enabled.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt
new file mode 100644
index 000000000..4d5b5055c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt
@@ -0,0 +1,10 @@
+Core.EscapeInvalidChildren
+TYPE: bool
+DEFAULT: false
+--DESCRIPTION--
+When true, a child is found that is not allowed in the context of the
+parent element will be transformed into text as if it were ASCII. When
+false, that element and all internal tags will be dropped, though text will
+be preserved. There is no option for dropping the element but preserving
+child nodes.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt
new file mode 100644
index 000000000..a7a5b249b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt
@@ -0,0 +1,7 @@
+Core.EscapeInvalidTags
+TYPE: bool
+DEFAULT: false
+--DESCRIPTION--
+When true, invalid tags will be written back to the document as plain text.
+Otherwise, they are silently dropped.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt
new file mode 100644
index 000000000..abb499948
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt
@@ -0,0 +1,13 @@
+Core.EscapeNonASCIICharacters
+TYPE: bool
+VERSION: 1.4.0
+DEFAULT: false
+--DESCRIPTION--
+This directive overcomes a deficiency in %Core.Encoding by blindly
+converting all non-ASCII characters into decimal numeric entities before
+converting it to its native encoding. This means that even characters that
+can be expressed in the non-UTF-8 encoding will be entity-ized, which can
+be a real downer for encodings like Big5. It also assumes that the ASCII
+repetoire is available, although this is the case for almost all encodings.
+Anyway, use UTF-8!
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt
new file mode 100644
index 000000000..915391edb
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt
@@ -0,0 +1,19 @@
+Core.HiddenElements
+TYPE: lookup
+--DEFAULT--
+array (
+ 'script' => true,
+ 'style' => true,
+)
+--DESCRIPTION--
+
+<p>
+ This directive is a lookup array of elements which should have their
+ contents removed when they are not allowed by the HTML definition.
+ For example, the contents of a <code>script</code> tag are not
+ normally shown in a document, so if script tags are to be removed,
+ their contents should be removed to. This is opposed to a <code>b</code>
+ tag, which defines some presentational changes but does not hide its
+ contents.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt
new file mode 100644
index 000000000..233fca14f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt
@@ -0,0 +1,10 @@
+Core.Language
+TYPE: string
+VERSION: 2.0.0
+DEFAULT: 'en'
+--DESCRIPTION--
+
+ISO 639 language code for localizable things in HTML Purifier to use,
+which is mainly error reporting. There is currently only an English (en)
+translation, so this directive is currently useless.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt
new file mode 100644
index 000000000..8983e2cca
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt
@@ -0,0 +1,34 @@
+Core.LexerImpl
+TYPE: mixed/null
+VERSION: 2.0.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ This parameter determines what lexer implementation can be used. The
+ valid values are:
+</p>
+<dl>
+ <dt><em>null</em></dt>
+ <dd>
+ Recommended, the lexer implementation will be auto-detected based on
+ your PHP-version and configuration.
+ </dd>
+ <dt><em>string</em> lexer identifier</dt>
+ <dd>
+ This is a slim way of manually overridding the implementation.
+ Currently recognized values are: DOMLex (the default PHP5
+implementation)
+ and DirectLex (the default PHP4 implementation). Only use this if
+ you know what you are doing: usually, the auto-detection will
+ manage things for cases you aren't even aware of.
+ </dd>
+ <dt><em>object</em> lexer instance</dt>
+ <dd>
+ Super-advanced: you can specify your own, custom, implementation that
+ implements the interface defined by <code>HTMLPurifier_Lexer</code>.
+ I may remove this option simply because I don't expect anyone
+ to use it.
+ </dd>
+</dl>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt
new file mode 100644
index 000000000..eb841a759
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt
@@ -0,0 +1,16 @@
+Core.MaintainLineNumbers
+TYPE: bool/null
+VERSION: 2.0.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ If true, HTML Purifier will add line number information to all tokens.
+ This is useful when error reporting is turned on, but can result in
+ significant performance degradation and should not be used when
+ unnecessary. This directive must be used with the DirectLex lexer,
+ as the DOMLex lexer does not (yet) support this functionality.
+ If the value is null, an appropriate value will be selected based
+ on other configuration.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt
new file mode 100644
index 000000000..4070c2a0d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt
@@ -0,0 +1,12 @@
+Core.RemoveInvalidImg
+TYPE: bool
+DEFAULT: true
+VERSION: 1.3.0
+--DESCRIPTION--
+
+<p>
+ This directive enables pre-emptive URI checking in <code>img</code>
+ tags, as the attribute validation strategy is not authorized to
+ remove elements from the document. Revert to pre-1.3.0 behavior by setting to false.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt
new file mode 100644
index 000000000..a4cd966df
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt
@@ -0,0 +1,12 @@
+Core.RemoveScriptContents
+TYPE: bool/null
+DEFAULT: NULL
+VERSION: 2.0.0
+DEPRECATED-VERSION: 2.1.0
+DEPRECATED-USE: Core.HiddenElements
+--DESCRIPTION--
+<p>
+ This directive enables HTML Purifier to remove not only script tags
+ but all of their contents.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt
new file mode 100644
index 000000000..3db50ef20
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt
@@ -0,0 +1,11 @@
+Filter.Custom
+TYPE: list
+VERSION: 3.1.0
+DEFAULT: array()
+--DESCRIPTION--
+<p>
+ This directive can be used to add custom filters; it is nearly the
+ equivalent of the now deprecated <code>HTMLPurifier-&gt;addFilter()</code>
+ method. Specify an array of concrete implementations.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt
new file mode 100644
index 000000000..16829bcda
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt
@@ -0,0 +1,14 @@
+Filter.ExtractStyleBlocks.Escaping
+TYPE: bool
+VERSION: 3.0.0
+DEFAULT: true
+ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping
+--DESCRIPTION--
+
+<p>
+ Whether or not to escape the dangerous characters &lt;, &gt; and &amp;
+ as \3C, \3E and \26, respectively. This is can be safely set to false
+ if the contents of StyleBlocks will be placed in an external stylesheet,
+ where there is no risk of it being interpreted as HTML.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt
new file mode 100644
index 000000000..7f95f54d1
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt
@@ -0,0 +1,29 @@
+Filter.ExtractStyleBlocks.Scope
+TYPE: string/null
+VERSION: 3.0.0
+DEFAULT: NULL
+ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope
+--DESCRIPTION--
+
+<p>
+ If you would like users to be able to define external stylesheets, but
+ only allow them to specify CSS declarations for a specific node and
+ prevent them from fiddling with other elements, use this directive.
+ It accepts any valid CSS selector, and will prepend this to any
+ CSS declaration extracted from the document. For example, if this
+ directive is set to <code>#user-content</code> and a user uses the
+ selector <code>a:hover</code>, the final selector will be
+ <code>#user-content a:hover</code>.
+</p>
+<p>
+ The comma shorthand may be used; consider the above example, with
+ <code>#user-content, #user-content2</code>, the final selector will
+ be <code>#user-content a:hover, #user-content2 a:hover</code>.
+</p>
+<p>
+ <strong>Warning:</strong> It is possible for users to bypass this measure
+ using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML
+ Purifier, and I am working to get it fixed. Until then, HTML Purifier
+ performs a basic check to prevent this.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt
new file mode 100644
index 000000000..6c231b2d7
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt
@@ -0,0 +1,16 @@
+Filter.ExtractStyleBlocks.TidyImpl
+TYPE: mixed/null
+VERSION: 3.1.0
+DEFAULT: NULL
+ALIASES: FilterParam.ExtractStyleBlocksTidyImpl
+--DESCRIPTION--
+<p>
+ If left NULL, HTML Purifier will attempt to instantiate a <code>csstidy</code>
+ class to use for internal cleaning. This will usually be good enough.
+</p>
+<p>
+ However, for trusted user input, you can set this to <code>false</code> to
+ disable cleaning. In addition, you can supply your own concrete implementation
+ of Tidy's interface to use, although I don't know why you'd want to do that.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt
new file mode 100644
index 000000000..078d08741
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt
@@ -0,0 +1,74 @@
+Filter.ExtractStyleBlocks
+TYPE: bool
+VERSION: 3.1.0
+DEFAULT: false
+EXTERNAL: CSSTidy
+--DESCRIPTION--
+<p>
+ This directive turns on the style block extraction filter, which removes
+ <code>style</code> blocks from input HTML, cleans them up with CSSTidy,
+ and places them in the <code>StyleBlocks</code> context variable, for further
+ use by you, usually to be placed in an external stylesheet, or a
+ <code>style</code> block in the <code>head</code> of your document.
+</p>
+<p>
+ Sample usage:
+</p>
+<pre><![CDATA[
+<?php
+ header('Content-type: text/html; charset=utf-8');
+ echo '<?xml version="1.0" encoding="UTF-8"?>';
+?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head>
+ <title>Filter.ExtractStyleBlocks</title>
+<?php
+ require_once '/path/to/library/HTMLPurifier.auto.php';
+ require_once '/path/to/csstidy.class.php';
+
+ $dirty = '<style>body {color:#F00;}</style> Some text';
+
+ $config = HTMLPurifier_Config::createDefault();
+ $config->set('Filter', 'ExtractStyleBlocks', true);
+ $purifier = new HTMLPurifier($config);
+
+ $html = $purifier->purify($dirty);
+
+ // This implementation writes the stylesheets to the styles/ directory.
+ // You can also echo the styles inside the document, but it's a bit
+ // more difficult to make sure they get interpreted properly by
+ // browsers; try the usual CSS armoring techniques.
+ $styles = $purifier->context->get('StyleBlocks');
+ $dir = 'styles/';
+ if (!is_dir($dir)) mkdir($dir);
+ $hash = sha1($_GET['html']);
+ foreach ($styles as $i => $style) {
+ file_put_contents($name = $dir . $hash . "_$i");
+ echo '<link rel="stylesheet" type="text/css" href="'.$name.'" />';
+ }
+?>
+</head>
+<body>
+ <div>
+ <?php echo $html; ?>
+ </div>
+</b]]><![CDATA[ody>
+</html>
+]]></pre>
+<p>
+ <strong>Warning:</strong> It is possible for a user to mount an
+ imagecrash attack using this CSS. Counter-measures are difficult;
+ it is not simply enough to limit the range of CSS lengths (using
+ relative lengths with many nesting levels allows for large values
+ to be attained without actually specifying them in the stylesheet),
+ and the flexible nature of selectors makes it difficult to selectively
+ disable lengths on image tags (HTML Purifier, however, does disable
+ CSS width and height in inline styling). There are probably two effective
+ counter measures: an explicit width and height set to auto in all
+ images in your document (unlikely) or the disabling of width and
+ height (somewhat reasonable). Whether or not these measures should be
+ used is left to the reader.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt
new file mode 100644
index 000000000..7fa6536b2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt
@@ -0,0 +1,11 @@
+Filter.YouTube
+TYPE: bool
+VERSION: 3.1.0
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ This directive enables YouTube video embedding in HTML Purifier. Check
+ <a href="http://htmlpurifier.org/docs/enduser-youtube.html">this document
+ on embedding videos</a> for more information on what this filter does.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt
new file mode 100644
index 000000000..3e231d2d1
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt
@@ -0,0 +1,22 @@
+HTML.Allowed
+TYPE: itext/null
+VERSION: 2.0.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ This is a convenience directive that rolls the functionality of
+ %HTML.AllowedElements and %HTML.AllowedAttributes into one directive.
+ Specify elements and attributes that are allowed using:
+ <code>element1[attr1|attr2],element2...</code>. You can also use
+ newlines instead of commas to separate elements.
+</p>
+<p>
+ <strong>Warning</strong>:
+ All of the constraints on the component directives are still enforced.
+ The syntax is a <em>subset</em> of TinyMCE's <code>valid_elements</code>
+ whitelist: directly copy-pasting it here will probably result in
+ broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes
+ are set, this directive has no effect.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt
new file mode 100644
index 000000000..fcf093f17
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt
@@ -0,0 +1,19 @@
+HTML.AllowedAttributes
+TYPE: lookup/null
+VERSION: 1.3.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ If HTML Purifier's attribute set is unsatisfactory, overload it!
+ The syntax is "tag.attr" or "*.attr" for the global attributes
+ (style, id, class, dir, lang, xml:lang).
+</p>
+<p>
+ <strong>Warning:</strong> If another directive conflicts with the
+ elements here, <em>that</em> directive will win and override. For
+ example, %HTML.EnableAttrID will take precedence over *.id in this
+ directive. You must set that directive to true before you can use
+ IDs at all.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt
new file mode 100644
index 000000000..888d55819
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt
@@ -0,0 +1,18 @@
+HTML.AllowedElements
+TYPE: lookup/null
+VERSION: 1.3.0
+DEFAULT: NULL
+--DESCRIPTION--
+<p>
+ If HTML Purifier's tag set is unsatisfactory for your needs, you
+ can overload it with your own list of tags to allow. Note that this
+ method is subtractive: it does its job by taking away from HTML Purifier
+ usual feature set, so you cannot add a tag that HTML Purifier never
+ supported in the first place (like embed, form or head). If you
+ change this, you probably also want to change %HTML.AllowedAttributes.
+</p>
+<p>
+ <strong>Warning:</strong> If another directive conflicts with the
+ elements here, <em>that</em> directive will win and override.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt
new file mode 100644
index 000000000..5a59a55c0
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt
@@ -0,0 +1,20 @@
+HTML.AllowedModules
+TYPE: lookup/null
+VERSION: 2.0.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ A doctype comes with a set of usual modules to use. Without having
+ to mucking about with the doctypes, you can quickly activate or
+ disable these modules by specifying which modules you wish to allow
+ with this directive. This is most useful for unit testing specific
+ modules, although end users may find it useful for their own ends.
+</p>
+<p>
+ If you specify a module that does not exist, the manager will silently
+ fail to use it, so be careful! User-defined modules are not affected
+ by this directive. Modules defined in %HTML.CoreModules are not
+ affected by this directive.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt
new file mode 100644
index 000000000..151fb7b82
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt
@@ -0,0 +1,11 @@
+HTML.Attr.Name.UseCDATA
+TYPE: bool
+DEFAULT: false
+VERSION: 4.0.0
+--DESCRIPTION--
+The W3C specification DTD defines the name attribute to be CDATA, not ID, due
+to limitations of DTD. In certain documents, this relaxed behavior is desired,
+whether it is to specify duplicate names, or to specify names that would be
+illegal IDs (for example, names that begin with a digit.) Set this configuration
+directive to true to use the relaxed parsing rules.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt
new file mode 100644
index 000000000..45ae469ec
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt
@@ -0,0 +1,18 @@
+HTML.BlockWrapper
+TYPE: string
+VERSION: 1.3.0
+DEFAULT: 'p'
+--DESCRIPTION--
+
+<p>
+ String name of element to wrap inline elements that are inside a block
+ context. This only occurs in the children of blockquote in strict mode.
+</p>
+<p>
+ Example: by default value,
+ <code>&lt;blockquote&gt;Foo&lt;/blockquote&gt;</code> would become
+ <code>&lt;blockquote&gt;&lt;p&gt;Foo&lt;/p&gt;&lt;/blockquote&gt;</code>.
+ The <code>&lt;p&gt;</code> tags can be replaced with whatever you desire,
+ as long as it is a block level element.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt
new file mode 100644
index 000000000..524618879
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt
@@ -0,0 +1,23 @@
+HTML.CoreModules
+TYPE: lookup
+VERSION: 2.0.0
+--DEFAULT--
+array (
+ 'Structure' => true,
+ 'Text' => true,
+ 'Hypertext' => true,
+ 'List' => true,
+ 'NonXMLCommonAttributes' => true,
+ 'XMLCommonAttributes' => true,
+ 'CommonAttributes' => true,
+)
+--DESCRIPTION--
+
+<p>
+ Certain modularized doctypes (XHTML, namely), have certain modules
+ that must be included for the doctype to be an conforming document
+ type: put those modules here. By default, XHTML's core modules
+ are used. You can set this to a blank array to disable core module
+ protection, but this is not recommended.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt
new file mode 100644
index 000000000..a64e3d7c3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt
@@ -0,0 +1,9 @@
+HTML.CustomDoctype
+TYPE: string/null
+VERSION: 2.0.1
+DEFAULT: NULL
+--DESCRIPTION--
+
+A custom doctype for power-users who defined there own document
+type. This directive only applies when %HTML.Doctype is blank.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt
new file mode 100644
index 000000000..103db754a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt
@@ -0,0 +1,33 @@
+HTML.DefinitionID
+TYPE: string/null
+DEFAULT: NULL
+VERSION: 2.0.0
+--DESCRIPTION--
+
+<p>
+ Unique identifier for a custom-built HTML definition. If you edit
+ the raw version of the HTMLDefinition, introducing changes that the
+ configuration object does not reflect, you must specify this variable.
+ If you change your custom edits, you should change this directive, or
+ clear your cache. Example:
+</p>
+<pre>
+$config = HTMLPurifier_Config::createDefault();
+$config->set('HTML', 'DefinitionID', '1');
+$def = $config->getHTMLDefinition();
+$def->addAttribute('a', 'tabindex', 'Number');
+</pre>
+<p>
+ In the above example, the configuration is still at the defaults, but
+ using the advanced API, an extra attribute has been added. The
+ configuration object normally has no way of knowing that this change
+ has taken place, so it needs an extra directive: %HTML.DefinitionID.
+ If someone else attempts to use the default configuration, these two
+ pieces of code will not clobber each other in the cache, since one has
+ an extra directive attached to it.
+</p>
+<p>
+ You <em>must</em> specify a value to this directive to use the
+ advanced API features.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt
new file mode 100644
index 000000000..229ae0267
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt
@@ -0,0 +1,16 @@
+HTML.DefinitionRev
+TYPE: int
+VERSION: 2.0.0
+DEFAULT: 1
+--DESCRIPTION--
+
+<p>
+ Revision identifier for your custom definition specified in
+ %HTML.DefinitionID. This serves the same purpose: uniquely identifying
+ your custom definition, but this one does so in a chronological
+ context: revision 3 is more up-to-date then revision 2. Thus, when
+ this gets incremented, the cache handling is smart enough to clean
+ up any older revisions of your definition as well as flush the
+ cache.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt
new file mode 100644
index 000000000..9dab497f2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt
@@ -0,0 +1,11 @@
+HTML.Doctype
+TYPE: string/null
+DEFAULT: NULL
+--DESCRIPTION--
+Doctype to use during filtering. Technically speaking this is not actually
+a doctype (as it does not identify a corresponding DTD), but we are using
+this name for sake of simplicity. When non-blank, this will override any
+older directives like %HTML.XHTML or %HTML.Strict.
+--ALLOWED--
+'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1'
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt
new file mode 100644
index 000000000..57358f9ba
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt
@@ -0,0 +1,21 @@
+HTML.ForbiddenAttributes
+TYPE: lookup
+VERSION: 3.1.0
+DEFAULT: array()
+--DESCRIPTION--
+<p>
+ While this directive is similar to %HTML.AllowedAttributes, for
+ forwards-compatibility with XML, this attribute has a different syntax. Instead of
+ <code>tag.attr</code>, use <code>tag@attr</code>. To disallow <code>href</code>
+ attributes in <code>a</code> tags, set this directive to
+ <code>a@href</code>. You can also disallow an attribute globally with
+ <code>attr</code> or <code>*@attr</code> (either syntax is fine; the latter
+ is provided for consistency with %HTML.AllowedAttributes).
+</p>
+<p>
+ <strong>Warning:</strong> This directive complements %HTML.ForbiddenElements,
+ accordingly, check
+ out that directive for a discussion of why you
+ should think twice before using this directive.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt
new file mode 100644
index 000000000..93a53e14f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt
@@ -0,0 +1,20 @@
+HTML.ForbiddenElements
+TYPE: lookup
+VERSION: 3.1.0
+DEFAULT: array()
+--DESCRIPTION--
+<p>
+ This was, perhaps, the most requested feature ever in HTML
+ Purifier. Please don't abuse it! This is the logical inverse of
+ %HTML.AllowedElements, and it will override that directive, or any
+ other directive.
+</p>
+<p>
+ If possible, %HTML.Allowed is recommended over this directive, because it
+ can sometimes be difficult to tell whether or not you've forbidden all of
+ the behavior you would like to disallow. If you forbid <code>img</code>
+ with the expectation of preventing images on your site, you'll be in for
+ a nasty surprise when people start using the <code>background-image</code>
+ CSS property.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt
new file mode 100644
index 000000000..e424c386e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt
@@ -0,0 +1,14 @@
+HTML.MaxImgLength
+TYPE: int/null
+DEFAULT: 1200
+VERSION: 3.1.1
+--DESCRIPTION--
+<p>
+ This directive controls the maximum number of pixels in the width and
+ height attributes in <code>img</code> tags. This is
+ in place to prevent imagecrash attacks, disable with null at your own risk.
+ This directive is similar to %CSS.MaxImgLength, and both should be
+ concurrently edited, although there are
+ subtle differences in the input format (the HTML max is an integer).
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt
new file mode 100644
index 000000000..62e8e160c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt
@@ -0,0 +1,12 @@
+HTML.Parent
+TYPE: string
+VERSION: 1.3.0
+DEFAULT: 'div'
+--DESCRIPTION--
+
+<p>
+ String name of element that HTML fragment passed to library will be
+ inserted in. An interesting variation would be using span as the
+ parent element, meaning that only inline tags would be allowed.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt
new file mode 100644
index 000000000..dfb720496
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt
@@ -0,0 +1,12 @@
+HTML.Proprietary
+TYPE: bool
+VERSION: 3.1.0
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ Whether or not to allow proprietary elements and attributes in your
+ documents, as per <code>HTMLPurifier_HTMLModule_Proprietary</code>.
+ <strong>Warning:</strong> This can cause your documents to stop
+ validating!
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt
new file mode 100644
index 000000000..f635a6854
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt
@@ -0,0 +1,14 @@
+HTML.SafeEmbed
+TYPE: bool
+VERSION: 3.1.1
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ Whether or not to permit embed tags in documents, with a number of extra
+ security features added to prevent script execution. This is similar to
+ what websites like MySpace do to embed tags. Embed is a proprietary
+ element and will cause your website to stop validating. You probably want
+ to enable this with %HTML.SafeObject.
+ <strong>Highly experimental.</strong>
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt
new file mode 100644
index 000000000..32967b88f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt
@@ -0,0 +1,14 @@
+HTML.SafeObject
+TYPE: bool
+VERSION: 3.1.1
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ Whether or not to permit object tags in documents, with a number of extra
+ security features added to prevent script execution. This is similar to
+ what websites like MySpace do to object tags. You may also want to
+ enable %HTML.SafeEmbed for maximum interoperability with Internet Explorer,
+ although embed tags will cause your website to stop validating.
+ <strong>Highly experimental.</strong>
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt
new file mode 100644
index 000000000..a8b1de56b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt
@@ -0,0 +1,9 @@
+HTML.Strict
+TYPE: bool
+VERSION: 1.3.0
+DEFAULT: false
+DEPRECATED-VERSION: 1.7.0
+DEPRECATED-USE: HTML.Doctype
+--DESCRIPTION--
+Determines whether or not to use Transitional (loose) or Strict rulesets.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt
new file mode 100644
index 000000000..b4c271b7f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt
@@ -0,0 +1,8 @@
+HTML.TidyAdd
+TYPE: lookup
+VERSION: 2.0.0
+DEFAULT: array()
+--DESCRIPTION--
+
+Fixes to add to the default set of Tidy fixes as per your level.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt
new file mode 100644
index 000000000..4186ccd0d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt
@@ -0,0 +1,24 @@
+HTML.TidyLevel
+TYPE: string
+VERSION: 2.0.0
+DEFAULT: 'medium'
+--DESCRIPTION--
+
+<p>General level of cleanliness the Tidy module should enforce.
+There are four allowed values:</p>
+<dl>
+ <dt>none</dt>
+ <dd>No extra tidying should be done</dd>
+ <dt>light</dt>
+ <dd>Only fix elements that would be discarded otherwise due to
+ lack of support in doctype</dd>
+ <dt>medium</dt>
+ <dd>Enforce best practices</dd>
+ <dt>heavy</dt>
+ <dd>Transform all deprecated elements and attributes to standards
+ compliant equivalents</dd>
+</dl>
+
+--ALLOWED--
+'none', 'light', 'medium', 'heavy'
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt
new file mode 100644
index 000000000..996762bd1
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt
@@ -0,0 +1,8 @@
+HTML.TidyRemove
+TYPE: lookup
+VERSION: 2.0.0
+DEFAULT: array()
+--DESCRIPTION--
+
+Fixes to remove from the default set of Tidy fixes as per your level.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt
new file mode 100644
index 000000000..89133b1a3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt
@@ -0,0 +1,8 @@
+HTML.Trusted
+TYPE: bool
+VERSION: 2.0.0
+DEFAULT: false
+--DESCRIPTION--
+Indicates whether or not the user input is trusted or not. If the input is
+trusted, a more expansive set of allowed tags and attributes will be used.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt
new file mode 100644
index 000000000..2a47e384f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt
@@ -0,0 +1,11 @@
+HTML.XHTML
+TYPE: bool
+DEFAULT: true
+VERSION: 1.1.0
+DEPRECATED-VERSION: 1.7.0
+DEPRECATED-USE: HTML.Doctype
+--DESCRIPTION--
+Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor.
+--ALIASES--
+Core.XHTML
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt
new file mode 100644
index 000000000..08921fde7
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt
@@ -0,0 +1,10 @@
+Output.CommentScriptContents
+TYPE: bool
+VERSION: 2.0.0
+DEFAULT: true
+--DESCRIPTION--
+Determines whether or not HTML Purifier should attempt to fix up the
+contents of script tags for legacy browsers with comments.
+--ALIASES--
+Core.CommentScriptContents
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt
new file mode 100644
index 000000000..79f8ad82c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt
@@ -0,0 +1,13 @@
+Output.Newline
+TYPE: string/null
+VERSION: 2.0.1
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ Newline string to format final output with. If left null, HTML Purifier
+ will auto-detect the default newline type of the system and use that;
+ you can manually override it here. Remember, \r\n is Windows, \r
+ is Mac, and \n is Unix.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt
new file mode 100644
index 000000000..232b02362
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt
@@ -0,0 +1,14 @@
+Output.SortAttr
+TYPE: bool
+VERSION: 3.2.0
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ If true, HTML Purifier will sort attributes by name before writing them back
+ to the document, converting a tag like: <code>&lt;el b="" a="" c="" /&gt;</code>
+ to <code>&lt;el a="" b="" c="" /&gt;</code>. This is a workaround for
+ a bug in FCKeditor which causes it to swap attributes order, adding noise
+ to text diffs. If you're not seeing this bug, chances are, you don't need
+ this directive.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt
new file mode 100644
index 000000000..06bab00a0
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt
@@ -0,0 +1,25 @@
+Output.TidyFormat
+TYPE: bool
+VERSION: 1.1.1
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ Determines whether or not to run Tidy on the final output for pretty
+ formatting reasons, such as indentation and wrap.
+</p>
+<p>
+ This can greatly improve readability for editors who are hand-editing
+ the HTML, but is by no means necessary as HTML Purifier has already
+ fixed all major errors the HTML may have had. Tidy is a non-default
+ extension, and this directive will silently fail if Tidy is not
+ available.
+</p>
+<p>
+ If you are looking to make the overall look of your page's source
+ better, I recommend running Tidy on the entire page rather than just
+ user-content (after all, the indentation relative to the containing
+ blocks will be incorrect).
+</p>
+--ALIASES--
+Core.TidyFormat
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt
new file mode 100644
index 000000000..071bc0295
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt
@@ -0,0 +1,7 @@
+Test.ForceNoIconv
+TYPE: bool
+DEFAULT: false
+--DESCRIPTION--
+When set to true, HTMLPurifier_Encoder will act as if iconv does not exist
+and use only pure PHP implementations.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt
new file mode 100644
index 000000000..98fdfe922
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt
@@ -0,0 +1,15 @@
+URI.AllowedSchemes
+TYPE: lookup
+--DEFAULT--
+array (
+ 'http' => true,
+ 'https' => true,
+ 'mailto' => true,
+ 'ftp' => true,
+ 'nntp' => true,
+ 'news' => true,
+)
+--DESCRIPTION--
+Whitelist that defines the schemes that a URI is allowed to have. This
+prevents XSS attacks from using pseudo-schemes like javascript or mocha.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt
new file mode 100644
index 000000000..876f0680c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt
@@ -0,0 +1,17 @@
+URI.Base
+TYPE: string/null
+VERSION: 2.1.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ The base URI is the URI of the document this purified HTML will be
+ inserted into. This information is important if HTML Purifier needs
+ to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute
+ is on. You may use a non-absolute URI for this value, but behavior
+ may vary (%URI.MakeAbsolute deals nicely with both absolute and
+ relative paths, but forwards-compatibility is not guaranteed).
+ <strong>Warning:</strong> If set, the scheme on this URI
+ overrides the one specified by %URI.DefaultScheme.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt
new file mode 100644
index 000000000..728e378cb
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt
@@ -0,0 +1,10 @@
+URI.DefaultScheme
+TYPE: string
+DEFAULT: 'http'
+--DESCRIPTION--
+
+<p>
+ Defines through what scheme the output will be served, in order to
+ select the proper object validator when no scheme information is present.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt
new file mode 100644
index 000000000..f05312ba8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt
@@ -0,0 +1,11 @@
+URI.DefinitionID
+TYPE: string/null
+VERSION: 2.1.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ Unique identifier for a custom-built URI definition. If you want
+ to add custom URIFilters, you must specify this value.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt
new file mode 100644
index 000000000..80cfea93f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt
@@ -0,0 +1,11 @@
+URI.DefinitionRev
+TYPE: int
+VERSION: 2.1.0
+DEFAULT: 1
+--DESCRIPTION--
+
+<p>
+ Revision identifier for your custom definition. See
+ %HTML.DefinitionRev for details.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt
new file mode 100644
index 000000000..71ce025a2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt
@@ -0,0 +1,14 @@
+URI.Disable
+TYPE: bool
+VERSION: 1.3.0
+DEFAULT: false
+--DESCRIPTION--
+
+<p>
+ Disables all URIs in all forms. Not sure why you'd want to do that
+ (after all, the Internet's founded on the notion of a hyperlink).
+</p>
+
+--ALIASES--
+Attr.DisableURI
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt
new file mode 100644
index 000000000..13c122c8c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt
@@ -0,0 +1,11 @@
+URI.DisableExternal
+TYPE: bool
+VERSION: 1.2.0
+DEFAULT: false
+--DESCRIPTION--
+Disables links to external websites. This is a highly effective anti-spam
+and anti-pagerank-leech measure, but comes at a hefty price: nolinks or
+images outside of your domain will be allowed. Non-linkified URIs will
+still be preserved. If you want to be able to link to subdomains or use
+absolute URIs, specify %URI.Host for your website.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt
new file mode 100644
index 000000000..abcc1efd6
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt
@@ -0,0 +1,13 @@
+URI.DisableExternalResources
+TYPE: bool
+VERSION: 1.3.0
+DEFAULT: false
+--DESCRIPTION--
+Disables the embedding of external resources, preventing users from
+embedding things like images from other hosts. This prevents access
+tracking (good for email viewers), bandwidth leeching, cross-site request
+forging, goatse.cx posting, and other nasties, but also results in a loss
+of end-user functionality (they can't directly post a pic they posted from
+Flickr anymore). Use it if you don't have a robust user-content moderation
+team.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt
new file mode 100644
index 000000000..51e6ea91f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt
@@ -0,0 +1,12 @@
+URI.DisableResources
+TYPE: bool
+VERSION: 1.3.0
+DEFAULT: false
+--DESCRIPTION--
+
+<p>
+ Disables embedding resources, essentially meaning no pictures. You can
+ still link to them though. See %URI.DisableExternalResources for why
+ this might be a good idea.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt
new file mode 100644
index 000000000..ee83b121d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt
@@ -0,0 +1,19 @@
+URI.Host
+TYPE: string/null
+VERSION: 1.2.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ Defines the domain name of the server, so we can determine whether or
+ an absolute URI is from your website or not. Not strictly necessary,
+ as users should be using relative URIs to reference resources on your
+ website. It will, however, let you use absolute URIs to link to
+ subdomains of the domain you post here: i.e. example.com will allow
+ sub.example.com. However, higher up domains will still be excluded:
+ if you set %URI.Host to sub.example.com, example.com will be blocked.
+ <strong>Note:</strong> This directive overrides %URI.Base because
+ a given page may be on a sub-domain, but you wish HTML Purifier to be
+ more relaxed and allow some of the parent domains too.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt
new file mode 100644
index 000000000..0b6df7625
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt
@@ -0,0 +1,9 @@
+URI.HostBlacklist
+TYPE: list
+VERSION: 1.3.0
+DEFAULT: array()
+--DESCRIPTION--
+List of strings that are forbidden in the host of any URI. Use it to kill
+domain names of spam, etc. Note that it will catch anything in the domain,
+so <tt>moo.com</tt> will catch <tt>moo.com.example.com</tt>.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt
new file mode 100644
index 000000000..4214900a5
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt
@@ -0,0 +1,13 @@
+URI.MakeAbsolute
+TYPE: bool
+VERSION: 2.1.0
+DEFAULT: false
+--DESCRIPTION--
+
+<p>
+ Converts all URIs into absolute forms. This is useful when the HTML
+ being filtered assumes a specific base path, but will actually be
+ viewed in a different context (and setting an alternate base URI is
+ not possible). %URI.Base must be set for this directive to work.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt
new file mode 100644
index 000000000..58c81dcc4
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt
@@ -0,0 +1,83 @@
+URI.Munge
+TYPE: string/null
+VERSION: 1.3.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+<p>
+ Munges all browsable (usually http, https and ftp)
+ absolute URIs into another URI, usually a URI redirection service.
+ This directive accepts a URI, formatted with a <code>%s</code> where
+ the url-encoded original URI should be inserted (sample:
+ <code>http://www.google.com/url?q=%s</code>).
+</p>
+<p>
+ Uses for this directive:
+</p>
+<ul>
+ <li>
+ Prevent PageRank leaks, while being fairly transparent
+ to users (you may also want to add some client side JavaScript to
+ override the text in the statusbar). <strong>Notice</strong>:
+ Many security experts believe that this form of protection does not deter spam-bots.
+ </li>
+ <li>
+ Redirect users to a splash page telling them they are leaving your
+ website. While this is poor usability practice, it is often mandated
+ in corporate environments.
+ </li>
+</ul>
+<p>
+ Prior to HTML Purifier 3.1.1, this directive also enabled the munging
+ of browsable external resources, which could break things if your redirection
+ script was a splash page or used <code>meta</code> tags. To revert to
+ previous behavior, please use %URI.MungeResources.
+</p>
+<p>
+ You may want to also use %URI.MungeSecretKey along with this directive
+ in order to enforce what URIs your redirector script allows. Open
+ redirector scripts can be a security risk and negatively affect the
+ reputation of your domain name.
+</p>
+<p>
+ Starting with HTML Purifier 3.1.1, there is also these substitutions:
+</p>
+<table>
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Description</th>
+ <th>Example <code>&lt;a href=""&gt;</code></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>%r</td>
+ <td>1 - The URI embeds a resource<br />(blank) - The URI is merely a link</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>%n</td>
+ <td>The name of the tag this URI came from</td>
+ <td>a</td>
+ </tr>
+ <tr>
+ <td>%m</td>
+ <td>The name of the attribute this URI came from</td>
+ <td>href</td>
+ </tr>
+ <tr>
+ <td>%p</td>
+ <td>The name of the CSS property this URI came from, or blank if irrelevant</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+<p>
+ Admittedly, these letters are somewhat arbitrary; the only stipulation
+ was that they couldn't be a through f. r is for resource (I would have preferred
+ e, but you take what you can get), n is for name, m
+ was picked because it came after n (and I couldn't use a), p is for
+ property.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt
new file mode 100644
index 000000000..6fce0fdc3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt
@@ -0,0 +1,17 @@
+URI.MungeResources
+TYPE: bool
+VERSION: 3.1.1
+DEFAULT: false
+--DESCRIPTION--
+<p>
+ If true, any URI munging directives like %URI.Munge
+ will also apply to embedded resources, such as <code>&lt;img src=""&gt;</code>.
+ Be careful enabling this directive if you have a redirector script
+ that does not use the <code>Location</code> HTTP header; all of your images
+ and other embedded resources will break.
+</p>
+<p>
+ <strong>Warning:</strong> It is strongly advised you use this in conjunction
+ %URI.MungeSecretKey to mitigate the security risk of an open redirector.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt
new file mode 100644
index 000000000..0d00f62ea
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt
@@ -0,0 +1,30 @@
+URI.MungeSecretKey
+TYPE: string/null
+VERSION: 3.1.1
+DEFAULT: NULL
+--DESCRIPTION--
+<p>
+ This directive enables secure checksum generation along with %URI.Munge.
+ It should be set to a secure key that is not shared with anyone else.
+ The checksum can be placed in the URI using %t. Use of this checksum
+ affords an additional level of protection by allowing a redirector
+ to check if a URI has passed through HTML Purifier with this line:
+</p>
+
+<pre>$checksum === sha1($secret_key . ':' . $url)</pre>
+
+<p>
+ If the output is TRUE, the redirector script should accept the URI.
+</p>
+
+<p>
+ Please note that it would still be possible for an attacker to procure
+ secure hashes en-mass by abusing your website's Preview feature or the
+ like, but this service affords an additional level of protection
+ that should be combined with website blacklisting.
+</p>
+
+<p>
+ Remember this has no effect if %URI.Munge is not on.
+</p>
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt
new file mode 100644
index 000000000..23331a4e7
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt
@@ -0,0 +1,9 @@
+URI.OverrideAllowedSchemes
+TYPE: bool
+DEFAULT: true
+--DESCRIPTION--
+If this is set to true (which it is by default), you can override
+%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the
+registry. If false, you will also have to update that directive in order
+to add more schemes.
+--# vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/info.ini b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/info.ini
new file mode 100644
index 000000000..5de4505e1
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/info.ini
@@ -0,0 +1,3 @@
+name = "HTML Purifier"
+
+; vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ContentSets.php b/extlib/HTMLPurifier/HTMLPurifier/ContentSets.php
new file mode 100644
index 000000000..3b6e96f5f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ContentSets.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @todo Unit test
+ */
+class HTMLPurifier_ContentSets
+{
+
+ /**
+ * List of content set strings (pipe seperators) indexed by name.
+ */
+ public $info = array();
+
+ /**
+ * List of content set lookups (element => true) indexed by name.
+ * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets
+ */
+ public $lookup = array();
+
+ /**
+ * Synchronized list of defined content sets (keys of info)
+ */
+ protected $keys = array();
+ /**
+ * Synchronized list of defined content values (values of info)
+ */
+ protected $values = array();
+
+ /**
+ * Merges in module's content sets, expands identifiers in the content
+ * sets and populates the keys, values and lookup member variables.
+ * @param $modules List of HTMLPurifier_HTMLModule
+ */
+ public function __construct($modules) {
+ if (!is_array($modules)) $modules = array($modules);
+ // populate content_sets based on module hints
+ // sorry, no way of overloading
+ foreach ($modules as $module_i => $module) {
+ foreach ($module->content_sets as $key => $value) {
+ $temp = $this->convertToLookup($value);
+ if (isset($this->lookup[$key])) {
+ // add it into the existing content set
+ $this->lookup[$key] = array_merge($this->lookup[$key], $temp);
+ } else {
+ $this->lookup[$key] = $temp;
+ }
+ }
+ }
+ $old_lookup = false;
+ while ($old_lookup !== $this->lookup) {
+ $old_lookup = $this->lookup;
+ foreach ($this->lookup as $i => $set) {
+ $add = array();
+ foreach ($set as $element => $x) {
+ if (isset($this->lookup[$element])) {
+ $add += $this->lookup[$element];
+ unset($this->lookup[$i][$element]);
+ }
+ }
+ $this->lookup[$i] += $add;
+ }
+ }
+
+ foreach ($this->lookup as $key => $lookup) {
+ $this->info[$key] = implode(' | ', array_keys($lookup));
+ }
+ $this->keys = array_keys($this->info);
+ $this->values = array_values($this->info);
+ }
+
+ /**
+ * Accepts a definition; generates and assigns a ChildDef for it
+ * @param $def HTMLPurifier_ElementDef reference
+ * @param $module Module that defined the ElementDef
+ */
+ public function generateChildDef(&$def, $module) {
+ if (!empty($def->child)) return; // already done!
+ $content_model = $def->content_model;
+ if (is_string($content_model)) {
+ // Assume that $this->keys is alphanumeric
+ $def->content_model = preg_replace_callback(
+ '/\b(' . implode('|', $this->keys) . ')\b/',
+ array($this, 'generateChildDefCallback'),
+ $content_model
+ );
+ //$def->content_model = str_replace(
+ // $this->keys, $this->values, $content_model);
+ }
+ $def->child = $this->getChildDef($def, $module);
+ }
+
+ public function generateChildDefCallback($matches) {
+ return $this->info[$matches[0]];
+ }
+
+ /**
+ * Instantiates a ChildDef based on content_model and content_model_type
+ * member variables in HTMLPurifier_ElementDef
+ * @note This will also defer to modules for custom HTMLPurifier_ChildDef
+ * subclasses that need content set expansion
+ * @param $def HTMLPurifier_ElementDef to have ChildDef extracted
+ * @return HTMLPurifier_ChildDef corresponding to ElementDef
+ */
+ public function getChildDef($def, $module) {
+ $value = $def->content_model;
+ if (is_object($value)) {
+ trigger_error(
+ 'Literal object child definitions should be stored in '.
+ 'ElementDef->child not ElementDef->content_model',
+ E_USER_NOTICE
+ );
+ return $value;
+ }
+ switch ($def->content_model_type) {
+ case 'required':
+ return new HTMLPurifier_ChildDef_Required($value);
+ case 'optional':
+ return new HTMLPurifier_ChildDef_Optional($value);
+ case 'empty':
+ return new HTMLPurifier_ChildDef_Empty();
+ case 'custom':
+ return new HTMLPurifier_ChildDef_Custom($value);
+ }
+ // defer to its module
+ $return = false;
+ if ($module->defines_child_def) { // save a func call
+ $return = $module->getChildDef($def);
+ }
+ if ($return !== false) return $return;
+ // error-out
+ trigger_error(
+ 'Could not determine which ChildDef class to instantiate',
+ E_USER_ERROR
+ );
+ return false;
+ }
+
+ /**
+ * Converts a string list of elements separated by pipes into
+ * a lookup array.
+ * @param $string List of elements
+ * @return Lookup array of elements
+ */
+ protected function convertToLookup($string) {
+ $array = explode('|', str_replace(' ', '', $string));
+ $ret = array();
+ foreach ($array as $i => $k) {
+ $ret[$k] = true;
+ }
+ return $ret;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Context.php b/extlib/HTMLPurifier/HTMLPurifier/Context.php
new file mode 100644
index 000000000..9ddf0c547
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Context.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Registry object that contains information about the current context.
+ * @warning Is a bit buggy when variables are set to null: it thinks
+ * they don't exist! So use false instead, please.
+ * @note Since the variables Context deals with may not be objects,
+ * references are very important here! Do not remove!
+ */
+class HTMLPurifier_Context
+{
+
+ /**
+ * Private array that stores the references.
+ */
+ private $_storage = array();
+
+ /**
+ * Registers a variable into the context.
+ * @param $name String name
+ * @param $ref Reference to variable to be registered
+ */
+ public function register($name, &$ref) {
+ if (isset($this->_storage[$name])) {
+ trigger_error("Name $name produces collision, cannot re-register",
+ E_USER_ERROR);
+ return;
+ }
+ $this->_storage[$name] =& $ref;
+ }
+
+ /**
+ * Retrieves a variable reference from the context.
+ * @param $name String name
+ * @param $ignore_error Boolean whether or not to ignore error
+ */
+ public function &get($name, $ignore_error = false) {
+ if (!isset($this->_storage[$name])) {
+ if (!$ignore_error) {
+ trigger_error("Attempted to retrieve non-existent variable $name",
+ E_USER_ERROR);
+ }
+ $var = null; // so we can return by reference
+ return $var;
+ }
+ return $this->_storage[$name];
+ }
+
+ /**
+ * Destorys a variable in the context.
+ * @param $name String name
+ */
+ public function destroy($name) {
+ if (!isset($this->_storage[$name])) {
+ trigger_error("Attempted to destroy non-existent variable $name",
+ E_USER_ERROR);
+ return;
+ }
+ unset($this->_storage[$name]);
+ }
+
+ /**
+ * Checks whether or not the variable exists.
+ * @param $name String name
+ */
+ public function exists($name) {
+ return isset($this->_storage[$name]);
+ }
+
+ /**
+ * Loads a series of variables from an associative array
+ * @param $context_array Assoc array of variables to load
+ */
+ public function loadArray($context_array) {
+ foreach ($context_array as $key => $discard) {
+ $this->register($key, $context_array[$key]);
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Definition.php b/extlib/HTMLPurifier/HTMLPurifier/Definition.php
new file mode 100644
index 000000000..a7408c974
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Definition.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * Super-class for definition datatype objects, implements serialization
+ * functions for the class.
+ */
+abstract class HTMLPurifier_Definition
+{
+
+ /**
+ * Has setup() been called yet?
+ */
+ public $setup = false;
+
+ /**
+ * What type of definition is it?
+ */
+ public $type;
+
+ /**
+ * Sets up the definition object into the final form, something
+ * not done by the constructor
+ * @param $config HTMLPurifier_Config instance
+ */
+ abstract protected function doSetup($config);
+
+ /**
+ * Setup function that aborts if already setup
+ * @param $config HTMLPurifier_Config instance
+ */
+ public function setup($config) {
+ if ($this->setup) return;
+ $this->setup = true;
+ $this->doSetup($config);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache.php
new file mode 100644
index 000000000..c6e1e388c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * Abstract class representing Definition cache managers that implements
+ * useful common methods and is a factory.
+ * @todo Create a separate maintenance file advanced users can use to
+ * cache their custom HTMLDefinition, which can be loaded
+ * via a configuration directive
+ * @todo Implement memcached
+ */
+abstract class HTMLPurifier_DefinitionCache
+{
+
+ public $type;
+
+ /**
+ * @param $name Type of definition objects this instance of the
+ * cache will handle.
+ */
+ public function __construct($type) {
+ $this->type = $type;
+ }
+
+ /**
+ * Generates a unique identifier for a particular configuration
+ * @param Instance of HTMLPurifier_Config
+ */
+ public function generateKey($config) {
+ return $config->version . ',' . // possibly replace with function calls
+ $config->getBatchSerial($this->type) . ',' .
+ $config->get($this->type . '.DefinitionRev');
+ }
+
+ /**
+ * Tests whether or not a key is old with respect to the configuration's
+ * version and revision number.
+ * @param $key Key to test
+ * @param $config Instance of HTMLPurifier_Config to test against
+ */
+ public function isOld($key, $config) {
+ if (substr_count($key, ',') < 2) return true;
+ list($version, $hash, $revision) = explode(',', $key, 3);
+ $compare = version_compare($version, $config->version);
+ // version mismatch, is always old
+ if ($compare != 0) return true;
+ // versions match, ids match, check revision number
+ if (
+ $hash == $config->getBatchSerial($this->type) &&
+ $revision < $config->get($this->type . '.DefinitionRev')
+ ) return true;
+ return false;
+ }
+
+ /**
+ * Checks if a definition's type jives with the cache's type
+ * @note Throws an error on failure
+ * @param $def Definition object to check
+ * @return Boolean true if good, false if not
+ */
+ public function checkDefType($def) {
+ if ($def->type !== $this->type) {
+ trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Adds a definition object to the cache
+ */
+ abstract public function add($def, $config);
+
+ /**
+ * Unconditionally saves a definition object to the cache
+ */
+ abstract public function set($def, $config);
+
+ /**
+ * Replace an object in the cache
+ */
+ abstract public function replace($def, $config);
+
+ /**
+ * Retrieves a definition object from the cache
+ */
+ abstract public function get($config);
+
+ /**
+ * Removes a definition object to the cache
+ */
+ abstract public function remove($config);
+
+ /**
+ * Clears all objects from cache
+ */
+ abstract public function flush($config);
+
+ /**
+ * Clears all expired (older version or revision) objects from cache
+ * @note Be carefuly implementing this method as flush. Flush must
+ * not interfere with other Definition types, and cleanup()
+ * should not be repeatedly called by userland code.
+ */
+ abstract public function cleanup($config);
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator.php
new file mode 100644
index 000000000..b0fb6d0cd
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator.php
@@ -0,0 +1,62 @@
+<?php
+
+class HTMLPurifier_DefinitionCache_Decorator extends HTMLPurifier_DefinitionCache
+{
+
+ /**
+ * Cache object we are decorating
+ */
+ public $cache;
+
+ public function __construct() {}
+
+ /**
+ * Lazy decorator function
+ * @param $cache Reference to cache object to decorate
+ */
+ public function decorate(&$cache) {
+ $decorator = $this->copy();
+ // reference is necessary for mocks in PHP 4
+ $decorator->cache =& $cache;
+ $decorator->type = $cache->type;
+ return $decorator;
+ }
+
+ /**
+ * Cross-compatible clone substitute
+ */
+ public function copy() {
+ return new HTMLPurifier_DefinitionCache_Decorator();
+ }
+
+ public function add($def, $config) {
+ return $this->cache->add($def, $config);
+ }
+
+ public function set($def, $config) {
+ return $this->cache->set($def, $config);
+ }
+
+ public function replace($def, $config) {
+ return $this->cache->replace($def, $config);
+ }
+
+ public function get($config) {
+ return $this->cache->get($config);
+ }
+
+ public function remove($config) {
+ return $this->cache->remove($config);
+ }
+
+ public function flush($config) {
+ return $this->cache->flush($config);
+ }
+
+ public function cleanup($config) {
+ return $this->cache->cleanup($config);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php
new file mode 100644
index 000000000..d4cc35c4b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Definition cache decorator class that cleans up the cache
+ * whenever there is a cache miss.
+ */
+class HTMLPurifier_DefinitionCache_Decorator_Cleanup extends
+ HTMLPurifier_DefinitionCache_Decorator
+{
+
+ public $name = 'Cleanup';
+
+ public function copy() {
+ return new HTMLPurifier_DefinitionCache_Decorator_Cleanup();
+ }
+
+ public function add($def, $config) {
+ $status = parent::add($def, $config);
+ if (!$status) parent::cleanup($config);
+ return $status;
+ }
+
+ public function set($def, $config) {
+ $status = parent::set($def, $config);
+ if (!$status) parent::cleanup($config);
+ return $status;
+ }
+
+ public function replace($def, $config) {
+ $status = parent::replace($def, $config);
+ if (!$status) parent::cleanup($config);
+ return $status;
+ }
+
+ public function get($config) {
+ $ret = parent::get($config);
+ if (!$ret) parent::cleanup($config);
+ return $ret;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Memory.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Memory.php
new file mode 100644
index 000000000..18f16d32b
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Memory.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Definition cache decorator class that saves all cache retrievals
+ * to PHP's memory; good for unit tests or circumstances where
+ * there are lots of configuration objects floating around.
+ */
+class HTMLPurifier_DefinitionCache_Decorator_Memory extends
+ HTMLPurifier_DefinitionCache_Decorator
+{
+
+ protected $definitions;
+ public $name = 'Memory';
+
+ public function copy() {
+ return new HTMLPurifier_DefinitionCache_Decorator_Memory();
+ }
+
+ public function add($def, $config) {
+ $status = parent::add($def, $config);
+ if ($status) $this->definitions[$this->generateKey($config)] = $def;
+ return $status;
+ }
+
+ public function set($def, $config) {
+ $status = parent::set($def, $config);
+ if ($status) $this->definitions[$this->generateKey($config)] = $def;
+ return $status;
+ }
+
+ public function replace($def, $config) {
+ $status = parent::replace($def, $config);
+ if ($status) $this->definitions[$this->generateKey($config)] = $def;
+ return $status;
+ }
+
+ public function get($config) {
+ $key = $this->generateKey($config);
+ if (isset($this->definitions[$key])) return $this->definitions[$key];
+ $this->definitions[$key] = parent::get($config);
+ return $this->definitions[$key];
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in
new file mode 100644
index 000000000..21a8fcfda
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in
@@ -0,0 +1,47 @@
+<?php
+
+require_once 'HTMLPurifier/DefinitionCache/Decorator.php';
+
+/**
+ * Definition cache decorator template.
+ */
+class HTMLPurifier_DefinitionCache_Decorator_Template extends
+ HTMLPurifier_DefinitionCache_Decorator
+{
+
+ var $name = 'Template'; // replace this
+
+ function copy() {
+ // replace class name with yours
+ return new HTMLPurifier_DefinitionCache_Decorator_Template();
+ }
+
+ // remove methods you don't need
+
+ function add($def, $config) {
+ return parent::add($def, $config);
+ }
+
+ function set($def, $config) {
+ return parent::set($def, $config);
+ }
+
+ function replace($def, $config) {
+ return parent::replace($def, $config);
+ }
+
+ function get($config) {
+ return parent::get($config);
+ }
+
+ function flush() {
+ return parent::flush();
+ }
+
+ function cleanup($config) {
+ return parent::cleanup($config);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Null.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Null.php
new file mode 100644
index 000000000..41d97e734
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Null.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * Null cache object to use when no caching is on.
+ */
+class HTMLPurifier_DefinitionCache_Null extends HTMLPurifier_DefinitionCache
+{
+
+ public function add($def, $config) {
+ return false;
+ }
+
+ public function set($def, $config) {
+ return false;
+ }
+
+ public function replace($def, $config) {
+ return false;
+ }
+
+ public function remove($config) {
+ return false;
+ }
+
+ public function get($config) {
+ return false;
+ }
+
+ public function flush($config) {
+ return false;
+ }
+
+ public function cleanup($config) {
+ return false;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer.php
new file mode 100644
index 000000000..7a6aa93f0
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer.php
@@ -0,0 +1,172 @@
+<?php
+
+class HTMLPurifier_DefinitionCache_Serializer extends
+ HTMLPurifier_DefinitionCache
+{
+
+ public function add($def, $config) {
+ if (!$this->checkDefType($def)) return;
+ $file = $this->generateFilePath($config);
+ if (file_exists($file)) return false;
+ if (!$this->_prepareDir($config)) return false;
+ return $this->_write($file, serialize($def));
+ }
+
+ public function set($def, $config) {
+ if (!$this->checkDefType($def)) return;
+ $file = $this->generateFilePath($config);
+ if (!$this->_prepareDir($config)) return false;
+ return $this->_write($file, serialize($def));
+ }
+
+ public function replace($def, $config) {
+ if (!$this->checkDefType($def)) return;
+ $file = $this->generateFilePath($config);
+ if (!file_exists($file)) return false;
+ if (!$this->_prepareDir($config)) return false;
+ return $this->_write($file, serialize($def));
+ }
+
+ public function get($config) {
+ $file = $this->generateFilePath($config);
+ if (!file_exists($file)) return false;
+ return unserialize(file_get_contents($file));
+ }
+
+ public function remove($config) {
+ $file = $this->generateFilePath($config);
+ if (!file_exists($file)) return false;
+ return unlink($file);
+ }
+
+ public function flush($config) {
+ if (!$this->_prepareDir($config)) return false;
+ $dir = $this->generateDirectoryPath($config);
+ $dh = opendir($dir);
+ while (false !== ($filename = readdir($dh))) {
+ if (empty($filename)) continue;
+ if ($filename[0] === '.') continue;
+ unlink($dir . '/' . $filename);
+ }
+ }
+
+ public function cleanup($config) {
+ if (!$this->_prepareDir($config)) return false;
+ $dir = $this->generateDirectoryPath($config);
+ $dh = opendir($dir);
+ while (false !== ($filename = readdir($dh))) {
+ if (empty($filename)) continue;
+ if ($filename[0] === '.') continue;
+ $key = substr($filename, 0, strlen($filename) - 4);
+ if ($this->isOld($key, $config)) unlink($dir . '/' . $filename);
+ }
+ }
+
+ /**
+ * Generates the file path to the serial file corresponding to
+ * the configuration and definition name
+ * @todo Make protected
+ */
+ public function generateFilePath($config) {
+ $key = $this->generateKey($config);
+ return $this->generateDirectoryPath($config) . '/' . $key . '.ser';
+ }
+
+ /**
+ * Generates the path to the directory contain this cache's serial files
+ * @note No trailing slash
+ * @todo Make protected
+ */
+ public function generateDirectoryPath($config) {
+ $base = $this->generateBaseDirectoryPath($config);
+ return $base . '/' . $this->type;
+ }
+
+ /**
+ * Generates path to base directory that contains all definition type
+ * serials
+ * @todo Make protected
+ */
+ public function generateBaseDirectoryPath($config) {
+ $base = $config->get('Cache.SerializerPath');
+ $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base;
+ return $base;
+ }
+
+ /**
+ * Convenience wrapper function for file_put_contents
+ * @param $file File name to write to
+ * @param $data Data to write into file
+ * @return Number of bytes written if success, or false if failure.
+ */
+ private function _write($file, $data) {
+ return file_put_contents($file, $data);
+ }
+
+ /**
+ * Prepares the directory that this type stores the serials in
+ * @return True if successful
+ */
+ private function _prepareDir($config) {
+ $directory = $this->generateDirectoryPath($config);
+ if (!is_dir($directory)) {
+ $base = $this->generateBaseDirectoryPath($config);
+ if (!is_dir($base)) {
+ trigger_error('Base directory '.$base.' does not exist,
+ please create or change using %Cache.SerializerPath',
+ E_USER_WARNING);
+ return false;
+ } elseif (!$this->_testPermissions($base)) {
+ return false;
+ }
+ $old = umask(0022); // disable group and world writes
+ mkdir($directory);
+ umask($old);
+ } elseif (!$this->_testPermissions($directory)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Tests permissions on a directory and throws out friendly
+ * error messages and attempts to chmod it itself if possible
+ */
+ private function _testPermissions($dir) {
+ // early abort, if it is writable, everything is hunky-dory
+ if (is_writable($dir)) return true;
+ if (!is_dir($dir)) {
+ // generally, you'll want to handle this beforehand
+ // so a more specific error message can be given
+ trigger_error('Directory '.$dir.' does not exist',
+ E_USER_WARNING);
+ return false;
+ }
+ if (function_exists('posix_getuid')) {
+ // POSIX system, we can give more specific advice
+ if (fileowner($dir) === posix_getuid()) {
+ // we can chmod it ourselves
+ chmod($dir, 0755);
+ return true;
+ } elseif (filegroup($dir) === posix_getgid()) {
+ $chmod = '775';
+ } else {
+ // PHP's probably running as nobody, so we'll
+ // need to give global permissions
+ $chmod = '777';
+ }
+ trigger_error('Directory '.$dir.' not writable, '.
+ 'please chmod to ' . $chmod,
+ E_USER_WARNING);
+ } else {
+ // generic error message
+ trigger_error('Directory '.$dir.' not writable, '.
+ 'please alter file permissions',
+ E_USER_WARNING);
+ }
+ return false;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer/README b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer/README
new file mode 100755
index 000000000..2e35c1c3d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer/README
@@ -0,0 +1,3 @@
+This is a dummy file to prevent Git from ignoring this empty directory.
+
+ vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCacheFactory.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCacheFactory.php
new file mode 100644
index 000000000..a6ead6281
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCacheFactory.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * Responsible for creating definition caches.
+ */
+class HTMLPurifier_DefinitionCacheFactory
+{
+
+ protected $caches = array('Serializer' => array());
+ protected $implementations = array();
+ protected $decorators = array();
+
+ /**
+ * Initialize default decorators
+ */
+ public function setup() {
+ $this->addDecorator('Cleanup');
+ }
+
+ /**
+ * Retrieves an instance of global definition cache factory.
+ */
+ public static function instance($prototype = null) {
+ static $instance;
+ if ($prototype !== null) {
+ $instance = $prototype;
+ } elseif ($instance === null || $prototype === true) {
+ $instance = new HTMLPurifier_DefinitionCacheFactory();
+ $instance->setup();
+ }
+ return $instance;
+ }
+
+ /**
+ * Registers a new definition cache object
+ * @param $short Short name of cache object, for reference
+ * @param $long Full class name of cache object, for construction
+ */
+ public function register($short, $long) {
+ $this->implementations[$short] = $long;
+ }
+
+ /**
+ * Factory method that creates a cache object based on configuration
+ * @param $name Name of definitions handled by cache
+ * @param $config Instance of HTMLPurifier_Config
+ */
+ public function create($type, $config) {
+ $method = $config->get('Cache.DefinitionImpl');
+ if ($method === null) {
+ return new HTMLPurifier_DefinitionCache_Null($type);
+ }
+ if (!empty($this->caches[$method][$type])) {
+ return $this->caches[$method][$type];
+ }
+ if (
+ isset($this->implementations[$method]) &&
+ class_exists($class = $this->implementations[$method], false)
+ ) {
+ $cache = new $class($type);
+ } else {
+ if ($method != 'Serializer') {
+ trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING);
+ }
+ $cache = new HTMLPurifier_DefinitionCache_Serializer($type);
+ }
+ foreach ($this->decorators as $decorator) {
+ $new_cache = $decorator->decorate($cache);
+ // prevent infinite recursion in PHP 4
+ unset($cache);
+ $cache = $new_cache;
+ }
+ $this->caches[$method][$type] = $cache;
+ return $this->caches[$method][$type];
+ }
+
+ /**
+ * Registers a decorator to add to all new cache objects
+ * @param
+ */
+ public function addDecorator($decorator) {
+ if (is_string($decorator)) {
+ $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator";
+ $decorator = new $class;
+ }
+ $this->decorators[$decorator->name] = $decorator;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Doctype.php b/extlib/HTMLPurifier/HTMLPurifier/Doctype.php
new file mode 100644
index 000000000..1e3c574c0
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Doctype.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * Represents a document type, contains information on which modules
+ * need to be loaded.
+ * @note This class is inspected by Printer_HTMLDefinition->renderDoctype.
+ * If structure changes, please update that function.
+ */
+class HTMLPurifier_Doctype
+{
+ /**
+ * Full name of doctype
+ */
+ public $name;
+
+ /**
+ * List of standard modules (string identifiers or literal objects)
+ * that this doctype uses
+ */
+ public $modules = array();
+
+ /**
+ * List of modules to use for tidying up code
+ */
+ public $tidyModules = array();
+
+ /**
+ * Is the language derived from XML (i.e. XHTML)?
+ */
+ public $xml = true;
+
+ /**
+ * List of aliases for this doctype
+ */
+ public $aliases = array();
+
+ /**
+ * Public DTD identifier
+ */
+ public $dtdPublic;
+
+ /**
+ * System DTD identifier
+ */
+ public $dtdSystem;
+
+ public function __construct($name = null, $xml = true, $modules = array(),
+ $tidyModules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
+ ) {
+ $this->name = $name;
+ $this->xml = $xml;
+ $this->modules = $modules;
+ $this->tidyModules = $tidyModules;
+ $this->aliases = $aliases;
+ $this->dtdPublic = $dtd_public;
+ $this->dtdSystem = $dtd_system;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/DoctypeRegistry.php b/extlib/HTMLPurifier/HTMLPurifier/DoctypeRegistry.php
new file mode 100644
index 000000000..86049e939
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/DoctypeRegistry.php
@@ -0,0 +1,103 @@
+<?php
+
+class HTMLPurifier_DoctypeRegistry
+{
+
+ /**
+ * Hash of doctype names to doctype objects
+ */
+ protected $doctypes;
+
+ /**
+ * Lookup table of aliases to real doctype names
+ */
+ protected $aliases;
+
+ /**
+ * Registers a doctype to the registry
+ * @note Accepts a fully-formed doctype object, or the
+ * parameters for constructing a doctype object
+ * @param $doctype Name of doctype or literal doctype object
+ * @param $modules Modules doctype will load
+ * @param $modules_for_modes Modules doctype will load for certain modes
+ * @param $aliases Alias names for doctype
+ * @return Editable registered doctype
+ */
+ public function register($doctype, $xml = true, $modules = array(),
+ $tidy_modules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null
+ ) {
+ if (!is_array($modules)) $modules = array($modules);
+ if (!is_array($tidy_modules)) $tidy_modules = array($tidy_modules);
+ if (!is_array($aliases)) $aliases = array($aliases);
+ if (!is_object($doctype)) {
+ $doctype = new HTMLPurifier_Doctype(
+ $doctype, $xml, $modules, $tidy_modules, $aliases, $dtd_public, $dtd_system
+ );
+ }
+ $this->doctypes[$doctype->name] = $doctype;
+ $name = $doctype->name;
+ // hookup aliases
+ foreach ($doctype->aliases as $alias) {
+ if (isset($this->doctypes[$alias])) continue;
+ $this->aliases[$alias] = $name;
+ }
+ // remove old aliases
+ if (isset($this->aliases[$name])) unset($this->aliases[$name]);
+ return $doctype;
+ }
+
+ /**
+ * Retrieves reference to a doctype of a certain name
+ * @note This function resolves aliases
+ * @note When possible, use the more fully-featured make()
+ * @param $doctype Name of doctype
+ * @return Editable doctype object
+ */
+ public function get($doctype) {
+ if (isset($this->aliases[$doctype])) $doctype = $this->aliases[$doctype];
+ if (!isset($this->doctypes[$doctype])) {
+ trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR);
+ $anon = new HTMLPurifier_Doctype($doctype);
+ return $anon;
+ }
+ return $this->doctypes[$doctype];
+ }
+
+ /**
+ * Creates a doctype based on a configuration object,
+ * will perform initialization on the doctype
+ * @note Use this function to get a copy of doctype that config
+ * can hold on to (this is necessary in order to tell
+ * Generator whether or not the current document is XML
+ * based or not).
+ */
+ public function make($config) {
+ return clone $this->get($this->getDoctypeFromConfig($config));
+ }
+
+ /**
+ * Retrieves the doctype from the configuration object
+ */
+ public function getDoctypeFromConfig($config) {
+ // recommended test
+ $doctype = $config->get('HTML.Doctype');
+ if (!empty($doctype)) return $doctype;
+ $doctype = $config->get('HTML.CustomDoctype');
+ if (!empty($doctype)) return $doctype;
+ // backwards-compatibility
+ if ($config->get('HTML.XHTML')) {
+ $doctype = 'XHTML 1.0';
+ } else {
+ $doctype = 'HTML 4.01';
+ }
+ if ($config->get('HTML.Strict')) {
+ $doctype .= ' Strict';
+ } else {
+ $doctype .= ' Transitional';
+ }
+ return $doctype;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ElementDef.php b/extlib/HTMLPurifier/HTMLPurifier/ElementDef.php
new file mode 100644
index 000000000..aede2c3bb
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ElementDef.php
@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * Structure that stores an HTML element definition. Used by
+ * HTMLPurifier_HTMLDefinition and HTMLPurifier_HTMLModule.
+ * @note This class is inspected by HTMLPurifier_Printer_HTMLDefinition.
+ * Please update that class too.
+ * @warning If you add new properties to this class, you MUST update
+ * the mergeIn() method.
+ */
+class HTMLPurifier_ElementDef
+{
+
+ /**
+ * Does the definition work by itself, or is it created solely
+ * for the purpose of merging into another definition?
+ */
+ public $standalone = true;
+
+ /**
+ * Associative array of attribute name to HTMLPurifier_AttrDef
+ * @note Before being processed by HTMLPurifier_AttrCollections
+ * when modules are finalized during
+ * HTMLPurifier_HTMLDefinition->setup(), this array may also
+ * contain an array at index 0 that indicates which attribute
+ * collections to load into the full array. It may also
+ * contain string indentifiers in lieu of HTMLPurifier_AttrDef,
+ * see HTMLPurifier_AttrTypes on how they are expanded during
+ * HTMLPurifier_HTMLDefinition->setup() processing.
+ */
+ public $attr = array();
+
+ /**
+ * Indexed list of tag's HTMLPurifier_AttrTransform to be done before validation
+ */
+ public $attr_transform_pre = array();
+
+ /**
+ * Indexed list of tag's HTMLPurifier_AttrTransform to be done after validation
+ */
+ public $attr_transform_post = array();
+
+ /**
+ * HTMLPurifier_ChildDef of this tag.
+ */
+ public $child;
+
+ /**
+ * Abstract string representation of internal ChildDef rules. See
+ * HTMLPurifier_ContentSets for how this is parsed and then transformed
+ * into an HTMLPurifier_ChildDef.
+ * @warning This is a temporary variable that is not available after
+ * being processed by HTMLDefinition
+ */
+ public $content_model;
+
+ /**
+ * Value of $child->type, used to determine which ChildDef to use,
+ * used in combination with $content_model.
+ * @warning This must be lowercase
+ * @warning This is a temporary variable that is not available after
+ * being processed by HTMLDefinition
+ */
+ public $content_model_type;
+
+
+
+ /**
+ * Does the element have a content model (#PCDATA | Inline)*? This
+ * is important for chameleon ins and del processing in
+ * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't
+ * have to worry about this one.
+ */
+ public $descendants_are_inline = false;
+
+ /**
+ * List of the names of required attributes this element has. Dynamically
+ * populated by HTMLPurifier_HTMLDefinition::getElement
+ */
+ public $required_attr = array();
+
+ /**
+ * Lookup table of tags excluded from all descendants of this tag.
+ * @note SGML permits exclusions for all descendants, but this is
+ * not possible with DTDs or XML Schemas. W3C has elected to
+ * use complicated compositions of content_models to simulate
+ * exclusion for children, but we go the simpler, SGML-style
+ * route of flat-out exclusions, which correctly apply to
+ * all descendants and not just children. Note that the XHTML
+ * Modularization Abstract Modules are blithely unaware of such
+ * distinctions.
+ */
+ public $excludes = array();
+
+ /**
+ * This tag is explicitly auto-closed by the following tags.
+ */
+ public $autoclose = array();
+
+ /**
+ * Whether or not this is a formatting element affected by the
+ * "Active Formatting Elements" algorithm.
+ */
+ public $formatting;
+
+ /**
+ * Low-level factory constructor for creating new standalone element defs
+ */
+ public static function create($content_model, $content_model_type, $attr) {
+ $def = new HTMLPurifier_ElementDef();
+ $def->content_model = $content_model;
+ $def->content_model_type = $content_model_type;
+ $def->attr = $attr;
+ return $def;
+ }
+
+ /**
+ * Merges the values of another element definition into this one.
+ * Values from the new element def take precedence if a value is
+ * not mergeable.
+ */
+ public function mergeIn($def) {
+
+ // later keys takes precedence
+ foreach($def->attr as $k => $v) {
+ if ($k === 0) {
+ // merge in the includes
+ // sorry, no way to override an include
+ foreach ($v as $v2) {
+ $this->attr[0][] = $v2;
+ }
+ continue;
+ }
+ if ($v === false) {
+ if (isset($this->attr[$k])) unset($this->attr[$k]);
+ continue;
+ }
+ $this->attr[$k] = $v;
+ }
+ $this->_mergeAssocArray($this->attr_transform_pre, $def->attr_transform_pre);
+ $this->_mergeAssocArray($this->attr_transform_post, $def->attr_transform_post);
+ $this->_mergeAssocArray($this->excludes, $def->excludes);
+
+ if(!empty($def->content_model)) {
+ $this->content_model =
+ str_replace("#SUPER", $this->content_model, $def->content_model);
+ $this->child = false;
+ }
+ if(!empty($def->content_model_type)) {
+ $this->content_model_type = $def->content_model_type;
+ $this->child = false;
+ }
+ if(!is_null($def->child)) $this->child = $def->child;
+ if(!is_null($def->formatting)) $this->formatting = $def->formatting;
+ if($def->descendants_are_inline) $this->descendants_are_inline = $def->descendants_are_inline;
+
+ }
+
+ /**
+ * Merges one array into another, removes values which equal false
+ * @param $a1 Array by reference that is merged into
+ * @param $a2 Array that merges into $a1
+ */
+ private function _mergeAssocArray(&$a1, $a2) {
+ foreach ($a2 as $k => $v) {
+ if ($v === false) {
+ if (isset($a1[$k])) unset($a1[$k]);
+ continue;
+ }
+ $a1[$k] = $v;
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Encoder.php b/extlib/HTMLPurifier/HTMLPurifier/Encoder.php
new file mode 100644
index 000000000..2b3140caa
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Encoder.php
@@ -0,0 +1,426 @@
+<?php
+
+/**
+ * A UTF-8 specific character encoder that handles cleaning and transforming.
+ * @note All functions in this class should be static.
+ */
+class HTMLPurifier_Encoder
+{
+
+ /**
+ * Constructor throws fatal error if you attempt to instantiate class
+ */
+ private function __construct() {
+ trigger_error('Cannot instantiate encoder, call methods statically', E_USER_ERROR);
+ }
+
+ /**
+ * Error-handler that mutes errors, alternative to shut-up operator.
+ */
+ public static function muteErrorHandler() {}
+
+ /**
+ * Cleans a UTF-8 string for well-formedness and SGML validity
+ *
+ * It will parse according to UTF-8 and return a valid UTF8 string, with
+ * non-SGML codepoints excluded.
+ *
+ * @note Just for reference, the non-SGML code points are 0 to 31 and
+ * 127 to 159, inclusive. However, we allow code points 9, 10
+ * and 13, which are the tab, line feed and carriage return
+ * respectively. 128 and above the code points map to multibyte
+ * UTF-8 representations.
+ *
+ * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and
+ * hsivonen@iki.fi at <http://iki.fi/hsivonen/php-utf8/> under the
+ * LGPL license. Notes on what changed are inside, but in general,
+ * the original code transformed UTF-8 text into an array of integer
+ * Unicode codepoints. Understandably, transforming that back to
+ * a string would be somewhat expensive, so the function was modded to
+ * directly operate on the string. However, this discourages code
+ * reuse, and the logic enumerated here would be useful for any
+ * function that needs to be able to understand UTF-8 characters.
+ * As of right now, only smart lossless character encoding converters
+ * would need that, and I'm probably not going to implement them.
+ * Once again, PHP 6 should solve all our problems.
+ */
+ public static function cleanUTF8($str, $force_php = false) {
+
+ // UTF-8 validity is checked since PHP 4.3.5
+ // This is an optimization: if the string is already valid UTF-8, no
+ // need to do PHP stuff. 99% of the time, this will be the case.
+ // The regexp matches the XML char production, as well as well as excluding
+ // non-SGML codepoints U+007F to U+009F
+ 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)) {
+ return $str;
+ }
+
+ $mState = 0; // cached expected number of octets after the current octet
+ // until the beginning of the next UTF8 character sequence
+ $mUcs4 = 0; // cached Unicode character
+ $mBytes = 1; // cached expected number of octets in the current sequence
+
+ // original code involved an $out that was an array of Unicode
+ // codepoints. Instead of having to convert back into UTF-8, we've
+ // decided to directly append valid UTF-8 characters onto a string
+ // $out once they're done. $char accumulates raw bytes, while $mUcs4
+ // turns into the Unicode code point, so there's some redundancy.
+
+ $out = '';
+ $char = '';
+
+ $len = strlen($str);
+ for($i = 0; $i < $len; $i++) {
+ $in = ord($str{$i});
+ $char .= $str[$i]; // append byte to char
+ if (0 == $mState) {
+ // When mState is zero we expect either a US-ASCII character
+ // or a multi-octet sequence.
+ if (0 == (0x80 & ($in))) {
+ // US-ASCII, pass straight through.
+ if (($in <= 31 || $in == 127) &&
+ !($in == 9 || $in == 13 || $in == 10) // save \r\t\n
+ ) {
+ // control characters, remove
+ } else {
+ $out .= $char;
+ }
+ // reset
+ $char = '';
+ $mBytes = 1;
+ } elseif (0xC0 == (0xE0 & ($in))) {
+ // First octet of 2 octet sequence
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 0x1F) << 6;
+ $mState = 1;
+ $mBytes = 2;
+ } elseif (0xE0 == (0xF0 & ($in))) {
+ // First octet of 3 octet sequence
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 0x0F) << 12;
+ $mState = 2;
+ $mBytes = 3;
+ } elseif (0xF0 == (0xF8 & ($in))) {
+ // First octet of 4 octet sequence
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 0x07) << 18;
+ $mState = 3;
+ $mBytes = 4;
+ } elseif (0xF8 == (0xFC & ($in))) {
+ // First octet of 5 octet sequence.
+ //
+ // This is illegal because the encoded codepoint must be
+ // either:
+ // (a) not the shortest form or
+ // (b) outside the Unicode range of 0-0x10FFFF.
+ // Rather than trying to resynchronize, we will carry on
+ // until the end of the sequence and let the later error
+ // handling code catch it.
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 0x03) << 24;
+ $mState = 4;
+ $mBytes = 5;
+ } elseif (0xFC == (0xFE & ($in))) {
+ // First octet of 6 octet sequence, see comments for 5
+ // octet sequence.
+ $mUcs4 = ($in);
+ $mUcs4 = ($mUcs4 & 1) << 30;
+ $mState = 5;
+ $mBytes = 6;
+ } else {
+ // Current octet is neither in the US-ASCII range nor a
+ // legal first octet of a multi-octet sequence.
+ $mState = 0;
+ $mUcs4 = 0;
+ $mBytes = 1;
+ $char = '';
+ }
+ } else {
+ // When mState is non-zero, we expect a continuation of the
+ // multi-octet sequence
+ if (0x80 == (0xC0 & ($in))) {
+ // Legal continuation.
+ $shift = ($mState - 1) * 6;
+ $tmp = $in;
+ $tmp = ($tmp & 0x0000003F) << $shift;
+ $mUcs4 |= $tmp;
+
+ if (0 == --$mState) {
+ // End of the multi-octet sequence. mUcs4 now contains
+ // the final Unicode codepoint to be output
+
+ // Check for illegal sequences and codepoints.
+
+ // From Unicode 3.1, non-shortest form is illegal
+ if (((2 == $mBytes) && ($mUcs4 < 0x0080)) ||
+ ((3 == $mBytes) && ($mUcs4 < 0x0800)) ||
+ ((4 == $mBytes) && ($mUcs4 < 0x10000)) ||
+ (4 < $mBytes) ||
+ // From Unicode 3.2, surrogate characters = illegal
+ (($mUcs4 & 0xFFFFF800) == 0xD800) ||
+ // Codepoints outside the Unicode range are illegal
+ ($mUcs4 > 0x10FFFF)
+ ) {
+
+ } elseif (0xFEFF != $mUcs4 && // omit BOM
+ // check for valid Char unicode codepoints
+ (
+ 0x9 == $mUcs4 ||
+ 0xA == $mUcs4 ||
+ 0xD == $mUcs4 ||
+ (0x20 <= $mUcs4 && 0x7E >= $mUcs4) ||
+ // 7F-9F is not strictly prohibited by XML,
+ // but it is non-SGML, and thus we don't allow it
+ (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) ||
+ (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4)
+ )
+ ) {
+ $out .= $char;
+ }
+ // initialize UTF8 cache (reset)
+ $mState = 0;
+ $mUcs4 = 0;
+ $mBytes = 1;
+ $char = '';
+ }
+ } else {
+ // ((0xC0 & (*in) != 0x80) && (mState != 0))
+ // Incomplete multi-octet sequence.
+ // used to result in complete fail, but we'll reset
+ $mState = 0;
+ $mUcs4 = 0;
+ $mBytes = 1;
+ $char ='';
+ }
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Translates a Unicode codepoint into its corresponding UTF-8 character.
+ * @note Based on Feyd's function at
+ * <http://forums.devnetwork.net/viewtopic.php?p=191404#191404>,
+ * which is in public domain.
+ * @note While we're going to do code point parsing anyway, a good
+ * optimization would be to refuse to translate code points that
+ * are non-SGML characters. However, this could lead to duplication.
+ * @note This is very similar to the unichr function in
+ * maintenance/generate-entity-file.php (although this is superior,
+ * due to its sanity checks).
+ */
+
+ // +----------+----------+----------+----------+
+ // | 33222222 | 22221111 | 111111 | |
+ // | 10987654 | 32109876 | 54321098 | 76543210 | bit
+ // +----------+----------+----------+----------+
+ // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F
+ // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF
+ // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF
+ // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF
+ // +----------+----------+----------+----------+
+ // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF)
+ // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes
+ // +----------+----------+----------+----------+
+
+ public static function unichr($code) {
+ if($code > 1114111 or $code < 0 or
+ ($code >= 55296 and $code <= 57343) ) {
+ // bits are set outside the "valid" range as defined
+ // by UNICODE 4.1.0
+ return '';
+ }
+
+ $x = $y = $z = $w = 0;
+ if ($code < 128) {
+ // regular ASCII character
+ $x = $code;
+ } else {
+ // set up bits for UTF-8
+ $x = ($code & 63) | 128;
+ if ($code < 2048) {
+ $y = (($code & 2047) >> 6) | 192;
+ } else {
+ $y = (($code & 4032) >> 6) | 128;
+ if($code < 65536) {
+ $z = (($code >> 12) & 15) | 224;
+ } else {
+ $z = (($code >> 12) & 63) | 128;
+ $w = (($code >> 18) & 7) | 240;
+ }
+ }
+ }
+ // set up the actual character
+ $ret = '';
+ if($w) $ret .= chr($w);
+ if($z) $ret .= chr($z);
+ if($y) $ret .= chr($y);
+ $ret .= chr($x);
+
+ return $ret;
+ }
+
+ /**
+ * Converts a string to UTF-8 based on configuration.
+ */
+ public static function convertToUTF8($str, $config, $context) {
+ $encoding = $config->get('Core.Encoding');
+ if ($encoding === 'utf-8') return $str;
+ static $iconv = null;
+ if ($iconv === null) $iconv = function_exists('iconv');
+ set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
+ if ($iconv && !$config->get('Test.ForceNoIconv')) {
+ $str = iconv($encoding, 'utf-8//IGNORE', $str);
+ if ($str === false) {
+ // $encoding is not a valid encoding
+ restore_error_handler();
+ trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR);
+ return '';
+ }
+ // If the string is bjorked by Shift_JIS or a similar encoding
+ // that doesn't support all of ASCII, convert the naughty
+ // characters to their true byte-wise ASCII/UTF-8 equivalents.
+ $str = strtr($str, HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding));
+ restore_error_handler();
+ return $str;
+ } elseif ($encoding === 'iso-8859-1') {
+ $str = utf8_encode($str);
+ restore_error_handler();
+ return $str;
+ }
+ trigger_error('Encoding not supported, please install iconv', E_USER_ERROR);
+ }
+
+ /**
+ * Converts a string from UTF-8 based on configuration.
+ * @note Currently, this is a lossy conversion, with unexpressable
+ * characters being omitted.
+ */
+ public static function convertFromUTF8($str, $config, $context) {
+ $encoding = $config->get('Core.Encoding');
+ if ($encoding === 'utf-8') return $str;
+ static $iconv = null;
+ if ($iconv === null) $iconv = function_exists('iconv');
+ if ($escape = $config->get('Core.EscapeNonASCIICharacters')) {
+ $str = HTMLPurifier_Encoder::convertToASCIIDumbLossless($str);
+ }
+ set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
+ if ($iconv && !$config->get('Test.ForceNoIconv')) {
+ // Undo our previous fix in convertToUTF8, otherwise iconv will barf
+ $ascii_fix = HTMLPurifier_Encoder::testEncodingSupportsASCII($encoding);
+ if (!$escape && !empty($ascii_fix)) {
+ $clear_fix = array();
+ foreach ($ascii_fix as $utf8 => $native) $clear_fix[$utf8] = '';
+ $str = strtr($str, $clear_fix);
+ }
+ $str = strtr($str, array_flip($ascii_fix));
+ // Normal stuff
+ $str = iconv('utf-8', $encoding . '//IGNORE', $str);
+ restore_error_handler();
+ return $str;
+ } elseif ($encoding === 'iso-8859-1') {
+ $str = utf8_decode($str);
+ restore_error_handler();
+ return $str;
+ }
+ trigger_error('Encoding not supported', E_USER_ERROR);
+ }
+
+ /**
+ * Lossless (character-wise) conversion of HTML to ASCII
+ * @param $str UTF-8 string to be converted to ASCII
+ * @returns ASCII encoded string with non-ASCII character entity-ized
+ * @warning Adapted from MediaWiki, claiming fair use: this is a common
+ * algorithm. If you disagree with this license fudgery,
+ * implement it yourself.
+ * @note Uses decimal numeric entities since they are best supported.
+ * @note This is a DUMB function: it has no concept of keeping
+ * character entities that the projected character encoding
+ * can allow. We could possibly implement a smart version
+ * but that would require it to also know which Unicode
+ * codepoints the charset supported (not an easy task).
+ * @note Sort of with cleanUTF8() but it assumes that $str is
+ * well-formed UTF-8
+ */
+ public static function convertToASCIIDumbLossless($str) {
+ $bytesleft = 0;
+ $result = '';
+ $working = 0;
+ $len = strlen($str);
+ for( $i = 0; $i < $len; $i++ ) {
+ $bytevalue = ord( $str[$i] );
+ if( $bytevalue <= 0x7F ) { //0xxx xxxx
+ $result .= chr( $bytevalue );
+ $bytesleft = 0;
+ } elseif( $bytevalue <= 0xBF ) { //10xx xxxx
+ $working = $working << 6;
+ $working += ($bytevalue & 0x3F);
+ $bytesleft--;
+ if( $bytesleft <= 0 ) {
+ $result .= "&#" . $working . ";";
+ }
+ } elseif( $bytevalue <= 0xDF ) { //110x xxxx
+ $working = $bytevalue & 0x1F;
+ $bytesleft = 1;
+ } elseif( $bytevalue <= 0xEF ) { //1110 xxxx
+ $working = $bytevalue & 0x0F;
+ $bytesleft = 2;
+ } else { //1111 0xxx
+ $working = $bytevalue & 0x07;
+ $bytesleft = 3;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * This expensive function tests whether or not a given character
+ * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will
+ * fail this test, and require special processing. Variable width
+ * encodings shouldn't ever fail.
+ *
+ * @param string $encoding Encoding name to test, as per iconv format
+ * @param bool $bypass Whether or not to bypass the precompiled arrays.
+ * @return Array of UTF-8 characters to their corresponding ASCII,
+ * which can be used to "undo" any overzealous iconv action.
+ */
+ public static function testEncodingSupportsASCII($encoding, $bypass = false) {
+ static $encodings = array();
+ if (!$bypass) {
+ if (isset($encodings[$encoding])) return $encodings[$encoding];
+ $lenc = strtolower($encoding);
+ switch ($lenc) {
+ case 'shift_jis':
+ return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~');
+ case 'johab':
+ return array("\xE2\x82\xA9" => '\\');
+ }
+ if (strpos($lenc, 'iso-8859-') === 0) return array();
+ }
+ $ret = array();
+ set_error_handler(array('HTMLPurifier_Encoder', 'muteErrorHandler'));
+ if (iconv('UTF-8', $encoding, 'a') === false) return false;
+ for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars
+ $c = chr($i); // UTF-8 char
+ $r = iconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion
+ if (
+ $r === '' ||
+ // This line is needed for iconv implementations that do not
+ // omit characters that do not exist in the target character set
+ ($r === $c && iconv($encoding, 'UTF-8//IGNORE', $r) !== $c)
+ ) {
+ // Reverse engineer: what's the UTF-8 equiv of this byte
+ // sequence? This assumes that there's no variable width
+ // encoding that doesn't support ASCII.
+ $ret[iconv($encoding, 'UTF-8//IGNORE', $c)] = $c;
+ }
+ }
+ restore_error_handler();
+ $encodings[$encoding] = $ret;
+ return $ret;
+ }
+
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/EntityLookup.php b/extlib/HTMLPurifier/HTMLPurifier/EntityLookup.php
new file mode 100644
index 000000000..b4dfce94c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/EntityLookup.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * Object that provides entity lookup table from entity name to character
+ */
+class HTMLPurifier_EntityLookup {
+
+ /**
+ * Assoc array of entity name to character represented.
+ */
+ public $table;
+
+ /**
+ * Sets up the entity lookup table from the serialized file contents.
+ * @note The serialized contents are versioned, but were generated
+ * using the maintenance script generate_entity_file.php
+ * @warning This is not in constructor to help enforce the Singleton
+ */
+ public function setup($file = false) {
+ if (!$file) {
+ $file = HTMLPURIFIER_PREFIX . '/HTMLPurifier/EntityLookup/entities.ser';
+ }
+ $this->table = unserialize(file_get_contents($file));
+ }
+
+ /**
+ * Retrieves sole instance of the object.
+ * @param Optional prototype of custom lookup table to overload with.
+ */
+ public static function instance($prototype = false) {
+ // no references, since PHP doesn't copy unless modified
+ static $instance = null;
+ if ($prototype) {
+ $instance = $prototype;
+ } elseif (!$instance) {
+ $instance = new HTMLPurifier_EntityLookup();
+ $instance->setup();
+ }
+ return $instance;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/EntityLookup/entities.ser b/extlib/HTMLPurifier/HTMLPurifier/EntityLookup/entities.ser
new file mode 100644
index 000000000..f2b8b8f2d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/EntityLookup/entities.ser
@@ -0,0 +1 @@
+a:246:{s:4:"nbsp";s:2:" ";s:5:"iexcl";s:2:"¡";s:4:"cent";s:2:"¢";s:5:"pound";s:2:"£";s:6:"curren";s:2:"¤";s:3:"yen";s:2:"¥";s:6:"brvbar";s:2:"¦";s:4:"sect";s:2:"§";s:3:"uml";s:2:"¨";s:4:"copy";s:2:"©";s:4:"ordf";s:2:"ª";s:5:"laquo";s:2:"«";s:3:"not";s:2:"¬";s:3:"shy";s:2:"­";s:3:"reg";s:2:"®";s:4:"macr";s:2:"¯";s:3:"deg";s:2:"°";s:6:"plusmn";s:2:"±";s:5:"acute";s:2:"´";s:5:"micro";s:2:"µ";s:4:"para";s:2:"¶";s:6:"middot";s:2:"·";s:5:"cedil";s:2:"¸";s:4:"ordm";s:2:"º";s:5:"raquo";s:2:"»";s:6:"iquest";s:2:"¿";s:6:"Agrave";s:2:"À";s:6:"Aacute";s:2:"Á";s:5:"Acirc";s:2:"Â";s:6:"Atilde";s:2:"Ã";s:4:"Auml";s:2:"Ä";s:5:"Aring";s:2:"Å";s:5:"AElig";s:2:"Æ";s:6:"Ccedil";s:2:"Ç";s:6:"Egrave";s:2:"È";s:6:"Eacute";s:2:"É";s:5:"Ecirc";s:2:"Ê";s:4:"Euml";s:2:"Ë";s:6:"Igrave";s:2:"Ì";s:6:"Iacute";s:2:"Í";s:5:"Icirc";s:2:"Î";s:4:"Iuml";s:2:"Ï";s:3:"ETH";s:2:"Ð";s:6:"Ntilde";s:2:"Ñ";s:6:"Ograve";s:2:"Ò";s:6:"Oacute";s:2:"Ó";s:5:"Ocirc";s:2:"Ô";s:6:"Otilde";s:2:"Õ";s:4:"Ouml";s:2:"Ö";s:5:"times";s:2:"×";s:6:"Oslash";s:2:"Ø";s:6:"Ugrave";s:2:"Ù";s:6:"Uacute";s:2:"Ú";s:5:"Ucirc";s:2:"Û";s:4:"Uuml";s:2:"Ü";s:6:"Yacute";s:2:"Ý";s:5:"THORN";s:2:"Þ";s:5:"szlig";s:2:"ß";s:6:"agrave";s:2:"à";s:6:"aacute";s:2:"á";s:5:"acirc";s:2:"â";s:6:"atilde";s:2:"ã";s:4:"auml";s:2:"ä";s:5:"aring";s:2:"å";s:5:"aelig";s:2:"æ";s:6:"ccedil";s:2:"ç";s:6:"egrave";s:2:"è";s:6:"eacute";s:2:"é";s:5:"ecirc";s:2:"ê";s:4:"euml";s:2:"ë";s:6:"igrave";s:2:"ì";s:6:"iacute";s:2:"í";s:5:"icirc";s:2:"î";s:4:"iuml";s:2:"ï";s:3:"eth";s:2:"ð";s:6:"ntilde";s:2:"ñ";s:6:"ograve";s:2:"ò";s:6:"oacute";s:2:"ó";s:5:"ocirc";s:2:"ô";s:6:"otilde";s:2:"õ";s:4:"ouml";s:2:"ö";s:6:"divide";s:2:"÷";s:6:"oslash";s:2:"ø";s:6:"ugrave";s:2:"ù";s:6:"uacute";s:2:"ú";s:5:"ucirc";s:2:"û";s:4:"uuml";s:2:"ü";s:6:"yacute";s:2:"ý";s:5:"thorn";s:2:"þ";s:4:"yuml";s:2:"ÿ";s:4:"quot";s:1:""";s:3:"amp";s:1:"&";s:2:"lt";s:1:"<";s:2:"gt";s:1:">";s:4:"apos";s:1:"'";s:5:"OElig";s:2:"Œ";s:5:"oelig";s:2:"œ";s:6:"Scaron";s:2:"Š";s:6:"scaron";s:2:"š";s:4:"Yuml";s:2:"Ÿ";s:4:"circ";s:2:"ˆ";s:5:"tilde";s:2:"˜";s:4:"ensp";s:3:" ";s:4:"emsp";s:3:" ";s:6:"thinsp";s:3:" ";s:4:"zwnj";s:3:"‌";s:3:"zwj";s:3:"‍";s:3:"lrm";s:3:"‎";s:3:"rlm";s:3:"‏";s:5:"ndash";s:3:"–";s:5:"mdash";s:3:"—";s:5:"lsquo";s:3:"‘";s:5:"rsquo";s:3:"’";s:5:"sbquo";s:3:"‚";s:5:"ldquo";s:3:"“";s:5:"rdquo";s:3:"”";s:5:"bdquo";s:3:"„";s:6:"dagger";s:3:"†";s:6:"Dagger";s:3:"‡";s:6:"permil";s:3:"‰";s:6:"lsaquo";s:3:"‹";s:6:"rsaquo";s:3:"›";s:4:"euro";s:3:"€";s:4:"fnof";s:2:"ƒ";s:5:"Alpha";s:2:"Α";s:4:"Beta";s:2:"Β";s:5:"Gamma";s:2:"Γ";s:5:"Delta";s:2:"Δ";s:7:"Epsilon";s:2:"Ε";s:4:"Zeta";s:2:"Ζ";s:3:"Eta";s:2:"Η";s:5:"Theta";s:2:"Θ";s:4:"Iota";s:2:"Ι";s:5:"Kappa";s:2:"Κ";s:6:"Lambda";s:2:"Λ";s:2:"Mu";s:2:"Μ";s:2:"Nu";s:2:"Ν";s:2:"Xi";s:2:"Ξ";s:7:"Omicron";s:2:"Ο";s:2:"Pi";s:2:"Π";s:3:"Rho";s:2:"Ρ";s:5:"Sigma";s:2:"Σ";s:3:"Tau";s:2:"Τ";s:7:"Upsilon";s:2:"Υ";s:3:"Phi";s:2:"Φ";s:3:"Chi";s:2:"Χ";s:3:"Psi";s:2:"Ψ";s:5:"Omega";s:2:"Ω";s:5:"alpha";s:2:"α";s:4:"beta";s:2:"β";s:5:"gamma";s:2:"γ";s:5:"delta";s:2:"δ";s:7:"epsilon";s:2:"ε";s:4:"zeta";s:2:"ζ";s:3:"eta";s:2:"η";s:5:"theta";s:2:"θ";s:4:"iota";s:2:"ι";s:5:"kappa";s:2:"κ";s:6:"lambda";s:2:"λ";s:2:"mu";s:2:"μ";s:2:"nu";s:2:"ν";s:2:"xi";s:2:"ξ";s:7:"omicron";s:2:"ο";s:2:"pi";s:2:"π";s:3:"rho";s:2:"ρ";s:6:"sigmaf";s:2:"ς";s:5:"sigma";s:2:"σ";s:3:"tau";s:2:"τ";s:7:"upsilon";s:2:"υ";s:3:"phi";s:2:"φ";s:3:"chi";s:2:"χ";s:3:"psi";s:2:"ψ";s:5:"omega";s:2:"ω";s:8:"thetasym";s:2:"ϑ";s:5:"upsih";s:2:"ϒ";s:3:"piv";s:2:"ϖ";s:4:"bull";s:3:"•";s:6:"hellip";s:3:"…";s:5:"prime";s:3:"′";s:5:"Prime";s:3:"″";s:5:"oline";s:3:"‾";s:5:"frasl";s:3:"⁄";s:6:"weierp";s:3:"℘";s:5:"image";s:3:"ℑ";s:4:"real";s:3:"ℜ";s:5:"trade";s:3:"™";s:7:"alefsym";s:3:"ℵ";s:4:"larr";s:3:"←";s:4:"uarr";s:3:"↑";s:4:"rarr";s:3:"→";s:4:"darr";s:3:"↓";s:4:"harr";s:3:"↔";s:5:"crarr";s:3:"↵";s:4:"lArr";s:3:"⇐";s:4:"uArr";s:3:"⇑";s:4:"rArr";s:3:"⇒";s:4:"dArr";s:3:"⇓";s:4:"hArr";s:3:"⇔";s:6:"forall";s:3:"∀";s:4:"part";s:3:"∂";s:5:"exist";s:3:"∃";s:5:"empty";s:3:"∅";s:5:"nabla";s:3:"∇";s:4:"isin";s:3:"∈";s:5:"notin";s:3:"∉";s:2:"ni";s:3:"∋";s:4:"prod";s:3:"∏";s:3:"sum";s:3:"∑";s:5:"minus";s:3:"−";s:6:"lowast";s:3:"∗";s:5:"radic";s:3:"√";s:4:"prop";s:3:"∝";s:5:"infin";s:3:"∞";s:3:"ang";s:3:"∠";s:3:"and";s:3:"∧";s:2:"or";s:3:"∨";s:3:"cap";s:3:"∩";s:3:"cup";s:3:"∪";s:3:"int";s:3:"∫";s:3:"sim";s:3:"∼";s:4:"cong";s:3:"≅";s:5:"asymp";s:3:"≈";s:2:"ne";s:3:"≠";s:5:"equiv";s:3:"≡";s:2:"le";s:3:"≤";s:2:"ge";s:3:"≥";s:3:"sub";s:3:"⊂";s:3:"sup";s:3:"⊃";s:4:"nsub";s:3:"⊄";s:4:"sube";s:3:"⊆";s:4:"supe";s:3:"⊇";s:5:"oplus";s:3:"⊕";s:6:"otimes";s:3:"⊗";s:4:"perp";s:3:"⊥";s:4:"sdot";s:3:"⋅";s:5:"lceil";s:3:"⌈";s:5:"rceil";s:3:"⌉";s:6:"lfloor";s:3:"⌊";s:6:"rfloor";s:3:"⌋";s:4:"lang";s:3:"〈";s:4:"rang";s:3:"〉";s:3:"loz";s:3:"◊";s:6:"spades";s:3:"♠";s:5:"clubs";s:3:"♣";s:6:"hearts";s:3:"♥";s:5:"diams";s:3:"♦";} \ No newline at end of file
diff --git a/extlib/HTMLPurifier/HTMLPurifier/EntityParser.php b/extlib/HTMLPurifier/HTMLPurifier/EntityParser.php
new file mode 100644
index 000000000..8c384472d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/EntityParser.php
@@ -0,0 +1,144 @@
+<?php
+
+// if want to implement error collecting here, we'll need to use some sort
+// of global data (probably trigger_error) because it's impossible to pass
+// $config or $context to the callback functions.
+
+/**
+ * Handles referencing and derefencing character entities
+ */
+class HTMLPurifier_EntityParser
+{
+
+ /**
+ * Reference to entity lookup table.
+ */
+ protected $_entity_lookup;
+
+ /**
+ * Callback regex string for parsing entities.
+ */
+ protected $_substituteEntitiesRegex =
+'/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/';
+// 1. hex 2. dec 3. string (XML style)
+
+
+ /**
+ * Decimal to parsed string conversion table for special entities.
+ */
+ protected $_special_dec2str =
+ array(
+ 34 => '"',
+ 38 => '&',
+ 39 => "'",
+ 60 => '<',
+ 62 => '>'
+ );
+
+ /**
+ * Stripped entity names to decimal conversion table for special entities.
+ */
+ protected $_special_ent2dec =
+ array(
+ 'quot' => 34,
+ 'amp' => 38,
+ 'lt' => 60,
+ 'gt' => 62
+ );
+
+ /**
+ * Substitutes non-special entities with their parsed equivalents. Since
+ * running this whenever you have parsed character is t3h 5uck, we run
+ * it before everything else.
+ *
+ * @param $string String to have non-special entities parsed.
+ * @returns Parsed string.
+ */
+ public function substituteNonSpecialEntities($string) {
+ // it will try to detect missing semicolons, but don't rely on it
+ return preg_replace_callback(
+ $this->_substituteEntitiesRegex,
+ array($this, 'nonSpecialEntityCallback'),
+ $string
+ );
+ }
+
+ /**
+ * Callback function for substituteNonSpecialEntities() that does the work.
+ *
+ * @param $matches PCRE matches array, with 0 the entire match, and
+ * either index 1, 2 or 3 set with a hex value, dec value,
+ * or string (respectively).
+ * @returns Replacement string.
+ */
+
+ protected function nonSpecialEntityCallback($matches) {
+ // replaces all but big five
+ $entity = $matches[0];
+ $is_num = (@$matches[0][1] === '#');
+ if ($is_num) {
+ $is_hex = (@$entity[2] === 'x');
+ $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
+
+ // abort for special characters
+ if (isset($this->_special_dec2str[$code])) return $entity;
+
+ return HTMLPurifier_Encoder::unichr($code);
+ } else {
+ if (isset($this->_special_ent2dec[$matches[3]])) return $entity;
+ if (!$this->_entity_lookup) {
+ $this->_entity_lookup = HTMLPurifier_EntityLookup::instance();
+ }
+ if (isset($this->_entity_lookup->table[$matches[3]])) {
+ return $this->_entity_lookup->table[$matches[3]];
+ } else {
+ return $entity;
+ }
+ }
+ }
+
+ /**
+ * Substitutes only special entities with their parsed equivalents.
+ *
+ * @notice We try to avoid calling this function because otherwise, it
+ * would have to be called a lot (for every parsed section).
+ *
+ * @param $string String to have non-special entities parsed.
+ * @returns Parsed string.
+ */
+ public function substituteSpecialEntities($string) {
+ return preg_replace_callback(
+ $this->_substituteEntitiesRegex,
+ array($this, 'specialEntityCallback'),
+ $string);
+ }
+
+ /**
+ * Callback function for substituteSpecialEntities() that does the work.
+ *
+ * This callback has same syntax as nonSpecialEntityCallback().
+ *
+ * @param $matches PCRE-style matches array, with 0 the entire match, and
+ * either index 1, 2 or 3 set with a hex value, dec value,
+ * or string (respectively).
+ * @returns Replacement string.
+ */
+ protected function specialEntityCallback($matches) {
+ $entity = $matches[0];
+ $is_num = (@$matches[0][1] === '#');
+ if ($is_num) {
+ $is_hex = (@$entity[2] === 'x');
+ $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2];
+ return isset($this->_special_dec2str[$int]) ?
+ $this->_special_dec2str[$int] :
+ $entity;
+ } else {
+ return isset($this->_special_ent2dec[$matches[3]]) ?
+ $this->_special_ent2dec[$matches[3]] :
+ $entity;
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ErrorCollector.php b/extlib/HTMLPurifier/HTMLPurifier/ErrorCollector.php
new file mode 100644
index 000000000..6713eaf77
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ErrorCollector.php
@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * Error collection class that enables HTML Purifier to report HTML
+ * problems back to the user
+ */
+class HTMLPurifier_ErrorCollector
+{
+
+ /**
+ * Identifiers for the returned error array. These are purposely numeric
+ * so list() can be used.
+ */
+ const LINENO = 0;
+ const SEVERITY = 1;
+ const MESSAGE = 2;
+ const CHILDREN = 3;
+
+ protected $errors;
+ protected $_current;
+ protected $_stacks = array(array());
+ protected $locale;
+ protected $generator;
+ protected $context;
+
+ protected $lines = array();
+
+ public function __construct($context) {
+ $this->locale =& $context->get('Locale');
+ $this->context = $context;
+ $this->_current =& $this->_stacks[0];
+ $this->errors =& $this->_stacks[0];
+ }
+
+ /**
+ * Sends an error message to the collector for later use
+ * @param $severity int Error severity, PHP error style (don't use E_USER_)
+ * @param $msg string Error message text
+ * @param $subst1 string First substitution for $msg
+ * @param $subst2 string ...
+ */
+ public function send($severity, $msg) {
+
+ $args = array();
+ if (func_num_args() > 2) {
+ $args = func_get_args();
+ array_shift($args);
+ unset($args[0]);
+ }
+
+ $token = $this->context->get('CurrentToken', true);
+ $line = $token ? $token->line : $this->context->get('CurrentLine', true);
+ $col = $token ? $token->col : $this->context->get('CurrentCol', true);
+ $attr = $this->context->get('CurrentAttr', true);
+
+ // perform special substitutions, also add custom parameters
+ $subst = array();
+ if (!is_null($token)) {
+ $args['CurrentToken'] = $token;
+ }
+ if (!is_null($attr)) {
+ $subst['$CurrentAttr.Name'] = $attr;
+ if (isset($token->attr[$attr])) $subst['$CurrentAttr.Value'] = $token->attr[$attr];
+ }
+
+ if (empty($args)) {
+ $msg = $this->locale->getMessage($msg);
+ } else {
+ $msg = $this->locale->formatMessage($msg, $args);
+ }
+
+ if (!empty($subst)) $msg = strtr($msg, $subst);
+
+ // (numerically indexed)
+ $error = array(
+ self::LINENO => $line,
+ self::SEVERITY => $severity,
+ self::MESSAGE => $msg,
+ self::CHILDREN => array()
+ );
+ $this->_current[] = $error;
+
+
+ // NEW CODE BELOW ...
+
+ $struct = null;
+ // Top-level errors are either:
+ // TOKEN type, if $value is set appropriately, or
+ // "syntax" type, if $value is null
+ $new_struct = new HTMLPurifier_ErrorStruct();
+ $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN;
+ if ($token) $new_struct->value = clone $token;
+ if (is_int($line) && is_int($col)) {
+ if (isset($this->lines[$line][$col])) {
+ $struct = $this->lines[$line][$col];
+ } else {
+ $struct = $this->lines[$line][$col] = $new_struct;
+ }
+ // These ksorts may present a performance problem
+ ksort($this->lines[$line], SORT_NUMERIC);
+ } else {
+ if (isset($this->lines[-1])) {
+ $struct = $this->lines[-1];
+ } else {
+ $struct = $this->lines[-1] = $new_struct;
+ }
+ }
+ ksort($this->lines, SORT_NUMERIC);
+
+ // Now, check if we need to operate on a lower structure
+ if (!empty($attr)) {
+ $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr);
+ if (!$struct->value) {
+ $struct->value = array($attr, 'PUT VALUE HERE');
+ }
+ }
+ if (!empty($cssprop)) {
+ $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop);
+ if (!$struct->value) {
+ // if we tokenize CSS this might be a little more difficult to do
+ $struct->value = array($cssprop, 'PUT VALUE HERE');
+ }
+ }
+
+ // Ok, structs are all setup, now time to register the error
+ $struct->addError($severity, $msg);
+ }
+
+ /**
+ * Retrieves raw error data for custom formatter to use
+ * @param List of arrays in format of array(line of error,
+ * error severity, error message,
+ * recursive sub-errors array)
+ */
+ public function getRaw() {
+ return $this->errors;
+ }
+
+ /**
+ * Default HTML formatting implementation for error messages
+ * @param $config Configuration array, vital for HTML output nature
+ * @param $errors Errors array to display; used for recursion.
+ */
+ public function getHTMLFormatted($config, $errors = null) {
+ $ret = array();
+
+ $this->generator = new HTMLPurifier_Generator($config, $this->context);
+ if ($errors === null) $errors = $this->errors;
+
+ // 'At line' message needs to be removed
+
+ // generation code for new structure goes here. It needs to be recursive.
+ foreach ($this->lines as $line => $col_array) {
+ if ($line == -1) continue;
+ foreach ($col_array as $col => $struct) {
+ $this->_renderStruct($ret, $struct, $line, $col);
+ }
+ }
+ if (isset($this->lines[-1])) {
+ $this->_renderStruct($ret, $this->lines[-1]);
+ }
+
+ if (empty($errors)) {
+ return '<p>' . $this->locale->getMessage('ErrorCollector: No errors') . '</p>';
+ } else {
+ return '<ul><li>' . implode('</li><li>', $ret) . '</li></ul>';
+ }
+
+ }
+
+ private function _renderStruct(&$ret, $struct, $line = null, $col = null) {
+ $stack = array($struct);
+ $context_stack = array(array());
+ while ($current = array_pop($stack)) {
+ $context = array_pop($context_stack);
+ foreach ($current->errors as $error) {
+ list($severity, $msg) = $error;
+ $string = '';
+ $string .= '<div>';
+ // W3C uses an icon to indicate the severity of the error.
+ $error = $this->locale->getErrorName($severity);
+ $string .= "<span class=\"error e$severity\"><strong>$error</strong></span> ";
+ if (!is_null($line) && !is_null($col)) {
+ $string .= "<em class=\"location\">Line $line, Column $col: </em> ";
+ } else {
+ $string .= '<em class="location">End of Document: </em> ';
+ }
+ $string .= '<strong class="description">' . $this->generator->escape($msg) . '</strong> ';
+ $string .= '</div>';
+ // Here, have a marker for the character on the column appropriate.
+ // Be sure to clip extremely long lines.
+ //$string .= '<pre>';
+ //$string .= '';
+ //$string .= '</pre>';
+ $ret[] = $string;
+ }
+ foreach ($current->children as $type => $array) {
+ $context[] = $current;
+ $stack = array_merge($stack, array_reverse($array, true));
+ for ($i = count($array); $i > 0; $i--) {
+ $context_stack[] = $context;
+ }
+ }
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/ErrorStruct.php b/extlib/HTMLPurifier/HTMLPurifier/ErrorStruct.php
new file mode 100644
index 000000000..9bc8996ec
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/ErrorStruct.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * Records errors for particular segments of an HTML document such as tokens,
+ * attributes or CSS properties. They can contain error structs (which apply
+ * to components of what they represent), but their main purpose is to hold
+ * errors applying to whatever struct is being used.
+ */
+class HTMLPurifier_ErrorStruct
+{
+
+ /**
+ * Possible values for $children first-key. Note that top-level structures
+ * are automatically token-level.
+ */
+ const TOKEN = 0;
+ const ATTR = 1;
+ const CSSPROP = 2;
+
+ /**
+ * Type of this struct.
+ */
+ public $type;
+
+ /**
+ * Value of the struct we are recording errors for. There are various
+ * values for this:
+ * - TOKEN: Instance of HTMLPurifier_Token
+ * - ATTR: array('attr-name', 'value')
+ * - CSSPROP: array('prop-name', 'value')
+ */
+ public $value;
+
+ /**
+ * Errors registered for this structure.
+ */
+ public $errors = array();
+
+ /**
+ * Child ErrorStructs that are from this structure. For example, a TOKEN
+ * ErrorStruct would contain ATTR ErrorStructs. This is a multi-dimensional
+ * array in structure: [TYPE]['identifier']
+ */
+ public $children = array();
+
+ public function getChild($type, $id) {
+ if (!isset($this->children[$type][$id])) {
+ $this->children[$type][$id] = new HTMLPurifier_ErrorStruct();
+ $this->children[$type][$id]->type = $type;
+ }
+ return $this->children[$type][$id];
+ }
+
+ public function addError($severity, $message) {
+ $this->errors[] = array($severity, $message);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Exception.php b/extlib/HTMLPurifier/HTMLPurifier/Exception.php
new file mode 100644
index 000000000..be85b4c56
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Exception.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * Global exception class for HTML Purifier; any exceptions we throw
+ * are from here.
+ */
+class HTMLPurifier_Exception extends Exception
+{
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Filter.php b/extlib/HTMLPurifier/HTMLPurifier/Filter.php
new file mode 100644
index 000000000..9a0e7b09f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Filter.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Represents a pre or post processing filter on HTML Purifier's output
+ *
+ * Sometimes, a little ad-hoc fixing of HTML has to be done before
+ * it gets sent through HTML Purifier: you can use filters to acheive
+ * this effect. For instance, YouTube videos can be preserved using
+ * this manner. You could have used a decorator for this task, but
+ * PHP's support for them is not terribly robust, so we're going
+ * to just loop through the filters.
+ *
+ * Filters should be exited first in, last out. If there are three filters,
+ * named 1, 2 and 3, the order of execution should go 1->preFilter,
+ * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter,
+ * 1->postFilter.
+ *
+ * @note Methods are not declared abstract as it is perfectly legitimate
+ * for an implementation not to want anything to happen on a step
+ */
+
+class HTMLPurifier_Filter
+{
+
+ /**
+ * Name of the filter for identification purposes
+ */
+ public $name;
+
+ /**
+ * Pre-processor function, handles HTML before HTML Purifier
+ */
+ public function preFilter($html, $config, $context) {
+ return $html;
+ }
+
+ /**
+ * Post-processor function, handles HTML after HTML Purifier
+ */
+ public function postFilter($html, $config, $context) {
+ return $html;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php b/extlib/HTMLPurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php
new file mode 100644
index 000000000..bbf78a663
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * This filter extracts <style> blocks from input HTML, cleans them up
+ * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks')
+ * so they can be used elsewhere in the document.
+ *
+ * @note
+ * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for
+ * sample usage.
+ *
+ * @note
+ * This filter can also be used on stylesheets not included in the
+ * document--something purists would probably prefer. Just directly
+ * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS()
+ */
+class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter
+{
+
+ public $name = 'ExtractStyleBlocks';
+ private $_styleMatches = array();
+ private $_tidy;
+
+ public function __construct() {
+ $this->_tidy = new csstidy();
+ }
+
+ /**
+ * Save the contents of CSS blocks to style matches
+ * @param $matches preg_replace style $matches array
+ */
+ protected function styleCallback($matches) {
+ $this->_styleMatches[] = $matches[1];
+ }
+
+ /**
+ * Removes inline <style> tags from HTML, saves them for later use
+ * @todo Extend to indicate non-text/css style blocks
+ */
+ public function preFilter($html, $config, $context) {
+ $tidy = $config->get('Filter.ExtractStyleBlocks.TidyImpl');
+ if ($tidy !== null) $this->_tidy = $tidy;
+ $html = preg_replace_callback('#<style(?:\s.*)?>(.+)</style>#isU', array($this, 'styleCallback'), $html);
+ $style_blocks = $this->_styleMatches;
+ $this->_styleMatches = array(); // reset
+ $context->register('StyleBlocks', $style_blocks); // $context must not be reused
+ if ($this->_tidy) {
+ foreach ($style_blocks as &$style) {
+ $style = $this->cleanCSS($style, $config, $context);
+ }
+ }
+ return $html;
+ }
+
+ /**
+ * Takes CSS (the stuff found in <style>) and cleans it.
+ * @warning Requires CSSTidy <http://csstidy.sourceforge.net/>
+ * @param $css CSS styling to clean
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ * @return Cleaned CSS
+ */
+ public function cleanCSS($css, $config, $context) {
+ // prepare scope
+ $scope = $config->get('Filter.ExtractStyleBlocks.Scope');
+ if ($scope !== null) {
+ $scopes = array_map('trim', explode(',', $scope));
+ } else {
+ $scopes = array();
+ }
+ // remove comments from CSS
+ $css = trim($css);
+ if (strncmp('<!--', $css, 4) === 0) {
+ $css = substr($css, 4);
+ }
+ if (strlen($css) > 3 && substr($css, -3) == '-->') {
+ $css = substr($css, 0, -3);
+ }
+ $css = trim($css);
+ $this->_tidy->parse($css);
+ $css_definition = $config->getDefinition('CSS');
+ foreach ($this->_tidy->css as $k => $decls) {
+ // $decls are all CSS declarations inside an @ selector
+ $new_decls = array();
+ foreach ($decls as $selector => $style) {
+ $selector = trim($selector);
+ if ($selector === '') continue; // should not happen
+ if ($selector[0] === '+') {
+ if ($selector !== '' && $selector[0] === '+') continue;
+ }
+ if (!empty($scopes)) {
+ $new_selector = array(); // because multiple ones are possible
+ $selectors = array_map('trim', explode(',', $selector));
+ foreach ($scopes as $s1) {
+ foreach ($selectors as $s2) {
+ $new_selector[] = "$s1 $s2";
+ }
+ }
+ $selector = implode(', ', $new_selector); // now it's a string
+ }
+ foreach ($style as $name => $value) {
+ if (!isset($css_definition->info[$name])) {
+ unset($style[$name]);
+ continue;
+ }
+ $def = $css_definition->info[$name];
+ $ret = $def->validate($value, $config, $context);
+ if ($ret === false) unset($style[$name]);
+ else $style[$name] = $ret;
+ }
+ $new_decls[$selector] = $style;
+ }
+ $this->_tidy->css[$k] = $new_decls;
+ }
+ // remove stuff that shouldn't be used, could be reenabled
+ // after security risks are analyzed
+ $this->_tidy->import = array();
+ $this->_tidy->charset = null;
+ $this->_tidy->namespace = null;
+ $css = $this->_tidy->print->plain();
+ // we are going to escape any special characters <>& to ensure
+ // that no funny business occurs (i.e. </style> in a font-family prop).
+ if ($config->get('Filter.ExtractStyleBlocks.Escaping')) {
+ $css = str_replace(
+ array('<', '>', '&'),
+ array('\3C ', '\3E ', '\26 '),
+ $css
+ );
+ }
+ return $css;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Filter/YouTube.php b/extlib/HTMLPurifier/HTMLPurifier/Filter/YouTube.php
new file mode 100644
index 000000000..aca972f6c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Filter/YouTube.php
@@ -0,0 +1,39 @@
+<?php
+
+class HTMLPurifier_Filter_YouTube extends HTMLPurifier_Filter
+{
+
+ public $name = 'YouTube';
+
+ public function preFilter($html, $config, $context) {
+ $pre_regex = '#<object[^>]+>.+?'.
+ 'http://www.youtube.com/v/([A-Za-z0-9\-_]+).+?</object>#s';
+ $pre_replace = '<span class="youtube-embed">\1</span>';
+ return preg_replace($pre_regex, $pre_replace, $html);
+ }
+
+ public function postFilter($html, $config, $context) {
+ $post_regex = '#<span class="youtube-embed">([A-Za-z0-9\-_]+)</span>#';
+ return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html);
+ }
+
+ protected function armorUrl($url) {
+ return str_replace('--', '-&#45;', $url);
+ }
+
+ protected function postFilterCallback($matches) {
+ $url = $this->armorUrl($matches[1]);
+ return '<object width="425" height="350" type="application/x-shockwave-flash" '.
+ 'data="http://www.youtube.com/v/'.$url.'">'.
+ '<param name="movie" value="http://www.youtube.com/v/'.$url.'"></param>'.
+ '<!--[if IE]>'.
+ '<embed src="http://www.youtube.com/v/'.$url.'"'.
+ 'type="application/x-shockwave-flash"'.
+ 'wmode="transparent" width="425" height="350" />'.
+ '<![endif]-->'.
+ '</object>';
+
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Generator.php b/extlib/HTMLPurifier/HTMLPurifier/Generator.php
new file mode 100644
index 000000000..24bd8a54e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Generator.php
@@ -0,0 +1,183 @@
+<?php
+
+/**
+ * Generates HTML from tokens.
+ * @todo Refactor interface so that configuration/context is determined
+ * upon instantiation, no need for messy generateFromTokens() calls
+ * @todo Make some of the more internal functions protected, and have
+ * unit tests work around that
+ */
+class HTMLPurifier_Generator
+{
+
+ /**
+ * Whether or not generator should produce XML output
+ */
+ private $_xhtml = true;
+
+ /**
+ * :HACK: Whether or not generator should comment the insides of <script> tags
+ */
+ private $_scriptFix = false;
+
+ /**
+ * Cache of HTMLDefinition during HTML output to determine whether or
+ * not attributes should be minimized.
+ */
+ private $_def;
+
+ /**
+ * Cache of %Output.SortAttr
+ */
+ private $_sortAttr;
+
+ /**
+ * Configuration for the generator
+ */
+ protected $config;
+
+ /**
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ */
+ public function __construct($config, $context) {
+ $this->config = $config;
+ $this->_scriptFix = $config->get('Output.CommentScriptContents');
+ $this->_sortAttr = $config->get('Output.SortAttr');
+ $this->_def = $config->getHTMLDefinition();
+ $this->_xhtml = $this->_def->doctype->xml;
+ }
+
+ /**
+ * Generates HTML from an array of tokens.
+ * @param $tokens Array of HTMLPurifier_Token
+ * @param $config HTMLPurifier_Config object
+ * @return Generated HTML
+ */
+ public function generateFromTokens($tokens) {
+ if (!$tokens) return '';
+
+ // Basic algorithm
+ $html = '';
+ for ($i = 0, $size = count($tokens); $i < $size; $i++) {
+ if ($this->_scriptFix && $tokens[$i]->name === 'script'
+ && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) {
+ // script special case
+ // the contents of the script block must be ONE token
+ // for this to work.
+ $html .= $this->generateFromToken($tokens[$i++]);
+ $html .= $this->generateScriptFromToken($tokens[$i++]);
+ }
+ $html .= $this->generateFromToken($tokens[$i]);
+ }
+
+ // Tidy cleanup
+ if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) {
+ $tidy = new Tidy;
+ $tidy->parseString($html, array(
+ 'indent'=> true,
+ 'output-xhtml' => $this->_xhtml,
+ 'show-body-only' => true,
+ 'indent-spaces' => 2,
+ 'wrap' => 68,
+ ), 'utf8');
+ $tidy->cleanRepair();
+ $html = (string) $tidy; // explicit cast necessary
+ }
+
+ // Normalize newlines to system defined value
+ $nl = $this->config->get('Output.Newline');
+ if ($nl === null) $nl = PHP_EOL;
+ if ($nl !== "\n") $html = str_replace("\n", $nl, $html);
+ return $html;
+ }
+
+ /**
+ * Generates HTML from a single token.
+ * @param $token HTMLPurifier_Token object.
+ * @return Generated HTML
+ */
+ public function generateFromToken($token) {
+ if (!$token instanceof HTMLPurifier_Token) {
+ trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING);
+ return '';
+
+ } elseif ($token instanceof HTMLPurifier_Token_Start) {
+ $attr = $this->generateAttributes($token->attr, $token->name);
+ return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>';
+
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ return '</' . $token->name . '>';
+
+ } elseif ($token instanceof HTMLPurifier_Token_Empty) {
+ $attr = $this->generateAttributes($token->attr, $token->name);
+ return '<' . $token->name . ($attr ? ' ' : '') . $attr .
+ ( $this->_xhtml ? ' /': '' ) // <br /> v. <br>
+ . '>';
+
+ } elseif ($token instanceof HTMLPurifier_Token_Text) {
+ return $this->escape($token->data, ENT_NOQUOTES);
+
+ } elseif ($token instanceof HTMLPurifier_Token_Comment) {
+ return '<!--' . $token->data . '-->';
+ } else {
+ return '';
+
+ }
+ }
+
+ /**
+ * Special case processor for the contents of script tags
+ * @warning This runs into problems if there's already a literal
+ * --> somewhere inside the script contents.
+ */
+ public function generateScriptFromToken($token) {
+ if (!$token instanceof HTMLPurifier_Token_Text) return $this->generateFromToken($token);
+ // Thanks <http://lachy.id.au/log/2005/05/script-comments>
+ $data = preg_replace('#//\s*$#', '', $token->data);
+ return '<!--//--><![CDATA[//><!--' . "\n" . trim($data) . "\n" . '//--><!]]>';
+ }
+
+ /**
+ * Generates attribute declarations from attribute array.
+ * @note This does not include the leading or trailing space.
+ * @param $assoc_array_of_attributes Attribute array
+ * @param $element Name of element attributes are for, used to check
+ * attribute minimization.
+ * @return Generate HTML fragment for insertion.
+ */
+ public function generateAttributes($assoc_array_of_attributes, $element = false) {
+ $html = '';
+ if ($this->_sortAttr) ksort($assoc_array_of_attributes);
+ foreach ($assoc_array_of_attributes as $key => $value) {
+ if (!$this->_xhtml) {
+ // Remove namespaced attributes
+ if (strpos($key, ':') !== false) continue;
+ // Check if we should minimize the attribute: val="val" -> val
+ if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) {
+ $html .= $key . ' ';
+ continue;
+ }
+ }
+ $html .= $key.'="'.$this->escape($value).'" ';
+ }
+ return rtrim($html);
+ }
+
+ /**
+ * Escapes raw text data.
+ * @todo This really ought to be protected, but until we have a facility
+ * for properly generating HTML here w/o using tokens, it stays
+ * public.
+ * @param $string String data to escape for HTML.
+ * @param $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is
+ * permissible for non-attribute output.
+ * @return String escaped data.
+ */
+ public function escape($string, $quote = ENT_COMPAT) {
+ return htmlspecialchars($string, $quote, 'UTF-8');
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLDefinition.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLDefinition.php
new file mode 100644
index 000000000..c99ac11eb
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLDefinition.php
@@ -0,0 +1,420 @@
+<?php
+
+/**
+ * Definition of the purified HTML that describes allowed children,
+ * attributes, and many other things.
+ *
+ * Conventions:
+ *
+ * All member variables that are prefixed with info
+ * (including the main $info array) are used by HTML Purifier internals
+ * and should not be directly edited when customizing the HTMLDefinition.
+ * They can usually be set via configuration directives or custom
+ * modules.
+ *
+ * On the other hand, member variables without the info prefix are used
+ * internally by the HTMLDefinition and MUST NOT be used by other HTML
+ * Purifier internals. Many of them, however, are public, and may be
+ * edited by userspace code to tweak the behavior of HTMLDefinition.
+ *
+ * @note This class is inspected by Printer_HTMLDefinition; please
+ * update that class if things here change.
+ *
+ * @warning Directives that change this object's structure must be in
+ * the HTML or Attr namespace!
+ */
+class HTMLPurifier_HTMLDefinition extends HTMLPurifier_Definition
+{
+
+ // FULLY-PUBLIC VARIABLES ---------------------------------------------
+
+ /**
+ * Associative array of element names to HTMLPurifier_ElementDef
+ */
+ public $info = array();
+
+ /**
+ * Associative array of global attribute name to attribute definition.
+ */
+ public $info_global_attr = array();
+
+ /**
+ * String name of parent element HTML will be going into.
+ */
+ public $info_parent = 'div';
+
+ /**
+ * Definition for parent element, allows parent element to be a
+ * tag that's not allowed inside the HTML fragment.
+ */
+ public $info_parent_def;
+
+ /**
+ * String name of element used to wrap inline elements in block context
+ * @note This is rarely used except for BLOCKQUOTEs in strict mode
+ */
+ public $info_block_wrapper = 'p';
+
+ /**
+ * Associative array of deprecated tag name to HTMLPurifier_TagTransform
+ */
+ public $info_tag_transform = array();
+
+ /**
+ * Indexed list of HTMLPurifier_AttrTransform to be performed before validation.
+ */
+ public $info_attr_transform_pre = array();
+
+ /**
+ * Indexed list of HTMLPurifier_AttrTransform to be performed after validation.
+ */
+ public $info_attr_transform_post = array();
+
+ /**
+ * Nested lookup array of content set name (Block, Inline) to
+ * element name to whether or not it belongs in that content set.
+ */
+ public $info_content_sets = array();
+
+ /**
+ * Indexed list of HTMLPurifier_Injector to be used.
+ */
+ public $info_injector = array();
+
+ /**
+ * Doctype object
+ */
+ public $doctype;
+
+
+
+ // RAW CUSTOMIZATION STUFF --------------------------------------------
+
+ /**
+ * Adds a custom attribute to a pre-existing element
+ * @note This is strictly convenience, and does not have a corresponding
+ * method in HTMLPurifier_HTMLModule
+ * @param $element_name String element name to add attribute to
+ * @param $attr_name String name of attribute
+ * @param $def Attribute definition, can be string or object, see
+ * HTMLPurifier_AttrTypes for details
+ */
+ public function addAttribute($element_name, $attr_name, $def) {
+ $module = $this->getAnonymousModule();
+ if (!isset($module->info[$element_name])) {
+ $element = $module->addBlankElement($element_name);
+ } else {
+ $element = $module->info[$element_name];
+ }
+ $element->attr[$attr_name] = $def;
+ }
+
+ /**
+ * Adds a custom element to your HTML definition
+ * @note See HTMLPurifier_HTMLModule::addElement for detailed
+ * parameter and return value descriptions.
+ */
+ public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) {
+ $module = $this->getAnonymousModule();
+ // assume that if the user is calling this, the element
+ // is safe. This may not be a good idea
+ $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes);
+ return $element;
+ }
+
+ /**
+ * Adds a blank element to your HTML definition, for overriding
+ * existing behavior
+ * @note See HTMLPurifier_HTMLModule::addBlankElement for detailed
+ * parameter and return value descriptions.
+ */
+ public function addBlankElement($element_name) {
+ $module = $this->getAnonymousModule();
+ $element = $module->addBlankElement($element_name);
+ return $element;
+ }
+
+ /**
+ * Retrieves a reference to the anonymous module, so you can
+ * bust out advanced features without having to make your own
+ * module.
+ */
+ public function getAnonymousModule() {
+ if (!$this->_anonModule) {
+ $this->_anonModule = new HTMLPurifier_HTMLModule();
+ $this->_anonModule->name = 'Anonymous';
+ }
+ return $this->_anonModule;
+ }
+
+ private $_anonModule;
+
+
+ // PUBLIC BUT INTERNAL VARIABLES --------------------------------------
+
+ public $type = 'HTML';
+ public $manager; /**< Instance of HTMLPurifier_HTMLModuleManager */
+
+ /**
+ * Performs low-cost, preliminary initialization.
+ */
+ public function __construct() {
+ $this->manager = new HTMLPurifier_HTMLModuleManager();
+ }
+
+ protected function doSetup($config) {
+ $this->processModules($config);
+ $this->setupConfigStuff($config);
+ unset($this->manager);
+
+ // cleanup some of the element definitions
+ foreach ($this->info as $k => $v) {
+ unset($this->info[$k]->content_model);
+ unset($this->info[$k]->content_model_type);
+ }
+ }
+
+ /**
+ * Extract out the information from the manager
+ */
+ protected function processModules($config) {
+
+ if ($this->_anonModule) {
+ // for user specific changes
+ // this is late-loaded so we don't have to deal with PHP4
+ // reference wonky-ness
+ $this->manager->addModule($this->_anonModule);
+ unset($this->_anonModule);
+ }
+
+ $this->manager->setup($config);
+ $this->doctype = $this->manager->doctype;
+
+ foreach ($this->manager->modules as $module) {
+ foreach($module->info_tag_transform as $k => $v) {
+ if ($v === false) unset($this->info_tag_transform[$k]);
+ else $this->info_tag_transform[$k] = $v;
+ }
+ foreach($module->info_attr_transform_pre as $k => $v) {
+ if ($v === false) unset($this->info_attr_transform_pre[$k]);
+ else $this->info_attr_transform_pre[$k] = $v;
+ }
+ foreach($module->info_attr_transform_post as $k => $v) {
+ if ($v === false) unset($this->info_attr_transform_post[$k]);
+ else $this->info_attr_transform_post[$k] = $v;
+ }
+ foreach ($module->info_injector as $k => $v) {
+ if ($v === false) unset($this->info_injector[$k]);
+ else $this->info_injector[$k] = $v;
+ }
+ }
+
+ $this->info = $this->manager->getElements();
+ $this->info_content_sets = $this->manager->contentSets->lookup;
+
+ }
+
+ /**
+ * Sets up stuff based on config. We need a better way of doing this.
+ */
+ protected function setupConfigStuff($config) {
+
+ $block_wrapper = $config->get('HTML.BlockWrapper');
+ if (isset($this->info_content_sets['Block'][$block_wrapper])) {
+ $this->info_block_wrapper = $block_wrapper;
+ } else {
+ trigger_error('Cannot use non-block element as block wrapper',
+ E_USER_ERROR);
+ }
+
+ $parent = $config->get('HTML.Parent');
+ $def = $this->manager->getElement($parent, true);
+ if ($def) {
+ $this->info_parent = $parent;
+ $this->info_parent_def = $def;
+ } else {
+ trigger_error('Cannot use unrecognized element as parent',
+ E_USER_ERROR);
+ $this->info_parent_def = $this->manager->getElement($this->info_parent, true);
+ }
+
+ // support template text
+ $support = "(for information on implementing this, see the ".
+ "support forums) ";
+
+ // setup allowed elements -----------------------------------------
+
+ $allowed_elements = $config->get('HTML.AllowedElements');
+ $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early
+
+ if (!is_array($allowed_elements) && !is_array($allowed_attributes)) {
+ $allowed = $config->get('HTML.Allowed');
+ if (is_string($allowed)) {
+ list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed);
+ }
+ }
+
+ if (is_array($allowed_elements)) {
+ foreach ($this->info as $name => $d) {
+ if(!isset($allowed_elements[$name])) unset($this->info[$name]);
+ unset($allowed_elements[$name]);
+ }
+ // emit errors
+ foreach ($allowed_elements as $element => $d) {
+ $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful!
+ trigger_error("Element '$element' is not supported $support", E_USER_WARNING);
+ }
+ }
+
+ // setup allowed attributes ---------------------------------------
+
+ $allowed_attributes_mutable = $allowed_attributes; // by copy!
+ if (is_array($allowed_attributes)) {
+
+ // This actually doesn't do anything, since we went away from
+ // global attributes. It's possible that userland code uses
+ // it, but HTMLModuleManager doesn't!
+ foreach ($this->info_global_attr as $attr => $x) {
+ $keys = array($attr, "*@$attr", "*.$attr");
+ $delete = true;
+ foreach ($keys as $key) {
+ if ($delete && isset($allowed_attributes[$key])) {
+ $delete = false;
+ }
+ if (isset($allowed_attributes_mutable[$key])) {
+ unset($allowed_attributes_mutable[$key]);
+ }
+ }
+ if ($delete) unset($this->info_global_attr[$attr]);
+ }
+
+ foreach ($this->info as $tag => $info) {
+ foreach ($info->attr as $attr => $x) {
+ $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr");
+ $delete = true;
+ foreach ($keys as $key) {
+ if ($delete && isset($allowed_attributes[$key])) {
+ $delete = false;
+ }
+ if (isset($allowed_attributes_mutable[$key])) {
+ unset($allowed_attributes_mutable[$key]);
+ }
+ }
+ if ($delete) unset($this->info[$tag]->attr[$attr]);
+ }
+ }
+ // emit errors
+ foreach ($allowed_attributes_mutable as $elattr => $d) {
+ $bits = preg_split('/[.@]/', $elattr, 2);
+ $c = count($bits);
+ switch ($c) {
+ case 2:
+ if ($bits[0] !== '*') {
+ $element = htmlspecialchars($bits[0]);
+ $attribute = htmlspecialchars($bits[1]);
+ if (!isset($this->info[$element])) {
+ trigger_error("Cannot allow attribute '$attribute' if element '$element' is not allowed/supported $support");
+ } else {
+ trigger_error("Attribute '$attribute' in element '$element' not supported $support",
+ E_USER_WARNING);
+ }
+ break;
+ }
+ // otherwise fall through
+ case 1:
+ $attribute = htmlspecialchars($bits[0]);
+ trigger_error("Global attribute '$attribute' is not ".
+ "supported in any elements $support",
+ E_USER_WARNING);
+ break;
+ }
+ }
+
+ }
+
+ // setup forbidden elements ---------------------------------------
+
+ $forbidden_elements = $config->get('HTML.ForbiddenElements');
+ $forbidden_attributes = $config->get('HTML.ForbiddenAttributes');
+
+ foreach ($this->info as $tag => $info) {
+ if (isset($forbidden_elements[$tag])) {
+ unset($this->info[$tag]);
+ continue;
+ }
+ foreach ($info->attr as $attr => $x) {
+ if (
+ isset($forbidden_attributes["$tag@$attr"]) ||
+ isset($forbidden_attributes["*@$attr"]) ||
+ isset($forbidden_attributes[$attr])
+ ) {
+ unset($this->info[$tag]->attr[$attr]);
+ continue;
+ } // this segment might get removed eventually
+ elseif (isset($forbidden_attributes["$tag.$attr"])) {
+ // $tag.$attr are not user supplied, so no worries!
+ trigger_error("Error with $tag.$attr: tag.attr syntax not supported for HTML.ForbiddenAttributes; use tag@attr instead", E_USER_WARNING);
+ }
+ }
+ }
+ foreach ($forbidden_attributes as $key => $v) {
+ if (strlen($key) < 2) continue;
+ if ($key[0] != '*') continue;
+ if ($key[1] == '.') {
+ trigger_error("Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", E_USER_WARNING);
+ }
+ }
+
+ // setup injectors -----------------------------------------------------
+ foreach ($this->info_injector as $i => $injector) {
+ if ($injector->checkNeeded($config) !== false) {
+ // remove injector that does not have it's required
+ // elements/attributes present, and is thus not needed.
+ unset($this->info_injector[$i]);
+ }
+ }
+ }
+
+ /**
+ * Parses a TinyMCE-flavored Allowed Elements and Attributes list into
+ * separate lists for processing. Format is element[attr1|attr2],element2...
+ * @warning Although it's largely drawn from TinyMCE's implementation,
+ * it is different, and you'll probably have to modify your lists
+ * @param $list String list to parse
+ * @param array($allowed_elements, $allowed_attributes)
+ * @todo Give this its own class, probably static interface
+ */
+ public function parseTinyMCEAllowedList($list) {
+
+ $list = str_replace(array(' ', "\t"), '', $list);
+
+ $elements = array();
+ $attributes = array();
+
+ $chunks = preg_split('/(,|[\n\r]+)/', $list);
+ foreach ($chunks as $chunk) {
+ if (empty($chunk)) continue;
+ // remove TinyMCE element control characters
+ if (!strpos($chunk, '[')) {
+ $element = $chunk;
+ $attr = false;
+ } else {
+ list($element, $attr) = explode('[', $chunk);
+ }
+ if ($element !== '*') $elements[$element] = true;
+ if (!$attr) continue;
+ $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ]
+ $attr = explode('|', $attr);
+ foreach ($attr as $key) {
+ $attributes["$element.$key"] = true;
+ }
+ }
+
+ return array($elements, $attributes);
+
+ }
+
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule.php
new file mode 100644
index 000000000..072cf6808
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule.php
@@ -0,0 +1,244 @@
+<?php
+
+/**
+ * Represents an XHTML 1.1 module, with information on elements, tags
+ * and attributes.
+ * @note Even though this is technically XHTML 1.1, it is also used for
+ * regular HTML parsing. We are using modulization as a convenient
+ * way to represent the internals of HTMLDefinition, and our
+ * implementation is by no means conforming and does not directly
+ * use the normative DTDs or XML schemas.
+ * @note The public variables in a module should almost directly
+ * correspond to the variables in HTMLPurifier_HTMLDefinition.
+ * However, the prefix info carries no special meaning in these
+ * objects (include it anyway if that's the correspondence though).
+ * @todo Consider making some member functions protected
+ */
+
+class HTMLPurifier_HTMLModule
+{
+
+ // -- Overloadable ----------------------------------------------------
+
+ /**
+ * Short unique string identifier of the module
+ */
+ public $name;
+
+ /**
+ * Informally, a list of elements this module changes. Not used in
+ * any significant way.
+ */
+ public $elements = array();
+
+ /**
+ * Associative array of element names to element definitions.
+ * Some definitions may be incomplete, to be merged in later
+ * with the full definition.
+ */
+ public $info = array();
+
+ /**
+ * Associative array of content set names to content set additions.
+ * This is commonly used to, say, add an A element to the Inline
+ * content set. This corresponds to an internal variable $content_sets
+ * and NOT info_content_sets member variable of HTMLDefinition.
+ */
+ public $content_sets = array();
+
+ /**
+ * Associative array of attribute collection names to attribute
+ * collection additions. More rarely used for adding attributes to
+ * the global collections. Example is the StyleAttribute module adding
+ * the style attribute to the Core. Corresponds to HTMLDefinition's
+ * attr_collections->info, since the object's data is only info,
+ * with extra behavior associated with it.
+ */
+ public $attr_collections = array();
+
+ /**
+ * Associative array of deprecated tag name to HTMLPurifier_TagTransform
+ */
+ public $info_tag_transform = array();
+
+ /**
+ * List of HTMLPurifier_AttrTransform to be performed before validation.
+ */
+ public $info_attr_transform_pre = array();
+
+ /**
+ * List of HTMLPurifier_AttrTransform to be performed after validation.
+ */
+ public $info_attr_transform_post = array();
+
+ /**
+ * List of HTMLPurifier_Injector to be performed during well-formedness fixing.
+ * An injector will only be invoked if all of it's pre-requisites are met;
+ * if an injector fails setup, there will be no error; it will simply be
+ * silently disabled.
+ */
+ public $info_injector = array();
+
+ /**
+ * Boolean flag that indicates whether or not getChildDef is implemented.
+ * For optimization reasons: may save a call to a function. Be sure
+ * to set it if you do implement getChildDef(), otherwise it will have
+ * no effect!
+ */
+ public $defines_child_def = false;
+
+ /**
+ * Boolean flag whether or not this module is safe. If it is not safe, all
+ * of its members are unsafe. Modules are safe by default (this might be
+ * slightly dangerous, but it doesn't make much sense to force HTML Purifier,
+ * which is based off of safe HTML, to explicitly say, "This is safe," even
+ * though there are modules which are "unsafe")
+ *
+ * @note Previously, safety could be applied at an element level granularity.
+ * We've removed this ability, so in order to add "unsafe" elements
+ * or attributes, a dedicated module with this property set to false
+ * must be used.
+ */
+ public $safe = true;
+
+ /**
+ * Retrieves a proper HTMLPurifier_ChildDef subclass based on
+ * content_model and content_model_type member variables of
+ * the HTMLPurifier_ElementDef class. There is a similar function
+ * in HTMLPurifier_HTMLDefinition.
+ * @param $def HTMLPurifier_ElementDef instance
+ * @return HTMLPurifier_ChildDef subclass
+ */
+ public function getChildDef($def) {return false;}
+
+ // -- Convenience -----------------------------------------------------
+
+ /**
+ * Convenience function that sets up a new element
+ * @param $element Name of element to add
+ * @param $type What content set should element be registered to?
+ * Set as false to skip this step.
+ * @param $contents Allowed children in form of:
+ * "$content_model_type: $content_model"
+ * @param $attr_includes What attribute collections to register to
+ * element?
+ * @param $attr What unique attributes does the element define?
+ * @note See ElementDef for in-depth descriptions of these parameters.
+ * @return Created element definition object, so you
+ * can set advanced parameters
+ */
+ public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) {
+ $this->elements[] = $element;
+ // parse content_model
+ list($content_model_type, $content_model) = $this->parseContents($contents);
+ // merge in attribute inclusions
+ $this->mergeInAttrIncludes($attr, $attr_includes);
+ // add element to content sets
+ if ($type) $this->addElementToContentSet($element, $type);
+ // create element
+ $this->info[$element] = HTMLPurifier_ElementDef::create(
+ $content_model, $content_model_type, $attr
+ );
+ // literal object $contents means direct child manipulation
+ if (!is_string($contents)) $this->info[$element]->child = $contents;
+ return $this->info[$element];
+ }
+
+ /**
+ * Convenience function that creates a totally blank, non-standalone
+ * element.
+ * @param $element Name of element to create
+ * @return Created element
+ */
+ public function addBlankElement($element) {
+ if (!isset($this->info[$element])) {
+ $this->elements[] = $element;
+ $this->info[$element] = new HTMLPurifier_ElementDef();
+ $this->info[$element]->standalone = false;
+ } else {
+ trigger_error("Definition for $element already exists in module, cannot redefine");
+ }
+ return $this->info[$element];
+ }
+
+ /**
+ * Convenience function that registers an element to a content set
+ * @param Element to register
+ * @param Name content set (warning: case sensitive, usually upper-case
+ * first letter)
+ */
+ public function addElementToContentSet($element, $type) {
+ if (!isset($this->content_sets[$type])) $this->content_sets[$type] = '';
+ else $this->content_sets[$type] .= ' | ';
+ $this->content_sets[$type] .= $element;
+ }
+
+ /**
+ * Convenience function that transforms single-string contents
+ * into separate content model and content model type
+ * @param $contents Allowed children in form of:
+ * "$content_model_type: $content_model"
+ * @note If contents is an object, an array of two nulls will be
+ * returned, and the callee needs to take the original $contents
+ * and use it directly.
+ */
+ public function parseContents($contents) {
+ if (!is_string($contents)) return array(null, null); // defer
+ switch ($contents) {
+ // check for shorthand content model forms
+ case 'Empty':
+ return array('empty', '');
+ case 'Inline':
+ return array('optional', 'Inline | #PCDATA');
+ case 'Flow':
+ return array('optional', 'Flow | #PCDATA');
+ }
+ list($content_model_type, $content_model) = explode(':', $contents);
+ $content_model_type = strtolower(trim($content_model_type));
+ $content_model = trim($content_model);
+ return array($content_model_type, $content_model);
+ }
+
+ /**
+ * Convenience function that merges a list of attribute includes into
+ * an attribute array.
+ * @param $attr Reference to attr array to modify
+ * @param $attr_includes Array of includes / string include to merge in
+ */
+ public function mergeInAttrIncludes(&$attr, $attr_includes) {
+ if (!is_array($attr_includes)) {
+ if (empty($attr_includes)) $attr_includes = array();
+ else $attr_includes = array($attr_includes);
+ }
+ $attr[0] = $attr_includes;
+ }
+
+ /**
+ * Convenience function that generates a lookup table with boolean
+ * true as value.
+ * @param $list List of values to turn into a lookup
+ * @note You can also pass an arbitrary number of arguments in
+ * place of the regular argument
+ * @return Lookup array equivalent of list
+ */
+ public function makeLookup($list) {
+ if (is_string($list)) $list = func_get_args();
+ $ret = array();
+ foreach ($list as $value) {
+ if (is_null($value)) continue;
+ $ret[$value] = true;
+ }
+ return $ret;
+ }
+
+ /**
+ * Lazy load construction of the module after determining whether
+ * or not it's needed, and also when a finalized configuration object
+ * is available.
+ * @param $config Instance of HTMLPurifier_Config
+ */
+ public function setup($config) {}
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Bdo.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Bdo.php
new file mode 100644
index 000000000..3d66f1b4e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Bdo.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * XHTML 1.1 Bi-directional Text Module, defines elements that
+ * declare directionality of content. Text Extension Module.
+ */
+class HTMLPurifier_HTMLModule_Bdo extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Bdo';
+ public $attr_collections = array(
+ 'I18N' => array('dir' => false)
+ );
+
+ public function setup($config) {
+ $bdo = $this->addElement(
+ 'bdo', 'Inline', 'Inline', array('Core', 'Lang'),
+ array(
+ 'dir' => 'Enum#ltr,rtl', // required
+ // The Abstract Module specification has the attribute
+ // inclusions wrong for bdo: bdo allows Lang
+ )
+ );
+ $bdo->attr_transform_post['required-dir'] = new HTMLPurifier_AttrTransform_BdoDir();
+
+ $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl';
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/CommonAttributes.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/CommonAttributes.php
new file mode 100644
index 000000000..7c15da84f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/CommonAttributes.php
@@ -0,0 +1,26 @@
+<?php
+
+class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule
+{
+ public $name = 'CommonAttributes';
+
+ public $attr_collections = array(
+ 'Core' => array(
+ 0 => array('Style'),
+ // 'xml:space' => false,
+ 'class' => 'Class',
+ 'id' => 'ID',
+ 'title' => 'CDATA',
+ ),
+ 'Lang' => array(),
+ 'I18N' => array(
+ 0 => array('Lang'), // proprietary, for xml:lang/lang
+ ),
+ 'Common' => array(
+ 0 => array('Core', 'I18N')
+ )
+ );
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Edit.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Edit.php
new file mode 100644
index 000000000..ff9369055
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Edit.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
+ * Module.
+ */
+class HTMLPurifier_HTMLModule_Edit extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Edit';
+
+ public function setup($config) {
+ $contents = 'Chameleon: #PCDATA | Inline ! #PCDATA | Flow';
+ $attr = array(
+ 'cite' => 'URI',
+ // 'datetime' => 'Datetime', // not implemented
+ );
+ $this->addElement('del', 'Inline', $contents, 'Common', $attr);
+ $this->addElement('ins', 'Inline', $contents, 'Common', $attr);
+ }
+
+ // HTML 4.01 specifies that ins/del must not contain block
+ // elements when used in an inline context, chameleon is
+ // a complicated workaround to acheive this effect
+
+ // Inline context ! Block context (exclamation mark is
+ // separator, see getChildDef for parsing)
+
+ public $defines_child_def = true;
+ public function getChildDef($def) {
+ if ($def->content_model_type != 'chameleon') return false;
+ $value = explode('!', $def->content_model);
+ return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Forms.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Forms.php
new file mode 100644
index 000000000..44c22f6f8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Forms.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * XHTML 1.1 Forms module, defines all form-related elements found in HTML 4.
+ */
+class HTMLPurifier_HTMLModule_Forms extends HTMLPurifier_HTMLModule
+{
+ public $name = 'Forms';
+ public $safe = false;
+
+ public $content_sets = array(
+ 'Block' => 'Form',
+ 'Inline' => 'Formctrl',
+ );
+
+ public function setup($config) {
+ $form = $this->addElement('form', 'Form',
+ 'Required: Heading | List | Block | fieldset', 'Common', array(
+ 'accept' => 'ContentTypes',
+ 'accept-charset' => 'Charsets',
+ 'action*' => 'URI',
+ 'method' => 'Enum#get,post',
+ // really ContentType, but these two are the only ones used today
+ 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data',
+ ));
+ $form->excludes = array('form' => true);
+
+ $input = $this->addElement('input', 'Formctrl', 'Empty', 'Common', array(
+ 'accept' => 'ContentTypes',
+ 'accesskey' => 'Character',
+ 'alt' => 'Text',
+ 'checked' => 'Bool#checked',
+ 'disabled' => 'Bool#disabled',
+ 'maxlength' => 'Number',
+ 'name' => 'CDATA',
+ 'readonly' => 'Bool#readonly',
+ 'size' => 'Number',
+ 'src' => 'URI#embeds',
+ 'tabindex' => 'Number',
+ 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image',
+ 'value' => 'CDATA',
+ ));
+ $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input();
+
+ $this->addElement('select', 'Formctrl', 'Required: optgroup | option', 'Common', array(
+ 'disabled' => 'Bool#disabled',
+ 'multiple' => 'Bool#multiple',
+ 'name' => 'CDATA',
+ 'size' => 'Number',
+ 'tabindex' => 'Number',
+ ));
+
+ $this->addElement('option', false, 'Optional: #PCDATA', 'Common', array(
+ 'disabled' => 'Bool#disabled',
+ 'label' => 'Text',
+ 'selected' => 'Bool#selected',
+ 'value' => 'CDATA',
+ ));
+ // It's illegal for there to be more than one selected, but not
+ // be multiple. Also, no selected means undefined behavior. This might
+ // be difficult to implement; perhaps an injector, or a context variable.
+
+ $textarea = $this->addElement('textarea', 'Formctrl', 'Optional: #PCDATA', 'Common', array(
+ 'accesskey' => 'Character',
+ 'cols*' => 'Number',
+ 'disabled' => 'Bool#disabled',
+ 'name' => 'CDATA',
+ 'readonly' => 'Bool#readonly',
+ 'rows*' => 'Number',
+ 'tabindex' => 'Number',
+ ));
+ $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea();
+
+ $button = $this->addElement('button', 'Formctrl', 'Optional: #PCDATA | Heading | List | Block | Inline', 'Common', array(
+ 'accesskey' => 'Character',
+ 'disabled' => 'Bool#disabled',
+ 'name' => 'CDATA',
+ 'tabindex' => 'Number',
+ 'type' => 'Enum#button,submit,reset',
+ 'value' => 'CDATA',
+ ));
+
+ // For exclusions, ideally we'd specify content sets, not literal elements
+ $button->excludes = $this->makeLookup(
+ 'form', 'fieldset', // Form
+ 'input', 'select', 'textarea', 'label', 'button', // Formctrl
+ 'a' // as per HTML 4.01 spec, this is omitted by modularization
+ );
+
+ // Extra exclusion: img usemap="" is not permitted within this element.
+ // We'll omit this for now, since we don't have any good way of
+ // indicating it yet.
+
+ // This is HIGHLY user-unfriendly; we need a custom child-def for this
+ $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common');
+
+ $label = $this->addElement('label', 'Formctrl', 'Optional: #PCDATA | Inline', 'Common', array(
+ 'accesskey' => 'Character',
+ // 'for' => 'IDREF', // IDREF not implemented, cannot allow
+ ));
+ $label->excludes = array('label' => true);
+
+ $this->addElement('legend', false, 'Optional: #PCDATA | Inline', 'Common', array(
+ 'accesskey' => 'Character',
+ ));
+
+ $this->addElement('optgroup', false, 'Required: option', 'Common', array(
+ 'disabled' => 'Bool#disabled',
+ 'label*' => 'Text',
+ ));
+
+ // Don't forget an injector for <isindex>. This one's a little complex
+ // because it maps to multiple elements.
+
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Hypertext.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Hypertext.php
new file mode 100644
index 000000000..d7e9bdd27
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Hypertext.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * XHTML 1.1 Hypertext Module, defines hypertext links. Core Module.
+ */
+class HTMLPurifier_HTMLModule_Hypertext extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Hypertext';
+
+ public function setup($config) {
+ $a = $this->addElement(
+ 'a', 'Inline', 'Inline', 'Common',
+ array(
+ // 'accesskey' => 'Character',
+ // 'charset' => 'Charset',
+ 'href' => 'URI',
+ // 'hreflang' => 'LanguageCode',
+ 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'),
+ 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'),
+ // 'tabindex' => 'Number',
+ // 'type' => 'ContentType',
+ )
+ );
+ $a->formatting = true;
+ $a->excludes = array('a' => true);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Image.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Image.php
new file mode 100644
index 000000000..948d435bc
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Image.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * XHTML 1.1 Image Module provides basic image embedding.
+ * @note There is specialized code for removing empty images in
+ * HTMLPurifier_Strategy_RemoveForeignElements
+ */
+class HTMLPurifier_HTMLModule_Image extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Image';
+
+ public function setup($config) {
+ $max = $config->get('HTML.MaxImgLength');
+ $img = $this->addElement(
+ 'img', 'Inline', 'Empty', 'Common',
+ array(
+ 'alt*' => 'Text',
+ // According to the spec, it's Length, but percents can
+ // be abused, so we allow only Pixels.
+ 'height' => 'Pixels#' . $max,
+ 'width' => 'Pixels#' . $max,
+ 'longdesc' => 'URI',
+ 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded
+ )
+ );
+ if ($max === null || $config->get('HTML.Trusted')) {
+ $img->attr['height'] =
+ $img->attr['width'] = 'Length';
+ }
+
+ // kind of strange, but splitting things up would be inefficient
+ $img->attr_transform_pre[] =
+ $img->attr_transform_post[] =
+ new HTMLPurifier_AttrTransform_ImgRequired();
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Legacy.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Legacy.php
new file mode 100644
index 000000000..df33927ba
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Legacy.php
@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * XHTML 1.1 Legacy module defines elements that were previously
+ * deprecated.
+ *
+ * @note Not all legacy elements have been implemented yet, which
+ * is a bit of a reverse problem as compared to browsers! In
+ * addition, this legacy module may implement a bit more than
+ * mandated by XHTML 1.1.
+ *
+ * This module can be used in combination with TransformToStrict in order
+ * to transform as many deprecated elements as possible, but retain
+ * questionably deprecated elements that do not have good alternatives
+ * as well as transform elements that don't have an implementation.
+ * See docs/ref-strictness.txt for more details.
+ */
+
+class HTMLPurifier_HTMLModule_Legacy extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Legacy';
+
+ public function setup($config) {
+
+ $this->addElement('basefont', 'Inline', 'Empty', false, array(
+ 'color' => 'Color',
+ 'face' => 'Text', // extremely broad, we should
+ 'size' => 'Text', // tighten it
+ 'id' => 'ID'
+ ));
+ $this->addElement('center', 'Block', 'Flow', 'Common');
+ $this->addElement('dir', 'Block', 'Required: li', 'Common', array(
+ 'compact' => 'Bool#compact'
+ ));
+ $this->addElement('font', 'Inline', 'Inline', array('Core', 'I18N'), array(
+ 'color' => 'Color',
+ 'face' => 'Text', // extremely broad, we should
+ 'size' => 'Text', // tighten it
+ ));
+ $this->addElement('menu', 'Block', 'Required: li', 'Common', array(
+ 'compact' => 'Bool#compact'
+ ));
+
+ $s = $this->addElement('s', 'Inline', 'Inline', 'Common');
+ $s->formatting = true;
+
+ $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common');
+ $strike->formatting = true;
+
+ $u = $this->addElement('u', 'Inline', 'Inline', 'Common');
+ $u->formatting = true;
+
+ // setup modifications to old elements
+
+ $align = 'Enum#left,right,center,justify';
+
+ $address = $this->addBlankElement('address');
+ $address->content_model = 'Inline | #PCDATA | p';
+ $address->content_model_type = 'optional';
+ $address->child = false;
+
+ $blockquote = $this->addBlankElement('blockquote');
+ $blockquote->content_model = 'Flow | #PCDATA';
+ $blockquote->content_model_type = 'optional';
+ $blockquote->child = false;
+
+ $br = $this->addBlankElement('br');
+ $br->attr['clear'] = 'Enum#left,all,right,none';
+
+ $caption = $this->addBlankElement('caption');
+ $caption->attr['align'] = 'Enum#top,bottom,left,right';
+
+ $div = $this->addBlankElement('div');
+ $div->attr['align'] = $align;
+
+ $dl = $this->addBlankElement('dl');
+ $dl->attr['compact'] = 'Bool#compact';
+
+ for ($i = 1; $i <= 6; $i++) {
+ $h = $this->addBlankElement("h$i");
+ $h->attr['align'] = $align;
+ }
+
+ $hr = $this->addBlankElement('hr');
+ $hr->attr['align'] = $align;
+ $hr->attr['noshade'] = 'Bool#noshade';
+ $hr->attr['size'] = 'Pixels';
+ $hr->attr['width'] = 'Length';
+
+ $img = $this->addBlankElement('img');
+ $img->attr['align'] = 'Enum#top,middle,bottom,left,right';
+ $img->attr['border'] = 'Pixels';
+ $img->attr['hspace'] = 'Pixels';
+ $img->attr['vspace'] = 'Pixels';
+
+ // figure out this integer business
+
+ $li = $this->addBlankElement('li');
+ $li->attr['value'] = new HTMLPurifier_AttrDef_Integer();
+ $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle';
+
+ $ol = $this->addBlankElement('ol');
+ $ol->attr['compact'] = 'Bool#compact';
+ $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer();
+ $ol->attr['type'] = 'Enum#s:1,i,I,a,A';
+
+ $p = $this->addBlankElement('p');
+ $p->attr['align'] = $align;
+
+ $pre = $this->addBlankElement('pre');
+ $pre->attr['width'] = 'Number';
+
+ // script omitted
+
+ $table = $this->addBlankElement('table');
+ $table->attr['align'] = 'Enum#left,center,right';
+ $table->attr['bgcolor'] = 'Color';
+
+ $tr = $this->addBlankElement('tr');
+ $tr->attr['bgcolor'] = 'Color';
+
+ $th = $this->addBlankElement('th');
+ $th->attr['bgcolor'] = 'Color';
+ $th->attr['height'] = 'Length';
+ $th->attr['nowrap'] = 'Bool#nowrap';
+ $th->attr['width'] = 'Length';
+
+ $td = $this->addBlankElement('td');
+ $td->attr['bgcolor'] = 'Color';
+ $td->attr['height'] = 'Length';
+ $td->attr['nowrap'] = 'Bool#nowrap';
+ $td->attr['width'] = 'Length';
+
+ $ul = $this->addBlankElement('ul');
+ $ul->attr['compact'] = 'Bool#compact';
+ $ul->attr['type'] = 'Enum#square,disc,circle';
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/List.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/List.php
new file mode 100644
index 000000000..1d15f2729
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/List.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * XHTML 1.1 List Module, defines list-oriented elements. Core Module.
+ */
+class HTMLPurifier_HTMLModule_List extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'List';
+
+ // According to the abstract schema, the List content set is a fully formed
+ // one or more expr, but it invariably occurs in an optional declaration
+ // so we're not going to do that subtlety. It might cause trouble
+ // if a user defines "List" and expects that multiple lists are
+ // allowed to be specified, but then again, that's not very intuitive.
+ // Furthermore, the actual XML Schema may disagree. Regardless,
+ // we don't have support for such nested expressions without using
+ // the incredibly inefficient and draconic Custom ChildDef.
+
+ public $content_sets = array('Flow' => 'List');
+
+ public function setup($config) {
+ $this->addElement('ol', 'List', 'Required: li', 'Common');
+ $this->addElement('ul', 'List', 'Required: li', 'Common');
+ $this->addElement('dl', 'List', 'Required: dt | dd', 'Common');
+
+ $this->addElement('li', false, 'Flow', 'Common');
+
+ $this->addElement('dd', false, 'Flow', 'Common');
+ $this->addElement('dt', false, 'Inline', 'Common');
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Name.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Name.php
new file mode 100644
index 000000000..05694b450
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Name.php
@@ -0,0 +1,21 @@
+<?php
+
+class HTMLPurifier_HTMLModule_Name extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Name';
+
+ public function setup($config) {
+ $elements = array('a', 'applet', 'form', 'frame', 'iframe', 'img', 'map');
+ foreach ($elements as $name) {
+ $element = $this->addBlankElement($name);
+ $element->attr['name'] = 'CDATA';
+ if (!$config->get('HTML.Attr.Name.UseCDATA')) {
+ $element->attr_transform_post['NameSync'] = new HTMLPurifier_AttrTransform_NameSync();
+ }
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php
new file mode 100644
index 000000000..5f1b14abb
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php
@@ -0,0 +1,14 @@
+<?php
+
+class HTMLPurifier_HTMLModule_NonXMLCommonAttributes extends HTMLPurifier_HTMLModule
+{
+ public $name = 'NonXMLCommonAttributes';
+
+ public $attr_collections = array(
+ 'Lang' => array(
+ 'lang' => 'LanguageCode',
+ )
+ );
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Object.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Object.php
new file mode 100644
index 000000000..193c1011f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Object.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * XHTML 1.1 Object Module, defines elements for generic object inclusion
+ * @warning Users will commonly use <embed> to cater to legacy browsers: this
+ * module does not allow this sort of behavior
+ */
+class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Object';
+ public $safe = false;
+
+ public function setup($config) {
+
+ $this->addElement('object', 'Inline', 'Optional: #PCDATA | Flow | param', 'Common',
+ array(
+ 'archive' => 'URI',
+ 'classid' => 'URI',
+ 'codebase' => 'URI',
+ 'codetype' => 'Text',
+ 'data' => 'URI',
+ 'declare' => 'Bool#declare',
+ 'height' => 'Length',
+ 'name' => 'CDATA',
+ 'standby' => 'Text',
+ 'tabindex' => 'Number',
+ 'type' => 'ContentType',
+ 'width' => 'Length'
+ )
+ );
+
+ $this->addElement('param', false, 'Empty', false,
+ array(
+ 'id' => 'ID',
+ 'name*' => 'Text',
+ 'type' => 'Text',
+ 'value' => 'Text',
+ 'valuetype' => 'Enum#data,ref,object'
+ )
+ );
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Presentation.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Presentation.php
new file mode 100644
index 000000000..8ff0b5ed7
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Presentation.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * XHTML 1.1 Presentation Module, defines simple presentation-related
+ * markup. Text Extension Module.
+ * @note The official XML Schema and DTD specs further divide this into
+ * two modules:
+ * - Block Presentation (hr)
+ * - Inline Presentation (b, big, i, small, sub, sup, tt)
+ * We have chosen not to heed this distinction, as content_sets
+ * provides satisfactory disambiguation.
+ */
+class HTMLPurifier_HTMLModule_Presentation extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Presentation';
+
+ public function setup($config) {
+ $this->addElement('hr', 'Block', 'Empty', 'Common');
+ $this->addElement('sub', 'Inline', 'Inline', 'Common');
+ $this->addElement('sup', 'Inline', 'Inline', 'Common');
+ $b = $this->addElement('b', 'Inline', 'Inline', 'Common');
+ $b->formatting = true;
+ $big = $this->addElement('big', 'Inline', 'Inline', 'Common');
+ $big->formatting = true;
+ $i = $this->addElement('i', 'Inline', 'Inline', 'Common');
+ $i->formatting = true;
+ $small = $this->addElement('small', 'Inline', 'Inline', 'Common');
+ $small->formatting = true;
+ $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common');
+ $tt->formatting = true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Proprietary.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Proprietary.php
new file mode 100644
index 000000000..dd36a3de0
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Proprietary.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * Module defines proprietary tags and attributes in HTML.
+ * @warning If this module is enabled, standards-compliance is off!
+ */
+class HTMLPurifier_HTMLModule_Proprietary extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Proprietary';
+
+ public function setup($config) {
+
+ $this->addElement('marquee', 'Inline', 'Flow', 'Common',
+ array(
+ 'direction' => 'Enum#left,right,up,down',
+ 'behavior' => 'Enum#alternate',
+ 'width' => 'Length',
+ 'height' => 'Length',
+ 'scrolldelay' => 'Number',
+ 'scrollamount' => 'Number',
+ 'loop' => 'Number',
+ 'bgcolor' => 'Color',
+ 'hspace' => 'Pixels',
+ 'vspace' => 'Pixels',
+ )
+ );
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Ruby.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Ruby.php
new file mode 100644
index 000000000..b26a0a30a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Ruby.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * XHTML 1.1 Ruby Annotation Module, defines elements that indicate
+ * short runs of text alongside base text for annotation or pronounciation.
+ */
+class HTMLPurifier_HTMLModule_Ruby extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Ruby';
+
+ public function setup($config) {
+ $this->addElement('ruby', 'Inline',
+ 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))',
+ 'Common');
+ $this->addElement('rbc', false, 'Required: rb', 'Common');
+ $this->addElement('rtc', false, 'Required: rt', 'Common');
+ $rb = $this->addElement('rb', false, 'Inline', 'Common');
+ $rb->excludes = array('ruby' => true);
+ $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number'));
+ $rt->excludes = array('ruby' => true);
+ $this->addElement('rp', false, 'Optional: #PCDATA', 'Common');
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/SafeEmbed.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/SafeEmbed.php
new file mode 100644
index 000000000..8fc03cb1c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/SafeEmbed.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * A "safe" embed module. See SafeObject. This is a proprietary element.
+ */
+class HTMLPurifier_HTMLModule_SafeEmbed extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'SafeEmbed';
+
+ public function setup($config) {
+
+ $max = $config->get('HTML.MaxImgLength');
+ $embed = $this->addElement(
+ 'embed', 'Inline', 'Empty', 'Common',
+ array(
+ 'src*' => 'URI#embedded',
+ 'type' => 'Enum#application/x-shockwave-flash',
+ 'width' => 'Pixels#' . $max,
+ 'height' => 'Pixels#' . $max,
+ 'allowscriptaccess' => 'Enum#never',
+ 'allownetworking' => 'Enum#internal',
+ 'wmode' => 'Enum#window',
+ 'name' => 'ID',
+ )
+ );
+ $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed();
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/SafeObject.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/SafeObject.php
new file mode 100644
index 000000000..33bac00cf
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/SafeObject.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * A "safe" object module. In theory, objects permitted by this module will
+ * be safe, and untrusted users can be allowed to embed arbitrary flash objects
+ * (maybe other types too, but only Flash is supported as of right now).
+ * Highly experimental.
+ */
+class HTMLPurifier_HTMLModule_SafeObject extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'SafeObject';
+
+ public function setup($config) {
+
+ // These definitions are not intrinsically safe: the attribute transforms
+ // are a vital part of ensuring safety.
+
+ $max = $config->get('HTML.MaxImgLength');
+ $object = $this->addElement(
+ 'object',
+ 'Inline',
+ 'Optional: param | Flow | #PCDATA',
+ 'Common',
+ array(
+ // While technically not required by the spec, we're forcing
+ // it to this value.
+ 'type' => 'Enum#application/x-shockwave-flash',
+ 'width' => 'Pixels#' . $max,
+ 'height' => 'Pixels#' . $max,
+ 'data' => 'URI#embedded'
+ )
+ );
+ $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject();
+
+ $param = $this->addElement('param', false, 'Empty', false,
+ array(
+ 'id' => 'ID',
+ 'name*' => 'Text',
+ 'value' => 'Text'
+ )
+ );
+ $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam();
+ $this->info_injector[] = 'SafeObject';
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Scripting.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Scripting.php
new file mode 100644
index 000000000..cecdea6c3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Scripting.php
@@ -0,0 +1,54 @@
+<?php
+
+/*
+
+WARNING: THIS MODULE IS EXTREMELY DANGEROUS AS IT ENABLES INLINE SCRIPTING
+INSIDE HTML PURIFIER DOCUMENTS. USE ONLY WITH TRUSTED USER INPUT!!!
+
+*/
+
+/**
+ * XHTML 1.1 Scripting module, defines elements that are used to contain
+ * information pertaining to executable scripts or the lack of support
+ * for executable scripts.
+ * @note This module does not contain inline scripting elements
+ */
+class HTMLPurifier_HTMLModule_Scripting extends HTMLPurifier_HTMLModule
+{
+ public $name = 'Scripting';
+ public $elements = array('script', 'noscript');
+ public $content_sets = array('Block' => 'script | noscript', 'Inline' => 'script | noscript');
+ public $safe = false;
+
+ public function setup($config) {
+ // TODO: create custom child-definition for noscript that
+ // auto-wraps stray #PCDATA in a similar manner to
+ // blockquote's custom definition (we would use it but
+ // blockquote's contents are optional while noscript's contents
+ // are required)
+
+ // TODO: convert this to new syntax, main problem is getting
+ // both content sets working
+
+ // In theory, this could be safe, but I don't see any reason to
+ // allow it.
+ $this->info['noscript'] = new HTMLPurifier_ElementDef();
+ $this->info['noscript']->attr = array( 0 => array('Common') );
+ $this->info['noscript']->content_model = 'Heading | List | Block';
+ $this->info['noscript']->content_model_type = 'required';
+
+ $this->info['script'] = new HTMLPurifier_ElementDef();
+ $this->info['script']->attr = array(
+ 'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')),
+ 'src' => new HTMLPurifier_AttrDef_URI(true),
+ 'type' => new HTMLPurifier_AttrDef_Enum(array('text/javascript'))
+ );
+ $this->info['script']->content_model = '#PCDATA';
+ $this->info['script']->content_model_type = 'optional';
+ $this->info['script']->attr_transform_pre['type'] =
+ $this->info['script']->attr_transform_post['type'] =
+ new HTMLPurifier_AttrTransform_ScriptRequired();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/StyleAttribute.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/StyleAttribute.php
new file mode 100644
index 000000000..eb78464cc
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/StyleAttribute.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * XHTML 1.1 Edit Module, defines editing-related elements. Text Extension
+ * Module.
+ */
+class HTMLPurifier_HTMLModule_StyleAttribute extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'StyleAttribute';
+ public $attr_collections = array(
+ // The inclusion routine differs from the Abstract Modules but
+ // is in line with the DTD and XML Schemas.
+ 'Style' => array('style' => false), // see constructor
+ 'Core' => array(0 => array('Style'))
+ );
+
+ public function setup($config) {
+ $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS();
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tables.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tables.php
new file mode 100644
index 000000000..f314ced3f
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tables.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * XHTML 1.1 Tables Module, fully defines accessible table elements.
+ */
+class HTMLPurifier_HTMLModule_Tables extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Tables';
+
+ public function setup($config) {
+
+ $this->addElement('caption', false, 'Inline', 'Common');
+
+ $this->addElement('table', 'Block',
+ new HTMLPurifier_ChildDef_Table(), 'Common',
+ array(
+ 'border' => 'Pixels',
+ 'cellpadding' => 'Length',
+ 'cellspacing' => 'Length',
+ 'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border',
+ 'rules' => 'Enum#none,groups,rows,cols,all',
+ 'summary' => 'Text',
+ 'width' => 'Length'
+ )
+ );
+
+ // common attributes
+ $cell_align = array(
+ 'align' => 'Enum#left,center,right,justify,char',
+ 'charoff' => 'Length',
+ 'valign' => 'Enum#top,middle,bottom,baseline',
+ );
+
+ $cell_t = array_merge(
+ array(
+ 'abbr' => 'Text',
+ 'colspan' => 'Number',
+ 'rowspan' => 'Number',
+ ),
+ $cell_align
+ );
+ $this->addElement('td', false, 'Flow', 'Common', $cell_t);
+ $this->addElement('th', false, 'Flow', 'Common', $cell_t);
+
+ $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align);
+
+ $cell_col = array_merge(
+ array(
+ 'span' => 'Number',
+ 'width' => 'MultiLength',
+ ),
+ $cell_align
+ );
+ $this->addElement('col', false, 'Empty', 'Common', $cell_col);
+ $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col);
+
+ $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align);
+ $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align);
+ $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align);
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Target.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Target.php
new file mode 100644
index 000000000..2b844ecc4
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Target.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * XHTML 1.1 Target Module, defines target attribute in link elements.
+ */
+class HTMLPurifier_HTMLModule_Target extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Target';
+
+ public function setup($config) {
+ $elements = array('a');
+ foreach ($elements as $name) {
+ $e = $this->addBlankElement($name);
+ $e->attr = array(
+ 'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget()
+ );
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Text.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Text.php
new file mode 100644
index 000000000..ae77c7188
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Text.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * XHTML 1.1 Text Module, defines basic text containers. Core Module.
+ * @note In the normative XML Schema specification, this module
+ * is further abstracted into the following modules:
+ * - Block Phrasal (address, blockquote, pre, h1, h2, h3, h4, h5, h6)
+ * - Block Structural (div, p)
+ * - Inline Phrasal (abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var)
+ * - Inline Structural (br, span)
+ * This module, functionally, does not distinguish between these
+ * sub-modules, but the code is internally structured to reflect
+ * these distinctions.
+ */
+class HTMLPurifier_HTMLModule_Text extends HTMLPurifier_HTMLModule
+{
+
+ public $name = 'Text';
+ public $content_sets = array(
+ 'Flow' => 'Heading | Block | Inline'
+ );
+
+ public function setup($config) {
+
+ // Inline Phrasal -------------------------------------------------
+ $this->addElement('abbr', 'Inline', 'Inline', 'Common');
+ $this->addElement('acronym', 'Inline', 'Inline', 'Common');
+ $this->addElement('cite', 'Inline', 'Inline', 'Common');
+ $this->addElement('dfn', 'Inline', 'Inline', 'Common');
+ $this->addElement('kbd', 'Inline', 'Inline', 'Common');
+ $this->addElement('q', 'Inline', 'Inline', 'Common', array('cite' => 'URI'));
+ $this->addElement('samp', 'Inline', 'Inline', 'Common');
+ $this->addElement('var', 'Inline', 'Inline', 'Common');
+
+ $em = $this->addElement('em', 'Inline', 'Inline', 'Common');
+ $em->formatting = true;
+
+ $strong = $this->addElement('strong', 'Inline', 'Inline', 'Common');
+ $strong->formatting = true;
+
+ $code = $this->addElement('code', 'Inline', 'Inline', 'Common');
+ $code->formatting = true;
+
+ // Inline Structural ----------------------------------------------
+ $this->addElement('span', 'Inline', 'Inline', 'Common');
+ $this->addElement('br', 'Inline', 'Empty', 'Core');
+
+ // Block Phrasal --------------------------------------------------
+ $this->addElement('address', 'Block', 'Inline', 'Common');
+ $this->addElement('blockquote', 'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI') );
+ $pre = $this->addElement('pre', 'Block', 'Inline', 'Common');
+ $pre->excludes = $this->makeLookup(
+ 'img', 'big', 'small', 'object', 'applet', 'font', 'basefont' );
+ $this->addElement('h1', 'Heading', 'Inline', 'Common');
+ $this->addElement('h2', 'Heading', 'Inline', 'Common');
+ $this->addElement('h3', 'Heading', 'Inline', 'Common');
+ $this->addElement('h4', 'Heading', 'Inline', 'Common');
+ $this->addElement('h5', 'Heading', 'Inline', 'Common');
+ $this->addElement('h6', 'Heading', 'Inline', 'Common');
+
+ // Block Structural -----------------------------------------------
+ $p = $this->addElement('p', 'Block', 'Inline', 'Common');
+ $p->autoclose = array_flip(array("address", "blockquote", "center", "dir", "div", "dl", "fieldset", "ol", "p", "ul"));
+
+ $this->addElement('div', 'Block', 'Flow', 'Common');
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy.php
new file mode 100644
index 000000000..21783f18e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy.php
@@ -0,0 +1,207 @@
+<?php
+
+/**
+ * Abstract class for a set of proprietary modules that clean up (tidy)
+ * poorly written HTML.
+ * @todo Figure out how to protect some of these methods/properties
+ */
+class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule
+{
+
+ /**
+ * List of supported levels. Index zero is a special case "no fixes"
+ * level.
+ */
+ public $levels = array(0 => 'none', 'light', 'medium', 'heavy');
+
+ /**
+ * Default level to place all fixes in. Disabled by default
+ */
+ public $defaultLevel = null;
+
+ /**
+ * Lists of fixes used by getFixesForLevel(). Format is:
+ * HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2');
+ */
+ public $fixesForLevel = array(
+ 'light' => array(),
+ 'medium' => array(),
+ 'heavy' => array()
+ );
+
+ /**
+ * Lazy load constructs the module by determining the necessary
+ * fixes to create and then delegating to the populate() function.
+ * @todo Wildcard matching and error reporting when an added or
+ * subtracted fix has no effect.
+ */
+ public function setup($config) {
+
+ // create fixes, initialize fixesForLevel
+ $fixes = $this->makeFixes();
+ $this->makeFixesForLevel($fixes);
+
+ // figure out which fixes to use
+ $level = $config->get('HTML.TidyLevel');
+ $fixes_lookup = $this->getFixesForLevel($level);
+
+ // get custom fix declarations: these need namespace processing
+ $add_fixes = $config->get('HTML.TidyAdd');
+ $remove_fixes = $config->get('HTML.TidyRemove');
+
+ foreach ($fixes as $name => $fix) {
+ // needs to be refactored a little to implement globbing
+ if (
+ isset($remove_fixes[$name]) ||
+ (!isset($add_fixes[$name]) && !isset($fixes_lookup[$name]))
+ ) {
+ unset($fixes[$name]);
+ }
+ }
+
+ // populate this module with necessary fixes
+ $this->populate($fixes);
+
+ }
+
+ /**
+ * Retrieves all fixes per a level, returning fixes for that specific
+ * level as well as all levels below it.
+ * @param $level String level identifier, see $levels for valid values
+ * @return Lookup up table of fixes
+ */
+ public function getFixesForLevel($level) {
+ if ($level == $this->levels[0]) {
+ return array();
+ }
+ $activated_levels = array();
+ for ($i = 1, $c = count($this->levels); $i < $c; $i++) {
+ $activated_levels[] = $this->levels[$i];
+ if ($this->levels[$i] == $level) break;
+ }
+ if ($i == $c) {
+ trigger_error(
+ 'Tidy level ' . htmlspecialchars($level) . ' not recognized',
+ E_USER_WARNING
+ );
+ return array();
+ }
+ $ret = array();
+ foreach ($activated_levels as $level) {
+ foreach ($this->fixesForLevel[$level] as $fix) {
+ $ret[$fix] = true;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Dynamically populates the $fixesForLevel member variable using
+ * the fixes array. It may be custom overloaded, used in conjunction
+ * with $defaultLevel, or not used at all.
+ */
+ public function makeFixesForLevel($fixes) {
+ if (!isset($this->defaultLevel)) return;
+ if (!isset($this->fixesForLevel[$this->defaultLevel])) {
+ trigger_error(
+ 'Default level ' . $this->defaultLevel . ' does not exist',
+ E_USER_ERROR
+ );
+ return;
+ }
+ $this->fixesForLevel[$this->defaultLevel] = array_keys($fixes);
+ }
+
+ /**
+ * Populates the module with transforms and other special-case code
+ * based on a list of fixes passed to it
+ * @param $lookup Lookup table of fixes to activate
+ */
+ public function populate($fixes) {
+ foreach ($fixes as $name => $fix) {
+ // determine what the fix is for
+ list($type, $params) = $this->getFixType($name);
+ switch ($type) {
+ case 'attr_transform_pre':
+ case 'attr_transform_post':
+ $attr = $params['attr'];
+ if (isset($params['element'])) {
+ $element = $params['element'];
+ if (empty($this->info[$element])) {
+ $e = $this->addBlankElement($element);
+ } else {
+ $e = $this->info[$element];
+ }
+ } else {
+ $type = "info_$type";
+ $e = $this;
+ }
+ // PHP does some weird parsing when I do
+ // $e->$type[$attr], so I have to assign a ref.
+ $f =& $e->$type;
+ $f[$attr] = $fix;
+ break;
+ case 'tag_transform':
+ $this->info_tag_transform[$params['element']] = $fix;
+ break;
+ case 'child':
+ case 'content_model_type':
+ $element = $params['element'];
+ if (empty($this->info[$element])) {
+ $e = $this->addBlankElement($element);
+ } else {
+ $e = $this->info[$element];
+ }
+ $e->$type = $fix;
+ break;
+ default:
+ trigger_error("Fix type $type not supported", E_USER_ERROR);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parses a fix name and determines what kind of fix it is, as well
+ * as other information defined by the fix
+ * @param $name String name of fix
+ * @return array(string $fix_type, array $fix_parameters)
+ * @note $fix_parameters is type dependant, see populate() for usage
+ * of these parameters
+ */
+ public function getFixType($name) {
+ // parse it
+ $property = $attr = null;
+ if (strpos($name, '#') !== false) list($name, $property) = explode('#', $name);
+ if (strpos($name, '@') !== false) list($name, $attr) = explode('@', $name);
+
+ // figure out the parameters
+ $params = array();
+ if ($name !== '') $params['element'] = $name;
+ if (!is_null($attr)) $params['attr'] = $attr;
+
+ // special case: attribute transform
+ if (!is_null($attr)) {
+ if (is_null($property)) $property = 'pre';
+ $type = 'attr_transform_' . $property;
+ return array($type, $params);
+ }
+
+ // special case: tag transform
+ if (is_null($property)) {
+ return array('tag_transform', $params);
+ }
+
+ return array($property, $params);
+
+ }
+
+ /**
+ * Defines all fixes the module will perform in a compact
+ * associative array of fix name to fix implementation.
+ */
+ public function makeFixes() {}
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Name.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Name.php
new file mode 100644
index 000000000..61ff85ce2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Name.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * Name is deprecated, but allowed in strict doctypes, so onl
+ */
+class HTMLPurifier_HTMLModule_Tidy_Name extends HTMLPurifier_HTMLModule_Tidy
+{
+ public $name = 'Tidy_Name';
+ public $defaultLevel = 'heavy';
+ public function makeFixes() {
+
+ $r = array();
+
+ // @name for img, a -----------------------------------------------
+ // Technically, it's allowed even on strict, so we allow authors to use
+ // it. However, it's deprecated in future versions of XHTML.
+ $r['img@name'] =
+ $r['a@name'] = new HTMLPurifier_AttrTransform_Name();
+
+ return $r;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Proprietary.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Proprietary.php
new file mode 100644
index 000000000..85fa90a94
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Proprietary.php
@@ -0,0 +1,23 @@
+<?php
+
+class HTMLPurifier_HTMLModule_Tidy_Proprietary extends HTMLPurifier_HTMLModule_Tidy
+{
+
+ public $name = 'Tidy_Proprietary';
+ public $defaultLevel = 'light';
+
+ public function makeFixes() {
+ $r = array();
+ $r['table@background'] = new HTMLPurifier_AttrTransform_Background();
+ $r['td@background'] = new HTMLPurifier_AttrTransform_Background();
+ $r['th@background'] = new HTMLPurifier_AttrTransform_Background();
+ $r['tr@background'] = new HTMLPurifier_AttrTransform_Background();
+ $r['thead@background'] = new HTMLPurifier_AttrTransform_Background();
+ $r['tfoot@background'] = new HTMLPurifier_AttrTransform_Background();
+ $r['tbody@background'] = new HTMLPurifier_AttrTransform_Background();
+ return $r;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Strict.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Strict.php
new file mode 100644
index 000000000..c73dc3c4d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Strict.php
@@ -0,0 +1,21 @@
+<?php
+
+class HTMLPurifier_HTMLModule_Tidy_Strict extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
+{
+ public $name = 'Tidy_Strict';
+ public $defaultLevel = 'light';
+
+ public function makeFixes() {
+ $r = parent::makeFixes();
+ $r['blockquote#content_model_type'] = 'strictblockquote';
+ return $r;
+ }
+
+ public $defines_child_def = true;
+ public function getChildDef($def) {
+ if ($def->content_model_type != 'strictblockquote') return parent::getChildDef($def);
+ return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Transitional.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Transitional.php
new file mode 100644
index 000000000..9960b1dd1
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/Transitional.php
@@ -0,0 +1,9 @@
+<?php
+
+class HTMLPurifier_HTMLModule_Tidy_Transitional extends HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4
+{
+ public $name = 'Tidy_Transitional';
+ public $defaultLevel = 'heavy';
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/XHTML.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/XHTML.php
new file mode 100644
index 000000000..db5a378e5
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/XHTML.php
@@ -0,0 +1,17 @@
+<?php
+
+class HTMLPurifier_HTMLModule_Tidy_XHTML extends HTMLPurifier_HTMLModule_Tidy
+{
+
+ public $name = 'Tidy_XHTML';
+ public $defaultLevel = 'medium';
+
+ public function makeFixes() {
+ $r = array();
+ $r['@lang'] = new HTMLPurifier_AttrTransform_Lang();
+ return $r;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php
new file mode 100644
index 000000000..02e943813
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php
@@ -0,0 +1,161 @@
+<?php
+
+class HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends HTMLPurifier_HTMLModule_Tidy
+{
+
+ public function makeFixes() {
+
+ $r = array();
+
+ // == deprecated tag transforms ===================================
+
+ $r['font'] = new HTMLPurifier_TagTransform_Font();
+ $r['menu'] = new HTMLPurifier_TagTransform_Simple('ul');
+ $r['dir'] = new HTMLPurifier_TagTransform_Simple('ul');
+ $r['center'] = new HTMLPurifier_TagTransform_Simple('div', 'text-align:center;');
+ $r['u'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:underline;');
+ $r['s'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;');
+ $r['strike'] = new HTMLPurifier_TagTransform_Simple('span', 'text-decoration:line-through;');
+
+ // == deprecated attribute transforms =============================
+
+ $r['caption@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
+ // we're following IE's behavior, not Firefox's, due
+ // to the fact that no one supports caption-side:right,
+ // W3C included (with CSS 2.1). This is a slightly
+ // unreasonable attribute!
+ 'left' => 'text-align:left;',
+ 'right' => 'text-align:right;',
+ 'top' => 'caption-side:top;',
+ 'bottom' => 'caption-side:bottom;' // not supported by IE
+ ));
+
+ // @align for img -------------------------------------------------
+ $r['img@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
+ 'left' => 'float:left;',
+ 'right' => 'float:right;',
+ 'top' => 'vertical-align:top;',
+ 'middle' => 'vertical-align:middle;',
+ 'bottom' => 'vertical-align:baseline;',
+ ));
+
+ // @align for table -----------------------------------------------
+ $r['table@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
+ 'left' => 'float:left;',
+ 'center' => 'margin-left:auto;margin-right:auto;',
+ 'right' => 'float:right;'
+ ));
+
+ // @align for hr -----------------------------------------------
+ $r['hr@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS('align', array(
+ // we use both text-align and margin because these work
+ // for different browsers (IE and Firefox, respectively)
+ // and the melange makes for a pretty cross-compatible
+ // solution
+ 'left' => 'margin-left:0;margin-right:auto;text-align:left;',
+ 'center' => 'margin-left:auto;margin-right:auto;text-align:center;',
+ 'right' => 'margin-left:auto;margin-right:0;text-align:right;'
+ ));
+
+ // @align for h1, h2, h3, h4, h5, h6, p, div ----------------------
+ // {{{
+ $align_lookup = array();
+ $align_values = array('left', 'right', 'center', 'justify');
+ foreach ($align_values as $v) $align_lookup[$v] = "text-align:$v;";
+ // }}}
+ $r['h1@align'] =
+ $r['h2@align'] =
+ $r['h3@align'] =
+ $r['h4@align'] =
+ $r['h5@align'] =
+ $r['h6@align'] =
+ $r['p@align'] =
+ $r['div@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup);
+
+ // @bgcolor for table, tr, td, th ---------------------------------
+ $r['table@bgcolor'] =
+ $r['td@bgcolor'] =
+ $r['th@bgcolor'] =
+ new HTMLPurifier_AttrTransform_BgColor();
+
+ // @border for img ------------------------------------------------
+ $r['img@border'] = new HTMLPurifier_AttrTransform_Border();
+
+ // @clear for br --------------------------------------------------
+ $r['br@clear'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS('clear', array(
+ 'left' => 'clear:left;',
+ 'right' => 'clear:right;',
+ 'all' => 'clear:both;',
+ 'none' => 'clear:none;',
+ ));
+
+ // @height for td, th ---------------------------------------------
+ $r['td@height'] =
+ $r['th@height'] =
+ new HTMLPurifier_AttrTransform_Length('height');
+
+ // @hspace for img ------------------------------------------------
+ $r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace');
+
+ // @noshade for hr ------------------------------------------------
+ // this transformation is not precise but often good enough.
+ // different browsers use different styles to designate noshade
+ $r['hr@noshade'] =
+ new HTMLPurifier_AttrTransform_BoolToCSS(
+ 'noshade',
+ 'color:#808080;background-color:#808080;border:0;'
+ );
+
+ // @nowrap for td, th ---------------------------------------------
+ $r['td@nowrap'] =
+ $r['th@nowrap'] =
+ new HTMLPurifier_AttrTransform_BoolToCSS(
+ 'nowrap',
+ 'white-space:nowrap;'
+ );
+
+ // @size for hr --------------------------------------------------
+ $r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size', 'height');
+
+ // @type for li, ol, ul -------------------------------------------
+ // {{{
+ $ul_types = array(
+ 'disc' => 'list-style-type:disc;',
+ 'square' => 'list-style-type:square;',
+ 'circle' => 'list-style-type:circle;'
+ );
+ $ol_types = array(
+ '1' => 'list-style-type:decimal;',
+ 'i' => 'list-style-type:lower-roman;',
+ 'I' => 'list-style-type:upper-roman;',
+ 'a' => 'list-style-type:lower-alpha;',
+ 'A' => 'list-style-type:upper-alpha;'
+ );
+ $li_types = $ul_types + $ol_types;
+ // }}}
+
+ $r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types);
+ $r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true);
+ $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true);
+
+ // @vspace for img ------------------------------------------------
+ $r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace');
+
+ // @width for hr, td, th ------------------------------------------
+ $r['td@width'] =
+ $r['th@width'] =
+ $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width');
+
+ return $r;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/XMLCommonAttributes.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/XMLCommonAttributes.php
new file mode 100644
index 000000000..9c0e03198
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModule/XMLCommonAttributes.php
@@ -0,0 +1,14 @@
+<?php
+
+class HTMLPurifier_HTMLModule_XMLCommonAttributes extends HTMLPurifier_HTMLModule
+{
+ public $name = 'XMLCommonAttributes';
+
+ public $attr_collections = array(
+ 'Lang' => array(
+ 'xml:lang' => 'LanguageCode',
+ )
+ );
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/HTMLModuleManager.php b/extlib/HTMLPurifier/HTMLPurifier/HTMLModuleManager.php
new file mode 100644
index 000000000..f5c4a1d2c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/HTMLModuleManager.php
@@ -0,0 +1,403 @@
+<?php
+
+class HTMLPurifier_HTMLModuleManager
+{
+
+ /**
+ * Instance of HTMLPurifier_DoctypeRegistry
+ */
+ public $doctypes;
+
+ /**
+ * Instance of current doctype
+ */
+ public $doctype;
+
+ /**
+ * Instance of HTMLPurifier_AttrTypes
+ */
+ public $attrTypes;
+
+ /**
+ * Active instances of modules for the specified doctype are
+ * indexed, by name, in this array.
+ */
+ public $modules = array();
+
+ /**
+ * Array of recognized HTMLPurifier_Module instances, indexed by
+ * module's class name. This array is usually lazy loaded, but a
+ * user can overload a module by pre-emptively registering it.
+ */
+ public $registeredModules = array();
+
+ /**
+ * List of extra modules that were added by the user using addModule().
+ * These get unconditionally merged into the current doctype, whatever
+ * it may be.
+ */
+ public $userModules = array();
+
+ /**
+ * Associative array of element name to list of modules that have
+ * definitions for the element; this array is dynamically filled.
+ */
+ public $elementLookup = array();
+
+ /** List of prefixes we should use for registering small names */
+ public $prefixes = array('HTMLPurifier_HTMLModule_');
+
+ public $contentSets; /**< Instance of HTMLPurifier_ContentSets */
+ public $attrCollections; /**< Instance of HTMLPurifier_AttrCollections */
+
+ /** If set to true, unsafe elements and attributes will be allowed */
+ public $trusted = false;
+
+ public function __construct() {
+
+ // editable internal objects
+ $this->attrTypes = new HTMLPurifier_AttrTypes();
+ $this->doctypes = new HTMLPurifier_DoctypeRegistry();
+
+ // setup basic modules
+ $common = array(
+ 'CommonAttributes', 'Text', 'Hypertext', 'List',
+ 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image',
+ 'StyleAttribute',
+ // Unsafe:
+ 'Scripting', 'Object', 'Forms',
+ // Sorta legacy, but present in strict:
+ 'Name',
+ );
+ $transitional = array('Legacy', 'Target');
+ $xml = array('XMLCommonAttributes');
+ $non_xml = array('NonXMLCommonAttributes');
+
+ // setup basic doctypes
+ $this->doctypes->register(
+ 'HTML 4.01 Transitional', false,
+ array_merge($common, $transitional, $non_xml),
+ array('Tidy_Transitional', 'Tidy_Proprietary'),
+ array(),
+ '-//W3C//DTD HTML 4.01 Transitional//EN',
+ 'http://www.w3.org/TR/html4/loose.dtd'
+ );
+
+ $this->doctypes->register(
+ 'HTML 4.01 Strict', false,
+ array_merge($common, $non_xml),
+ array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
+ array(),
+ '-//W3C//DTD HTML 4.01//EN',
+ 'http://www.w3.org/TR/html4/strict.dtd'
+ );
+
+ $this->doctypes->register(
+ 'XHTML 1.0 Transitional', true,
+ array_merge($common, $transitional, $xml, $non_xml),
+ array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'),
+ array(),
+ '-//W3C//DTD XHTML 1.0 Transitional//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
+ );
+
+ $this->doctypes->register(
+ 'XHTML 1.0 Strict', true,
+ array_merge($common, $xml, $non_xml),
+ array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
+ array(),
+ '-//W3C//DTD XHTML 1.0 Strict//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
+ );
+
+ $this->doctypes->register(
+ 'XHTML 1.1', true,
+ array_merge($common, $xml, array('Ruby')),
+ array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1
+ array(),
+ '-//W3C//DTD XHTML 1.1//EN',
+ 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
+ );
+
+ }
+
+ /**
+ * Registers a module to the recognized module list, useful for
+ * overloading pre-existing modules.
+ * @param $module Mixed: string module name, with or without
+ * HTMLPurifier_HTMLModule prefix, or instance of
+ * subclass of HTMLPurifier_HTMLModule.
+ * @param $overload Boolean whether or not to overload previous modules.
+ * If this is not set, and you do overload a module,
+ * HTML Purifier will complain with a warning.
+ * @note This function will not call autoload, you must instantiate
+ * (and thus invoke) autoload outside the method.
+ * @note If a string is passed as a module name, different variants
+ * will be tested in this order:
+ * - Check for HTMLPurifier_HTMLModule_$name
+ * - Check all prefixes with $name in order they were added
+ * - Check for literal object name
+ * - Throw fatal error
+ * If your object name collides with an internal class, specify
+ * your module manually. All modules must have been included
+ * externally: registerModule will not perform inclusions for you!
+ */
+ public function registerModule($module, $overload = false) {
+ if (is_string($module)) {
+ // attempt to load the module
+ $original_module = $module;
+ $ok = false;
+ foreach ($this->prefixes as $prefix) {
+ $module = $prefix . $original_module;
+ if (class_exists($module)) {
+ $ok = true;
+ break;
+ }
+ }
+ if (!$ok) {
+ $module = $original_module;
+ if (!class_exists($module)) {
+ trigger_error($original_module . ' module does not exist',
+ E_USER_ERROR);
+ return;
+ }
+ }
+ $module = new $module();
+ }
+ if (empty($module->name)) {
+ trigger_error('Module instance of ' . get_class($module) . ' must have name');
+ return;
+ }
+ if (!$overload && isset($this->registeredModules[$module->name])) {
+ trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING);
+ }
+ $this->registeredModules[$module->name] = $module;
+ }
+
+ /**
+ * Adds a module to the current doctype by first registering it,
+ * and then tacking it on to the active doctype
+ */
+ public function addModule($module) {
+ $this->registerModule($module);
+ if (is_object($module)) $module = $module->name;
+ $this->userModules[] = $module;
+ }
+
+ /**
+ * Adds a class prefix that registerModule() will use to resolve a
+ * string name to a concrete class
+ */
+ public function addPrefix($prefix) {
+ $this->prefixes[] = $prefix;
+ }
+
+ /**
+ * Performs processing on modules, after being called you may
+ * use getElement() and getElements()
+ * @param $config Instance of HTMLPurifier_Config
+ */
+ public function setup($config) {
+
+ $this->trusted = $config->get('HTML.Trusted');
+
+ // generate
+ $this->doctype = $this->doctypes->make($config);
+ $modules = $this->doctype->modules;
+
+ // take out the default modules that aren't allowed
+ $lookup = $config->get('HTML.AllowedModules');
+ $special_cases = $config->get('HTML.CoreModules');
+
+ if (is_array($lookup)) {
+ foreach ($modules as $k => $m) {
+ if (isset($special_cases[$m])) continue;
+ if (!isset($lookup[$m])) unset($modules[$k]);
+ }
+ }
+
+ // add proprietary module (this gets special treatment because
+ // it is completely removed from doctypes, etc.)
+ if ($config->get('HTML.Proprietary')) {
+ $modules[] = 'Proprietary';
+ }
+
+ // add SafeObject/Safeembed modules
+ if ($config->get('HTML.SafeObject')) {
+ $modules[] = 'SafeObject';
+ }
+ if ($config->get('HTML.SafeEmbed')) {
+ $modules[] = 'SafeEmbed';
+ }
+
+ // merge in custom modules
+ $modules = array_merge($modules, $this->userModules);
+
+ foreach ($modules as $module) {
+ $this->processModule($module);
+ $this->modules[$module]->setup($config);
+ }
+
+ foreach ($this->doctype->tidyModules as $module) {
+ $this->processModule($module);
+ $this->modules[$module]->setup($config);
+ }
+
+ // prepare any injectors
+ foreach ($this->modules as $module) {
+ $n = array();
+ foreach ($module->info_injector as $i => $injector) {
+ if (!is_object($injector)) {
+ $class = "HTMLPurifier_Injector_$injector";
+ $injector = new $class;
+ }
+ $n[$injector->name] = $injector;
+ }
+ $module->info_injector = $n;
+ }
+
+ // setup lookup table based on all valid modules
+ foreach ($this->modules as $module) {
+ foreach ($module->info as $name => $def) {
+ if (!isset($this->elementLookup[$name])) {
+ $this->elementLookup[$name] = array();
+ }
+ $this->elementLookup[$name][] = $module->name;
+ }
+ }
+
+ // note the different choice
+ $this->contentSets = new HTMLPurifier_ContentSets(
+ // content set assembly deals with all possible modules,
+ // not just ones deemed to be "safe"
+ $this->modules
+ );
+ $this->attrCollections = new HTMLPurifier_AttrCollections(
+ $this->attrTypes,
+ // there is no way to directly disable a global attribute,
+ // but using AllowedAttributes or simply not including
+ // the module in your custom doctype should be sufficient
+ $this->modules
+ );
+ }
+
+ /**
+ * Takes a module and adds it to the active module collection,
+ * registering it if necessary.
+ */
+ public function processModule($module) {
+ if (!isset($this->registeredModules[$module]) || is_object($module)) {
+ $this->registerModule($module);
+ }
+ $this->modules[$module] = $this->registeredModules[$module];
+ }
+
+ /**
+ * Retrieves merged element definitions.
+ * @return Array of HTMLPurifier_ElementDef
+ */
+ public function getElements() {
+
+ $elements = array();
+ foreach ($this->modules as $module) {
+ if (!$this->trusted && !$module->safe) continue;
+ foreach ($module->info as $name => $v) {
+ if (isset($elements[$name])) continue;
+ $elements[$name] = $this->getElement($name);
+ }
+ }
+
+ // remove dud elements, this happens when an element that
+ // appeared to be safe actually wasn't
+ foreach ($elements as $n => $v) {
+ if ($v === false) unset($elements[$n]);
+ }
+
+ return $elements;
+
+ }
+
+ /**
+ * Retrieves a single merged element definition
+ * @param $name Name of element
+ * @param $trusted Boolean trusted overriding parameter: set to true
+ * if you want the full version of an element
+ * @return Merged HTMLPurifier_ElementDef
+ * @note You may notice that modules are getting iterated over twice (once
+ * in getElements() and once here). This
+ * is because
+ */
+ public function getElement($name, $trusted = null) {
+
+ if (!isset($this->elementLookup[$name])) {
+ return false;
+ }
+
+ // setup global state variables
+ $def = false;
+ if ($trusted === null) $trusted = $this->trusted;
+
+ // iterate through each module that has registered itself to this
+ // element
+ foreach($this->elementLookup[$name] as $module_name) {
+
+ $module = $this->modules[$module_name];
+
+ // refuse to create/merge from a module that is deemed unsafe--
+ // pretend the module doesn't exist--when trusted mode is not on.
+ if (!$trusted && !$module->safe) {
+ continue;
+ }
+
+ // clone is used because, ideally speaking, the original
+ // definition should not be modified. Usually, this will
+ // make no difference, but for consistency's sake
+ $new_def = clone $module->info[$name];
+
+ if (!$def && $new_def->standalone) {
+ $def = $new_def;
+ } elseif ($def) {
+ // This will occur even if $new_def is standalone. In practice,
+ // this will usually result in a full replacement.
+ $def->mergeIn($new_def);
+ } else {
+ // :TODO:
+ // non-standalone definitions that don't have a standalone
+ // to merge into could be deferred to the end
+ continue;
+ }
+
+ // attribute value expansions
+ $this->attrCollections->performInclusions($def->attr);
+ $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes);
+
+ // descendants_are_inline, for ChildDef_Chameleon
+ if (is_string($def->content_model) &&
+ strpos($def->content_model, 'Inline') !== false) {
+ if ($name != 'del' && $name != 'ins') {
+ // this is for you, ins/del
+ $def->descendants_are_inline = true;
+ }
+ }
+
+ $this->contentSets->generateChildDef($def, $module);
+ }
+
+ // This can occur if there is a blank definition, but no base to
+ // mix it in with
+ if (!$def) return false;
+
+ // add information on required attributes
+ foreach ($def->attr as $attr_name => $attr_def) {
+ if ($attr_def->required) {
+ $def->required_attr[] = $attr_name;
+ }
+ }
+
+ return $def;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/IDAccumulator.php b/extlib/HTMLPurifier/HTMLPurifier/IDAccumulator.php
new file mode 100644
index 000000000..73215295a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/IDAccumulator.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * Component of HTMLPurifier_AttrContext that accumulates IDs to prevent dupes
+ * @note In Slashdot-speak, dupe means duplicate.
+ * @note The default constructor does not accept $config or $context objects:
+ * use must use the static build() factory method to perform initialization.
+ */
+class HTMLPurifier_IDAccumulator
+{
+
+ /**
+ * Lookup table of IDs we've accumulated.
+ * @public
+ */
+ public $ids = array();
+
+ /**
+ * Builds an IDAccumulator, also initializing the default blacklist
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ * @return Fully initialized HTMLPurifier_IDAccumulator
+ */
+ public static function build($config, $context) {
+ $id_accumulator = new HTMLPurifier_IDAccumulator();
+ $id_accumulator->load($config->get('Attr.IDBlacklist'));
+ return $id_accumulator;
+ }
+
+ /**
+ * Add an ID to the lookup table.
+ * @param $id ID to be added.
+ * @return Bool status, true if success, false if there's a dupe
+ */
+ public function add($id) {
+ if (isset($this->ids[$id])) return false;
+ return $this->ids[$id] = true;
+ }
+
+ /**
+ * Load a list of IDs into the lookup table
+ * @param $array_of_ids Array of IDs to load
+ * @note This function doesn't care about duplicates
+ */
+ public function load($array_of_ids) {
+ foreach ($array_of_ids as $id) {
+ $this->ids[$id] = true;
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Injector.php b/extlib/HTMLPurifier/HTMLPurifier/Injector.php
new file mode 100644
index 000000000..5922f8130
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Injector.php
@@ -0,0 +1,239 @@
+<?php
+
+/**
+ * Injects tokens into the document while parsing for well-formedness.
+ * This enables "formatter-like" functionality such as auto-paragraphing,
+ * smiley-ification and linkification to take place.
+ *
+ * A note on how handlers create changes; this is done by assigning a new
+ * value to the $token reference. These values can take a variety of forms and
+ * are best described HTMLPurifier_Strategy_MakeWellFormed->processToken()
+ * documentation.
+ *
+ * @todo Allow injectors to request a re-run on their output. This
+ * would help if an operation is recursive.
+ */
+abstract class HTMLPurifier_Injector
+{
+
+ /**
+ * Advisory name of injector, this is for friendly error messages
+ */
+ public $name;
+
+ /**
+ * Instance of HTMLPurifier_HTMLDefinition
+ */
+ protected $htmlDefinition;
+
+ /**
+ * Reference to CurrentNesting variable in Context. This is an array
+ * list of tokens that we are currently "inside"
+ */
+ protected $currentNesting;
+
+ /**
+ * Reference to InputTokens variable in Context. This is an array
+ * list of the input tokens that are being processed.
+ */
+ protected $inputTokens;
+
+ /**
+ * Reference to InputIndex variable in Context. This is an integer
+ * array index for $this->inputTokens that indicates what token
+ * is currently being processed.
+ */
+ protected $inputIndex;
+
+ /**
+ * Array of elements and attributes this injector creates and therefore
+ * need to be allowed by the definition. Takes form of
+ * array('element' => array('attr', 'attr2'), 'element2')
+ */
+ public $needed = array();
+
+ /**
+ * Index of inputTokens to rewind to.
+ */
+ protected $rewind = false;
+
+ /**
+ * Rewind to a spot to re-perform processing. This is useful if you
+ * deleted a node, and now need to see if this change affected any
+ * earlier nodes. Rewinding does not affect other injectors, and can
+ * result in infinite loops if not used carefully.
+ * @warning HTML Purifier will prevent you from fast-forwarding with this
+ * function.
+ */
+ public function rewind($index) {
+ $this->rewind = $index;
+ }
+
+ /**
+ * Retrieves rewind, and then unsets it.
+ */
+ public function getRewind() {
+ $r = $this->rewind;
+ $this->rewind = false;
+ return $r;
+ }
+
+ /**
+ * Prepares the injector by giving it the config and context objects:
+ * this allows references to important variables to be made within
+ * the injector. This function also checks if the HTML environment
+ * will work with the Injector (see checkNeeded()).
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ * @return Boolean false if success, string of missing needed element/attribute if failure
+ */
+ public function prepare($config, $context) {
+ $this->htmlDefinition = $config->getHTMLDefinition();
+ // Even though this might fail, some unit tests ignore this and
+ // still test checkNeeded, so be careful. Maybe get rid of that
+ // dependency.
+ $result = $this->checkNeeded($config);
+ if ($result !== false) return $result;
+ $this->currentNesting =& $context->get('CurrentNesting');
+ $this->inputTokens =& $context->get('InputTokens');
+ $this->inputIndex =& $context->get('InputIndex');
+ return false;
+ }
+
+ /**
+ * This function checks if the HTML environment
+ * will work with the Injector: if p tags are not allowed, the
+ * Auto-Paragraphing injector should not be enabled.
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ * @return Boolean false if success, string of missing needed element/attribute if failure
+ */
+ public function checkNeeded($config) {
+ $def = $config->getHTMLDefinition();
+ foreach ($this->needed as $element => $attributes) {
+ if (is_int($element)) $element = $attributes;
+ if (!isset($def->info[$element])) return $element;
+ if (!is_array($attributes)) continue;
+ foreach ($attributes as $name) {
+ if (!isset($def->info[$element]->attr[$name])) return "$element.$name";
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Tests if the context node allows a certain element
+ * @param $name Name of element to test for
+ * @return True if element is allowed, false if it is not
+ */
+ public function allowsElement($name) {
+ if (!empty($this->currentNesting)) {
+ $parent_token = array_pop($this->currentNesting);
+ $this->currentNesting[] = $parent_token;
+ $parent = $this->htmlDefinition->info[$parent_token->name];
+ } else {
+ $parent = $this->htmlDefinition->info_parent_def;
+ }
+ if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) {
+ return false;
+ }
+ // check for exclusion
+ for ($i = count($this->currentNesting) - 2; $i >= 0; $i--) {
+ $node = $this->currentNesting[$i];
+ $def = $this->htmlDefinition->info[$node->name];
+ if (isset($def->excludes[$name])) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Iterator function, which starts with the next token and continues until
+ * you reach the end of the input tokens.
+ * @warning Please prevent previous references from interfering with this
+ * functions by setting $i = null beforehand!
+ * @param &$i Current integer index variable for inputTokens
+ * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
+ */
+ protected function forward(&$i, &$current) {
+ if ($i === null) $i = $this->inputIndex + 1;
+ else $i++;
+ if (!isset($this->inputTokens[$i])) return false;
+ $current = $this->inputTokens[$i];
+ return true;
+ }
+
+ /**
+ * Similar to _forward, but accepts a third parameter $nesting (which
+ * should be initialized at 0) and stops when we hit the end tag
+ * for the node $this->inputIndex starts in.
+ */
+ protected function forwardUntilEndToken(&$i, &$current, &$nesting) {
+ $result = $this->forward($i, $current);
+ if (!$result) return false;
+ if ($nesting === null) $nesting = 0;
+ if ($current instanceof HTMLPurifier_Token_Start) $nesting++;
+ elseif ($current instanceof HTMLPurifier_Token_End) {
+ if ($nesting <= 0) return false;
+ $nesting--;
+ }
+ return true;
+ }
+
+ /**
+ * Iterator function, starts with the previous token and continues until
+ * you reach the beginning of input tokens.
+ * @warning Please prevent previous references from interfering with this
+ * functions by setting $i = null beforehand!
+ * @param &$i Current integer index variable for inputTokens
+ * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
+ */
+ protected function backward(&$i, &$current) {
+ if ($i === null) $i = $this->inputIndex - 1;
+ else $i--;
+ if ($i < 0) return false;
+ $current = $this->inputTokens[$i];
+ return true;
+ }
+
+ /**
+ * Initializes the iterator at the current position. Use in a do {} while;
+ * loop to force the _forward and _backward functions to start at the
+ * current location.
+ * @warning Please prevent previous references from interfering with this
+ * functions by setting $i = null beforehand!
+ * @param &$i Current integer index variable for inputTokens
+ * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference
+ */
+ protected function current(&$i, &$current) {
+ if ($i === null) $i = $this->inputIndex;
+ $current = $this->inputTokens[$i];
+ }
+
+ /**
+ * Handler that is called when a text token is processed
+ */
+ public function handleText(&$token) {}
+
+ /**
+ * Handler that is called when a start or empty token is processed
+ */
+ public function handleElement(&$token) {}
+
+ /**
+ * Handler that is called when an end token is processed
+ */
+ public function handleEnd(&$token) {
+ $this->notifyEnd($token);
+ }
+
+ /**
+ * Notifier that is called when an end token is processed
+ * @note This differs from handlers in that the token is read-only
+ * @deprecated
+ */
+ public function notifyEnd($token) {}
+
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Injector/AutoParagraph.php b/extlib/HTMLPurifier/HTMLPurifier/Injector/AutoParagraph.php
new file mode 100644
index 000000000..8cc952549
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Injector/AutoParagraph.php
@@ -0,0 +1,340 @@
+<?php
+
+/**
+ * Injector that auto paragraphs text in the root node based on
+ * double-spacing.
+ * @todo Ensure all states are unit tested, including variations as well.
+ * @todo Make a graph of the flow control for this Injector.
+ */
+class HTMLPurifier_Injector_AutoParagraph extends HTMLPurifier_Injector
+{
+
+ public $name = 'AutoParagraph';
+ public $needed = array('p');
+
+ private function _pStart() {
+ $par = new HTMLPurifier_Token_Start('p');
+ $par->armor['MakeWellFormed_TagClosedError'] = true;
+ return $par;
+ }
+
+ public function handleText(&$token) {
+ $text = $token->data;
+ // Does the current parent allow <p> tags?
+ if ($this->allowsElement('p')) {
+ if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) {
+ // Note that we have differing behavior when dealing with text
+ // in the anonymous root node, or a node inside the document.
+ // If the text as a double-newline, the treatment is the same;
+ // if it doesn't, see the next if-block if you're in the document.
+
+ $i = $nesting = null;
+ if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) {
+ // State 1.1: ... ^ (whitespace, then document end)
+ // ----
+ // This is a degenerate case
+ } else {
+ // State 1.2: PAR1
+ // ----
+
+ // State 1.3: PAR1\n\nPAR2
+ // ------------
+
+ // State 1.4: <div>PAR1\n\nPAR2 (see State 2)
+ // ------------
+ $token = array($this->_pStart());
+ $this->_splitText($text, $token);
+ }
+ } else {
+ // State 2: <div>PAR1... (similar to 1.4)
+ // ----
+
+ // We're in an element that allows paragraph tags, but we're not
+ // sure if we're going to need them.
+ if ($this->_pLookAhead()) {
+ // State 2.1: <div>PAR1<b>PAR1\n\nPAR2
+ // ----
+ // Note: This will always be the first child, since any
+ // previous inline element would have triggered this very
+ // same routine, and found the double newline. One possible
+ // exception would be a comment.
+ $token = array($this->_pStart(), $token);
+ } else {
+ // State 2.2.1: <div>PAR1<div>
+ // ----
+
+ // State 2.2.2: <div>PAR1<b>PAR1</b></div>
+ // ----
+ }
+ }
+ // Is the current parent a <p> tag?
+ } elseif (
+ !empty($this->currentNesting) &&
+ $this->currentNesting[count($this->currentNesting)-1]->name == 'p'
+ ) {
+ // State 3.1: ...<p>PAR1
+ // ----
+
+ // State 3.2: ...<p>PAR1\n\nPAR2
+ // ------------
+ $token = array();
+ $this->_splitText($text, $token);
+ // Abort!
+ } else {
+ // State 4.1: ...<b>PAR1
+ // ----
+
+ // State 4.2: ...<b>PAR1\n\nPAR2
+ // ------------
+ }
+ }
+
+ public function handleElement(&$token) {
+ // We don't have to check if we're already in a <p> tag for block
+ // tokens, because the tag would have been autoclosed by MakeWellFormed.
+ if ($this->allowsElement('p')) {
+ if (!empty($this->currentNesting)) {
+ if ($this->_isInline($token)) {
+ // State 1: <div>...<b>
+ // ---
+
+ // Check if this token is adjacent to the parent token
+ // (seek backwards until token isn't whitespace)
+ $i = null;
+ $this->backward($i, $prev);
+
+ if (!$prev instanceof HTMLPurifier_Token_Start) {
+ // Token wasn't adjacent
+
+ if (
+ $prev instanceof HTMLPurifier_Token_Text &&
+ substr($prev->data, -2) === "\n\n"
+ ) {
+ // State 1.1.4: <div><p>PAR1</p>\n\n<b>
+ // ---
+
+ // Quite frankly, this should be handled by splitText
+ $token = array($this->_pStart(), $token);
+ } else {
+ // State 1.1.1: <div><p>PAR1</p><b>
+ // ---
+
+ // State 1.1.2: <div><br /><b>
+ // ---
+
+ // State 1.1.3: <div>PAR<b>
+ // ---
+ }
+
+ } else {
+ // State 1.2.1: <div><b>
+ // ---
+
+ // Lookahead to see if <p> is needed.
+ if ($this->_pLookAhead()) {
+ // State 1.3.1: <div><b>PAR1\n\nPAR2
+ // ---
+ $token = array($this->_pStart(), $token);
+ } else {
+ // State 1.3.2: <div><b>PAR1</b></div>
+ // ---
+
+ // State 1.3.3: <div><b>PAR1</b><div></div>\n\n</div>
+ // ---
+ }
+ }
+ } else {
+ // State 2.3: ...<div>
+ // -----
+ }
+ } else {
+ if ($this->_isInline($token)) {
+ // State 3.1: <b>
+ // ---
+ // This is where the {p} tag is inserted, not reflected in
+ // inputTokens yet, however.
+ $token = array($this->_pStart(), $token);
+ } else {
+ // State 3.2: <div>
+ // -----
+ }
+
+ $i = null;
+ if ($this->backward($i, $prev)) {
+ if (
+ !$prev instanceof HTMLPurifier_Token_Text
+ ) {
+ // State 3.1.1: ...</p>{p}<b>
+ // ---
+
+ // State 3.2.1: ...</p><div>
+ // -----
+
+ if (!is_array($token)) $token = array($token);
+ array_unshift($token, new HTMLPurifier_Token_Text("\n\n"));
+ } else {
+ // State 3.1.2: ...</p>\n\n{p}<b>
+ // ---
+
+ // State 3.2.2: ...</p>\n\n<div>
+ // -----
+
+ // Note: PAR<ELEM> cannot occur because PAR would have been
+ // wrapped in <p> tags.
+ }
+ }
+ }
+ } else {
+ // State 2.2: <ul><li>
+ // ----
+
+ // State 2.4: <p><b>
+ // ---
+ }
+ }
+
+ /**
+ * Splits up a text in paragraph tokens and appends them
+ * to the result stream that will replace the original
+ * @param $data String text data that will be processed
+ * into paragraphs
+ * @param $result Reference to array of tokens that the
+ * tags will be appended onto
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ */
+ private function _splitText($data, &$result) {
+ $raw_paragraphs = explode("\n\n", $data);
+ $paragraphs = array(); // without empty paragraphs
+ $needs_start = false;
+ $needs_end = false;
+
+ $c = count($raw_paragraphs);
+ if ($c == 1) {
+ // There were no double-newlines, abort quickly. In theory this
+ // should never happen.
+ $result[] = new HTMLPurifier_Token_Text($data);
+ return;
+ }
+ for ($i = 0; $i < $c; $i++) {
+ $par = $raw_paragraphs[$i];
+ if (trim($par) !== '') {
+ $paragraphs[] = $par;
+ } else {
+ if ($i == 0) {
+ // Double newline at the front
+ if (empty($result)) {
+ // The empty result indicates that the AutoParagraph
+ // injector did not add any start paragraph tokens.
+ // This means that we have been in a paragraph for
+ // a while, and the newline means we should start a new one.
+ $result[] = new HTMLPurifier_Token_End('p');
+ $result[] = new HTMLPurifier_Token_Text("\n\n");
+ // However, the start token should only be added if
+ // there is more processing to be done (i.e. there are
+ // real paragraphs in here). If there are none, the
+ // next start paragraph tag will be handled by the
+ // next call to the injector
+ $needs_start = true;
+ } else {
+ // We just started a new paragraph!
+ // Reinstate a double-newline for presentation's sake, since
+ // it was in the source code.
+ array_unshift($result, new HTMLPurifier_Token_Text("\n\n"));
+ }
+ } elseif ($i + 1 == $c) {
+ // Double newline at the end
+ // There should be a trailing </p> when we're finally done.
+ $needs_end = true;
+ }
+ }
+ }
+
+ // Check if this was just a giant blob of whitespace. Move this earlier,
+ // perhaps?
+ if (empty($paragraphs)) {
+ return;
+ }
+
+ // Add the start tag indicated by \n\n at the beginning of $data
+ if ($needs_start) {
+ $result[] = $this->_pStart();
+ }
+
+ // Append the paragraphs onto the result
+ foreach ($paragraphs as $par) {
+ $result[] = new HTMLPurifier_Token_Text($par);
+ $result[] = new HTMLPurifier_Token_End('p');
+ $result[] = new HTMLPurifier_Token_Text("\n\n");
+ $result[] = $this->_pStart();
+ }
+
+ // Remove trailing start token; Injector will handle this later if
+ // it was indeed needed. This prevents from needing to do a lookahead,
+ // at the cost of a lookbehind later.
+ array_pop($result);
+
+ // If there is no need for an end tag, remove all of it and let
+ // MakeWellFormed close it later.
+ if (!$needs_end) {
+ array_pop($result); // removes \n\n
+ array_pop($result); // removes </p>
+ }
+
+ }
+
+ /**
+ * Returns true if passed token is inline (and, ergo, allowed in
+ * paragraph tags)
+ */
+ private function _isInline($token) {
+ return isset($this->htmlDefinition->info['p']->child->elements[$token->name]);
+ }
+
+ /**
+ * Looks ahead in the token list and determines whether or not we need
+ * to insert a <p> tag.
+ */
+ private function _pLookAhead() {
+ $this->current($i, $current);
+ if ($current instanceof HTMLPurifier_Token_Start) $nesting = 1;
+ else $nesting = 0;
+ $ok = false;
+ while ($this->forwardUntilEndToken($i, $current, $nesting)) {
+ $result = $this->_checkNeedsP($current);
+ if ($result !== null) {
+ $ok = $result;
+ break;
+ }
+ }
+ return $ok;
+ }
+
+ /**
+ * Determines if a particular token requires an earlier inline token
+ * to get a paragraph. This should be used with _forwardUntilEndToken
+ */
+ private function _checkNeedsP($current) {
+ if ($current instanceof HTMLPurifier_Token_Start){
+ if (!$this->_isInline($current)) {
+ // <div>PAR1<div>
+ // ----
+ // Terminate early, since we hit a block element
+ return false;
+ }
+ } elseif ($current instanceof HTMLPurifier_Token_Text) {
+ if (strpos($current->data, "\n\n") !== false) {
+ // <div>PAR1<b>PAR1\n\nPAR2
+ // ----
+ return true;
+ } else {
+ // <div>PAR1<b>PAR1...
+ // ----
+ }
+ }
+ return null;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Injector/DisplayLinkURI.php b/extlib/HTMLPurifier/HTMLPurifier/Injector/DisplayLinkURI.php
new file mode 100644
index 000000000..9dce9bd08
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Injector/DisplayLinkURI.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * Injector that displays the URL of an anchor instead of linking to it, in addition to showing the text of the link.
+ */
+class HTMLPurifier_Injector_DisplayLinkURI extends HTMLPurifier_Injector
+{
+
+ public $name = 'DisplayLinkURI';
+ public $needed = array('a');
+
+ public function handleElement(&$token) {
+ }
+
+ public function handleEnd(&$token) {
+ if (isset($token->start->attr['href'])){
+ $url = $token->start->attr['href'];
+ unset($token->start->attr['href']);
+ $token = array($token, new HTMLPurifier_Token_Text(" ($url)"));
+ } else {
+ // nothing to display
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Injector/Linkify.php b/extlib/HTMLPurifier/HTMLPurifier/Injector/Linkify.php
new file mode 100644
index 000000000..296dac282
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Injector/Linkify.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Injector that converts http, https and ftp text URLs to actual links.
+ */
+class HTMLPurifier_Injector_Linkify extends HTMLPurifier_Injector
+{
+
+ public $name = 'Linkify';
+ public $needed = array('a' => array('href'));
+
+ public function handleText(&$token) {
+ if (!$this->allowsElement('a')) return;
+
+ if (strpos($token->data, '://') === false) {
+ // our really quick heuristic failed, abort
+ // this may not work so well if we want to match things like
+ // "google.com", but then again, most people don't
+ return;
+ }
+
+ // there is/are URL(s). Let's split the string:
+ // Note: this regex is extremely permissive
+ $bits = preg_split('#((?:https?|ftp)://[^\s\'"<>()]+)#S', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+ $token = array();
+
+ // $i = index
+ // $c = count
+ // $l = is link
+ for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
+ if (!$l) {
+ if ($bits[$i] === '') continue;
+ $token[] = new HTMLPurifier_Token_Text($bits[$i]);
+ } else {
+ $token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i]));
+ $token[] = new HTMLPurifier_Token_Text($bits[$i]);
+ $token[] = new HTMLPurifier_Token_End('a');
+ }
+ }
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Injector/PurifierLinkify.php b/extlib/HTMLPurifier/HTMLPurifier/Injector/PurifierLinkify.php
new file mode 100644
index 000000000..ad2455a91
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Injector/PurifierLinkify.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * Injector that converts configuration directive syntax %Namespace.Directive
+ * to links
+ */
+class HTMLPurifier_Injector_PurifierLinkify extends HTMLPurifier_Injector
+{
+
+ public $name = 'PurifierLinkify';
+ public $docURL;
+ public $needed = array('a' => array('href'));
+
+ public function prepare($config, $context) {
+ $this->docURL = $config->get('AutoFormat.PurifierLinkify.DocURL');
+ return parent::prepare($config, $context);
+ }
+
+ public function handleText(&$token) {
+ if (!$this->allowsElement('a')) return;
+ if (strpos($token->data, '%') === false) return;
+
+ $bits = preg_split('#%([a-z0-9]+\.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
+ $token = array();
+
+ // $i = index
+ // $c = count
+ // $l = is link
+ for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
+ if (!$l) {
+ if ($bits[$i] === '') continue;
+ $token[] = new HTMLPurifier_Token_Text($bits[$i]);
+ } else {
+ $token[] = new HTMLPurifier_Token_Start('a',
+ array('href' => str_replace('%s', $bits[$i], $this->docURL)));
+ $token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]);
+ $token[] = new HTMLPurifier_Token_End('a');
+ }
+ }
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Injector/RemoveEmpty.php b/extlib/HTMLPurifier/HTMLPurifier/Injector/RemoveEmpty.php
new file mode 100644
index 000000000..638bfca03
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Injector/RemoveEmpty.php
@@ -0,0 +1,51 @@
+<?php
+
+class HTMLPurifier_Injector_RemoveEmpty extends HTMLPurifier_Injector
+{
+
+ private $context, $config, $attrValidator, $removeNbsp, $removeNbspExceptions;
+
+ public function prepare($config, $context) {
+ parent::prepare($config, $context);
+ $this->config = $config;
+ $this->context = $context;
+ $this->removeNbsp = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp');
+ $this->removeNbspExceptions = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions');
+ $this->attrValidator = new HTMLPurifier_AttrValidator();
+ }
+
+ public function handleElement(&$token) {
+ if (!$token instanceof HTMLPurifier_Token_Start) return;
+ $next = false;
+ for ($i = $this->inputIndex + 1, $c = count($this->inputTokens); $i < $c; $i++) {
+ $next = $this->inputTokens[$i];
+ if ($next instanceof HTMLPurifier_Token_Text) {
+ if ($next->is_whitespace) continue;
+ if ($this->removeNbsp && !isset($this->removeNbspExceptions[$token->name])) {
+ $plain = str_replace("\xC2\xA0", "", $next->data);
+ $isWsOrNbsp = $plain === '' || ctype_space($plain);
+ if ($isWsOrNbsp) continue;
+ }
+ }
+ break;
+ }
+ if (!$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) {
+ if ($token->name == 'colgroup') return;
+ $this->attrValidator->validateToken($token, $this->config, $this->context);
+ $token->armor['ValidateAttributes'] = true;
+ if (isset($token->attr['id']) || isset($token->attr['name'])) return;
+ $token = $i - $this->inputIndex + 1;
+ for ($b = $this->inputIndex - 1; $b > 0; $b--) {
+ $prev = $this->inputTokens[$b];
+ if ($prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) continue;
+ break;
+ }
+ // This is safe because we removed the token that triggered this.
+ $this->rewind($b - 1);
+ return;
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Injector/SafeObject.php b/extlib/HTMLPurifier/HTMLPurifier/Injector/SafeObject.php
new file mode 100644
index 000000000..341582868
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Injector/SafeObject.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * Adds important param elements to inside of object in order to make
+ * things safe.
+ */
+class HTMLPurifier_Injector_SafeObject extends HTMLPurifier_Injector
+{
+ public $name = 'SafeObject';
+ public $needed = array('object', 'param');
+
+ protected $objectStack = array();
+ protected $paramStack = array();
+
+ // Keep this synchronized with AttrTransform/SafeParam.php
+ protected $addParam = array(
+ 'allowScriptAccess' => 'never',
+ 'allowNetworking' => 'internal',
+ );
+ protected $allowedParam = array(
+ 'wmode' => true,
+ 'movie' => true,
+ );
+
+ public function prepare($config, $context) {
+ parent::prepare($config, $context);
+ }
+
+ public function handleElement(&$token) {
+ if ($token->name == 'object') {
+ $this->objectStack[] = $token;
+ $this->paramStack[] = array();
+ $new = array($token);
+ foreach ($this->addParam as $name => $value) {
+ $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value));
+ }
+ $token = $new;
+ } elseif ($token->name == 'param') {
+ $nest = count($this->currentNesting) - 1;
+ if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') {
+ $i = count($this->objectStack) - 1;
+ if (!isset($token->attr['name'])) {
+ $token = false;
+ return;
+ }
+ $n = $token->attr['name'];
+ // We need this fix because YouTube doesn't supply a data
+ // attribute, which we need if a type is specified. This is
+ // *very* Flash specific.
+ if (!isset($this->objectStack[$i]->attr['data']) && $token->attr['name'] == 'movie') {
+ $this->objectStack[$i]->attr['data'] = $token->attr['value'];
+ }
+ // Check if the parameter is the correct value but has not
+ // already been added
+ if (
+ !isset($this->paramStack[$i][$n]) &&
+ isset($this->addParam[$n]) &&
+ $token->attr['name'] === $this->addParam[$n]
+ ) {
+ // keep token, and add to param stack
+ $this->paramStack[$i][$n] = true;
+ } elseif (isset($this->allowedParam[$n])) {
+ // keep token, don't do anything to it
+ // (could possibly check for duplicates here)
+ } else {
+ $token = false;
+ }
+ } else {
+ // not directly inside an object, DENY!
+ $token = false;
+ }
+ }
+ }
+
+ public function handleEnd(&$token) {
+ // This is the WRONG way of handling the object and param stacks;
+ // we should be inserting them directly on the relevant object tokens
+ // so that the global stack handling handles it.
+ if ($token->name == 'object') {
+ array_pop($this->objectStack);
+ array_pop($this->paramStack);
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Language.php b/extlib/HTMLPurifier/HTMLPurifier/Language.php
new file mode 100644
index 000000000..3e2be03b5
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Language.php
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * Represents a language and defines localizable string formatting and
+ * other functions, as well as the localized messages for HTML Purifier.
+ */
+class HTMLPurifier_Language
+{
+
+ /**
+ * ISO 639 language code of language. Prefers shortest possible version
+ */
+ public $code = 'en';
+
+ /**
+ * Fallback language code
+ */
+ public $fallback = false;
+
+ /**
+ * Array of localizable messages
+ */
+ public $messages = array();
+
+ /**
+ * Array of localizable error codes
+ */
+ public $errorNames = array();
+
+ /**
+ * True if no message file was found for this language, so English
+ * is being used instead. Check this if you'd like to notify the
+ * user that they've used a non-supported language.
+ */
+ public $error = false;
+
+ /**
+ * Has the language object been loaded yet?
+ * @todo Make it private, fix usage in HTMLPurifier_LanguageTest
+ */
+ public $_loaded = false;
+
+ /**
+ * Instances of HTMLPurifier_Config and HTMLPurifier_Context
+ */
+ protected $config, $context;
+
+ public function __construct($config, $context) {
+ $this->config = $config;
+ $this->context = $context;
+ }
+
+ /**
+ * Loads language object with necessary info from factory cache
+ * @note This is a lazy loader
+ */
+ public function load() {
+ if ($this->_loaded) return;
+ $factory = HTMLPurifier_LanguageFactory::instance();
+ $factory->loadLanguage($this->code);
+ foreach ($factory->keys as $key) {
+ $this->$key = $factory->cache[$this->code][$key];
+ }
+ $this->_loaded = true;
+ }
+
+ /**
+ * Retrieves a localised message.
+ * @param $key string identifier of message
+ * @return string localised message
+ */
+ public function getMessage($key) {
+ if (!$this->_loaded) $this->load();
+ if (!isset($this->messages[$key])) return "[$key]";
+ return $this->messages[$key];
+ }
+
+ /**
+ * Retrieves a localised error name.
+ * @param $int integer error number, corresponding to PHP's error
+ * reporting
+ * @return string localised message
+ */
+ public function getErrorName($int) {
+ if (!$this->_loaded) $this->load();
+ if (!isset($this->errorNames[$int])) return "[Error: $int]";
+ return $this->errorNames[$int];
+ }
+
+ /**
+ * Converts an array list into a string readable representation
+ */
+ public function listify($array) {
+ $sep = $this->getMessage('Item separator');
+ $sep_last = $this->getMessage('Item separator last');
+ $ret = '';
+ for ($i = 0, $c = count($array); $i < $c; $i++) {
+ if ($i == 0) {
+ } elseif ($i + 1 < $c) {
+ $ret .= $sep;
+ } else {
+ $ret .= $sep_last;
+ }
+ $ret .= $array[$i];
+ }
+ return $ret;
+ }
+
+ /**
+ * Formats a localised message with passed parameters
+ * @param $key string identifier of message
+ * @param $args Parameters to substitute in
+ * @return string localised message
+ * @todo Implement conditionals? Right now, some messages make
+ * reference to line numbers, but those aren't always available
+ */
+ public function formatMessage($key, $args = array()) {
+ if (!$this->_loaded) $this->load();
+ if (!isset($this->messages[$key])) return "[$key]";
+ $raw = $this->messages[$key];
+ $subst = array();
+ $generator = false;
+ foreach ($args as $i => $value) {
+ if (is_object($value)) {
+ if ($value instanceof HTMLPurifier_Token) {
+ // factor this out some time
+ if (!$generator) $generator = $this->context->get('Generator');
+ if (isset($value->name)) $subst['$'.$i.'.Name'] = $value->name;
+ if (isset($value->data)) $subst['$'.$i.'.Data'] = $value->data;
+ $subst['$'.$i.'.Compact'] =
+ $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value);
+ // a more complex algorithm for compact representation
+ // could be introduced for all types of tokens. This
+ // may need to be factored out into a dedicated class
+ if (!empty($value->attr)) {
+ $stripped_token = clone $value;
+ $stripped_token->attr = array();
+ $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token);
+ }
+ $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown';
+ }
+ continue;
+ } elseif (is_array($value)) {
+ $keys = array_keys($value);
+ if (array_keys($keys) === $keys) {
+ // list
+ $subst['$'.$i] = $this->listify($value);
+ } else {
+ // associative array
+ // no $i implementation yet, sorry
+ $subst['$'.$i.'.Keys'] = $this->listify($keys);
+ $subst['$'.$i.'.Values'] = $this->listify(array_values($value));
+ }
+ continue;
+ }
+ $subst['$' . $i] = $value;
+ }
+ return strtr($raw, $subst);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Language/classes/en-x-test.php b/extlib/HTMLPurifier/HTMLPurifier/Language/classes/en-x-test.php
new file mode 100644
index 000000000..d52fcb7ac
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Language/classes/en-x-test.php
@@ -0,0 +1,12 @@
+<?php
+
+// private class for unit testing
+
+class HTMLPurifier_Language_en_x_test extends HTMLPurifier_Language
+{
+
+
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en-x-test.php b/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en-x-test.php
new file mode 100644
index 000000000..1c046f379
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en-x-test.php
@@ -0,0 +1,11 @@
+<?php
+
+// private language message file for unit testing purposes
+
+$fallback = 'en';
+
+$messages = array(
+ 'HTMLPurifier' => 'HTML Purifier X'
+);
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en-x-testmini.php b/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en-x-testmini.php
new file mode 100644
index 000000000..806c83fbf
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en-x-testmini.php
@@ -0,0 +1,12 @@
+<?php
+
+// private language message file for unit testing purposes
+// this language file has no class associated with it
+
+$fallback = 'en';
+
+$messages = array(
+ 'HTMLPurifier' => 'HTML Purifier XNone'
+);
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en.php b/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en.php
new file mode 100644
index 000000000..aab2e52eb
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Language/messages/en.php
@@ -0,0 +1,62 @@
+<?php
+
+$fallback = false;
+
+$messages = array(
+
+'HTMLPurifier' => 'HTML Purifier',
+
+// for unit testing purposes
+'LanguageFactoryTest: Pizza' => 'Pizza',
+'LanguageTest: List' => '$1',
+'LanguageTest: Hash' => '$1.Keys; $1.Values',
+
+'Item separator' => ', ',
+'Item separator last' => ' and ', // non-Harvard style
+
+'ErrorCollector: No errors' => 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.',
+'ErrorCollector: At line' => ' at line $line',
+'ErrorCollector: Incidental errors' => 'Incidental errors',
+
+'Lexer: Unclosed comment' => 'Unclosed comment',
+'Lexer: Unescaped lt' => 'Unescaped less-than sign (<) should be &lt;',
+'Lexer: Missing gt' => 'Missing greater-than sign (>), previous less-than sign (<) should be escaped',
+'Lexer: Missing attribute key' => 'Attribute declaration has no key',
+'Lexer: Missing end quote' => 'Attribute declaration has no end quote',
+
+'Strategy_RemoveForeignElements: Tag transform' => '<$1> element transformed into $CurrentToken.Serialized',
+'Strategy_RemoveForeignElements: Missing required attribute' => '$CurrentToken.Compact element missing required attribute $1',
+'Strategy_RemoveForeignElements: Foreign element to text' => 'Unrecognized $CurrentToken.Serialized tag converted to text',
+'Strategy_RemoveForeignElements: Foreign element removed' => 'Unrecognized $CurrentToken.Serialized tag removed',
+'Strategy_RemoveForeignElements: Comment removed' => 'Comment containing "$CurrentToken.Data" removed',
+'Strategy_RemoveForeignElements: Foreign meta element removed' => 'Unrecognized $CurrentToken.Serialized meta tag and all descendants removed',
+'Strategy_RemoveForeignElements: Token removed to end' => 'Tags and text starting from $1 element where removed to end',
+'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' => 'Trailing hyphen(s) in comment removed',
+'Strategy_RemoveForeignElements: Hyphens in comment collapsed' => 'Double hyphens in comments are not allowed, and were collapsed into single hyphens',
+
+'Strategy_MakeWellFormed: Unnecessary end tag removed' => 'Unnecessary $CurrentToken.Serialized tag removed',
+'Strategy_MakeWellFormed: Unnecessary end tag to text' => 'Unnecessary $CurrentToken.Serialized tag converted to text',
+'Strategy_MakeWellFormed: Tag auto closed' => '$1.Compact started on line $1.Line auto-closed by $CurrentToken.Compact',
+'Strategy_MakeWellFormed: Tag carryover' => '$1.Compact started on line $1.Line auto-continued into $CurrentToken.Compact',
+'Strategy_MakeWellFormed: Stray end tag removed' => 'Stray $CurrentToken.Serialized tag removed',
+'Strategy_MakeWellFormed: Stray end tag to text' => 'Stray $CurrentToken.Serialized tag converted to text',
+'Strategy_MakeWellFormed: Tag closed by element end' => '$1.Compact tag started on line $1.Line closed by end of $CurrentToken.Serialized',
+'Strategy_MakeWellFormed: Tag closed by document end' => '$1.Compact tag started on line $1.Line closed by end of document',
+
+'Strategy_FixNesting: Node removed' => '$CurrentToken.Compact node removed',
+'Strategy_FixNesting: Node excluded' => '$CurrentToken.Compact node removed due to descendant exclusion by ancestor element',
+'Strategy_FixNesting: Node reorganized' => 'Contents of $CurrentToken.Compact node reorganized to enforce its content model',
+'Strategy_FixNesting: Node contents removed' => 'Contents of $CurrentToken.Compact node removed',
+
+'AttrValidator: Attributes transformed' => 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys',
+'AttrValidator: Attribute removed' => '$CurrentAttr.Name attribute on $CurrentToken.Compact removed',
+
+);
+
+$errorNames = array(
+ E_ERROR => 'Error',
+ E_WARNING => 'Warning',
+ E_NOTICE => 'Notice'
+);
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/LanguageFactory.php b/extlib/HTMLPurifier/HTMLPurifier/LanguageFactory.php
new file mode 100644
index 000000000..134ef8c74
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/LanguageFactory.php
@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * Class responsible for generating HTMLPurifier_Language objects, managing
+ * caching and fallbacks.
+ * @note Thanks to MediaWiki for the general logic, although this version
+ * has been entirely rewritten
+ * @todo Serialized cache for languages
+ */
+class HTMLPurifier_LanguageFactory
+{
+
+ /**
+ * Cache of language code information used to load HTMLPurifier_Language objects
+ * Structure is: $factory->cache[$language_code][$key] = $value
+ * @value array map
+ */
+ public $cache;
+
+ /**
+ * Valid keys in the HTMLPurifier_Language object. Designates which
+ * variables to slurp out of a message file.
+ * @value array list
+ */
+ public $keys = array('fallback', 'messages', 'errorNames');
+
+ /**
+ * Instance of HTMLPurifier_AttrDef_Lang to validate language codes
+ * @value object HTMLPurifier_AttrDef_Lang
+ */
+ protected $validator;
+
+ /**
+ * Cached copy of dirname(__FILE__), directory of current file without
+ * trailing slash
+ * @value string filename
+ */
+ protected $dir;
+
+ /**
+ * Keys whose contents are a hash map and can be merged
+ * @value array lookup
+ */
+ protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true);
+
+ /**
+ * Keys whose contents are a list and can be merged
+ * @value array lookup
+ */
+ protected $mergeable_keys_list = array();
+
+ /**
+ * Retrieve sole instance of the factory.
+ * @param $prototype Optional prototype to overload sole instance with,
+ * or bool true to reset to default factory.
+ */
+ public static function instance($prototype = null) {
+ static $instance = null;
+ if ($prototype !== null) {
+ $instance = $prototype;
+ } elseif ($instance === null || $prototype == true) {
+ $instance = new HTMLPurifier_LanguageFactory();
+ $instance->setup();
+ }
+ return $instance;
+ }
+
+ /**
+ * Sets up the singleton, much like a constructor
+ * @note Prevents people from getting this outside of the singleton
+ */
+ public function setup() {
+ $this->validator = new HTMLPurifier_AttrDef_Lang();
+ $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier';
+ }
+
+ /**
+ * Creates a language object, handles class fallbacks
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ * @param $code Code to override configuration with. Private parameter.
+ */
+ public function create($config, $context, $code = false) {
+
+ // validate language code
+ if ($code === false) {
+ $code = $this->validator->validate(
+ $config->get('Core.Language'), $config, $context
+ );
+ } else {
+ $code = $this->validator->validate($code, $config, $context);
+ }
+ if ($code === false) $code = 'en'; // malformed code becomes English
+
+ $pcode = str_replace('-', '_', $code); // make valid PHP classname
+ static $depth = 0; // recursion protection
+
+ if ($code == 'en') {
+ $lang = new HTMLPurifier_Language($config, $context);
+ } else {
+ $class = 'HTMLPurifier_Language_' . $pcode;
+ $file = $this->dir . '/Language/classes/' . $code . '.php';
+ if (file_exists($file) || class_exists($class, false)) {
+ $lang = new $class($config, $context);
+ } else {
+ // Go fallback
+ $raw_fallback = $this->getFallbackFor($code);
+ $fallback = $raw_fallback ? $raw_fallback : 'en';
+ $depth++;
+ $lang = $this->create($config, $context, $fallback);
+ if (!$raw_fallback) {
+ $lang->error = true;
+ }
+ $depth--;
+ }
+ }
+
+ $lang->code = $code;
+
+ return $lang;
+
+ }
+
+ /**
+ * Returns the fallback language for language
+ * @note Loads the original language into cache
+ * @param $code string language code
+ */
+ public function getFallbackFor($code) {
+ $this->loadLanguage($code);
+ return $this->cache[$code]['fallback'];
+ }
+
+ /**
+ * Loads language into the cache, handles message file and fallbacks
+ * @param $code string language code
+ */
+ public function loadLanguage($code) {
+ static $languages_seen = array(); // recursion guard
+
+ // abort if we've already loaded it
+ if (isset($this->cache[$code])) return;
+
+ // generate filename
+ $filename = $this->dir . '/Language/messages/' . $code . '.php';
+
+ // default fallback : may be overwritten by the ensuing include
+ $fallback = ($code != 'en') ? 'en' : false;
+
+ // load primary localisation
+ if (!file_exists($filename)) {
+ // skip the include: will rely solely on fallback
+ $filename = $this->dir . '/Language/messages/en.php';
+ $cache = array();
+ } else {
+ include $filename;
+ $cache = compact($this->keys);
+ }
+
+ // load fallback localisation
+ if (!empty($fallback)) {
+
+ // infinite recursion guard
+ if (isset($languages_seen[$code])) {
+ trigger_error('Circular fallback reference in language ' .
+ $code, E_USER_ERROR);
+ $fallback = 'en';
+ }
+ $language_seen[$code] = true;
+
+ // load the fallback recursively
+ $this->loadLanguage($fallback);
+ $fallback_cache = $this->cache[$fallback];
+
+ // merge fallback with current language
+ foreach ( $this->keys as $key ) {
+ if (isset($cache[$key]) && isset($fallback_cache[$key])) {
+ if (isset($this->mergeable_keys_map[$key])) {
+ $cache[$key] = $cache[$key] + $fallback_cache[$key];
+ } elseif (isset($this->mergeable_keys_list[$key])) {
+ $cache[$key] = array_merge( $fallback_cache[$key], $cache[$key] );
+ }
+ } else {
+ $cache[$key] = $fallback_cache[$key];
+ }
+ }
+
+ }
+
+ // save to cache for later retrieval
+ $this->cache[$code] = $cache;
+
+ return;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Length.php b/extlib/HTMLPurifier/HTMLPurifier/Length.php
new file mode 100644
index 000000000..8d2a46b7d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Length.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * Represents a measurable length, with a string numeric magnitude
+ * and a unit. This object is immutable.
+ */
+class HTMLPurifier_Length
+{
+
+ /**
+ * String numeric magnitude.
+ */
+ protected $n;
+
+ /**
+ * String unit. False is permitted if $n = 0.
+ */
+ protected $unit;
+
+ /**
+ * Whether or not this length is valid. Null if not calculated yet.
+ */
+ protected $isValid;
+
+ /**
+ * Lookup array of units recognized by CSS 2.1
+ */
+ protected static $allowedUnits = array(
+ 'em' => true, 'ex' => true, 'px' => true, 'in' => true,
+ 'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true
+ );
+
+ /**
+ * @param number $n Magnitude
+ * @param string $u Unit
+ */
+ public function __construct($n = '0', $u = false) {
+ $this->n = (string) $n;
+ $this->unit = $u !== false ? (string) $u : false;
+ }
+
+ /**
+ * @param string $s Unit string, like '2em' or '3.4in'
+ * @warning Does not perform validation.
+ */
+ static public function make($s) {
+ if ($s instanceof HTMLPurifier_Length) return $s;
+ $n_length = strspn($s, '1234567890.+-');
+ $n = substr($s, 0, $n_length);
+ $unit = substr($s, $n_length);
+ if ($unit === '') $unit = false;
+ return new HTMLPurifier_Length($n, $unit);
+ }
+
+ /**
+ * Validates the number and unit.
+ */
+ protected function validate() {
+ // Special case:
+ if ($this->n === '+0' || $this->n === '-0') $this->n = '0';
+ if ($this->n === '0' && $this->unit === false) return true;
+ if (!ctype_lower($this->unit)) $this->unit = strtolower($this->unit);
+ if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) return false;
+ // Hack:
+ $def = new HTMLPurifier_AttrDef_CSS_Number();
+ $result = $def->validate($this->n, false, false);
+ if ($result === false) return false;
+ $this->n = $result;
+ return true;
+ }
+
+ /**
+ * Returns string representation of number.
+ */
+ public function toString() {
+ if (!$this->isValid()) return false;
+ return $this->n . $this->unit;
+ }
+
+ /**
+ * Retrieves string numeric magnitude.
+ */
+ public function getN() {return $this->n;}
+
+ /**
+ * Retrieves string unit.
+ */
+ public function getUnit() {return $this->unit;}
+
+ /**
+ * Returns true if this length unit is valid.
+ */
+ public function isValid() {
+ if ($this->isValid === null) $this->isValid = $this->validate();
+ return $this->isValid;
+ }
+
+ /**
+ * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal.
+ * @warning If both values are too large or small, this calculation will
+ * not work properly
+ */
+ public function compareTo($l) {
+ if ($l === false) return false;
+ if ($l->unit !== $this->unit) {
+ $converter = new HTMLPurifier_UnitConverter();
+ $l = $converter->convert($l, $this->unit);
+ if ($l === false) return false;
+ }
+ return $this->n - $l->n;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Lexer.php b/extlib/HTMLPurifier/HTMLPurifier/Lexer.php
new file mode 100644
index 000000000..8cce008d3
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Lexer.php
@@ -0,0 +1,298 @@
+<?php
+
+/**
+ * Forgivingly lexes HTML (SGML-style) markup into tokens.
+ *
+ * A lexer parses a string of SGML-style markup and converts them into
+ * corresponding tokens. It doesn't check for well-formedness, although its
+ * internal mechanism may make this automatic (such as the case of
+ * HTMLPurifier_Lexer_DOMLex). There are several implementations to choose
+ * from.
+ *
+ * A lexer is HTML-oriented: it might work with XML, but it's not
+ * recommended, as we adhere to a subset of the specification for optimization
+ * reasons. This might change in the future. Also, most tokenizers are not
+ * expected to handle DTDs or PIs.
+ *
+ * This class should not be directly instantiated, but you may use create() to
+ * retrieve a default copy of the lexer. Being a supertype, this class
+ * does not actually define any implementation, but offers commonly used
+ * convenience functions for subclasses.
+ *
+ * @note The unit tests will instantiate this class for testing purposes, as
+ * many of the utility functions require a class to be instantiated.
+ * This means that, even though this class is not runnable, it will
+ * not be declared abstract.
+ *
+ * @par
+ *
+ * @note
+ * We use tokens rather than create a DOM representation because DOM would:
+ *
+ * @par
+ * -# Require more processing and memory to create,
+ * -# Is not streamable, and
+ * -# Has the entire document structure (html and body not needed).
+ *
+ * @par
+ * However, DOM is helpful in that it makes it easy to move around nodes
+ * without a lot of lookaheads to see when a tag is closed. This is a
+ * limitation of the token system and some workarounds would be nice.
+ */
+class HTMLPurifier_Lexer
+{
+
+ /**
+ * Whether or not this lexer implements line-number/column-number tracking.
+ * If it does, set to true.
+ */
+ public $tracksLineNumbers = false;
+
+ // -- STATIC ----------------------------------------------------------
+
+ /**
+ * Retrieves or sets the default Lexer as a Prototype Factory.
+ *
+ * By default HTMLPurifier_Lexer_DOMLex will be returned. There are
+ * a few exceptions involving special features that only DirectLex
+ * implements.
+ *
+ * @note The behavior of this class has changed, rather than accepting
+ * a prototype object, it now accepts a configuration object.
+ * To specify your own prototype, set %Core.LexerImpl to it.
+ * This change in behavior de-singletonizes the lexer object.
+ *
+ * @param $config Instance of HTMLPurifier_Config
+ * @return Concrete lexer.
+ */
+ public static function create($config) {
+
+ if (!($config instanceof HTMLPurifier_Config)) {
+ $lexer = $config;
+ trigger_error("Passing a prototype to
+ HTMLPurifier_Lexer::create() is deprecated, please instead
+ use %Core.LexerImpl", E_USER_WARNING);
+ } else {
+ $lexer = $config->get('Core.LexerImpl');
+ }
+
+ $needs_tracking =
+ $config->get('Core.MaintainLineNumbers') ||
+ $config->get('Core.CollectErrors');
+
+ $inst = null;
+ if (is_object($lexer)) {
+ $inst = $lexer;
+ } else {
+
+ if (is_null($lexer)) { do {
+ // auto-detection algorithm
+
+ if ($needs_tracking) {
+ $lexer = 'DirectLex';
+ break;
+ }
+
+ if (
+ class_exists('DOMDocument') &&
+ method_exists('DOMDocument', 'loadHTML') &&
+ !extension_loaded('domxml')
+ ) {
+ // check for DOM support, because while it's part of the
+ // core, it can be disabled compile time. Also, the PECL
+ // domxml extension overrides the default DOM, and is evil
+ // and nasty and we shan't bother to support it
+ $lexer = 'DOMLex';
+ } else {
+ $lexer = 'DirectLex';
+ }
+
+ } while(0); } // do..while so we can break
+
+ // instantiate recognized string names
+ switch ($lexer) {
+ case 'DOMLex':
+ $inst = new HTMLPurifier_Lexer_DOMLex();
+ break;
+ case 'DirectLex':
+ $inst = new HTMLPurifier_Lexer_DirectLex();
+ break;
+ case 'PH5P':
+ $inst = new HTMLPurifier_Lexer_PH5P();
+ break;
+ default:
+ throw new HTMLPurifier_Exception("Cannot instantiate unrecognized Lexer type " . htmlspecialchars($lexer));
+ }
+ }
+
+ if (!$inst) throw new HTMLPurifier_Exception('No lexer was instantiated');
+
+ // once PHP DOM implements native line numbers, or we
+ // hack out something using XSLT, remove this stipulation
+ if ($needs_tracking && !$inst->tracksLineNumbers) {
+ throw new HTMLPurifier_Exception('Cannot use lexer that does not support line numbers with Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)');
+ }
+
+ return $inst;
+
+ }
+
+ // -- CONVENIENCE MEMBERS ---------------------------------------------
+
+ public function __construct() {
+ $this->_entity_parser = new HTMLPurifier_EntityParser();
+ }
+
+ /**
+ * Most common entity to raw value conversion table for special entities.
+ */
+ protected $_special_entity2str =
+ array(
+ '&quot;' => '"',
+ '&amp;' => '&',
+ '&lt;' => '<',
+ '&gt;' => '>',
+ '&#39;' => "'",
+ '&#039;' => "'",
+ '&#x27;' => "'"
+ );
+
+ /**
+ * Parses special entities into the proper characters.
+ *
+ * This string will translate escaped versions of the special characters
+ * into the correct ones.
+ *
+ * @warning
+ * You should be able to treat the output of this function as
+ * completely parsed, but that's only because all other entities should
+ * have been handled previously in substituteNonSpecialEntities()
+ *
+ * @param $string String character data to be parsed.
+ * @returns Parsed character data.
+ */
+ public function parseData($string) {
+
+ // following functions require at least one character
+ if ($string === '') return '';
+
+ // subtracts amps that cannot possibly be escaped
+ $num_amp = substr_count($string, '&') - substr_count($string, '& ') -
+ ($string[strlen($string)-1] === '&' ? 1 : 0);
+
+ if (!$num_amp) return $string; // abort if no entities
+ $num_esc_amp = substr_count($string, '&amp;');
+ $string = strtr($string, $this->_special_entity2str);
+
+ // code duplication for sake of optimization, see above
+ $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') -
+ ($string[strlen($string)-1] === '&' ? 1 : 0);
+
+ if ($num_amp_2 <= $num_esc_amp) return $string;
+
+ // hmm... now we have some uncommon entities. Use the callback.
+ $string = $this->_entity_parser->substituteSpecialEntities($string);
+ return $string;
+ }
+
+ /**
+ * Lexes an HTML string into tokens.
+ *
+ * @param $string String HTML.
+ * @return HTMLPurifier_Token array representation of HTML.
+ */
+ public function tokenizeHTML($string, $config, $context) {
+ trigger_error('Call to abstract class', E_USER_ERROR);
+ }
+
+ /**
+ * Translates CDATA sections into regular sections (through escaping).
+ *
+ * @param $string HTML string to process.
+ * @returns HTML with CDATA sections escaped.
+ */
+ protected static function escapeCDATA($string) {
+ return preg_replace_callback(
+ '/<!\[CDATA\[(.+?)\]\]>/s',
+ array('HTMLPurifier_Lexer', 'CDATACallback'),
+ $string
+ );
+ }
+
+ /**
+ * Special CDATA case that is especially convoluted for <script>
+ */
+ protected static function escapeCommentedCDATA($string) {
+ return preg_replace_callback(
+ '#<!--//--><!\[CDATA\[//><!--(.+?)//--><!\]\]>#s',
+ array('HTMLPurifier_Lexer', 'CDATACallback'),
+ $string
+ );
+ }
+
+ /**
+ * Callback function for escapeCDATA() that does the work.
+ *
+ * @warning Though this is public in order to let the callback happen,
+ * calling it directly is not recommended.
+ * @params $matches PCRE matches array, with index 0 the entire match
+ * and 1 the inside of the CDATA section.
+ * @returns Escaped internals of the CDATA section.
+ */
+ protected static function CDATACallback($matches) {
+ // not exactly sure why the character set is needed, but whatever
+ return htmlspecialchars($matches[1], ENT_COMPAT, 'UTF-8');
+ }
+
+ /**
+ * Takes a piece of HTML and normalizes it by converting entities, fixing
+ * encoding, extracting bits, and other good stuff.
+ * @todo Consider making protected
+ */
+ public function normalize($html, $config, $context) {
+
+ // normalize newlines to \n
+ $html = str_replace("\r\n", "\n", $html);
+ $html = str_replace("\r", "\n", $html);
+
+ if ($config->get('HTML.Trusted')) {
+ // escape convoluted CDATA
+ $html = $this->escapeCommentedCDATA($html);
+ }
+
+ // escape CDATA
+ $html = $this->escapeCDATA($html);
+
+ // extract body from document if applicable
+ if ($config->get('Core.ConvertDocumentToFragment')) {
+ $html = $this->extractBody($html);
+ }
+
+ // expand entities that aren't the big five
+ $html = $this->_entity_parser->substituteNonSpecialEntities($html);
+
+ // clean into wellformed UTF-8 string for an SGML context: this has
+ // to be done after entity expansion because the entities sometimes
+ // represent non-SGML characters (horror, horror!)
+ $html = HTMLPurifier_Encoder::cleanUTF8($html);
+
+ return $html;
+ }
+
+ /**
+ * Takes a string of HTML (fragment or document) and returns the content
+ * @todo Consider making protected
+ */
+ public function extractBody($html) {
+ $matches = array();
+ $result = preg_match('!<body[^>]*>(.*)</body>!is', $html, $matches);
+ if ($result) {
+ return $matches[1];
+ } else {
+ return $html;
+ }
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Lexer/DOMLex.php b/extlib/HTMLPurifier/HTMLPurifier/Lexer/DOMLex.php
new file mode 100644
index 000000000..20dc2ed48
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Lexer/DOMLex.php
@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * Parser that uses PHP 5's DOM extension (part of the core).
+ *
+ * In PHP 5, the DOM XML extension was revamped into DOM and added to the core.
+ * It gives us a forgiving HTML parser, which we use to transform the HTML
+ * into a DOM, and then into the tokens. It is blazingly fast (for large
+ * documents, it performs twenty times faster than
+ * HTMLPurifier_Lexer_DirectLex,and is the default choice for PHP 5.
+ *
+ * @note Any empty elements will have empty tokens associated with them, even if
+ * this is prohibited by the spec. This is cannot be fixed until the spec
+ * comes into play.
+ *
+ * @note PHP's DOM extension does not actually parse any entities, we use
+ * our own function to do that.
+ *
+ * @warning DOM tends to drop whitespace, which may wreak havoc on indenting.
+ * If this is a huge problem, due to the fact that HTML is hand
+ * edited and you are unable to get a parser cache that caches the
+ * the output of HTML Purifier while keeping the original HTML lying
+ * around, you may want to run Tidy on the resulting output or use
+ * HTMLPurifier_DirectLex
+ */
+
+class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
+{
+
+ private $factory;
+
+ public function __construct() {
+ // setup the factory
+ parent::__construct();
+ $this->factory = new HTMLPurifier_TokenFactory();
+ }
+
+ public function tokenizeHTML($html, $config, $context) {
+
+ $html = $this->normalize($html, $config, $context);
+
+ // attempt to armor stray angled brackets that cannot possibly
+ // form tags and thus are probably being used as emoticons
+ if ($config->get('Core.AggressivelyFixLt')) {
+ $char = '[^a-z!\/]';
+ $comment = "/<!--(.*?)(-->|\z)/is";
+ $html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html);
+ do {
+ $old = $html;
+ $html = preg_replace("/<($char)/i", '&lt;\\1', $html);
+ } while ($html !== $old);
+ $html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments
+ }
+
+ // preprocess html, essential for UTF-8
+ $html = $this->wrapHTML($html, $config, $context);
+
+ $doc = new DOMDocument();
+ $doc->encoding = 'UTF-8'; // theoretically, the above has this covered
+
+ set_error_handler(array($this, 'muteErrorHandler'));
+ $doc->loadHTML($html);
+ restore_error_handler();
+
+ $tokens = array();
+ $this->tokenizeDOM(
+ $doc->getElementsByTagName('html')->item(0)-> // <html>
+ getElementsByTagName('body')->item(0)-> // <body>
+ getElementsByTagName('div')->item(0) // <div>
+ , $tokens);
+ return $tokens;
+ }
+
+ /**
+ * Recursive function that tokenizes a node, putting it into an accumulator.
+ *
+ * @param $node DOMNode to be tokenized.
+ * @param $tokens Array-list of already tokenized tokens.
+ * @param $collect Says whether or start and close are collected, set to
+ * false at first recursion because it's the implicit DIV
+ * tag you're dealing with.
+ * @returns Tokens of node appended to previously passed tokens.
+ */
+ protected function tokenizeDOM($node, &$tokens, $collect = false) {
+
+ // intercept non element nodes. WE MUST catch all of them,
+ // but we're not getting the character reference nodes because
+ // those should have been preprocessed
+ if ($node->nodeType === XML_TEXT_NODE) {
+ $tokens[] = $this->factory->createText($node->data);
+ return;
+ } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) {
+ // undo libxml's special treatment of <script> and <style> tags
+ $last = end($tokens);
+ $data = $node->data;
+ // (note $node->tagname is already normalized)
+ if ($last instanceof HTMLPurifier_Token_Start && ($last->name == 'script' || $last->name == 'style')) {
+ $new_data = trim($data);
+ if (substr($new_data, 0, 4) === '<!--') {
+ $data = substr($new_data, 4);
+ if (substr($data, -3) === '-->') {
+ $data = substr($data, 0, -3);
+ } else {
+ // Highly suspicious! Not sure what to do...
+ }
+ }
+ }
+ $tokens[] = $this->factory->createText($this->parseData($data));
+ return;
+ } elseif ($node->nodeType === XML_COMMENT_NODE) {
+ // this is code is only invoked for comments in script/style in versions
+ // of libxml pre-2.6.28 (regular comments, of course, are still
+ // handled regularly)
+ $tokens[] = $this->factory->createComment($node->data);
+ return;
+ } elseif (
+ // not-well tested: there may be other nodes we have to grab
+ $node->nodeType !== XML_ELEMENT_NODE
+ ) {
+ return;
+ }
+
+ $attr = $node->hasAttributes() ?
+ $this->transformAttrToAssoc($node->attributes) :
+ array();
+
+ // We still have to make sure that the element actually IS empty
+ if (!$node->childNodes->length) {
+ if ($collect) {
+ $tokens[] = $this->factory->createEmpty($node->tagName, $attr);
+ }
+ } else {
+ if ($collect) { // don't wrap on first iteration
+ $tokens[] = $this->factory->createStart(
+ $tag_name = $node->tagName, // somehow, it get's dropped
+ $attr
+ );
+ }
+ foreach ($node->childNodes as $node) {
+ // remember, it's an accumulator. Otherwise, we'd have
+ // to use array_merge
+ $this->tokenizeDOM($node, $tokens, true);
+ }
+ if ($collect) {
+ $tokens[] = $this->factory->createEnd($tag_name);
+ }
+ }
+
+ }
+
+ /**
+ * Converts a DOMNamedNodeMap of DOMAttr objects into an assoc array.
+ *
+ * @param $attribute_list DOMNamedNodeMap of DOMAttr objects.
+ * @returns Associative array of attributes.
+ */
+ protected function transformAttrToAssoc($node_map) {
+ // NamedNodeMap is documented very well, so we're using undocumented
+ // features, namely, the fact that it implements Iterator and
+ // has a ->length attribute
+ if ($node_map->length === 0) return array();
+ $array = array();
+ foreach ($node_map as $attr) {
+ $array[$attr->name] = $attr->value;
+ }
+ return $array;
+ }
+
+ /**
+ * An error handler that mutes all errors
+ */
+ public function muteErrorHandler($errno, $errstr) {}
+
+ /**
+ * Callback function for undoing escaping of stray angled brackets
+ * in comments
+ */
+ public function callbackUndoCommentSubst($matches) {
+ return '<!--' . strtr($matches[1], array('&amp;'=>'&','&lt;'=>'<')) . $matches[2];
+ }
+
+ /**
+ * Callback function that entity-izes ampersands in comments so that
+ * callbackUndoCommentSubst doesn't clobber them
+ */
+ public function callbackArmorCommentEntities($matches) {
+ return '<!--' . str_replace('&', '&amp;', $matches[1]) . $matches[2];
+ }
+
+ /**
+ * Wraps an HTML fragment in the necessary HTML
+ */
+ protected function wrapHTML($html, $config, $context) {
+ $def = $config->getDefinition('HTML');
+ $ret = '';
+
+ if (!empty($def->doctype->dtdPublic) || !empty($def->doctype->dtdSystem)) {
+ $ret .= '<!DOCTYPE html ';
+ if (!empty($def->doctype->dtdPublic)) $ret .= 'PUBLIC "' . $def->doctype->dtdPublic . '" ';
+ if (!empty($def->doctype->dtdSystem)) $ret .= '"' . $def->doctype->dtdSystem . '" ';
+ $ret .= '>';
+ }
+
+ $ret .= '<html><head>';
+ $ret .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
+ // No protection if $html contains a stray </div>!
+ $ret .= '</head><body><div>'.$html.'</div></body></html>';
+ return $ret;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Lexer/DirectLex.php b/extlib/HTMLPurifier/HTMLPurifier/Lexer/DirectLex.php
new file mode 100644
index 000000000..439409d05
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Lexer/DirectLex.php
@@ -0,0 +1,490 @@
+<?php
+
+/**
+ * Our in-house implementation of a parser.
+ *
+ * A pure PHP parser, DirectLex has absolutely no dependencies, making
+ * it a reasonably good default for PHP4. Written with efficiency in mind,
+ * it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it
+ * pales in comparison to HTMLPurifier_Lexer_DOMLex.
+ *
+ * @todo Reread XML spec and document differences.
+ */
+class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
+{
+
+ public $tracksLineNumbers = true;
+
+ /**
+ * Whitespace characters for str(c)spn.
+ */
+ protected $_whitespace = "\x20\x09\x0D\x0A";
+
+ /**
+ * Callback function for script CDATA fudge
+ * @param $matches, in form of array(opening tag, contents, closing tag)
+ */
+ protected function scriptCallback($matches) {
+ return $matches[1] . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8') . $matches[3];
+ }
+
+ public function tokenizeHTML($html, $config, $context) {
+
+ // special normalization for script tags without any armor
+ // our "armor" heurstic is a < sign any number of whitespaces after
+ // the first script tag
+ if ($config->get('HTML.Trusted')) {
+ $html = preg_replace_callback('#(<script[^>]*>)(\s*[^<].+?)(</script>)#si',
+ array($this, 'scriptCallback'), $html);
+ }
+
+ $html = $this->normalize($html, $config, $context);
+
+ $cursor = 0; // our location in the text
+ $inside_tag = false; // whether or not we're parsing the inside of a tag
+ $array = array(); // result array
+
+ // This is also treated to mean maintain *column* numbers too
+ $maintain_line_numbers = $config->get('Core.MaintainLineNumbers');
+
+ if ($maintain_line_numbers === null) {
+ // automatically determine line numbering by checking
+ // if error collection is on
+ $maintain_line_numbers = $config->get('Core.CollectErrors');
+ }
+
+ if ($maintain_line_numbers) {
+ $current_line = 1;
+ $current_col = 0;
+ $length = strlen($html);
+ } else {
+ $current_line = false;
+ $current_col = false;
+ $length = false;
+ }
+ $context->register('CurrentLine', $current_line);
+ $context->register('CurrentCol', $current_col);
+ $nl = "\n";
+ // how often to manually recalculate. This will ALWAYS be right,
+ // but it's pretty wasteful. Set to 0 to turn off
+ $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval');
+
+ $e = false;
+ if ($config->get('Core.CollectErrors')) {
+ $e =& $context->get('ErrorCollector');
+ }
+
+ // for testing synchronization
+ $loops = 0;
+
+ while(++$loops) {
+
+ // $cursor is either at the start of a token, or inside of
+ // a tag (i.e. there was a < immediately before it), as indicated
+ // by $inside_tag
+
+ if ($maintain_line_numbers) {
+
+ // $rcursor, however, is always at the start of a token.
+ $rcursor = $cursor - (int) $inside_tag;
+
+ // Column number is cheap, so we calculate it every round.
+ // We're interested at the *end* of the newline string, so
+ // we need to add strlen($nl) == 1 to $nl_pos before subtracting it
+ // from our "rcursor" position.
+ $nl_pos = strrpos($html, $nl, $rcursor - $length);
+ $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1);
+
+ // recalculate lines
+ if (
+ $synchronize_interval && // synchronization is on
+ $cursor > 0 && // cursor is further than zero
+ $loops % $synchronize_interval === 0 // time to synchronize!
+ ) {
+ $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
+ }
+
+ }
+
+ $position_next_lt = strpos($html, '<', $cursor);
+ $position_next_gt = strpos($html, '>', $cursor);
+
+ // triggers on "<b>asdf</b>" but not "asdf <b></b>"
+ // special case to set up context
+ if ($position_next_lt === $cursor) {
+ $inside_tag = true;
+ $cursor++;
+ }
+
+ if (!$inside_tag && $position_next_lt !== false) {
+ // We are not inside tag and there still is another tag to parse
+ $token = new
+ HTMLPurifier_Token_Text(
+ $this->parseData(
+ substr(
+ $html, $cursor, $position_next_lt - $cursor
+ )
+ )
+ );
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor);
+ }
+ $array[] = $token;
+ $cursor = $position_next_lt + 1;
+ $inside_tag = true;
+ continue;
+ } elseif (!$inside_tag) {
+ // We are not inside tag but there are no more tags
+ // If we're already at the end, break
+ if ($cursor === strlen($html)) break;
+ // Create Text of rest of string
+ $token = new
+ HTMLPurifier_Token_Text(
+ $this->parseData(
+ substr(
+ $html, $cursor
+ )
+ )
+ );
+ if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
+ $array[] = $token;
+ break;
+ } elseif ($inside_tag && $position_next_gt !== false) {
+ // We are in tag and it is well formed
+ // Grab the internals of the tag
+ $strlen_segment = $position_next_gt - $cursor;
+
+ if ($strlen_segment < 1) {
+ // there's nothing to process!
+ $token = new HTMLPurifier_Token_Text('<');
+ $cursor++;
+ continue;
+ }
+
+ $segment = substr($html, $cursor, $strlen_segment);
+
+ if ($segment === false) {
+ // somehow, we attempted to access beyond the end of
+ // the string, defense-in-depth, reported by Nate Abele
+ break;
+ }
+
+ // Check if it's a comment
+ if (
+ substr($segment, 0, 3) === '!--'
+ ) {
+ // re-determine segment length, looking for -->
+ $position_comment_end = strpos($html, '-->', $cursor);
+ if ($position_comment_end === false) {
+ // uh oh, we have a comment that extends to
+ // infinity. Can't be helped: set comment
+ // end position to end of string
+ if ($e) $e->send(E_WARNING, 'Lexer: Unclosed comment');
+ $position_comment_end = strlen($html);
+ $end = true;
+ } else {
+ $end = false;
+ }
+ $strlen_segment = $position_comment_end - $cursor;
+ $segment = substr($html, $cursor, $strlen_segment);
+ $token = new
+ HTMLPurifier_Token_Comment(
+ substr(
+ $segment, 3, $strlen_segment - 3
+ )
+ );
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment);
+ }
+ $array[] = $token;
+ $cursor = $end ? $position_comment_end : $position_comment_end + 3;
+ $inside_tag = false;
+ continue;
+ }
+
+ // Check if it's an end tag
+ $is_end_tag = (strpos($segment,'/') === 0);
+ if ($is_end_tag) {
+ $type = substr($segment, 1);
+ $token = new HTMLPurifier_Token_End($type);
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
+ }
+ $array[] = $token;
+ $inside_tag = false;
+ $cursor = $position_next_gt + 1;
+ continue;
+ }
+
+ // Check leading character is alnum, if not, we may
+ // have accidently grabbed an emoticon. Translate into
+ // text and go our merry way
+ if (!ctype_alpha($segment[0])) {
+ // XML: $segment[0] !== '_' && $segment[0] !== ':'
+ if ($e) $e->send(E_NOTICE, 'Lexer: Unescaped lt');
+ $token = new HTMLPurifier_Token_Text('<');
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
+ }
+ $array[] = $token;
+ $inside_tag = false;
+ continue;
+ }
+
+ // Check if it is explicitly self closing, if so, remove
+ // trailing slash. Remember, we could have a tag like <br>, so
+ // any later token processing scripts must convert improperly
+ // classified EmptyTags from StartTags.
+ $is_self_closing = (strrpos($segment,'/') === $strlen_segment-1);
+ if ($is_self_closing) {
+ $strlen_segment--;
+ $segment = substr($segment, 0, $strlen_segment);
+ }
+
+ // Check if there are any attributes
+ $position_first_space = strcspn($segment, $this->_whitespace);
+
+ if ($position_first_space >= $strlen_segment) {
+ if ($is_self_closing) {
+ $token = new HTMLPurifier_Token_Empty($segment);
+ } else {
+ $token = new HTMLPurifier_Token_Start($segment);
+ }
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
+ }
+ $array[] = $token;
+ $inside_tag = false;
+ $cursor = $position_next_gt + 1;
+ continue;
+ }
+
+ // Grab out all the data
+ $type = substr($segment, 0, $position_first_space);
+ $attribute_string =
+ trim(
+ substr(
+ $segment, $position_first_space
+ )
+ );
+ if ($attribute_string) {
+ $attr = $this->parseAttributeString(
+ $attribute_string
+ , $config, $context
+ );
+ } else {
+ $attr = array();
+ }
+
+ if ($is_self_closing) {
+ $token = new HTMLPurifier_Token_Empty($type, $attr);
+ } else {
+ $token = new HTMLPurifier_Token_Start($type, $attr);
+ }
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
+ }
+ $array[] = $token;
+ $cursor = $position_next_gt + 1;
+ $inside_tag = false;
+ continue;
+ } else {
+ // inside tag, but there's no ending > sign
+ if ($e) $e->send(E_WARNING, 'Lexer: Missing gt');
+ $token = new
+ HTMLPurifier_Token_Text(
+ '<' .
+ $this->parseData(
+ substr($html, $cursor)
+ )
+ );
+ if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
+ // no cursor scroll? Hmm...
+ $array[] = $token;
+ break;
+ }
+ break;
+ }
+
+ $context->destroy('CurrentLine');
+ $context->destroy('CurrentCol');
+ return $array;
+ }
+
+ /**
+ * PHP 5.0.x compatible substr_count that implements offset and length
+ */
+ protected function substrCount($haystack, $needle, $offset, $length) {
+ static $oldVersion;
+ if ($oldVersion === null) {
+ $oldVersion = version_compare(PHP_VERSION, '5.1', '<');
+ }
+ if ($oldVersion) {
+ $haystack = substr($haystack, $offset, $length);
+ return substr_count($haystack, $needle);
+ } else {
+ return substr_count($haystack, $needle, $offset, $length);
+ }
+ }
+
+ /**
+ * Takes the inside of an HTML tag and makes an assoc array of attributes.
+ *
+ * @param $string Inside of tag excluding name.
+ * @returns Assoc array of attributes.
+ */
+ public function parseAttributeString($string, $config, $context) {
+ $string = (string) $string; // quick typecast
+
+ if ($string == '') return array(); // no attributes
+
+ $e = false;
+ if ($config->get('Core.CollectErrors')) {
+ $e =& $context->get('ErrorCollector');
+ }
+
+ // let's see if we can abort as quickly as possible
+ // one equal sign, no spaces => one attribute
+ $num_equal = substr_count($string, '=');
+ $has_space = strpos($string, ' ');
+ if ($num_equal === 0 && !$has_space) {
+ // bool attribute
+ return array($string => $string);
+ } elseif ($num_equal === 1 && !$has_space) {
+ // only one attribute
+ list($key, $quoted_value) = explode('=', $string);
+ $quoted_value = trim($quoted_value);
+ if (!$key) {
+ if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
+ return array();
+ }
+ if (!$quoted_value) return array($key => '');
+ $first_char = @$quoted_value[0];
+ $last_char = @$quoted_value[strlen($quoted_value)-1];
+
+ $same_quote = ($first_char == $last_char);
+ $open_quote = ($first_char == '"' || $first_char == "'");
+
+ if ( $same_quote && $open_quote) {
+ // well behaved
+ $value = substr($quoted_value, 1, strlen($quoted_value) - 2);
+ } else {
+ // not well behaved
+ if ($open_quote) {
+ if ($e) $e->send(E_ERROR, 'Lexer: Missing end quote');
+ $value = substr($quoted_value, 1);
+ } else {
+ $value = $quoted_value;
+ }
+ }
+ if ($value === false) $value = '';
+ return array($key => $value);
+ }
+
+ // setup loop environment
+ $array = array(); // return assoc array of attributes
+ $cursor = 0; // current position in string (moves forward)
+ $size = strlen($string); // size of the string (stays the same)
+
+ // if we have unquoted attributes, the parser expects a terminating
+ // space, so let's guarantee that there's always a terminating space.
+ $string .= ' ';
+
+ while(true) {
+
+ if ($cursor >= $size) {
+ break;
+ }
+
+ $cursor += ($value = strspn($string, $this->_whitespace, $cursor));
+ // grab the key
+
+ $key_begin = $cursor; //we're currently at the start of the key
+
+ // scroll past all characters that are the key (not whitespace or =)
+ $cursor += strcspn($string, $this->_whitespace . '=', $cursor);
+
+ $key_end = $cursor; // now at the end of the key
+
+ $key = substr($string, $key_begin, $key_end - $key_begin);
+
+ if (!$key) {
+ if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
+ $cursor += strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop
+ continue; // empty key
+ }
+
+ // scroll past all whitespace
+ $cursor += strspn($string, $this->_whitespace, $cursor);
+
+ if ($cursor >= $size) {
+ $array[$key] = $key;
+ break;
+ }
+
+ // if the next character is an equal sign, we've got a regular
+ // pair, otherwise, it's a bool attribute
+ $first_char = @$string[$cursor];
+
+ if ($first_char == '=') {
+ // key="value"
+
+ $cursor++;
+ $cursor += strspn($string, $this->_whitespace, $cursor);
+
+ if ($cursor === false) {
+ $array[$key] = '';
+ break;
+ }
+
+ // we might be in front of a quote right now
+
+ $char = @$string[$cursor];
+
+ if ($char == '"' || $char == "'") {
+ // it's quoted, end bound is $char
+ $cursor++;
+ $value_begin = $cursor;
+ $cursor = strpos($string, $char, $cursor);
+ $value_end = $cursor;
+ } else {
+ // it's not quoted, end bound is whitespace
+ $value_begin = $cursor;
+ $cursor += strcspn($string, $this->_whitespace, $cursor);
+ $value_end = $cursor;
+ }
+
+ // we reached a premature end
+ if ($cursor === false) {
+ $cursor = $size;
+ $value_end = $cursor;
+ }
+
+ $value = substr($string, $value_begin, $value_end - $value_begin);
+ if ($value === false) $value = '';
+ $array[$key] = $this->parseData($value);
+ $cursor++;
+
+ } else {
+ // boolattr
+ if ($key !== '') {
+ $array[$key] = $key;
+ } else {
+ // purely theoretical
+ if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
+ }
+
+ }
+ }
+ return $array;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Lexer/PEARSax3.php b/extlib/HTMLPurifier/HTMLPurifier/Lexer/PEARSax3.php
new file mode 100644
index 000000000..57cffa82a
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Lexer/PEARSax3.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * Proof-of-concept lexer that uses the PEAR package XML_HTMLSax3 to parse HTML.
+ *
+ * PEAR, not suprisingly, also has a SAX parser for HTML. I don't know
+ * very much about implementation, but it's fairly well written. However, that
+ * abstraction comes at a price: performance. You need to have it installed,
+ * and if the API changes, it might break our adapter. Not sure whether or not
+ * it's UTF-8 aware, but it has some entity parsing trouble (in all areas,
+ * text and attributes).
+ *
+ * Quite personally, I don't recommend using the PEAR class, and the defaults
+ * don't use it. The unit tests do perform the tests on the SAX parser too, but
+ * whatever it does for poorly formed HTML is up to it.
+ *
+ * @todo Generalize so that XML_HTMLSax is also supported.
+ *
+ * @warning Entity-resolution inside attributes is broken.
+ */
+
+class HTMLPurifier_Lexer_PEARSax3 extends HTMLPurifier_Lexer
+{
+
+ /**
+ * Internal accumulator array for SAX parsers.
+ */
+ protected $tokens = array();
+
+ public function tokenizeHTML($string, $config, $context) {
+
+ $this->tokens = array();
+
+ $string = $this->normalize($string, $config, $context);
+
+ $parser = new XML_HTMLSax3();
+ $parser->set_object($this);
+ $parser->set_element_handler('openHandler','closeHandler');
+ $parser->set_data_handler('dataHandler');
+ $parser->set_escape_handler('escapeHandler');
+
+ // doesn't seem to work correctly for attributes
+ $parser->set_option('XML_OPTION_ENTITIES_PARSED', 1);
+
+ $parser->parse($string);
+
+ return $this->tokens;
+
+ }
+
+ /**
+ * Open tag event handler, interface is defined by PEAR package.
+ */
+ public function openHandler(&$parser, $name, $attrs, $closed) {
+ // entities are not resolved in attrs
+ foreach ($attrs as $key => $attr) {
+ $attrs[$key] = $this->parseData($attr);
+ }
+ if ($closed) {
+ $this->tokens[] = new HTMLPurifier_Token_Empty($name, $attrs);
+ } else {
+ $this->tokens[] = new HTMLPurifier_Token_Start($name, $attrs);
+ }
+ return true;
+ }
+
+ /**
+ * Close tag event handler, interface is defined by PEAR package.
+ */
+ public function closeHandler(&$parser, $name) {
+ // HTMLSax3 seems to always send empty tags an extra close tag
+ // check and ignore if you see it:
+ // [TESTME] to make sure it doesn't overreach
+ if ($this->tokens[count($this->tokens)-1] instanceof HTMLPurifier_Token_Empty) {
+ return true;
+ }
+ $this->tokens[] = new HTMLPurifier_Token_End($name);
+ return true;
+ }
+
+ /**
+ * Data event handler, interface is defined by PEAR package.
+ */
+ public function dataHandler(&$parser, $data) {
+ $this->tokens[] = new HTMLPurifier_Token_Text($data);
+ return true;
+ }
+
+ /**
+ * Escaped text handler, interface is defined by PEAR package.
+ */
+ public function escapeHandler(&$parser, $data) {
+ if (strpos($data, '--') === 0) {
+ $this->tokens[] = new HTMLPurifier_Token_Comment($data);
+ }
+ // CDATA is handled elsewhere, but if it was handled here:
+ //if (strpos($data, '[CDATA[') === 0) {
+ // $this->tokens[] = new HTMLPurifier_Token_Text(
+ // substr($data, 7, strlen($data) - 9) );
+ //}
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Lexer/PH5P.php b/extlib/HTMLPurifier/HTMLPurifier/Lexer/PH5P.php
new file mode 100644
index 000000000..fa1bf973e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Lexer/PH5P.php
@@ -0,0 +1,3906 @@
+<?php
+
+/**
+ * Experimental HTML5-based parser using Jeroen van der Meer's PH5P library.
+ * Occupies space in the HTML5 pseudo-namespace, which may cause conflicts.
+ *
+ * @note
+ * Recent changes to PHP's DOM extension have resulted in some fatal
+ * error conditions with the original version of PH5P. Pending changes,
+ * this lexer will punt to DirectLex if DOM throughs an exception.
+ */
+
+class HTMLPurifier_Lexer_PH5P extends HTMLPurifier_Lexer_DOMLex {
+
+ public function tokenizeHTML($html, $config, $context) {
+ $new_html = $this->normalize($html, $config, $context);
+ $new_html = $this->wrapHTML($new_html, $config, $context);
+ try {
+ $parser = new HTML5($new_html);
+ $doc = $parser->save();
+ } catch (DOMException $e) {
+ // Uh oh, it failed. Punt to DirectLex.
+ $lexer = new HTMLPurifier_Lexer_DirectLex();
+ $context->register('PH5PError', $e); // save the error, so we can detect it
+ return $lexer->tokenizeHTML($html, $config, $context); // use original HTML
+ }
+ $tokens = array();
+ $this->tokenizeDOM(
+ $doc->getElementsByTagName('html')->item(0)-> // <html>
+ getElementsByTagName('body')->item(0)-> // <body>
+ getElementsByTagName('div')->item(0) // <div>
+ , $tokens);
+ return $tokens;
+ }
+
+}
+
+/*
+
+Copyright 2007 Jeroen van der Meer <http://jero.net/>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+class HTML5 {
+ private $data;
+ private $char;
+ private $EOF;
+ private $state;
+ private $tree;
+ private $token;
+ private $content_model;
+ private $escape = false;
+ private $entities = array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute',
+ 'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;',
+ 'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;',
+ 'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;',
+ 'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;',
+ 'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;',
+ 'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;',
+ 'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;',
+ 'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;',
+ 'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN',
+ 'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;',
+ 'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;',
+ 'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig',
+ 'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;',
+ 'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;',
+ 'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil',
+ 'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;',
+ 'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;',
+ 'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;',
+ 'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth',
+ 'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12',
+ 'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt',
+ 'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc',
+ 'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;',
+ 'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;',
+ 'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;',
+ 'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro',
+ 'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;',
+ 'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;',
+ 'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;',
+ 'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash',
+ 'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;',
+ 'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;',
+ 'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;',
+ 'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;',
+ 'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;',
+ 'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;',
+ 'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;',
+ 'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;',
+ 'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc',
+ 'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;',
+ 'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;');
+
+ const PCDATA = 0;
+ const RCDATA = 1;
+ const CDATA = 2;
+ const PLAINTEXT = 3;
+
+ const DOCTYPE = 0;
+ const STARTTAG = 1;
+ const ENDTAG = 2;
+ const COMMENT = 3;
+ const CHARACTR = 4;
+ const EOF = 5;
+
+ public function __construct($data) {
+ $data = str_replace("\r\n", "\n", $data);
+ $data = str_replace("\r", null, $data);
+
+ $this->data = $data;
+ $this->char = -1;
+ $this->EOF = strlen($data);
+ $this->tree = new HTML5TreeConstructer;
+ $this->content_model = self::PCDATA;
+
+ $this->state = 'data';
+
+ while($this->state !== null) {
+ $this->{$this->state.'State'}();
+ }
+ }
+
+ public function save() {
+ return $this->tree->save();
+ }
+
+ private function char() {
+ return ($this->char < $this->EOF)
+ ? $this->data[$this->char]
+ : false;
+ }
+
+ private function character($s, $l = 0) {
+ if($s + $l < $this->EOF) {
+ if($l === 0) {
+ return $this->data[$s];
+ } else {
+ return substr($this->data, $s, $l);
+ }
+ }
+ }
+
+ private function characters($char_class, $start) {
+ return preg_replace('#^(['.$char_class.']+).*#s', '\\1', substr($this->data, $start));
+ }
+
+ private function dataState() {
+ // Consume the next input character
+ $this->char++;
+ $char = $this->char();
+
+ if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) {
+ /* U+0026 AMPERSAND (&)
+ When the content model flag is set to one of the PCDATA or RCDATA
+ states: switch to the entity data state. Otherwise: treat it as per
+ the "anything else" entry below. */
+ $this->state = 'entityData';
+
+ } elseif($char === '-') {
+ /* If the content model flag is set to either the RCDATA state or
+ the CDATA state, and the escape flag is false, and there are at
+ least three characters before this one in the input stream, and the
+ last four characters in the input stream, including this one, are
+ U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS,
+ and U+002D HYPHEN-MINUS ("<!--"), then set the escape flag to true. */
+ if(($this->content_model === self::RCDATA || $this->content_model ===
+ self::CDATA) && $this->escape === false &&
+ $this->char >= 3 && $this->character($this->char - 4, 4) === '<!--') {
+ $this->escape = true;
+ }
+
+ /* In any case, emit the input character as a character token. Stay
+ in the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ ));
+
+ /* U+003C LESS-THAN SIGN (<) */
+ } elseif($char === '<' && ($this->content_model === self::PCDATA ||
+ (($this->content_model === self::RCDATA ||
+ $this->content_model === self::CDATA) && $this->escape === false))) {
+ /* When the content model flag is set to the PCDATA state: switch
+ to the tag open state.
+
+ When the content model flag is set to either the RCDATA state or
+ the CDATA state and the escape flag is false: switch to the tag
+ open state.
+
+ Otherwise: treat it as per the "anything else" entry below. */
+ $this->state = 'tagOpen';
+
+ /* U+003E GREATER-THAN SIGN (>) */
+ } elseif($char === '>') {
+ /* If the content model flag is set to either the RCDATA state or
+ the CDATA state, and the escape flag is true, and the last three
+ characters in the input stream including this one are U+002D
+ HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN ("-->"),
+ set the escape flag to false. */
+ if(($this->content_model === self::RCDATA ||
+ $this->content_model === self::CDATA) && $this->escape === true &&
+ $this->character($this->char, 3) === '-->') {
+ $this->escape = false;
+ }
+
+ /* In any case, emit the input character as a character token.
+ Stay in the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ ));
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Emit an end-of-file token. */
+ $this->EOF();
+
+ } elseif($this->content_model === self::PLAINTEXT) {
+ /* When the content model flag is set to the PLAINTEXT state
+ THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of
+ the text and emit it as a character token. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => substr($this->data, $this->char)
+ ));
+
+ $this->EOF();
+
+ } else {
+ /* Anything else
+ THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that
+ otherwise would also be treated as a character token and emit it
+ as a single character token. Stay in the data state. */
+ $len = strcspn($this->data, '<&', $this->char);
+ $char = substr($this->data, $this->char, $len);
+ $this->char += $len - 1;
+
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ ));
+
+ $this->state = 'data';
+ }
+ }
+
+ private function entityDataState() {
+ // Attempt to consume an entity.
+ $entity = $this->entity();
+
+ // If nothing is returned, emit a U+0026 AMPERSAND character token.
+ // Otherwise, emit the character token that was returned.
+ $char = (!$entity) ? '&' : $entity;
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ ));
+
+ // Finally, switch to the data state.
+ $this->state = 'data';
+ }
+
+ private function tagOpenState() {
+ switch($this->content_model) {
+ case self::RCDATA:
+ case self::CDATA:
+ /* If the next input character is a U+002F SOLIDUS (/) character,
+ consume it and switch to the close tag open state. If the next
+ input character is not a U+002F SOLIDUS (/) character, emit a
+ U+003C LESS-THAN SIGN character token and switch to the data
+ state to process the next input character. */
+ if($this->character($this->char + 1) === '/') {
+ $this->char++;
+ $this->state = 'closeTagOpen';
+
+ } else {
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => '<'
+ ));
+
+ $this->state = 'data';
+ }
+ break;
+
+ case self::PCDATA:
+ // If the content model flag is set to the PCDATA state
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->char();
+
+ if($char === '!') {
+ /* U+0021 EXCLAMATION MARK (!)
+ Switch to the markup declaration open state. */
+ $this->state = 'markupDeclarationOpen';
+
+ } elseif($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Switch to the close tag open state. */
+ $this->state = 'closeTagOpen';
+
+ } elseif(preg_match('/^[A-Za-z]$/', $char)) {
+ /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
+ Create a new start tag token, set its tag name to the lowercase
+ version of the input character (add 0x0020 to the character's code
+ point), then switch to the tag name state. (Don't emit the token
+ yet; further details will be filled in before it is emitted.) */
+ $this->token = array(
+ 'name' => strtolower($char),
+ 'type' => self::STARTTAG,
+ 'attr' => array()
+ );
+
+ $this->state = 'tagName';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and a
+ U+003E GREATER-THAN SIGN character token. Switch to the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => '<>'
+ ));
+
+ $this->state = 'data';
+
+ } elseif($char === '?') {
+ /* U+003F QUESTION MARK (?)
+ Parse error. Switch to the bogus comment state. */
+ $this->state = 'bogusComment';
+
+ } else {
+ /* Anything else
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and
+ reconsume the current input character in the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => '<'
+ ));
+
+ $this->char--;
+ $this->state = 'data';
+ }
+ break;
+ }
+ }
+
+ private function closeTagOpenState() {
+ $next_node = strtolower($this->characters('A-Za-z', $this->char + 1));
+ $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName;
+
+ if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA) &&
+ (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/',
+ $this->character($this->char + 1 + strlen($next_node))) || $this->EOF === $this->char)))) {
+ /* If the content model flag is set to the RCDATA or CDATA states then
+ examine the next few characters. If they do not match the tag name of
+ the last start tag token emitted (case insensitively), or if they do but
+ they are not immediately followed by one of the following characters:
+ * U+0009 CHARACTER TABULATION
+ * U+000A LINE FEED (LF)
+ * U+000B LINE TABULATION
+ * U+000C FORM FEED (FF)
+ * U+0020 SPACE
+ * U+003E GREATER-THAN SIGN (>)
+ * U+002F SOLIDUS (/)
+ * EOF
+ ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character
+ token, a U+002F SOLIDUS character token, and switch to the data state
+ to process the next input character. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => '</'
+ ));
+
+ $this->state = 'data';
+
+ } else {
+ /* Otherwise, if the content model flag is set to the PCDATA state,
+ or if the next few characters do match that tag name, consume the
+ next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[A-Za-z]$/', $char)) {
+ /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
+ Create a new end tag token, set its tag name to the lowercase version
+ of the input character (add 0x0020 to the character's code point), then
+ switch to the tag name state. (Don't emit the token yet; further details
+ will be filled in before it is emitted.) */
+ $this->token = array(
+ 'name' => strtolower($char),
+ 'type' => self::ENDTAG
+ );
+
+ $this->state = 'tagName';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Parse error. Switch to the data state. */
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F
+ SOLIDUS character token. Reconsume the EOF character in the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => '</'
+ ));
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Parse error. Switch to the bogus comment state. */
+ $this->state = 'bogusComment';
+ }
+ }
+ }
+
+ private function tagNameState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } elseif($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current tag token's tag name.
+ Stay in the tag name state. */
+ $this->token['name'] .= strtolower($char);
+ $this->state = 'tagName';
+ }
+ }
+
+ private function beforeAttributeNameState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Stay in the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Start a new attribute in the current tag token. Set that attribute's
+ name to the current input character, and its value to the empty string.
+ Switch to the attribute name state. */
+ $this->token['attr'][] = array(
+ 'name' => strtolower($char),
+ 'value' => null
+ );
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function attributeNameState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute name state. */
+ $this->state = 'afterAttributeName';
+
+ } elseif($char === '=') {
+ /* U+003D EQUALS SIGN (=)
+ Switch to the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($char === '/' && $this->character($this->char + 1) !== '>') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's name.
+ Stay in the attribute name state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['name'] .= strtolower($char);
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function afterAttributeNameState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the after attribute name state. */
+ $this->state = 'afterAttributeName';
+
+ } elseif($char === '=') {
+ /* U+003D EQUALS SIGN (=)
+ Switch to the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($char === '/' && $this->character($this->char + 1) !== '>') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the
+ before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Start a new attribute in the current tag token. Set that attribute's
+ name to the current input character, and its value to the empty string.
+ Switch to the attribute name state. */
+ $this->token['attr'][] = array(
+ 'name' => strtolower($char),
+ 'value' => null
+ );
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function beforeAttributeValueState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif($char === '"') {
+ /* U+0022 QUOTATION MARK (")
+ Switch to the attribute value (double-quoted) state. */
+ $this->state = 'attributeValueDoubleQuoted';
+
+ } elseif($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the attribute value (unquoted) state and reconsume
+ this input character. */
+ $this->char--;
+ $this->state = 'attributeValueUnquoted';
+
+ } elseif($char === '\'') {
+ /* U+0027 APOSTROPHE (')
+ Switch to the attribute value (single-quoted) state. */
+ $this->state = 'attributeValueSingleQuoted';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Switch to the attribute value (unquoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueUnquoted';
+ }
+ }
+
+ private function attributeValueDoubleQuotedState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if($char === '"') {
+ /* U+0022 QUOTATION MARK (")
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState('double');
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the character
+ in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (double-quoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueDoubleQuoted';
+ }
+ }
+
+ private function attributeValueSingleQuotedState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if($char === '\'') {
+ /* U+0022 QUOTATION MARK (')
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState('single');
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the character
+ in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (single-quoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueSingleQuoted';
+ }
+ }
+
+ private function attributeValueUnquotedState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState();
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (unquoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueUnquoted';
+ }
+ }
+
+ private function entityInAttributeValueState() {
+ // Attempt to consume an entity.
+ $entity = $this->entity();
+
+ // If nothing is returned, append a U+0026 AMPERSAND character to the
+ // current attribute's value. Otherwise, emit the character token that
+ // was returned.
+ $char = (!$entity)
+ ? '&'
+ : $entity;
+
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+ }
+
+ private function bogusCommentState() {
+ /* Consume every character up to the first U+003E GREATER-THAN SIGN
+ character (>) or the end of the file (EOF), whichever comes first. Emit
+ a comment token whose data is the concatenation of all the characters
+ starting from and including the character that caused the state machine
+ to switch into the bogus comment state, up to and including the last
+ consumed character before the U+003E character, if any, or up to the
+ end of the file otherwise. (If the comment was started by the end of
+ the file (EOF), the token is empty.) */
+ $data = $this->characters('^>', $this->char);
+ $this->emitToken(array(
+ 'data' => $data,
+ 'type' => self::COMMENT
+ ));
+
+ $this->char += strlen($data);
+
+ /* Switch to the data state. */
+ $this->state = 'data';
+
+ /* If the end of the file was reached, reconsume the EOF character. */
+ if($this->char === $this->EOF) {
+ $this->char = $this->EOF - 1;
+ }
+ }
+
+ private function markupDeclarationOpenState() {
+ /* If the next two characters are both U+002D HYPHEN-MINUS (-)
+ characters, consume those two characters, create a comment token whose
+ data is the empty string, and switch to the comment state. */
+ if($this->character($this->char + 1, 2) === '--') {
+ $this->char += 2;
+ $this->state = 'comment';
+ $this->token = array(
+ 'data' => null,
+ 'type' => self::COMMENT
+ );
+
+ /* Otherwise if the next seven chacacters are a case-insensitive match
+ for the word "DOCTYPE", then consume those characters and switch to the
+ DOCTYPE state. */
+ } elseif(strtolower($this->character($this->char + 1, 7)) === 'doctype') {
+ $this->char += 7;
+ $this->state = 'doctype';
+
+ /* Otherwise, is is a parse error. Switch to the bogus comment state.
+ The next character that is consumed, if any, is the first character
+ that will be in the comment. */
+ } else {
+ $this->char++;
+ $this->state = 'bogusComment';
+ }
+ }
+
+ private function commentState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ /* U+002D HYPHEN-MINUS (-) */
+ if($char === '-') {
+ /* Switch to the comment dash state */
+ $this->state = 'commentDash';
+
+ /* EOF */
+ } elseif($this->char === $this->EOF) {
+ /* Parse error. Emit the comment token. Reconsume the EOF character
+ in the data state. */
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ /* Anything else */
+ } else {
+ /* Append the input character to the comment token's data. Stay in
+ the comment state. */
+ $this->token['data'] .= $char;
+ }
+ }
+
+ private function commentDashState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ /* U+002D HYPHEN-MINUS (-) */
+ if($char === '-') {
+ /* Switch to the comment end state */
+ $this->state = 'commentEnd';
+
+ /* EOF */
+ } elseif($this->char === $this->EOF) {
+ /* Parse error. Emit the comment token. Reconsume the EOF character
+ in the data state. */
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ /* Anything else */
+ } else {
+ /* Append a U+002D HYPHEN-MINUS (-) character and the input
+ character to the comment token's data. Switch to the comment state. */
+ $this->token['data'] .= '-'.$char;
+ $this->state = 'comment';
+ }
+ }
+
+ private function commentEndState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($char === '-') {
+ $this->token['data'] .= '-';
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['data'] .= '--'.$char;
+ $this->state = 'comment';
+ }
+ }
+
+ private function doctypeState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ $this->state = 'beforeDoctypeName';
+
+ } else {
+ $this->char--;
+ $this->state = 'beforeDoctypeName';
+ }
+ }
+
+ private function beforeDoctypeNameState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ // Stay in the before DOCTYPE name state.
+
+ } elseif(preg_match('/^[a-z]$/', $char)) {
+ $this->token = array(
+ 'name' => strtoupper($char),
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ );
+
+ $this->state = 'doctypeName';
+
+ } elseif($char === '>') {
+ $this->emitToken(array(
+ 'name' => null,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ ));
+
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken(array(
+ 'name' => null,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ ));
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token = array(
+ 'name' => $char,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ );
+
+ $this->state = 'doctypeName';
+ }
+ }
+
+ private function doctypeNameState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ $this->state = 'AfterDoctypeName';
+
+ } elseif($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif(preg_match('/^[a-z]$/', $char)) {
+ $this->token['name'] .= strtoupper($char);
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['name'] .= $char;
+ }
+
+ $this->token['error'] = ($this->token['name'] === 'HTML')
+ ? false
+ : true;
+ }
+
+ private function afterDoctypeNameState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ // Stay in the DOCTYPE name state.
+
+ } elseif($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['error'] = true;
+ $this->state = 'bogusDoctype';
+ }
+ }
+
+ private function bogusDoctypeState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ // Stay in the bogus DOCTYPE state.
+ }
+ }
+
+ private function entity() {
+ $start = $this->char;
+
+ // This section defines how to consume an entity. This definition is
+ // used when parsing entities in text and in attributes.
+
+ // The behaviour depends on the identity of the next character (the
+ // one immediately after the U+0026 AMPERSAND character):
+
+ switch($this->character($this->char + 1)) {
+ // U+0023 NUMBER SIGN (#)
+ case '#':
+
+ // The behaviour further depends on the character after the
+ // U+0023 NUMBER SIGN:
+ switch($this->character($this->char + 1)) {
+ // U+0078 LATIN SMALL LETTER X
+ // U+0058 LATIN CAPITAL LETTER X
+ case 'x':
+ case 'X':
+ // Follow the steps below, but using the range of
+ // characters U+0030 DIGIT ZERO through to U+0039 DIGIT
+ // NINE, U+0061 LATIN SMALL LETTER A through to U+0066
+ // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER
+ // A, through to U+0046 LATIN CAPITAL LETTER F (in other
+ // words, 0-9, A-F, a-f).
+ $char = 1;
+ $char_class = '0-9A-Fa-f';
+ break;
+
+ // Anything else
+ default:
+ // Follow the steps below, but using the range of
+ // characters U+0030 DIGIT ZERO through to U+0039 DIGIT
+ // NINE (i.e. just 0-9).
+ $char = 0;
+ $char_class = '0-9';
+ break;
+ }
+
+ // Consume as many characters as match the range of characters
+ // given above.
+ $this->char++;
+ $e_name = $this->characters($char_class, $this->char + $char + 1);
+ $entity = $this->character($start, $this->char);
+ $cond = strlen($e_name) > 0;
+
+ // The rest of the parsing happens bellow.
+ break;
+
+ // Anything else
+ default:
+ // Consume the maximum number of characters possible, with the
+ // consumed characters case-sensitively matching one of the
+ // identifiers in the first column of the entities table.
+ $e_name = $this->characters('0-9A-Za-z;', $this->char + 1);
+ $len = strlen($e_name);
+
+ for($c = 1; $c <= $len; $c++) {
+ $id = substr($e_name, 0, $c);
+ $this->char++;
+
+ if(in_array($id, $this->entities)) {
+ if ($e_name[$c-1] !== ';') {
+ if ($c < $len && $e_name[$c] == ';') {
+ $this->char++; // consume extra semicolon
+ }
+ }
+ $entity = $id;
+ break;
+ }
+ }
+
+ $cond = isset($entity);
+ // The rest of the parsing happens bellow.
+ break;
+ }
+
+ if(!$cond) {
+ // If no match can be made, then this is a parse error. No
+ // characters are consumed, and nothing is returned.
+ $this->char = $start;
+ return false;
+ }
+
+ // Return a character token for the character corresponding to the
+ // entity name (as given by the second column of the entities table).
+ return html_entity_decode('&'.$entity.';', ENT_QUOTES, 'UTF-8');
+ }
+
+ private function emitToken($token) {
+ $emit = $this->tree->emitToken($token);
+
+ if(is_int($emit)) {
+ $this->content_model = $emit;
+
+ } elseif($token['type'] === self::ENDTAG) {
+ $this->content_model = self::PCDATA;
+ }
+ }
+
+ private function EOF() {
+ $this->state = null;
+ $this->tree->emitToken(array(
+ 'type' => self::EOF
+ ));
+ }
+}
+
+class HTML5TreeConstructer {
+ public $stack = array();
+
+ private $phase;
+ private $mode;
+ private $dom;
+ private $foster_parent = null;
+ private $a_formatting = array();
+
+ private $head_pointer = null;
+ private $form_pointer = null;
+
+ private $scoping = array('button','caption','html','marquee','object','table','td','th');
+ private $formatting = array('a','b','big','em','font','i','nobr','s','small','strike','strong','tt','u');
+ private $special = array('address','area','base','basefont','bgsound',
+ 'blockquote','body','br','center','col','colgroup','dd','dir','div','dl',
+ 'dt','embed','fieldset','form','frame','frameset','h1','h2','h3','h4','h5',
+ 'h6','head','hr','iframe','image','img','input','isindex','li','link',
+ 'listing','menu','meta','noembed','noframes','noscript','ol','optgroup',
+ 'option','p','param','plaintext','pre','script','select','spacer','style',
+ 'tbody','textarea','tfoot','thead','title','tr','ul','wbr');
+
+ // The different phases.
+ const INIT_PHASE = 0;
+ const ROOT_PHASE = 1;
+ const MAIN_PHASE = 2;
+ const END_PHASE = 3;
+
+ // The different insertion modes for the main phase.
+ const BEFOR_HEAD = 0;
+ const IN_HEAD = 1;
+ const AFTER_HEAD = 2;
+ const IN_BODY = 3;
+ const IN_TABLE = 4;
+ const IN_CAPTION = 5;
+ const IN_CGROUP = 6;
+ const IN_TBODY = 7;
+ const IN_ROW = 8;
+ const IN_CELL = 9;
+ const IN_SELECT = 10;
+ const AFTER_BODY = 11;
+ const IN_FRAME = 12;
+ const AFTR_FRAME = 13;
+
+ // The different types of elements.
+ const SPECIAL = 0;
+ const SCOPING = 1;
+ const FORMATTING = 2;
+ const PHRASING = 3;
+
+ const MARKER = 0;
+
+ public function __construct() {
+ $this->phase = self::INIT_PHASE;
+ $this->mode = self::BEFOR_HEAD;
+ $this->dom = new DOMDocument;
+
+ $this->dom->encoding = 'UTF-8';
+ $this->dom->preserveWhiteSpace = true;
+ $this->dom->substituteEntities = true;
+ $this->dom->strictErrorChecking = false;
+ }
+
+ // Process tag tokens
+ public function emitToken($token) {
+ switch($this->phase) {
+ case self::INIT_PHASE: return $this->initPhase($token); break;
+ case self::ROOT_PHASE: return $this->rootElementPhase($token); break;
+ case self::MAIN_PHASE: return $this->mainPhase($token); break;
+ case self::END_PHASE : return $this->trailingEndPhase($token); break;
+ }
+ }
+
+ private function initPhase($token) {
+ /* Initially, the tree construction stage must handle each token
+ emitted from the tokenisation stage as follows: */
+
+ /* A DOCTYPE token that is marked as being in error
+ A comment token
+ A start tag token
+ An end tag token
+ A character token that is not one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE
+ An end-of-file token */
+ if((isset($token['error']) && $token['error']) ||
+ $token['type'] === HTML5::COMMENT ||
+ $token['type'] === HTML5::STARTTAG ||
+ $token['type'] === HTML5::ENDTAG ||
+ $token['type'] === HTML5::EOF ||
+ ($token['type'] === HTML5::CHARACTR && isset($token['data']) &&
+ !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))) {
+ /* This specification does not define how to handle this case. In
+ particular, user agents may ignore the entirety of this specification
+ altogether for such documents, and instead invoke special parse modes
+ with a greater emphasis on backwards compatibility. */
+
+ $this->phase = self::ROOT_PHASE;
+ return $this->rootElementPhase($token);
+
+ /* A DOCTYPE token marked as being correct */
+ } elseif(isset($token['error']) && !$token['error']) {
+ /* Append a DocumentType node to the Document node, with the name
+ attribute set to the name given in the DOCTYPE token (which will be
+ "HTML"), and the other attributes specific to DocumentType objects
+ set to null, empty lists, or the empty string as appropriate. */
+ $doctype = new DOMDocumentType(null, null, 'HTML');
+
+ /* Then, switch to the root element phase of the tree construction
+ stage. */
+ $this->phase = self::ROOT_PHASE;
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif(isset($token['data']) && preg_match('/^[\t\n\x0b\x0c ]+$/',
+ $token['data'])) {
+ /* Append that character to the Document node. */
+ $text = $this->dom->createTextNode($token['data']);
+ $this->dom->appendChild($text);
+ }
+ }
+
+ private function rootElementPhase($token) {
+ /* After the initial phase, as each token is emitted from the tokenisation
+ stage, it must be processed as described in this section. */
+
+ /* A DOCTYPE token */
+ if($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the Document object with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->dom->appendChild($comment);
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append that character to the Document node. */
+ $text = $this->dom->createTextNode($token['data']);
+ $this->dom->appendChild($text);
+
+ /* A character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED
+ (FF), or U+0020 SPACE
+ A start tag token
+ An end tag token
+ An end-of-file token */
+ } elseif(($token['type'] === HTML5::CHARACTR &&
+ !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ||
+ $token['type'] === HTML5::STARTTAG ||
+ $token['type'] === HTML5::ENDTAG ||
+ $token['type'] === HTML5::EOF) {
+ /* Create an HTMLElement node with the tag name html, in the HTML
+ namespace. Append it to the Document object. Switch to the main
+ phase and reprocess the current token. */
+ $html = $this->dom->createElement('html');
+ $this->dom->appendChild($html);
+ $this->stack[] = $html;
+
+ $this->phase = self::MAIN_PHASE;
+ return $this->mainPhase($token);
+ }
+ }
+
+ private function mainPhase($token) {
+ /* Tokens in the main phase must be handled as follows: */
+
+ /* A DOCTYPE token */
+ if($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A start tag token with the tag name "html" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') {
+ /* If this start tag token was not the first start tag token, then
+ it is a parse error. */
+
+ /* For each attribute on the token, check to see if the attribute
+ is already present on the top element of the stack of open elements.
+ If it is not, add the attribute and its corresponding value to that
+ element. */
+ foreach($token['attr'] as $attr) {
+ if(!$this->stack[0]->hasAttribute($attr['name'])) {
+ $this->stack[0]->setAttribute($attr['name'], $attr['value']);
+ }
+ }
+
+ /* An end-of-file token */
+ } elseif($token['type'] === HTML5::EOF) {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Anything else. */
+ } else {
+ /* Depends on the insertion mode: */
+ switch($this->mode) {
+ case self::BEFOR_HEAD: return $this->beforeHead($token); break;
+ case self::IN_HEAD: return $this->inHead($token); break;
+ case self::AFTER_HEAD: return $this->afterHead($token); break;
+ case self::IN_BODY: return $this->inBody($token); break;
+ case self::IN_TABLE: return $this->inTable($token); break;
+ case self::IN_CAPTION: return $this->inCaption($token); break;
+ case self::IN_CGROUP: return $this->inColumnGroup($token); break;
+ case self::IN_TBODY: return $this->inTableBody($token); break;
+ case self::IN_ROW: return $this->inRow($token); break;
+ case self::IN_CELL: return $this->inCell($token); break;
+ case self::IN_SELECT: return $this->inSelect($token); break;
+ case self::AFTER_BODY: return $this->afterBody($token); break;
+ case self::IN_FRAME: return $this->inFrameset($token); break;
+ case self::AFTR_FRAME: return $this->afterFrameset($token); break;
+ case self::END_PHASE: return $this->trailingEndPhase($token); break;
+ }
+ }
+ }
+
+ private function beforeHead($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token with the tag name "head" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') {
+ /* Create an element for the token, append the new element to the
+ current node and push it onto the stack of open elements. */
+ $element = $this->insertElement($token);
+
+ /* Set the head element pointer to this new element node. */
+ $this->head_pointer = $element;
+
+ /* Change the insertion mode to "in head". */
+ $this->mode = self::IN_HEAD;
+
+ /* A start tag token whose tag name is one of: "base", "link", "meta",
+ "script", "style", "title". Or an end tag with the tag name "html".
+ Or a character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE. Or any other start tag token */
+ } elseif($token['type'] === HTML5::STARTTAG ||
+ ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') ||
+ ($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]$/',
+ $token['data']))) {
+ /* Act as if a start tag token with the tag name "head" and no
+ attributes had been seen, then reprocess the current token. */
+ $this->beforeHead(array(
+ 'name' => 'head',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ return $this->inHead($token);
+
+ /* Any other end tag */
+ } elseif($token['type'] === HTML5::ENDTAG) {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function inHead($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE.
+
+ THIS DIFFERS FROM THE SPEC: If the current node is either a title, style
+ or script element, append the character to the current node regardless
+ of its content. */
+ if(($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || (
+ $token['type'] === HTML5::CHARACTR && in_array(end($this->stack)->nodeName,
+ array('title', 'style', 'script')))) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('title', 'style', 'script'))) {
+ array_pop($this->stack);
+ return HTML5::PCDATA;
+
+ /* A start tag with the tag name "title" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ } else {
+ $element = $this->insertElement($token);
+ }
+
+ /* Switch the tokeniser's content model flag to the RCDATA state. */
+ return HTML5::RCDATA;
+
+ /* A start tag with the tag name "style" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ } else {
+ $this->insertElement($token);
+ }
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+
+ /* A start tag with the tag name "script" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') {
+ /* Create an element for the token. */
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+
+ /* A start tag with the tag name "base", "link", or "meta" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('base', 'link', 'meta'))) {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+ array_pop($this->stack);
+
+ } else {
+ $this->insertElement($token);
+ }
+
+ /* An end tag with the tag name "head" */
+ } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') {
+ /* If the current node is a head element, pop the current node off
+ the stack of open elements. */
+ if($this->head_pointer->isSameNode(end($this->stack))) {
+ array_pop($this->stack);
+
+ /* Otherwise, this is a parse error. */
+ } else {
+ // k
+ }
+
+ /* Change the insertion mode to "after head". */
+ $this->mode = self::AFTER_HEAD;
+
+ /* A start tag with the tag name "head" or an end tag except "html". */
+ } elseif(($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') ||
+ ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* If the current node is a head element, act as if an end tag
+ token with the tag name "head" had been seen. */
+ if($this->head_pointer->isSameNode(end($this->stack))) {
+ $this->inHead(array(
+ 'name' => 'head',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* Otherwise, change the insertion mode to "after head". */
+ } else {
+ $this->mode = self::AFTER_HEAD;
+ }
+
+ /* Then, reprocess the current token. */
+ return $this->afterHead($token);
+ }
+ }
+
+ private function afterHead($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token with the tag name "body" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') {
+ /* Insert a body element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in body". */
+ $this->mode = self::IN_BODY;
+
+ /* A start tag token with the tag name "frameset" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') {
+ /* Insert a frameset element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in frameset". */
+ $this->mode = self::IN_FRAME;
+
+ /* A start tag token whose tag name is one of: "base", "link", "meta",
+ "script", "style", "title" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('base', 'link', 'meta', 'script', 'style', 'title'))) {
+ /* Parse error. Switch the insertion mode back to "in head" and
+ reprocess the token. */
+ $this->mode = self::IN_HEAD;
+ return $this->inHead($token);
+
+ /* Anything else */
+ } else {
+ /* Act as if a start tag token with the tag name "body" and no
+ attributes had been seen, and then reprocess the current token. */
+ $this->afterHead(array(
+ 'name' => 'body',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ return $this->inBody($token);
+ }
+ }
+
+ private function inBody($token) {
+ /* Handle the token as follows: */
+
+ switch($token['type']) {
+ /* A character token */
+ case HTML5::CHARACTR:
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Append the token's character to the current node. */
+ $this->insertText($token['data']);
+ break;
+
+ /* A comment token */
+ case HTML5::COMMENT:
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+ break;
+
+ case HTML5::STARTTAG:
+ switch($token['name']) {
+ /* A start tag token whose tag name is one of: "script",
+ "style" */
+ case 'script': case 'style':
+ /* Process the token as if the insertion mode had been "in
+ head". */
+ return $this->inHead($token);
+ break;
+
+ /* A start tag token whose tag name is one of: "base", "link",
+ "meta", "title" */
+ case 'base': case 'link': case 'meta': case 'title':
+ /* Parse error. Process the token as if the insertion mode
+ had been "in head". */
+ return $this->inHead($token);
+ break;
+
+ /* A start tag token with the tag name "body" */
+ case 'body':
+ /* Parse error. If the second element on the stack of open
+ elements is not a body element, or, if the stack of open
+ elements has only one node on it, then ignore the token.
+ (innerHTML case) */
+ if(count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') {
+ // Ignore
+
+ /* Otherwise, for each attribute on the token, check to see
+ if the attribute is already present on the body element (the
+ second element) on the stack of open elements. If it is not,
+ add the attribute and its corresponding value to that
+ element. */
+ } else {
+ foreach($token['attr'] as $attr) {
+ if(!$this->stack[1]->hasAttribute($attr['name'])) {
+ $this->stack[1]->setAttribute($attr['name'], $attr['value']);
+ }
+ }
+ }
+ break;
+
+ /* A start tag whose tag name is one of: "address",
+ "blockquote", "center", "dir", "div", "dl", "fieldset",
+ "listing", "menu", "ol", "p", "ul" */
+ case 'address': case 'blockquote': case 'center': case 'dir':
+ case 'div': case 'dl': case 'fieldset': case 'listing':
+ case 'menu': case 'ol': case 'p': case 'ul':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag whose tag name is "form" */
+ case 'form':
+ /* If the form element pointer is not null, ignore the
+ token with a parse error. */
+ if($this->form_pointer !== null) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* If the stack of open elements has a p element in
+ scope, then act as if an end tag with the tag name p
+ had been seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token, and set the
+ form element pointer to point to the element created. */
+ $element = $this->insertElement($token);
+ $this->form_pointer = $element;
+ }
+ break;
+
+ /* A start tag whose tag name is "li", "dd" or "dt" */
+ case 'li': case 'dd': case 'dt':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ $stack_length = count($this->stack) - 1;
+
+ for($n = $stack_length; 0 <= $n; $n--) {
+ /* 1. Initialise node to be the current node (the
+ bottommost node of the stack). */
+ $stop = false;
+ $node = $this->stack[$n];
+ $cat = $this->getElementCategory($node->tagName);
+
+ /* 2. If node is an li, dd or dt element, then pop all
+ the nodes from the current node up to node, including
+ node, then stop this algorithm. */
+ if($token['name'] === $node->tagName || ($token['name'] !== 'li'
+ && ($node->tagName === 'dd' || $node->tagName === 'dt'))) {
+ for($x = $stack_length; $x >= $n ; $x--) {
+ array_pop($this->stack);
+ }
+
+ break;
+ }
+
+ /* 3. If node is not in the formatting category, and is
+ not in the phrasing category, and is not an address or
+ div element, then stop this algorithm. */
+ if($cat !== self::FORMATTING && $cat !== self::PHRASING &&
+ $node->tagName !== 'address' && $node->tagName !== 'div') {
+ break;
+ }
+ }
+
+ /* Finally, insert an HTML element with the same tag
+ name as the token's. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag token whose tag name is "plaintext" */
+ case 'plaintext':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ return HTML5::PLAINTEXT;
+ break;
+
+ /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4",
+ "h5", "h6" */
+ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* If the stack of open elements has in scope an element whose
+ tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
+ this is a parse error; pop elements from the stack until an
+ element with one of those tag names has been popped from the
+ stack. */
+ while($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) {
+ array_pop($this->stack);
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag whose tag name is "a" */
+ case 'a':
+ /* If the list of active formatting elements contains
+ an element whose tag name is "a" between the end of the
+ list and the last marker on the list (or the start of
+ the list if there is no marker on the list), then this
+ is a parse error; act as if an end tag with the tag name
+ "a" had been seen, then remove that element from the list
+ of active formatting elements and the stack of open
+ elements if the end tag didn't already remove it (it
+ might not have if the element is not in table scope). */
+ $leng = count($this->a_formatting);
+
+ for($n = $leng - 1; $n >= 0; $n--) {
+ if($this->a_formatting[$n] === self::MARKER) {
+ break;
+
+ } elseif($this->a_formatting[$n]->nodeName === 'a') {
+ $this->emitToken(array(
+ 'name' => 'a',
+ 'type' => HTML5::ENDTAG
+ ));
+ break;
+ }
+ }
+
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $el = $this->insertElement($token);
+
+ /* Add that element to the list of active formatting
+ elements. */
+ $this->a_formatting[] = $el;
+ break;
+
+ /* A start tag whose tag name is one of: "b", "big", "em", "font",
+ "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
+ case 'b': case 'big': case 'em': case 'font': case 'i':
+ case 'nobr': case 's': case 'small': case 'strike':
+ case 'strong': case 'tt': case 'u':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $el = $this->insertElement($token);
+
+ /* Add that element to the list of active formatting
+ elements. */
+ $this->a_formatting[] = $el;
+ break;
+
+ /* A start tag token whose tag name is "button" */
+ case 'button':
+ /* If the stack of open elements has a button element in scope,
+ then this is a parse error; act as if an end tag with the tag
+ name "button" had been seen, then reprocess the token. (We don't
+ do that. Unnecessary.) */
+ if($this->elementInScope('button')) {
+ $this->inBody(array(
+ 'name' => 'button',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+ break;
+
+ /* A start tag token whose tag name is one of: "marquee", "object" */
+ case 'marquee': case 'object':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+ break;
+
+ /* A start tag token whose tag name is "xmp" */
+ case 'xmp':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Switch the content model flag to the CDATA state. */
+ return HTML5::CDATA;
+ break;
+
+ /* A start tag whose tag name is "table" */
+ case 'table':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in table". */
+ $this->mode = self::IN_TABLE;
+ break;
+
+ /* A start tag whose tag name is one of: "area", "basefont",
+ "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */
+ case 'area': case 'basefont': case 'bgsound': case 'br':
+ case 'embed': case 'img': case 'param': case 'spacer':
+ case 'wbr':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "hr" */
+ case 'hr':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "image" */
+ case 'image':
+ /* Parse error. Change the token's tag name to "img" and
+ reprocess it. (Don't ask.) */
+ $token['name'] = 'img';
+ return $this->inBody($token);
+ break;
+
+ /* A start tag whose tag name is "input" */
+ case 'input':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an input element for the token. */
+ $element = $this->insertElement($token, false);
+
+ /* If the form element pointer is not null, then associate the
+ input element with the form element pointed to by the form
+ element pointer. */
+ $this->form_pointer !== null
+ ? $this->form_pointer->appendChild($element)
+ : end($this->stack)->appendChild($element);
+
+ /* Pop that input element off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "isindex" */
+ case 'isindex':
+ /* Parse error. */
+ // w/e
+
+ /* If the form element pointer is not null,
+ then ignore the token. */
+ if($this->form_pointer === null) {
+ /* Act as if a start tag token with the tag name "form" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'body',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ /* Act as if a start tag token with the tag name "hr" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'hr',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ /* Act as if a start tag token with the tag name "p" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'p',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ /* Act as if a start tag token with the tag name "label"
+ had been seen. */
+ $this->inBody(array(
+ 'name' => 'label',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ /* Act as if a stream of character tokens had been seen. */
+ $this->insertText('This is a searchable index. '.
+ 'Insert your search keywords here: ');
+
+ /* Act as if a start tag token with the tag name "input"
+ had been seen, with all the attributes from the "isindex"
+ token, except with the "name" attribute set to the value
+ "isindex" (ignoring any explicit "name" attribute). */
+ $attr = $token['attr'];
+ $attr[] = array('name' => 'name', 'value' => 'isindex');
+
+ $this->inBody(array(
+ 'name' => 'input',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => $attr
+ ));
+
+ /* Act as if a stream of character tokens had been seen
+ (see below for what they should say). */
+ $this->insertText('This is a searchable index. '.
+ 'Insert your search keywords here: ');
+
+ /* Act as if an end tag token with the tag name "label"
+ had been seen. */
+ $this->inBody(array(
+ 'name' => 'label',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* Act as if an end tag token with the tag name "p" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* Act as if a start tag token with the tag name "hr" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'hr',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* Act as if an end tag token with the tag name "form" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'form',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+ break;
+
+ /* A start tag whose tag name is "textarea" */
+ case 'textarea':
+ $this->insertElement($token);
+
+ /* Switch the tokeniser's content model flag to the
+ RCDATA state. */
+ return HTML5::RCDATA;
+ break;
+
+ /* A start tag whose tag name is one of: "iframe", "noembed",
+ "noframes" */
+ case 'iframe': case 'noembed': case 'noframes':
+ $this->insertElement($token);
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+ break;
+
+ /* A start tag whose tag name is "select" */
+ case 'select':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in select". */
+ $this->mode = self::IN_SELECT;
+ break;
+
+ /* A start or end tag whose tag name is one of: "caption", "col",
+ "colgroup", "frame", "frameset", "head", "option", "optgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr". */
+ case 'caption': case 'col': case 'colgroup': case 'frame':
+ case 'frameset': case 'head': case 'option': case 'optgroup':
+ case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead':
+ case 'tr':
+ // Parse error. Ignore the token.
+ break;
+
+ /* A start or end tag whose tag name is one of: "event-source",
+ "section", "nav", "article", "aside", "header", "footer",
+ "datagrid", "command" */
+ case 'event-source': case 'section': case 'nav': case 'article':
+ case 'aside': case 'header': case 'footer': case 'datagrid':
+ case 'command':
+ // Work in progress!
+ break;
+
+ /* A start tag token not covered by the previous entries */
+ default:
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ $this->insertElement($token, true, true);
+ break;
+ }
+ break;
+
+ case HTML5::ENDTAG:
+ switch($token['name']) {
+ /* An end tag with the tag name "body" */
+ case 'body':
+ /* If the second element in the stack of open elements is
+ not a body element, this is a parse error. Ignore the token.
+ (innerHTML case) */
+ if(count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') {
+ // Ignore.
+
+ /* If the current node is not the body element, then this
+ is a parse error. */
+ } elseif(end($this->stack)->nodeName !== 'body') {
+ // Parse error.
+ }
+
+ /* Change the insertion mode to "after body". */
+ $this->mode = self::AFTER_BODY;
+ break;
+
+ /* An end tag with the tag name "html" */
+ case 'html':
+ /* Act as if an end tag with tag name "body" had been seen,
+ then, if that token wasn't ignored, reprocess the current
+ token. */
+ $this->inBody(array(
+ 'name' => 'body',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->afterBody($token);
+ break;
+
+ /* An end tag whose tag name is one of: "address", "blockquote",
+ "center", "dir", "div", "dl", "fieldset", "listing", "menu",
+ "ol", "pre", "ul" */
+ case 'address': case 'blockquote': case 'center': case 'dir':
+ case 'div': case 'dl': case 'fieldset': case 'listing':
+ case 'menu': case 'ol': case 'pre': case 'ul':
+ /* If the stack of open elements has an element in scope
+ with the same tag name as that of the token, then generate
+ implied end tags. */
+ if($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with
+ the same tag name as that of the token, then this
+ is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in
+ scope with the same tag name as that of the token,
+ then pop elements from this stack until an element
+ with that tag name has been popped from the stack. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is "form" */
+ case 'form':
+ /* If the stack of open elements has an element in scope
+ with the same tag name as that of the token, then generate
+ implied end tags. */
+ if($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ }
+
+ if(end($this->stack)->nodeName !== $token['name']) {
+ /* Now, if the current node is not an element with the
+ same tag name as that of the token, then this is a parse
+ error. */
+ // w/e
+
+ } else {
+ /* Otherwise, if the current node is an element with
+ the same tag name as that of the token pop that element
+ from the stack. */
+ array_pop($this->stack);
+ }
+
+ /* In any case, set the form element pointer to null. */
+ $this->form_pointer = null;
+ break;
+
+ /* An end tag whose tag name is "p" */
+ case 'p':
+ /* If the stack of open elements has a p element in scope,
+ then generate implied end tags, except for p elements. */
+ if($this->elementInScope('p')) {
+ $this->generateImpliedEndTags(array('p'));
+
+ /* If the current node is not a p element, then this is
+ a parse error. */
+ // k
+
+ /* If the stack of open elements has a p element in
+ scope, then pop elements from this stack until the stack
+ no longer has a p element in scope. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->elementInScope('p')) {
+ array_pop($this->stack);
+
+ } else {
+ break;
+ }
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is "dd", "dt", or "li" */
+ case 'dd': case 'dt': case 'li':
+ /* If the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then
+ generate implied end tags, except for elements with the
+ same tag name as the token. */
+ if($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags(array($token['name']));
+
+ /* If the current node is not an element with the same
+ tag name as the token, then this is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then
+ pop elements from this stack until an element with that
+ tag name has been popped from the stack. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
+ "h5", "h6" */
+ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
+ $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
+
+ /* If the stack of open elements has in scope an element whose
+ tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
+ generate implied end tags. */
+ if($this->elementInScope($elements)) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with the same
+ tag name as that of the token, then this is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has in scope an element
+ whose tag name is one of "h1", "h2", "h3", "h4", "h5", or
+ "h6", then pop elements from the stack until an element
+ with one of those tag names has been popped from the stack. */
+ while($this->elementInScope($elements)) {
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is one of: "a", "b", "big", "em",
+ "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
+ case 'a': case 'b': case 'big': case 'em': case 'font':
+ case 'i': case 'nobr': case 's': case 'small': case 'strike':
+ case 'strong': case 'tt': case 'u':
+ /* 1. Let the formatting element be the last element in
+ the list of active formatting elements that:
+ * is between the end of the list and the last scope
+ marker in the list, if any, or the start of the list
+ otherwise, and
+ * has the same tag name as the token.
+ */
+ while(true) {
+ for($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
+ if($this->a_formatting[$a] === self::MARKER) {
+ break;
+
+ } elseif($this->a_formatting[$a]->tagName === $token['name']) {
+ $formatting_element = $this->a_formatting[$a];
+ $in_stack = in_array($formatting_element, $this->stack, true);
+ $fe_af_pos = $a;
+ break;
+ }
+ }
+
+ /* If there is no such node, or, if that node is
+ also in the stack of open elements but the element
+ is not in scope, then this is a parse error. Abort
+ these steps. The token is ignored. */
+ if(!isset($formatting_element) || ($in_stack &&
+ !$this->elementInScope($token['name']))) {
+ break;
+
+ /* Otherwise, if there is such a node, but that node
+ is not in the stack of open elements, then this is a
+ parse error; remove the element from the list, and
+ abort these steps. */
+ } elseif(isset($formatting_element) && !$in_stack) {
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+ break;
+ }
+
+ /* 2. Let the furthest block be the topmost node in the
+ stack of open elements that is lower in the stack
+ than the formatting element, and is not an element in
+ the phrasing or formatting categories. There might
+ not be one. */
+ $fe_s_pos = array_search($formatting_element, $this->stack, true);
+ $length = count($this->stack);
+
+ for($s = $fe_s_pos + 1; $s < $length; $s++) {
+ $category = $this->getElementCategory($this->stack[$s]->nodeName);
+
+ if($category !== self::PHRASING && $category !== self::FORMATTING) {
+ $furthest_block = $this->stack[$s];
+ }
+ }
+
+ /* 3. If there is no furthest block, then the UA must
+ skip the subsequent steps and instead just pop all
+ the nodes from the bottom of the stack of open
+ elements, from the current node up to the formatting
+ element, and remove the formatting element from the
+ list of active formatting elements. */
+ if(!isset($furthest_block)) {
+ for($n = $length - 1; $n >= $fe_s_pos; $n--) {
+ array_pop($this->stack);
+ }
+
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+ break;
+ }
+
+ /* 4. Let the common ancestor be the element
+ immediately above the formatting element in the stack
+ of open elements. */
+ $common_ancestor = $this->stack[$fe_s_pos - 1];
+
+ /* 5. If the furthest block has a parent node, then
+ remove the furthest block from its parent node. */
+ if($furthest_block->parentNode !== null) {
+ $furthest_block->parentNode->removeChild($furthest_block);
+ }
+
+ /* 6. Let a bookmark note the position of the
+ formatting element in the list of active formatting
+ elements relative to the elements on either side
+ of it in the list. */
+ $bookmark = $fe_af_pos;
+
+ /* 7. Let node and last node be the furthest block.
+ Follow these steps: */
+ $node = $furthest_block;
+ $last_node = $furthest_block;
+
+ while(true) {
+ for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) {
+ /* 7.1 Let node be the element immediately
+ prior to node in the stack of open elements. */
+ $node = $this->stack[$n];
+
+ /* 7.2 If node is not in the list of active
+ formatting elements, then remove node from
+ the stack of open elements and then go back
+ to step 1. */
+ if(!in_array($node, $this->a_formatting, true)) {
+ unset($this->stack[$n]);
+ $this->stack = array_merge($this->stack);
+
+ } else {
+ break;
+ }
+ }
+
+ /* 7.3 Otherwise, if node is the formatting
+ element, then go to the next step in the overall
+ algorithm. */
+ if($node === $formatting_element) {
+ break;
+
+ /* 7.4 Otherwise, if last node is the furthest
+ block, then move the aforementioned bookmark to
+ be immediately after the node in the list of
+ active formatting elements. */
+ } elseif($last_node === $furthest_block) {
+ $bookmark = array_search($node, $this->a_formatting, true) + 1;
+ }
+
+ /* 7.5 If node has any children, perform a
+ shallow clone of node, replace the entry for
+ node in the list of active formatting elements
+ with an entry for the clone, replace the entry
+ for node in the stack of open elements with an
+ entry for the clone, and let node be the clone. */
+ if($node->hasChildNodes()) {
+ $clone = $node->cloneNode();
+ $s_pos = array_search($node, $this->stack, true);
+ $a_pos = array_search($node, $this->a_formatting, true);
+
+ $this->stack[$s_pos] = $clone;
+ $this->a_formatting[$a_pos] = $clone;
+ $node = $clone;
+ }
+
+ /* 7.6 Insert last node into node, first removing
+ it from its previous parent node if any. */
+ if($last_node->parentNode !== null) {
+ $last_node->parentNode->removeChild($last_node);
+ }
+
+ $node->appendChild($last_node);
+
+ /* 7.7 Let last node be node. */
+ $last_node = $node;
+ }
+
+ /* 8. Insert whatever last node ended up being in
+ the previous step into the common ancestor node,
+ first removing it from its previous parent node if
+ any. */
+ if($last_node->parentNode !== null) {
+ $last_node->parentNode->removeChild($last_node);
+ }
+
+ $common_ancestor->appendChild($last_node);
+
+ /* 9. Perform a shallow clone of the formatting
+ element. */
+ $clone = $formatting_element->cloneNode();
+
+ /* 10. Take all of the child nodes of the furthest
+ block and append them to the clone created in the
+ last step. */
+ while($furthest_block->hasChildNodes()) {
+ $child = $furthest_block->firstChild;
+ $furthest_block->removeChild($child);
+ $clone->appendChild($child);
+ }
+
+ /* 11. Append that clone to the furthest block. */
+ $furthest_block->appendChild($clone);
+
+ /* 12. Remove the formatting element from the list
+ of active formatting elements, and insert the clone
+ into the list of active formatting elements at the
+ position of the aforementioned bookmark. */
+ $fe_af_pos = array_search($formatting_element, $this->a_formatting, true);
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+
+ $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1);
+ $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting));
+ $this->a_formatting = array_merge($af_part1, array($clone), $af_part2);
+
+ /* 13. Remove the formatting element from the stack
+ of open elements, and insert the clone into the stack
+ of open elements immediately after (i.e. in a more
+ deeply nested position than) the position of the
+ furthest block in that stack. */
+ $fe_s_pos = array_search($formatting_element, $this->stack, true);
+ $fb_s_pos = array_search($furthest_block, $this->stack, true);
+ unset($this->stack[$fe_s_pos]);
+
+ $s_part1 = array_slice($this->stack, 0, $fb_s_pos);
+ $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack));
+ $this->stack = array_merge($s_part1, array($clone), $s_part2);
+
+ /* 14. Jump back to step 1 in this series of steps. */
+ unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
+ }
+ break;
+
+ /* An end tag token whose tag name is one of: "button",
+ "marquee", "object" */
+ case 'button': case 'marquee': case 'object':
+ /* If the stack of open elements has an element in scope whose
+ tag name matches the tag name of the token, then generate implied
+ tags. */
+ if($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with the same
+ tag name as the token, then this is a parse error. */
+ // k
+
+ /* Now, if the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then pop
+ elements from the stack until that element has been popped from
+ the stack, and clear the list of active formatting elements up
+ to the last marker. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+
+ $marker = end(array_keys($this->a_formatting, self::MARKER, true));
+
+ for($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
+ array_pop($this->a_formatting);
+ }
+ }
+ break;
+
+ /* Or an end tag whose tag name is one of: "area", "basefont",
+ "bgsound", "br", "embed", "hr", "iframe", "image", "img",
+ "input", "isindex", "noembed", "noframes", "param", "select",
+ "spacer", "table", "textarea", "wbr" */
+ case 'area': case 'basefont': case 'bgsound': case 'br':
+ case 'embed': case 'hr': case 'iframe': case 'image':
+ case 'img': case 'input': case 'isindex': case 'noembed':
+ case 'noframes': case 'param': case 'select': case 'spacer':
+ case 'table': case 'textarea': case 'wbr':
+ // Parse error. Ignore the token.
+ break;
+
+ /* An end tag token not covered by the previous entries */
+ default:
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ /* Initialise node to be the current node (the bottommost
+ node of the stack). */
+ $node = end($this->stack);
+
+ /* If node has the same tag name as the end tag token,
+ then: */
+ if($token['name'] === $node->nodeName) {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* If the tag name of the end tag token does not
+ match the tag name of the current node, this is a
+ parse error. */
+ // k
+
+ /* Pop all the nodes from the current node up to
+ node, including node, then stop this algorithm. */
+ for($x = count($this->stack) - $n; $x >= $n; $x--) {
+ array_pop($this->stack);
+ }
+
+ } else {
+ $category = $this->getElementCategory($node);
+
+ if($category !== self::SPECIAL && $category !== self::SCOPING) {
+ /* Otherwise, if node is in neither the formatting
+ category nor the phrasing category, then this is a
+ parse error. Stop this algorithm. The end tag token
+ is ignored. */
+ return false;
+ }
+ }
+ }
+ break;
+ }
+ break;
+ }
+ }
+
+ private function inTable($token) {
+ $clear = array('html', 'table');
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $text = $this->dom->createTextNode($token['data']);
+ end($this->stack)->appendChild($text);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ end($this->stack)->appendChild($comment);
+
+ /* A start tag whose tag name is "caption" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'caption') {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+
+ /* Insert an HTML element for the token, then switch the
+ insertion mode to "in caption". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CAPTION;
+
+ /* A start tag whose tag name is "colgroup" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'colgroup') {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the
+ insertion mode to "in column group". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CGROUP;
+
+ /* A start tag whose tag name is "col" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'col') {
+ $this->inTable(array(
+ 'name' => 'colgroup',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ $this->inColumnGroup($token);
+
+ /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('tbody', 'tfoot', 'thead'))) {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the insertion
+ mode to "in table body". */
+ $this->insertElement($token);
+ $this->mode = self::IN_TBODY;
+
+ /* A start tag whose tag name is one of: "td", "th", "tr" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ in_array($token['name'], array('td', 'th', 'tr'))) {
+ /* Act as if a start tag token with the tag name "tbody" had been
+ seen, then reprocess the current token. */
+ $this->inTable(array(
+ 'name' => 'tbody',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ return $this->inTableBody($token);
+
+ /* A start tag whose tag name is "table" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'table') {
+ /* Parse error. Act as if an end tag token with the tag name "table"
+ had been seen, then, if that token wasn't ignored, reprocess the
+ current token. */
+ $this->inTable(array(
+ 'name' => 'table',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->mainPhase($token);
+
+ /* An end tag whose tag name is "table" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'table') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if(!$this->elementInScope($token['name'], true)) {
+ return false;
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not a table element, then this
+ is a parse error. */
+ // w/e
+
+ /* Pop elements from this stack until a table element has been
+ popped from the stack. */
+ while(true) {
+ $current = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if($current === 'table') {
+ break;
+ }
+ }
+
+ /* Reset the insertion mode appropriately. */
+ $this->resetInsertionMode();
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td',
+ 'tfoot', 'th', 'thead', 'tr'))) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* Parse error. Process the token as if the insertion mode was "in
+ body", with the following exception: */
+
+ /* If the current node is a table, tbody, tfoot, thead, or tr
+ element, then, whenever a node would be inserted into the current
+ node, it must instead be inserted into the foster parent element. */
+ if(in_array(end($this->stack)->nodeName,
+ array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
+ /* The foster parent element is the parent element of the last
+ table element in the stack of open elements, if there is a
+ table element and it has such a parent element. If there is no
+ table element in the stack of open elements (innerHTML case),
+ then the foster parent element is the first element in the
+ stack of open elements (the html element). Otherwise, if there
+ is a table element in the stack of open elements, but the last
+ table element in the stack of open elements has no parent, or
+ its parent node is not an element, then the foster parent
+ element is the element before the last table element in the
+ stack of open elements. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === 'table') {
+ $table = $this->stack[$n];
+ break;
+ }
+ }
+
+ if(isset($table) && $table->parentNode !== null) {
+ $this->foster_parent = $table->parentNode;
+
+ } elseif(!isset($table)) {
+ $this->foster_parent = $this->stack[0];
+
+ } elseif(isset($table) && ($table->parentNode === null ||
+ $table->parentNode->nodeType !== XML_ELEMENT_NODE)) {
+ $this->foster_parent = $this->stack[$n - 1];
+ }
+ }
+
+ $this->inBody($token);
+ }
+ }
+
+ private function inCaption($token) {
+ /* An end tag whose tag name is "caption" */
+ if($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not a caption element, then this
+ is a parse error. */
+ // w/e
+
+ /* Pop elements from this stack until a caption element has
+ been popped from the stack. */
+ while(true) {
+ $node = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if($node === 'caption') {
+ break;
+ }
+ }
+
+ /* Clear the list of active formatting elements up to the last
+ marker. */
+ $this->clearTheActiveFormattingElementsUpToTheLastMarker();
+
+ /* Switch the insertion mode to "in table". */
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag
+ name is "table" */
+ } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
+ 'thead', 'tr'))) || ($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'table')) {
+ /* Parse error. Act as if an end tag with the tag name "caption"
+ had been seen, then, if that token wasn't ignored, reprocess the
+ current token. */
+ $this->inCaption(array(
+ 'name' => 'caption',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->inTable($token);
+
+ /* An end tag whose tag name is one of: "body", "col", "colgroup",
+ "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th',
+ 'thead', 'tr'))) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in body". */
+ $this->inBody($token);
+ }
+ }
+
+ private function inColumnGroup($token) {
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $text = $this->dom->createTextNode($token['data']);
+ end($this->stack)->appendChild($text);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ end($this->stack)->appendChild($comment);
+
+ /* A start tag whose tag name is "col" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') {
+ /* Insert a col element for the token. Immediately pop the current
+ node off the stack of open elements. */
+ $this->insertElement($token);
+ array_pop($this->stack);
+
+ /* An end tag whose tag name is "colgroup" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'colgroup') {
+ /* If the current node is the root html element, then this is a
+ parse error, ignore the token. (innerHTML case) */
+ if(end($this->stack)->nodeName === 'html') {
+ // Ignore
+
+ /* Otherwise, pop the current node (which will be a colgroup
+ element) from the stack of open elements. Switch the insertion
+ mode to "in table". */
+ } else {
+ array_pop($this->stack);
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* An end tag whose tag name is "col" */
+ } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Act as if an end tag with the tag name "colgroup" had been seen,
+ and then, if that token wasn't ignored, reprocess the current token. */
+ $this->inColumnGroup(array(
+ 'name' => 'colgroup',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->inTable($token);
+ }
+ }
+
+ private function inTableBody($token) {
+ $clear = array('tbody', 'tfoot', 'thead', 'html');
+
+ /* A start tag whose tag name is "tr" */
+ if($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert a tr element for the token, then switch the insertion
+ mode to "in row". */
+ $this->insertElement($token);
+ $this->mode = self::IN_ROW;
+
+ /* A start tag whose tag name is one of: "th", "td" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ ($token['name'] === 'th' || $token['name'] === 'td')) {
+ /* Parse error. Act as if a start tag with the tag name "tr" had
+ been seen, then reprocess the current token. */
+ $this->inTableBody(array(
+ 'name' => 'tr',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ return $this->inRow($token);
+
+ /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Pop the current node from the stack of open elements. Switch
+ the insertion mode to "in table". */
+ array_pop($this->stack);
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */
+ } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead'))) ||
+ ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')) {
+ /* If the stack of open elements does not have a tbody, thead, or
+ tfoot element in table scope, this is a parse error. Ignore the
+ token. (innerHTML case) */
+ if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Act as if an end tag with the same tag name as the current
+ node ("tbody", "tfoot", or "thead") had been seen, then
+ reprocess the current token. */
+ $this->inTableBody(array(
+ 'name' => end($this->stack)->nodeName,
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->mainPhase($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "td", "th", "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in table". */
+ $this->inTable($token);
+ }
+ }
+
+ private function inRow($token) {
+ $clear = array('tr', 'html');
+
+ /* A start tag whose tag name is one of: "th", "td" */
+ if($token['type'] === HTML5::STARTTAG &&
+ ($token['name'] === 'th' || $token['name'] === 'td')) {
+ /* Clear the stack back to a table row context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the insertion
+ mode to "in cell". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CELL;
+
+ /* Insert a marker at the end of the list of active formatting
+ elements. */
+ $this->a_formatting[] = self::MARKER;
+
+ /* An end tag whose tag name is "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table row context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Pop the current node (which will be a tr element) from the
+ stack of open elements. Switch the insertion mode to "in table
+ body". */
+ array_pop($this->stack);
+ $this->mode = self::IN_TBODY;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) {
+ /* Act as if an end tag with the tag name "tr" had been seen, then,
+ if that token wasn't ignored, reprocess the current token. */
+ $this->inRow(array(
+ 'name' => 'tr',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->inCell($token);
+
+ /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Otherwise, act as if an end tag with the tag name "tr" had
+ been seen, then reprocess the current token. */
+ $this->inRow(array(
+ 'name' => 'tr',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->inCell($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "td", "th" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in table". */
+ $this->inTable($token);
+ }
+ }
+
+ private function inCell($token) {
+ /* An end tag whose tag name is one of: "td", "th" */
+ if($token['type'] === HTML5::ENDTAG &&
+ ($token['name'] === 'td' || $token['name'] === 'th')) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as that of the token, then this is a
+ parse error and the token must be ignored. */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags, except for elements with the same
+ tag name as the token. */
+ $this->generateImpliedEndTags(array($token['name']));
+
+ /* Now, if the current node is not an element with the same tag
+ name as the token, then this is a parse error. */
+ // k
+
+ /* Pop elements from this stack until an element with the same
+ tag name as the token has been popped from the stack. */
+ while(true) {
+ $node = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if($node === $token['name']) {
+ break;
+ }
+ }
+
+ /* Clear the list of active formatting elements up to the last
+ marker. */
+ $this->clearTheActiveFormattingElementsUpToTheLastMarker();
+
+ /* Switch the insertion mode to "in row". (The current node
+ will be a tr element at this point.) */
+ $this->mode = self::IN_ROW;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
+ 'thead', 'tr'))) {
+ /* If the stack of open elements does not have a td or th element
+ in table scope, then this is a parse error; ignore the token.
+ (innerHTML case) */
+ if(!$this->elementInScope(array('td', 'th'), true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
+ 'thead', 'tr'))) {
+ /* If the stack of open elements does not have a td or th element
+ in table scope, then this is a parse error; ignore the token.
+ (innerHTML case) */
+ if(!$this->elementInScope(array('td', 'th'), true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html'))) {
+ /* Parse error. Ignore the token. */
+
+ /* An end tag whose tag name is one of: "table", "tbody", "tfoot",
+ "thead", "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as that of the token (which can only
+ happen for "tbody", "tfoot" and "thead", or, in the innerHTML case),
+ then this is a parse error and the token must be ignored. */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in body". */
+ $this->inBody($token);
+ }
+ }
+
+ private function inSelect($token) {
+ /* Handle the token as follows: */
+
+ /* A character token */
+ if($token['type'] === HTML5::CHARACTR) {
+ /* Append the token's character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token whose tag name is "option" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'option') {
+ /* If the current node is an option element, act as if an end tag
+ with the tag name "option" had been seen. */
+ if(end($this->stack)->nodeName === 'option') {
+ $this->inSelect(array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* A start tag token whose tag name is "optgroup" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'optgroup') {
+ /* If the current node is an option element, act as if an end tag
+ with the tag name "option" had been seen. */
+ if(end($this->stack)->nodeName === 'option') {
+ $this->inSelect(array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* If the current node is an optgroup element, act as if an end tag
+ with the tag name "optgroup" had been seen. */
+ if(end($this->stack)->nodeName === 'optgroup') {
+ $this->inSelect(array(
+ 'name' => 'optgroup',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* An end tag token whose tag name is "optgroup" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'optgroup') {
+ /* First, if the current node is an option element, and the node
+ immediately before it in the stack of open elements is an optgroup
+ element, then act as if an end tag with the tag name "option" had
+ been seen. */
+ $elements_in_stack = count($this->stack);
+
+ if($this->stack[$elements_in_stack - 1]->nodeName === 'option' &&
+ $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup') {
+ $this->inSelect(array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* If the current node is an optgroup element, then pop that node
+ from the stack of open elements. Otherwise, this is a parse error,
+ ignore the token. */
+ if($this->stack[$elements_in_stack - 1] === 'optgroup') {
+ array_pop($this->stack);
+ }
+
+ /* An end tag token whose tag name is "option" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'option') {
+ /* If the current node is an option element, then pop that node
+ from the stack of open elements. Otherwise, this is a parse error,
+ ignore the token. */
+ if(end($this->stack)->nodeName === 'option') {
+ array_pop($this->stack);
+ }
+
+ /* An end tag whose tag name is "select" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'select') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if(!$this->elementInScope($token['name'], true)) {
+ // w/e
+
+ /* Otherwise: */
+ } else {
+ /* Pop elements from the stack of open elements until a select
+ element has been popped from the stack. */
+ while(true) {
+ $current = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if($current === 'select') {
+ break;
+ }
+ }
+
+ /* Reset the insertion mode appropriately. */
+ $this->resetInsertionMode();
+ }
+
+ /* A start tag whose tag name is "select" */
+ } elseif($token['name'] === 'select' &&
+ $token['type'] === HTML5::STARTTAG) {
+ /* Parse error. Act as if the token had been an end tag with the
+ tag name "select" instead. */
+ $this->inSelect(array(
+ 'name' => 'select',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* An end tag whose tag name is one of: "caption", "table", "tbody",
+ "tfoot", "thead", "tr", "td", "th" */
+ } elseif(in_array($token['name'], array('caption', 'table', 'tbody',
+ 'tfoot', 'thead', 'tr', 'td', 'th')) && $token['type'] === HTML5::ENDTAG) {
+ /* Parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in table scope with
+ the same tag name as that of the token, then act as if an end tag
+ with the tag name "select" had been seen, and reprocess the token.
+ Otherwise, ignore the token. */
+ if($this->elementInScope($token['name'], true)) {
+ $this->inSelect(array(
+ 'name' => 'select',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ $this->mainPhase($token);
+ }
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function afterBody($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Process the token as it would be processed if the insertion mode
+ was "in body". */
+ $this->inBody($token);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the first element in the stack of open
+ elements (the html element), with the data attribute set to the
+ data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->stack[0]->appendChild($comment);
+
+ /* An end tag with the tag name "html" */
+ } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') {
+ /* If the parser was originally created in order to handle the
+ setting of an element's innerHTML attribute, this is a parse error;
+ ignore the token. (The element will be an html element in this
+ case.) (innerHTML case) */
+
+ /* Otherwise, switch to the trailing end phase. */
+ $this->phase = self::END_PHASE;
+
+ /* Anything else */
+ } else {
+ /* Parse error. Set the insertion mode to "in body" and reprocess
+ the token. */
+ $this->mode = self::IN_BODY;
+ return $this->inBody($token);
+ }
+ }
+
+ private function inFrameset($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag with the tag name "frameset" */
+ } elseif($token['name'] === 'frameset' &&
+ $token['type'] === HTML5::STARTTAG) {
+ $this->insertElement($token);
+
+ /* An end tag with the tag name "frameset" */
+ } elseif($token['name'] === 'frameset' &&
+ $token['type'] === HTML5::ENDTAG) {
+ /* If the current node is the root html element, then this is a
+ parse error; ignore the token. (innerHTML case) */
+ if(end($this->stack)->nodeName === 'html') {
+ // Ignore
+
+ } else {
+ /* Otherwise, pop the current node from the stack of open
+ elements. */
+ array_pop($this->stack);
+
+ /* If the parser was not originally created in order to handle
+ the setting of an element's innerHTML attribute (innerHTML case),
+ and the current node is no longer a frameset element, then change
+ the insertion mode to "after frameset". */
+ $this->mode = self::AFTR_FRAME;
+ }
+
+ /* A start tag with the tag name "frame" */
+ } elseif($token['name'] === 'frame' &&
+ $token['type'] === HTML5::STARTTAG) {
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+
+ /* A start tag with the tag name "noframes" */
+ } elseif($token['name'] === 'noframes' &&
+ $token['type'] === HTML5::STARTTAG) {
+ /* Process the token as if the insertion mode had been "in body". */
+ $this->inBody($token);
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function afterFrameset($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* An end tag with the tag name "html" */
+ } elseif($token['name'] === 'html' &&
+ $token['type'] === HTML5::ENDTAG) {
+ /* Switch to the trailing end phase. */
+ $this->phase = self::END_PHASE;
+
+ /* A start tag with the tag name "noframes" */
+ } elseif($token['name'] === 'noframes' &&
+ $token['type'] === HTML5::STARTTAG) {
+ /* Process the token as if the insertion mode had been "in body". */
+ $this->inBody($token);
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function trailingEndPhase($token) {
+ /* After the main phase, as each token is emitted from the tokenisation
+ stage, it must be processed as described in this section. */
+
+ /* A DOCTYPE token */
+ if($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the Document object with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->dom->appendChild($comment);
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Process the token as it would be processed in the main phase. */
+ $this->mainPhase($token);
+
+ /* A character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE. Or a start tag token. Or an end tag token. */
+ } elseif(($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ||
+ $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG) {
+ /* Parse error. Switch back to the main phase and reprocess the
+ token. */
+ $this->phase = self::MAIN_PHASE;
+ return $this->mainPhase($token);
+
+ /* An end-of-file token */
+ } elseif($token['type'] === HTML5::EOF) {
+ /* OMG DONE!! */
+ }
+ }
+
+ private function insertElement($token, $append = true, $check = false) {
+ // Proprietary workaround for libxml2's limitations with tag names
+ if ($check) {
+ // Slightly modified HTML5 tag-name modification,
+ // removing anything that's not an ASCII letter, digit, or hyphen
+ $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']);
+ // Remove leading hyphens and numbers
+ $token['name'] = ltrim($token['name'], '-0..9');
+ // In theory, this should ever be needed, but just in case
+ if ($token['name'] === '') $token['name'] = 'span'; // arbitrary generic choice
+ }
+
+ $el = $this->dom->createElement($token['name']);
+
+ foreach($token['attr'] as $attr) {
+ if(!$el->hasAttribute($attr['name'])) {
+ $el->setAttribute($attr['name'], $attr['value']);
+ }
+ }
+
+ $this->appendToRealParent($el);
+ $this->stack[] = $el;
+
+ return $el;
+ }
+
+ private function insertText($data) {
+ $text = $this->dom->createTextNode($data);
+ $this->appendToRealParent($text);
+ }
+
+ private function insertComment($data) {
+ $comment = $this->dom->createComment($data);
+ $this->appendToRealParent($comment);
+ }
+
+ private function appendToRealParent($node) {
+ if($this->foster_parent === null) {
+ end($this->stack)->appendChild($node);
+
+ } elseif($this->foster_parent !== null) {
+ /* If the foster parent element is the parent element of the
+ last table element in the stack of open elements, then the new
+ node must be inserted immediately before the last table element
+ in the stack of open elements in the foster parent element;
+ otherwise, the new node must be appended to the foster parent
+ element. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === 'table' &&
+ $this->stack[$n]->parentNode !== null) {
+ $table = $this->stack[$n];
+ break;
+ }
+ }
+
+ if(isset($table) && $this->foster_parent->isSameNode($table->parentNode))
+ $this->foster_parent->insertBefore($node, $table);
+ else
+ $this->foster_parent->appendChild($node);
+
+ $this->foster_parent = null;
+ }
+ }
+
+ private function elementInScope($el, $table = false) {
+ if(is_array($el)) {
+ foreach($el as $element) {
+ if($this->elementInScope($element, $table)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ $leng = count($this->stack);
+
+ for($n = 0; $n < $leng; $n++) {
+ /* 1. Initialise node to be the current node (the bottommost node of
+ the stack). */
+ $node = $this->stack[$leng - 1 - $n];
+
+ if($node->tagName === $el) {
+ /* 2. If node is the target node, terminate in a match state. */
+ return true;
+
+ } elseif($node->tagName === 'table') {
+ /* 3. Otherwise, if node is a table element, terminate in a failure
+ state. */
+ return false;
+
+ } elseif($table === true && in_array($node->tagName, array('caption', 'td',
+ 'th', 'button', 'marquee', 'object'))) {
+ /* 4. Otherwise, if the algorithm is the "has an element in scope"
+ variant (rather than the "has an element in table scope" variant),
+ and node is one of the following, terminate in a failure state. */
+ return false;
+
+ } elseif($node === $node->ownerDocument->documentElement) {
+ /* 5. Otherwise, if node is an html element (root element), terminate
+ in a failure state. (This can only happen if the node is the topmost
+ node of the stack of open elements, and prevents the next step from
+ being invoked if there are no more elements in the stack.) */
+ return false;
+ }
+
+ /* Otherwise, set node to the previous entry in the stack of open
+ elements and return to step 2. (This will never fail, since the loop
+ will always terminate in the previous step if the top of the stack
+ is reached.) */
+ }
+ }
+
+ private function reconstructActiveFormattingElements() {
+ /* 1. If there are no entries in the list of active formatting elements,
+ then there is nothing to reconstruct; stop this algorithm. */
+ $formatting_elements = count($this->a_formatting);
+
+ if($formatting_elements === 0) {
+ return false;
+ }
+
+ /* 3. Let entry be the last (most recently added) element in the list
+ of active formatting elements. */
+ $entry = end($this->a_formatting);
+
+ /* 2. If the last (most recently added) entry in the list of active
+ formatting elements is a marker, or if it is an element that is in the
+ stack of open elements, then there is nothing to reconstruct; stop this
+ algorithm. */
+ if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
+ return false;
+ }
+
+ for($a = $formatting_elements - 1; $a >= 0; true) {
+ /* 4. If there are no entries before entry in the list of active
+ formatting elements, then jump to step 8. */
+ if($a === 0) {
+ $step_seven = false;
+ break;
+ }
+
+ /* 5. Let entry be the entry one earlier than entry in the list of
+ active formatting elements. */
+ $a--;
+ $entry = $this->a_formatting[$a];
+
+ /* 6. If entry is neither a marker nor an element that is also in
+ thetack of open elements, go to step 4. */
+ if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
+ break;
+ }
+ }
+
+ while(true) {
+ /* 7. Let entry be the element one later than entry in the list of
+ active formatting elements. */
+ if(isset($step_seven) && $step_seven === true) {
+ $a++;
+ $entry = $this->a_formatting[$a];
+ }
+
+ /* 8. Perform a shallow clone of the element entry to obtain clone. */
+ $clone = $entry->cloneNode();
+
+ /* 9. Append clone to the current node and push it onto the stack
+ of open elements so that it is the new current node. */
+ end($this->stack)->appendChild($clone);
+ $this->stack[] = $clone;
+
+ /* 10. Replace the entry for entry in the list with an entry for
+ clone. */
+ $this->a_formatting[$a] = $clone;
+
+ /* 11. If the entry for clone in the list of active formatting
+ elements is not the last entry in the list, return to step 7. */
+ if(end($this->a_formatting) !== $clone) {
+ $step_seven = true;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private function clearTheActiveFormattingElementsUpToTheLastMarker() {
+ /* When the steps below require the UA to clear the list of active
+ formatting elements up to the last marker, the UA must perform the
+ following steps: */
+
+ while(true) {
+ /* 1. Let entry be the last (most recently added) entry in the list
+ of active formatting elements. */
+ $entry = end($this->a_formatting);
+
+ /* 2. Remove entry from the list of active formatting elements. */
+ array_pop($this->a_formatting);
+
+ /* 3. If entry was a marker, then stop the algorithm at this point.
+ The list has been cleared up to the last marker. */
+ if($entry === self::MARKER) {
+ break;
+ }
+ }
+ }
+
+ private function generateImpliedEndTags($exclude = array()) {
+ /* When the steps below require the UA to generate implied end tags,
+ then, if the current node is a dd element, a dt element, an li element,
+ a p element, a td element, a th element, or a tr element, the UA must
+ act as if an end tag with the respective tag name had been seen and
+ then generate implied end tags again. */
+ $node = end($this->stack);
+ $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude);
+
+ while(in_array(end($this->stack)->nodeName, $elements)) {
+ array_pop($this->stack);
+ }
+ }
+
+ private function getElementCategory($node) {
+ $name = $node->tagName;
+ if(in_array($name, $this->special))
+ return self::SPECIAL;
+
+ elseif(in_array($name, $this->scoping))
+ return self::SCOPING;
+
+ elseif(in_array($name, $this->formatting))
+ return self::FORMATTING;
+
+ else
+ return self::PHRASING;
+ }
+
+ private function clearStackToTableContext($elements) {
+ /* When the steps above require the UA to clear the stack back to a
+ table context, it means that the UA must, while the current node is not
+ a table element or an html element, pop elements from the stack of open
+ elements. If this causes any elements to be popped from the stack, then
+ this is a parse error. */
+ while(true) {
+ $node = end($this->stack)->nodeName;
+
+ if(in_array($node, $elements)) {
+ break;
+ } else {
+ array_pop($this->stack);
+ }
+ }
+ }
+
+ private function resetInsertionMode() {
+ /* 1. Let last be false. */
+ $last = false;
+ $leng = count($this->stack);
+
+ for($n = $leng - 1; $n >= 0; $n--) {
+ /* 2. Let node be the last node in the stack of open elements. */
+ $node = $this->stack[$n];
+
+ /* 3. If node is the first node in the stack of open elements, then
+ set last to true. If the element whose innerHTML attribute is being
+ set is neither a td element nor a th element, then set node to the
+ element whose innerHTML attribute is being set. (innerHTML case) */
+ if($this->stack[0]->isSameNode($node)) {
+ $last = true;
+ }
+
+ /* 4. If node is a select element, then switch the insertion mode to
+ "in select" and abort these steps. (innerHTML case) */
+ if($node->nodeName === 'select') {
+ $this->mode = self::IN_SELECT;
+ break;
+
+ /* 5. If node is a td or th element, then switch the insertion mode
+ to "in cell" and abort these steps. */
+ } elseif($node->nodeName === 'td' || $node->nodeName === 'th') {
+ $this->mode = self::IN_CELL;
+ break;
+
+ /* 6. If node is a tr element, then switch the insertion mode to
+ "in row" and abort these steps. */
+ } elseif($node->nodeName === 'tr') {
+ $this->mode = self::IN_ROW;
+ break;
+
+ /* 7. If node is a tbody, thead, or tfoot element, then switch the
+ insertion mode to "in table body" and abort these steps. */
+ } elseif(in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) {
+ $this->mode = self::IN_TBODY;
+ break;
+
+ /* 8. If node is a caption element, then switch the insertion mode
+ to "in caption" and abort these steps. */
+ } elseif($node->nodeName === 'caption') {
+ $this->mode = self::IN_CAPTION;
+ break;
+
+ /* 9. If node is a colgroup element, then switch the insertion mode
+ to "in column group" and abort these steps. (innerHTML case) */
+ } elseif($node->nodeName === 'colgroup') {
+ $this->mode = self::IN_CGROUP;
+ break;
+
+ /* 10. If node is a table element, then switch the insertion mode
+ to "in table" and abort these steps. */
+ } elseif($node->nodeName === 'table') {
+ $this->mode = self::IN_TABLE;
+ break;
+
+ /* 11. If node is a head element, then switch the insertion mode
+ to "in body" ("in body"! not "in head"!) and abort these steps.
+ (innerHTML case) */
+ } elseif($node->nodeName === 'head') {
+ $this->mode = self::IN_BODY;
+ break;
+
+ /* 12. If node is a body element, then switch the insertion mode to
+ "in body" and abort these steps. */
+ } elseif($node->nodeName === 'body') {
+ $this->mode = self::IN_BODY;
+ break;
+
+ /* 13. If node is a frameset element, then switch the insertion
+ mode to "in frameset" and abort these steps. (innerHTML case) */
+ } elseif($node->nodeName === 'frameset') {
+ $this->mode = self::IN_FRAME;
+ break;
+
+ /* 14. If node is an html element, then: if the head element
+ pointer is null, switch the insertion mode to "before head",
+ otherwise, switch the insertion mode to "after head". In either
+ case, abort these steps. (innerHTML case) */
+ } elseif($node->nodeName === 'html') {
+ $this->mode = ($this->head_pointer === null)
+ ? self::BEFOR_HEAD
+ : self::AFTER_HEAD;
+
+ break;
+
+ /* 15. If last is true, then set the insertion mode to "in body"
+ and abort these steps. (innerHTML case) */
+ } elseif($last) {
+ $this->mode = self::IN_BODY;
+ break;
+ }
+ }
+ }
+
+ private function closeCell() {
+ /* If the stack of open elements has a td or th element in table scope,
+ then act as if an end tag token with that tag name had been seen. */
+ foreach(array('td', 'th') as $cell) {
+ if($this->elementInScope($cell, true)) {
+ $this->inCell(array(
+ 'name' => $cell,
+ 'type' => HTML5::ENDTAG
+ ));
+
+ break;
+ }
+ }
+ }
+
+ public function save() {
+ return $this->dom;
+ }
+}
+?>
diff --git a/extlib/HTMLPurifier/HTMLPurifier/PercentEncoder.php b/extlib/HTMLPurifier/HTMLPurifier/PercentEncoder.php
new file mode 100644
index 000000000..a43c44f4c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/PercentEncoder.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * Class that handles operations involving percent-encoding in URIs.
+ *
+ * @warning
+ * Be careful when reusing instances of PercentEncoder. The object
+ * you use for normalize() SHOULD NOT be used for encode(), or
+ * vice-versa.
+ */
+class HTMLPurifier_PercentEncoder
+{
+
+ /**
+ * Reserved characters to preserve when using encode().
+ */
+ protected $preserve = array();
+
+ /**
+ * String of characters that should be preserved while using encode().
+ */
+ public function __construct($preserve = false) {
+ // unreserved letters, ought to const-ify
+ for ($i = 48; $i <= 57; $i++) $this->preserve[$i] = true; // digits
+ for ($i = 65; $i <= 90; $i++) $this->preserve[$i] = true; // upper-case
+ for ($i = 97; $i <= 122; $i++) $this->preserve[$i] = true; // lower-case
+ $this->preserve[45] = true; // Dash -
+ $this->preserve[46] = true; // Period .
+ $this->preserve[95] = true; // Underscore _
+ $this->preserve[126]= true; // Tilde ~
+
+ // extra letters not to escape
+ if ($preserve !== false) {
+ for ($i = 0, $c = strlen($preserve); $i < $c; $i++) {
+ $this->preserve[ord($preserve[$i])] = true;
+ }
+ }
+ }
+
+ /**
+ * Our replacement for urlencode, it encodes all non-reserved characters,
+ * as well as any extra characters that were instructed to be preserved.
+ * @note
+ * Assumes that the string has already been normalized, making any
+ * and all percent escape sequences valid. Percents will not be
+ * re-escaped, regardless of their status in $preserve
+ * @param $string String to be encoded
+ * @return Encoded string.
+ */
+ public function encode($string) {
+ $ret = '';
+ for ($i = 0, $c = strlen($string); $i < $c; $i++) {
+ if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])]) ) {
+ $ret .= '%' . sprintf('%02X', $int);
+ } else {
+ $ret .= $string[$i];
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Fix up percent-encoding by decoding unreserved characters and normalizing.
+ * @warning This function is affected by $preserve, even though the
+ * usual desired behavior is for this not to preserve those
+ * characters. Be careful when reusing instances of PercentEncoder!
+ * @param $string String to normalize
+ */
+ public function normalize($string) {
+ if ($string == '') return '';
+ $parts = explode('%', $string);
+ $ret = array_shift($parts);
+ foreach ($parts as $part) {
+ $length = strlen($part);
+ if ($length < 2) {
+ $ret .= '%25' . $part;
+ continue;
+ }
+ $encoding = substr($part, 0, 2);
+ $text = substr($part, 2);
+ if (!ctype_xdigit($encoding)) {
+ $ret .= '%25' . $part;
+ continue;
+ }
+ $int = hexdec($encoding);
+ if (isset($this->preserve[$int])) {
+ $ret .= chr($int) . $text;
+ continue;
+ }
+ $encoding = strtoupper($encoding);
+ $ret .= '%' . $encoding . $text;
+ }
+ return $ret;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Printer.php b/extlib/HTMLPurifier/HTMLPurifier/Printer.php
new file mode 100644
index 000000000..e7eb82e83
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Printer.php
@@ -0,0 +1,176 @@
+<?php
+
+// OUT OF DATE, NEEDS UPDATING!
+// USE XMLWRITER!
+
+class HTMLPurifier_Printer
+{
+
+ /**
+ * Instance of HTMLPurifier_Generator for HTML generation convenience funcs
+ */
+ protected $generator;
+
+ /**
+ * Instance of HTMLPurifier_Config, for easy access
+ */
+ protected $config;
+
+ /**
+ * Initialize $generator.
+ */
+ public function __construct() {
+ }
+
+ /**
+ * Give generator necessary configuration if possible
+ */
+ public function prepareGenerator($config) {
+ $all = $config->getAll();
+ $context = new HTMLPurifier_Context();
+ $this->generator = new HTMLPurifier_Generator($config, $context);
+ }
+
+ /**
+ * Main function that renders object or aspect of that object
+ * @note Parameters vary depending on printer
+ */
+ // function render() {}
+
+ /**
+ * Returns a start tag
+ * @param $tag Tag name
+ * @param $attr Attribute array
+ */
+ protected function start($tag, $attr = array()) {
+ return $this->generator->generateFromToken(
+ new HTMLPurifier_Token_Start($tag, $attr ? $attr : array())
+ );
+ }
+
+ /**
+ * Returns an end teg
+ * @param $tag Tag name
+ */
+ protected function end($tag) {
+ return $this->generator->generateFromToken(
+ new HTMLPurifier_Token_End($tag)
+ );
+ }
+
+ /**
+ * Prints a complete element with content inside
+ * @param $tag Tag name
+ * @param $contents Element contents
+ * @param $attr Tag attributes
+ * @param $escape Bool whether or not to escape contents
+ */
+ protected function element($tag, $contents, $attr = array(), $escape = true) {
+ return $this->start($tag, $attr) .
+ ($escape ? $this->escape($contents) : $contents) .
+ $this->end($tag);
+ }
+
+ protected function elementEmpty($tag, $attr = array()) {
+ return $this->generator->generateFromToken(
+ new HTMLPurifier_Token_Empty($tag, $attr)
+ );
+ }
+
+ protected function text($text) {
+ return $this->generator->generateFromToken(
+ new HTMLPurifier_Token_Text($text)
+ );
+ }
+
+ /**
+ * Prints a simple key/value row in a table.
+ * @param $name Key
+ * @param $value Value
+ */
+ protected function row($name, $value) {
+ if (is_bool($value)) $value = $value ? 'On' : 'Off';
+ return
+ $this->start('tr') . "\n" .
+ $this->element('th', $name) . "\n" .
+ $this->element('td', $value) . "\n" .
+ $this->end('tr')
+ ;
+ }
+
+ /**
+ * Escapes a string for HTML output.
+ * @param $string String to escape
+ */
+ protected function escape($string) {
+ $string = HTMLPurifier_Encoder::cleanUTF8($string);
+ $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8');
+ return $string;
+ }
+
+ /**
+ * Takes a list of strings and turns them into a single list
+ * @param $array List of strings
+ * @param $polite Bool whether or not to add an end before the last
+ */
+ protected function listify($array, $polite = false) {
+ if (empty($array)) return 'None';
+ $ret = '';
+ $i = count($array);
+ foreach ($array as $value) {
+ $i--;
+ $ret .= $value;
+ if ($i > 0 && !($polite && $i == 1)) $ret .= ', ';
+ if ($polite && $i == 1) $ret .= 'and ';
+ }
+ return $ret;
+ }
+
+ /**
+ * Retrieves the class of an object without prefixes, as well as metadata
+ * @param $obj Object to determine class of
+ * @param $prefix Further prefix to remove
+ */
+ protected function getClass($obj, $sec_prefix = '') {
+ static $five = null;
+ if ($five === null) $five = version_compare(PHP_VERSION, '5', '>=');
+ $prefix = 'HTMLPurifier_' . $sec_prefix;
+ if (!$five) $prefix = strtolower($prefix);
+ $class = str_replace($prefix, '', get_class($obj));
+ $lclass = strtolower($class);
+ $class .= '(';
+ switch ($lclass) {
+ case 'enum':
+ $values = array();
+ foreach ($obj->valid_values as $value => $bool) {
+ $values[] = $value;
+ }
+ $class .= implode(', ', $values);
+ break;
+ case 'css_composite':
+ $values = array();
+ foreach ($obj->defs as $def) {
+ $values[] = $this->getClass($def, $sec_prefix);
+ }
+ $class .= implode(', ', $values);
+ break;
+ case 'css_multiple':
+ $class .= $this->getClass($obj->single, $sec_prefix) . ', ';
+ $class .= $obj->max;
+ break;
+ case 'css_denyelementdecorator':
+ $class .= $this->getClass($obj->def, $sec_prefix) . ', ';
+ $class .= $obj->element;
+ break;
+ case 'css_importantdecorator':
+ $class .= $this->getClass($obj->def, $sec_prefix);
+ if ($obj->allow) $class .= ', !important';
+ break;
+ }
+ $class .= ')';
+ return $class;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Printer/CSSDefinition.php b/extlib/HTMLPurifier/HTMLPurifier/Printer/CSSDefinition.php
new file mode 100644
index 000000000..81f986590
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Printer/CSSDefinition.php
@@ -0,0 +1,38 @@
+<?php
+
+class HTMLPurifier_Printer_CSSDefinition extends HTMLPurifier_Printer
+{
+
+ protected $def;
+
+ public function render($config) {
+ $this->def = $config->getCSSDefinition();
+ $ret = '';
+
+ $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer'));
+ $ret .= $this->start('table');
+
+ $ret .= $this->element('caption', 'Properties ($info)');
+
+ $ret .= $this->start('thead');
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Property', array('class' => 'heavy'));
+ $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;'));
+ $ret .= $this->end('tr');
+ $ret .= $this->end('thead');
+
+ ksort($this->def->info);
+ foreach ($this->def->info as $property => $obj) {
+ $name = $this->getClass($obj, 'AttrDef_');
+ $ret .= $this->row($property, $name);
+ }
+
+ $ret .= $this->end('table');
+ $ret .= $this->end('div');
+
+ return $ret;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.css b/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.css
new file mode 100644
index 000000000..3ff1a88aa
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.css
@@ -0,0 +1,10 @@
+
+.hp-config {}
+
+.hp-config tbody th {text-align:right; padding-right:0.5em;}
+.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;}
+.hp-config .namespace th {text-align:center;}
+.hp-config .verbose {display:none;}
+.hp-config .controls {text-align:center;}
+
+/* vim: et sw=4 sts=4 */
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.js b/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.js
new file mode 100644
index 000000000..cba00c9b8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.js
@@ -0,0 +1,5 @@
+function toggleWriteability(id_of_patient, checked) {
+ document.getElementById(id_of_patient).disabled = checked;
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.php b/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.php
new file mode 100644
index 000000000..02aa65689
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Printer/ConfigForm.php
@@ -0,0 +1,368 @@
+<?php
+
+/**
+ * @todo Rewrite to use Interchange objects
+ */
+class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer
+{
+
+ /**
+ * Printers for specific fields
+ */
+ protected $fields = array();
+
+ /**
+ * Documentation URL, can have fragment tagged on end
+ */
+ protected $docURL;
+
+ /**
+ * Name of form element to stuff config in
+ */
+ protected $name;
+
+ /**
+ * Whether or not to compress directive names, clipping them off
+ * after a certain amount of letters. False to disable or integer letters
+ * before clipping.
+ */
+ protected $compress = false;
+
+ /**
+ * @param $name Form element name for directives to be stuffed into
+ * @param $doc_url String documentation URL, will have fragment tagged on
+ * @param $compress Integer max length before compressing a directive name, set to false to turn off
+ */
+ public function __construct(
+ $name, $doc_url = null, $compress = false
+ ) {
+ parent::__construct();
+ $this->docURL = $doc_url;
+ $this->name = $name;
+ $this->compress = $compress;
+ // initialize sub-printers
+ $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default();
+ $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool();
+ }
+
+ /**
+ * Sets default column and row size for textareas in sub-printers
+ * @param $cols Integer columns of textarea, null to use default
+ * @param $rows Integer rows of textarea, null to use default
+ */
+ public function setTextareaDimensions($cols = null, $rows = null) {
+ if ($cols) $this->fields['default']->cols = $cols;
+ if ($rows) $this->fields['default']->rows = $rows;
+ }
+
+ /**
+ * Retrieves styling, in case it is not accessible by webserver
+ */
+ public static function getCSS() {
+ return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css');
+ }
+
+ /**
+ * Retrieves JavaScript, in case it is not accessible by webserver
+ */
+ public static function getJavaScript() {
+ return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js');
+ }
+
+ /**
+ * Returns HTML output for a configuration form
+ * @param $config Configuration object of current form state, or an array
+ * where [0] has an HTML namespace and [1] is being rendered.
+ * @param $allowed Optional namespace(s) and directives to restrict form to.
+ */
+ public function render($config, $allowed = true, $render_controls = true) {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+
+ $this->config = $config;
+ $this->genConfig = $gen_config;
+ $this->prepareGenerator($gen_config);
+
+ $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def);
+ $all = array();
+ foreach ($allowed as $key) {
+ list($ns, $directive) = $key;
+ $all[$ns][$directive] = $config->get($ns .'.'. $directive);
+ }
+
+ $ret = '';
+ $ret .= $this->start('table', array('class' => 'hp-config'));
+ $ret .= $this->start('thead');
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive'));
+ $ret .= $this->element('th', 'Value', array('class' => 'hp-value'));
+ $ret .= $this->end('tr');
+ $ret .= $this->end('thead');
+ foreach ($all as $ns => $directives) {
+ $ret .= $this->renderNamespace($ns, $directives);
+ }
+ if ($render_controls) {
+ $ret .= $this->start('tbody');
+ $ret .= $this->start('tr');
+ $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls'));
+ $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit'));
+ $ret .= '[<a href="?">Reset</a>]';
+ $ret .= $this->end('td');
+ $ret .= $this->end('tr');
+ $ret .= $this->end('tbody');
+ }
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+ /**
+ * Renders a single namespace
+ * @param $ns String namespace name
+ * @param $directive Associative array of directives to values
+ */
+ protected function renderNamespace($ns, $directives) {
+ $ret = '';
+ $ret .= $this->start('tbody', array('class' => 'namespace'));
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', $ns, array('colspan' => 2));
+ $ret .= $this->end('tr');
+ $ret .= $this->end('tbody');
+ $ret .= $this->start('tbody');
+ foreach ($directives as $directive => $value) {
+ $ret .= $this->start('tr');
+ $ret .= $this->start('th');
+ if ($this->docURL) {
+ $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL);
+ $ret .= $this->start('a', array('href' => $url));
+ }
+ $attr = array('for' => "{$this->name}:$ns.$directive");
+
+ // crop directive name if it's too long
+ if (!$this->compress || (strlen($directive) < $this->compress)) {
+ $directive_disp = $directive;
+ } else {
+ $directive_disp = substr($directive, 0, $this->compress - 2) . '...';
+ $attr['title'] = $directive;
+ }
+
+ $ret .= $this->element(
+ 'label',
+ $directive_disp,
+ // component printers must create an element with this id
+ $attr
+ );
+ if ($this->docURL) $ret .= $this->end('a');
+ $ret .= $this->end('th');
+
+ $ret .= $this->start('td');
+ $def = $this->config->def->info["$ns.$directive"];
+ if (is_int($def)) {
+ $allow_null = $def < 0;
+ $type = abs($def);
+ } else {
+ $type = $def->type;
+ $allow_null = isset($def->allow_null);
+ }
+ if (!isset($this->fields[$type])) $type = 0; // default
+ $type_obj = $this->fields[$type];
+ if ($allow_null) {
+ $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj);
+ }
+ $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config));
+ $ret .= $this->end('td');
+ $ret .= $this->end('tr');
+ }
+ $ret .= $this->end('tbody');
+ return $ret;
+ }
+
+}
+
+/**
+ * Printer decorator for directives that accept null
+ */
+class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer {
+ /**
+ * Printer being decorated
+ */
+ protected $obj;
+ /**
+ * @param $obj Printer to decorate
+ */
+ public function __construct($obj) {
+ parent::__construct();
+ $this->obj = $obj;
+ }
+ public function render($ns, $directive, $value, $name, $config) {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
+
+ $ret = '';
+ $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive"));
+ $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
+ $ret .= $this->text(' Null/Disabled');
+ $ret .= $this->end('label');
+ $attr = array(
+ 'type' => 'checkbox',
+ 'value' => '1',
+ 'class' => 'null-toggle',
+ 'name' => "$name"."[Null_$ns.$directive]",
+ 'id' => "$name:Null_$ns.$directive",
+ 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!!
+ );
+ if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) {
+ // modify inline javascript slightly
+ $attr['onclick'] = "toggleWriteability('$name:Yes_$ns.$directive',checked);toggleWriteability('$name:No_$ns.$directive',checked)";
+ }
+ if ($value === null) $attr['checked'] = 'checked';
+ $ret .= $this->elementEmpty('input', $attr);
+ $ret .= $this->text(' or ');
+ $ret .= $this->elementEmpty('br');
+ $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config));
+ return $ret;
+ }
+}
+
+/**
+ * Swiss-army knife configuration form field printer
+ */
+class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer {
+ public $cols = 18;
+ public $rows = 5;
+ public function render($ns, $directive, $value, $name, $config) {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
+ // this should probably be split up a little
+ $ret = '';
+ $def = $config->def->info["$ns.$directive"];
+ if (is_int($def)) {
+ $type = abs($def);
+ } else {
+ $type = $def->type;
+ }
+ if (is_array($value)) {
+ switch ($type) {
+ case HTMLPurifier_VarParser::LOOKUP:
+ $array = $value;
+ $value = array();
+ foreach ($array as $val => $b) {
+ $value[] = $val;
+ }
+ case HTMLPurifier_VarParser::ALIST:
+ $value = implode(PHP_EOL, $value);
+ break;
+ case HTMLPurifier_VarParser::HASH:
+ $nvalue = '';
+ foreach ($value as $i => $v) {
+ $nvalue .= "$i:$v" . PHP_EOL;
+ }
+ $value = $nvalue;
+ break;
+ default:
+ $value = '';
+ }
+ }
+ if ($type === HTMLPurifier_VarParser::MIXED) {
+ return 'Not supported';
+ $value = serialize($value);
+ }
+ $attr = array(
+ 'name' => "$name"."[$ns.$directive]",
+ 'id' => "$name:$ns.$directive"
+ );
+ if ($value === null) $attr['disabled'] = 'disabled';
+ if (isset($def->allowed)) {
+ $ret .= $this->start('select', $attr);
+ foreach ($def->allowed as $val => $b) {
+ $attr = array();
+ if ($value == $val) $attr['selected'] = 'selected';
+ $ret .= $this->element('option', $val, $attr);
+ }
+ $ret .= $this->end('select');
+ } elseif (
+ $type === HTMLPurifier_VarParser::TEXT ||
+ $type === HTMLPurifier_VarParser::ITEXT ||
+ $type === HTMLPurifier_VarParser::ALIST ||
+ $type === HTMLPurifier_VarParser::HASH ||
+ $type === HTMLPurifier_VarParser::LOOKUP
+ ) {
+ $attr['cols'] = $this->cols;
+ $attr['rows'] = $this->rows;
+ $ret .= $this->start('textarea', $attr);
+ $ret .= $this->text($value);
+ $ret .= $this->end('textarea');
+ } else {
+ $attr['value'] = $value;
+ $attr['type'] = 'text';
+ $ret .= $this->elementEmpty('input', $attr);
+ }
+ return $ret;
+ }
+}
+
+/**
+ * Bool form field printer
+ */
+class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer {
+ public function render($ns, $directive, $value, $name, $config) {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
+ $ret = '';
+ $ret .= $this->start('div', array('id' => "$name:$ns.$directive"));
+
+ $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive"));
+ $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
+ $ret .= $this->text(' Yes');
+ $ret .= $this->end('label');
+
+ $attr = array(
+ 'type' => 'radio',
+ 'name' => "$name"."[$ns.$directive]",
+ 'id' => "$name:Yes_$ns.$directive",
+ 'value' => '1'
+ );
+ if ($value === true) $attr['checked'] = 'checked';
+ if ($value === null) $attr['disabled'] = 'disabled';
+ $ret .= $this->elementEmpty('input', $attr);
+
+ $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive"));
+ $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
+ $ret .= $this->text(' No');
+ $ret .= $this->end('label');
+
+ $attr = array(
+ 'type' => 'radio',
+ 'name' => "$name"."[$ns.$directive]",
+ 'id' => "$name:No_$ns.$directive",
+ 'value' => '0'
+ );
+ if ($value === false) $attr['checked'] = 'checked';
+ if ($value === null) $attr['disabled'] = 'disabled';
+ $ret .= $this->elementEmpty('input', $attr);
+
+ $ret .= $this->end('div');
+
+ return $ret;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Printer/HTMLDefinition.php b/extlib/HTMLPurifier/HTMLPurifier/Printer/HTMLDefinition.php
new file mode 100644
index 000000000..8a8f126b8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Printer/HTMLDefinition.php
@@ -0,0 +1,272 @@
+<?php
+
+class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
+{
+
+ /**
+ * Instance of HTMLPurifier_HTMLDefinition, for easy access
+ */
+ protected $def;
+
+ public function render($config) {
+ $ret = '';
+ $this->config =& $config;
+
+ $this->def = $config->getHTMLDefinition();
+
+ $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer'));
+
+ $ret .= $this->renderDoctype();
+ $ret .= $this->renderEnvironment();
+ $ret .= $this->renderContentSets();
+ $ret .= $this->renderInfo();
+
+ $ret .= $this->end('div');
+
+ return $ret;
+ }
+
+ /**
+ * Renders the Doctype table
+ */
+ protected function renderDoctype() {
+ $doctype = $this->def->doctype;
+ $ret = '';
+ $ret .= $this->start('table');
+ $ret .= $this->element('caption', 'Doctype');
+ $ret .= $this->row('Name', $doctype->name);
+ $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No');
+ $ret .= $this->row('Default Modules', implode($doctype->modules, ', '));
+ $ret .= $this->row('Default Tidy Modules', implode($doctype->tidyModules, ', '));
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+
+ /**
+ * Renders environment table, which is miscellaneous info
+ */
+ protected function renderEnvironment() {
+ $def = $this->def;
+
+ $ret = '';
+
+ $ret .= $this->start('table');
+ $ret .= $this->element('caption', 'Environment');
+
+ $ret .= $this->row('Parent of fragment', $def->info_parent);
+ $ret .= $this->renderChildren($def->info_parent_def->child);
+ $ret .= $this->row('Block wrap name', $def->info_block_wrapper);
+
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Global attributes');
+ $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr),0,0);
+ $ret .= $this->end('tr');
+
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Tag transforms');
+ $list = array();
+ foreach ($def->info_tag_transform as $old => $new) {
+ $new = $this->getClass($new, 'TagTransform_');
+ $list[] = "<$old> with $new";
+ }
+ $ret .= $this->element('td', $this->listify($list));
+ $ret .= $this->end('tr');
+
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Pre-AttrTransform');
+ $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre));
+ $ret .= $this->end('tr');
+
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Post-AttrTransform');
+ $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post));
+ $ret .= $this->end('tr');
+
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+ /**
+ * Renders the Content Sets table
+ */
+ protected function renderContentSets() {
+ $ret = '';
+ $ret .= $this->start('table');
+ $ret .= $this->element('caption', 'Content Sets');
+ foreach ($this->def->info_content_sets as $name => $lookup) {
+ $ret .= $this->heavyHeader($name);
+ $ret .= $this->start('tr');
+ $ret .= $this->element('td', $this->listifyTagLookup($lookup));
+ $ret .= $this->end('tr');
+ }
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+ /**
+ * Renders the Elements ($info) table
+ */
+ protected function renderInfo() {
+ $ret = '';
+ $ret .= $this->start('table');
+ $ret .= $this->element('caption', 'Elements ($info)');
+ ksort($this->def->info);
+ $ret .= $this->heavyHeader('Allowed tags', 2);
+ $ret .= $this->start('tr');
+ $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2));
+ $ret .= $this->end('tr');
+ foreach ($this->def->info as $name => $def) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', "<$name>", array('class'=>'heavy', 'colspan' => 2));
+ $ret .= $this->end('tr');
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Inline content');
+ $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No');
+ $ret .= $this->end('tr');
+ if (!empty($def->excludes)) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Excludes');
+ $ret .= $this->element('td', $this->listifyTagLookup($def->excludes));
+ $ret .= $this->end('tr');
+ }
+ if (!empty($def->attr_transform_pre)) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Pre-AttrTransform');
+ $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre));
+ $ret .= $this->end('tr');
+ }
+ if (!empty($def->attr_transform_post)) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Post-AttrTransform');
+ $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post));
+ $ret .= $this->end('tr');
+ }
+ if (!empty($def->auto_close)) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Auto closed by');
+ $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close));
+ $ret .= $this->end('tr');
+ }
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Allowed attributes');
+ $ret .= $this->element('td',$this->listifyAttr($def->attr), array(), 0);
+ $ret .= $this->end('tr');
+
+ if (!empty($def->required_attr)) {
+ $ret .= $this->row('Required attributes', $this->listify($def->required_attr));
+ }
+
+ $ret .= $this->renderChildren($def->child);
+ }
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+ /**
+ * Renders a row describing the allowed children of an element
+ * @param $def HTMLPurifier_ChildDef of pertinent element
+ */
+ protected function renderChildren($def) {
+ $context = new HTMLPurifier_Context();
+ $ret = '';
+ $ret .= $this->start('tr');
+ $elements = array();
+ $attr = array();
+ if (isset($def->elements)) {
+ if ($def->type == 'strictblockquote') {
+ $def->validateChildren(array(), $this->config, $context);
+ }
+ $elements = $def->elements;
+ }
+ if ($def->type == 'chameleon') {
+ $attr['rowspan'] = 2;
+ } elseif ($def->type == 'empty') {
+ $elements = array();
+ } elseif ($def->type == 'table') {
+ $elements = array_flip(array('col', 'caption', 'colgroup', 'thead',
+ 'tfoot', 'tbody', 'tr'));
+ }
+ $ret .= $this->element('th', 'Allowed children', $attr);
+
+ if ($def->type == 'chameleon') {
+
+ $ret .= $this->element('td',
+ '<em>Block</em>: ' .
+ $this->escape($this->listifyTagLookup($def->block->elements)),0,0);
+ $ret .= $this->end('tr');
+ $ret .= $this->start('tr');
+ $ret .= $this->element('td',
+ '<em>Inline</em>: ' .
+ $this->escape($this->listifyTagLookup($def->inline->elements)),0,0);
+
+ } elseif ($def->type == 'custom') {
+
+ $ret .= $this->element('td', '<em>'.ucfirst($def->type).'</em>: ' .
+ $def->dtd_regex);
+
+ } else {
+ $ret .= $this->element('td',
+ '<em>'.ucfirst($def->type).'</em>: ' .
+ $this->escape($this->listifyTagLookup($elements)),0,0);
+ }
+ $ret .= $this->end('tr');
+ return $ret;
+ }
+
+ /**
+ * Listifies a tag lookup table.
+ * @param $array Tag lookup array in form of array('tagname' => true)
+ */
+ protected function listifyTagLookup($array) {
+ ksort($array);
+ $list = array();
+ foreach ($array as $name => $discard) {
+ if ($name !== '#PCDATA' && !isset($this->def->info[$name])) continue;
+ $list[] = $name;
+ }
+ return $this->listify($list);
+ }
+
+ /**
+ * Listifies a list of objects by retrieving class names and internal state
+ * @param $array List of objects
+ * @todo Also add information about internal state
+ */
+ protected function listifyObjectList($array) {
+ ksort($array);
+ $list = array();
+ foreach ($array as $discard => $obj) {
+ $list[] = $this->getClass($obj, 'AttrTransform_');
+ }
+ return $this->listify($list);
+ }
+
+ /**
+ * Listifies a hash of attributes to AttrDef classes
+ * @param $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef)
+ */
+ protected function listifyAttr($array) {
+ ksort($array);
+ $list = array();
+ foreach ($array as $name => $obj) {
+ if ($obj === false) continue;
+ $list[] = "$name&nbsp;=&nbsp;<i>" . $this->getClass($obj, 'AttrDef_') . '</i>';
+ }
+ return $this->listify($list);
+ }
+
+ /**
+ * Creates a heavy header row
+ */
+ protected function heavyHeader($text, $num = 1) {
+ $ret = '';
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy'));
+ $ret .= $this->end('tr');
+ return $ret;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/PropertyList.php b/extlib/HTMLPurifier/HTMLPurifier/PropertyList.php
new file mode 100644
index 000000000..2b99fb7bc
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/PropertyList.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * Generic property list implementation
+ */
+class HTMLPurifier_PropertyList
+{
+ /**
+ * Internal data-structure for properties
+ */
+ protected $data = array();
+
+ /**
+ * Parent plist
+ */
+ protected $parent;
+
+ protected $cache;
+
+ public function __construct($parent = null) {
+ $this->parent = $parent;
+ }
+
+ /**
+ * Recursively retrieves the value for a key
+ */
+ public function get($name) {
+ if ($this->has($name)) return $this->data[$name];
+ // possible performance bottleneck, convert to iterative if necessary
+ if ($this->parent) return $this->parent->get($name);
+ throw new HTMLPurifier_Exception("Key '$name' not found");
+ }
+
+ /**
+ * Sets the value of a key, for this plist
+ */
+ public function set($name, $value) {
+ $this->data[$name] = $value;
+ }
+
+ /**
+ * Returns true if a given key exists
+ */
+ public function has($name) {
+ return array_key_exists($name, $this->data);
+ }
+
+ /**
+ * Resets a value to the value of it's parent, usually the default. If
+ * no value is specified, the entire plist is reset.
+ */
+ public function reset($name = null) {
+ if ($name == null) $this->data = array();
+ else unset($this->data[$name]);
+ }
+
+ /**
+ * Squashes this property list and all of its property lists into a single
+ * array, and returns the array. This value is cached by default.
+ * @param $force If true, ignores the cache and regenerates the array.
+ */
+ public function squash($force = false) {
+ if ($this->cache !== null && !$force) return $this->cache;
+ if ($this->parent) {
+ return $this->cache = array_merge($this->parent->squash($force), $this->data);
+ } else {
+ return $this->cache = $this->data;
+ }
+ }
+
+ /**
+ * Returns the parent plist.
+ */
+ public function getParent() {
+ return $this->parent;
+ }
+
+ /**
+ * Sets the parent plist.
+ */
+ public function setParent($plist) {
+ $this->parent = $plist;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/PropertyListIterator.php b/extlib/HTMLPurifier/HTMLPurifier/PropertyListIterator.php
new file mode 100644
index 000000000..8f250443e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/PropertyListIterator.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Property list iterator. Do not instantiate this class directly.
+ */
+class HTMLPurifier_PropertyListIterator extends FilterIterator
+{
+
+ protected $l;
+ protected $filter;
+
+ /**
+ * @param $data Array of data to iterate over
+ * @param $filter Optional prefix to only allow values of
+ */
+ public function __construct(Iterator $iterator, $filter = null) {
+ parent::__construct($iterator);
+ $this->l = strlen($filter);
+ $this->filter = $filter;
+ }
+
+ public function accept() {
+ $key = $this->getInnerIterator()->key();
+ if( strncmp($key, $this->filter, $this->l) !== 0 ) {
+ return false;
+ }
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Strategy.php b/extlib/HTMLPurifier/HTMLPurifier/Strategy.php
new file mode 100644
index 000000000..246286521
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Strategy.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * Supertype for classes that define a strategy for modifying/purifying tokens.
+ *
+ * While HTMLPurifier's core purpose is fixing HTML into something proper,
+ * strategies provide plug points for extra configuration or even extra
+ * features, such as custom tags, custom parsing of text, etc.
+ */
+
+
+abstract class HTMLPurifier_Strategy
+{
+
+ /**
+ * Executes the strategy on the tokens.
+ *
+ * @param $tokens Array of HTMLPurifier_Token objects to be operated on.
+ * @param $config Configuration options
+ * @returns Processed array of token objects.
+ */
+ abstract public function execute($tokens, $config, $context);
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Strategy/Composite.php b/extlib/HTMLPurifier/HTMLPurifier/Strategy/Composite.php
new file mode 100644
index 000000000..816490b79
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Strategy/Composite.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * Composite strategy that runs multiple strategies on tokens.
+ */
+abstract class HTMLPurifier_Strategy_Composite extends HTMLPurifier_Strategy
+{
+
+ /**
+ * List of strategies to run tokens through.
+ */
+ protected $strategies = array();
+
+ abstract public function __construct();
+
+ public function execute($tokens, $config, $context) {
+ foreach ($this->strategies as $strategy) {
+ $tokens = $strategy->execute($tokens, $config, $context);
+ }
+ return $tokens;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Strategy/Core.php b/extlib/HTMLPurifier/HTMLPurifier/Strategy/Core.php
new file mode 100644
index 000000000..d90e15860
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Strategy/Core.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Core strategy composed of the big four strategies.
+ */
+class HTMLPurifier_Strategy_Core extends HTMLPurifier_Strategy_Composite
+{
+
+ public function __construct() {
+ $this->strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements();
+ $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed();
+ $this->strategies[] = new HTMLPurifier_Strategy_FixNesting();
+ $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes();
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Strategy/FixNesting.php b/extlib/HTMLPurifier/HTMLPurifier/Strategy/FixNesting.php
new file mode 100644
index 000000000..f81802391
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Strategy/FixNesting.php
@@ -0,0 +1,328 @@
+<?php
+
+/**
+ * Takes a well formed list of tokens and fixes their nesting.
+ *
+ * HTML elements dictate which elements are allowed to be their children,
+ * for example, you can't have a p tag in a span tag. Other elements have
+ * much more rigorous definitions: tables, for instance, require a specific
+ * order for their elements. There are also constraints not expressible by
+ * document type definitions, such as the chameleon nature of ins/del
+ * tags and global child exclusions.
+ *
+ * The first major objective of this strategy is to iterate through all the
+ * nodes (not tokens) of the list of tokens and determine whether or not
+ * their children conform to the element's definition. If they do not, the
+ * child definition may optionally supply an amended list of elements that
+ * is valid or require that the entire node be deleted (and the previous
+ * node rescanned).
+ *
+ * The second objective is to ensure that explicitly excluded elements of
+ * an element do not appear in its children. Code that accomplishes this
+ * task is pervasive through the strategy, though the two are distinct tasks
+ * and could, theoretically, be seperated (although it's not recommended).
+ *
+ * @note Whether or not unrecognized children are silently dropped or
+ * translated into text depends on the child definitions.
+ *
+ * @todo Enable nodes to be bubbled out of the structure.
+ */
+
+class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
+{
+
+ public function execute($tokens, $config, $context) {
+ //####################################################################//
+ // Pre-processing
+
+ // get a copy of the HTML definition
+ $definition = $config->getHTMLDefinition();
+
+ // insert implicit "parent" node, will be removed at end.
+ // DEFINITION CALL
+ $parent_name = $definition->info_parent;
+ array_unshift($tokens, new HTMLPurifier_Token_Start($parent_name));
+ $tokens[] = new HTMLPurifier_Token_End($parent_name);
+
+ // setup the context variable 'IsInline', for chameleon processing
+ // is 'false' when we are not inline, 'true' when it must always
+ // be inline, and an integer when it is inline for a certain
+ // branch of the document tree
+ $is_inline = $definition->info_parent_def->descendants_are_inline;
+ $context->register('IsInline', $is_inline);
+
+ // setup error collector
+ $e =& $context->get('ErrorCollector', true);
+
+ //####################################################################//
+ // Loop initialization
+
+ // stack that contains the indexes of all parents,
+ // $stack[count($stack)-1] being the current parent
+ $stack = array();
+
+ // stack that contains all elements that are excluded
+ // it is organized by parent elements, similar to $stack,
+ // but it is only populated when an element with exclusions is
+ // processed, i.e. there won't be empty exclusions.
+ $exclude_stack = array();
+
+ // variable that contains the start token while we are processing
+ // nodes. This enables error reporting to do its job
+ $start_token = false;
+ $context->register('CurrentToken', $start_token);
+
+ //####################################################################//
+ // Loop
+
+ // iterate through all start nodes. Determining the start node
+ // is complicated so it has been omitted from the loop construct
+ for ($i = 0, $size = count($tokens) ; $i < $size; ) {
+
+ //################################################################//
+ // Gather information on children
+
+ // child token accumulator
+ $child_tokens = array();
+
+ // scroll to the end of this node, report number, and collect
+ // all children
+ for ($j = $i, $depth = 0; ; $j++) {
+ if ($tokens[$j] instanceof HTMLPurifier_Token_Start) {
+ $depth++;
+ // skip token assignment on first iteration, this is the
+ // token we currently are on
+ if ($depth == 1) continue;
+ } elseif ($tokens[$j] instanceof HTMLPurifier_Token_End) {
+ $depth--;
+ // skip token assignment on last iteration, this is the
+ // end token of the token we're currently on
+ if ($depth == 0) break;
+ }
+ $child_tokens[] = $tokens[$j];
+ }
+
+ // $i is index of start token
+ // $j is index of end token
+
+ $start_token = $tokens[$i]; // to make token available via CurrentToken
+
+ //################################################################//
+ // Gather information on parent
+
+ // calculate parent information
+ if ($count = count($stack)) {
+ $parent_index = $stack[$count-1];
+ $parent_name = $tokens[$parent_index]->name;
+ if ($parent_index == 0) {
+ $parent_def = $definition->info_parent_def;
+ } else {
+ $parent_def = $definition->info[$parent_name];
+ }
+ } else {
+ // processing as if the parent were the "root" node
+ // unknown info, it won't be used anyway, in the future,
+ // we may want to enforce one element only (this is
+ // necessary for HTML Purifier to clean entire documents
+ $parent_index = $parent_name = $parent_def = null;
+ }
+
+ // calculate context
+ if ($is_inline === false) {
+ // check if conditions make it inline
+ if (!empty($parent_def) && $parent_def->descendants_are_inline) {
+ $is_inline = $count - 1;
+ }
+ } else {
+ // check if we're out of inline
+ if ($count === $is_inline) {
+ $is_inline = false;
+ }
+ }
+
+ //################################################################//
+ // Determine whether element is explicitly excluded SGML-style
+
+ // determine whether or not element is excluded by checking all
+ // parent exclusions. The array should not be very large, two
+ // elements at most.
+ $excluded = false;
+ if (!empty($exclude_stack)) {
+ foreach ($exclude_stack as $lookup) {
+ if (isset($lookup[$tokens[$i]->name])) {
+ $excluded = true;
+ // no need to continue processing
+ break;
+ }
+ }
+ }
+
+ //################################################################//
+ // Perform child validation
+
+ if ($excluded) {
+ // there is an exclusion, remove the entire node
+ $result = false;
+ $excludes = array(); // not used, but good to initialize anyway
+ } else {
+ // DEFINITION CALL
+ if ($i === 0) {
+ // special processing for the first node
+ $def = $definition->info_parent_def;
+ } else {
+ $def = $definition->info[$tokens[$i]->name];
+
+ }
+
+ if (!empty($def->child)) {
+ // have DTD child def validate children
+ $result = $def->child->validateChildren(
+ $child_tokens, $config, $context);
+ } else {
+ // weird, no child definition, get rid of everything
+ $result = false;
+ }
+
+ // determine whether or not this element has any exclusions
+ $excludes = $def->excludes;
+ }
+
+ // $result is now a bool or array
+
+ //################################################################//
+ // Process result by interpreting $result
+
+ if ($result === true || $child_tokens === $result) {
+ // leave the node as is
+
+ // register start token as a parental node start
+ $stack[] = $i;
+
+ // register exclusions if there are any
+ if (!empty($excludes)) $exclude_stack[] = $excludes;
+
+ // move cursor to next possible start node
+ $i++;
+
+ } elseif($result === false) {
+ // remove entire node
+
+ if ($e) {
+ if ($excluded) {
+ $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded');
+ } else {
+ $e->send(E_ERROR, 'Strategy_FixNesting: Node removed');
+ }
+ }
+
+ // calculate length of inner tokens and current tokens
+ $length = $j - $i + 1;
+
+ // perform removal
+ array_splice($tokens, $i, $length);
+
+ // update size
+ $size -= $length;
+
+ // there is no start token to register,
+ // current node is now the next possible start node
+ // unless it turns out that we need to do a double-check
+
+ // this is a rought heuristic that covers 100% of HTML's
+ // cases and 99% of all other cases. A child definition
+ // that would be tricked by this would be something like:
+ // ( | a b c) where it's all or nothing. Fortunately,
+ // our current implementation claims that that case would
+ // not allow empty, even if it did
+ if (!$parent_def->child->allow_empty) {
+ // we need to do a double-check
+ $i = $parent_index;
+ array_pop($stack);
+ }
+
+ // PROJECTED OPTIMIZATION: Process all children elements before
+ // reprocessing parent node.
+
+ } else {
+ // replace node with $result
+
+ // calculate length of inner tokens
+ $length = $j - $i - 1;
+
+ if ($e) {
+ if (empty($result) && $length) {
+ $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed');
+ } else {
+ $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized');
+ }
+ }
+
+ // perform replacement
+ array_splice($tokens, $i + 1, $length, $result);
+
+ // update size
+ $size -= $length;
+ $size += count($result);
+
+ // register start token as a parental node start
+ $stack[] = $i;
+
+ // register exclusions if there are any
+ if (!empty($excludes)) $exclude_stack[] = $excludes;
+
+ // move cursor to next possible start node
+ $i++;
+
+ }
+
+ //################################################################//
+ // Scroll to next start node
+
+ // We assume, at this point, that $i is the index of the token
+ // that is the first possible new start point for a node.
+
+ // Test if the token indeed is a start tag, if not, move forward
+ // and test again.
+ $size = count($tokens);
+ while ($i < $size and !$tokens[$i] instanceof HTMLPurifier_Token_Start) {
+ if ($tokens[$i] instanceof HTMLPurifier_Token_End) {
+ // pop a token index off the stack if we ended a node
+ array_pop($stack);
+ // pop an exclusion lookup off exclusion stack if
+ // we ended node and that node had exclusions
+ if ($i == 0 || $i == $size - 1) {
+ // use specialized var if it's the super-parent
+ $s_excludes = $definition->info_parent_def->excludes;
+ } else {
+ $s_excludes = $definition->info[$tokens[$i]->name]->excludes;
+ }
+ if ($s_excludes) {
+ array_pop($exclude_stack);
+ }
+ }
+ $i++;
+ }
+
+ }
+
+ //####################################################################//
+ // Post-processing
+
+ // remove implicit parent tokens at the beginning and end
+ array_shift($tokens);
+ array_pop($tokens);
+
+ // remove context variables
+ $context->destroy('IsInline');
+ $context->destroy('CurrentToken');
+
+ //####################################################################//
+ // Return
+
+ return $tokens;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Strategy/MakeWellFormed.php b/extlib/HTMLPurifier/HTMLPurifier/Strategy/MakeWellFormed.php
new file mode 100644
index 000000000..feb0c32b4
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Strategy/MakeWellFormed.php
@@ -0,0 +1,457 @@
+<?php
+
+/**
+ * Takes tokens makes them well-formed (balance end tags, etc.)
+ */
+class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
+{
+
+ /**
+ * Array stream of tokens being processed.
+ */
+ protected $tokens;
+
+ /**
+ * Current index in $tokens.
+ */
+ protected $t;
+
+ /**
+ * Current nesting of elements.
+ */
+ protected $stack;
+
+ /**
+ * Injectors active in this stream processing.
+ */
+ protected $injectors;
+
+ /**
+ * Current instance of HTMLPurifier_Config.
+ */
+ protected $config;
+
+ /**
+ * Current instance of HTMLPurifier_Context.
+ */
+ protected $context;
+
+ public function execute($tokens, $config, $context) {
+
+ $definition = $config->getHTMLDefinition();
+
+ // local variables
+ $generator = new HTMLPurifier_Generator($config, $context);
+ $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
+ $e = $context->get('ErrorCollector', true);
+ $t = false; // token index
+ $i = false; // injector index
+ $token = false; // the current token
+ $reprocess = false; // whether or not to reprocess the same token
+ $stack = array();
+
+ // member variables
+ $this->stack =& $stack;
+ $this->t =& $t;
+ $this->tokens =& $tokens;
+ $this->config = $config;
+ $this->context = $context;
+
+ // context variables
+ $context->register('CurrentNesting', $stack);
+ $context->register('InputIndex', $t);
+ $context->register('InputTokens', $tokens);
+ $context->register('CurrentToken', $token);
+
+ // -- begin INJECTOR --
+
+ $this->injectors = array();
+
+ $injectors = $config->getBatch('AutoFormat');
+ $def_injectors = $definition->info_injector;
+ $custom_injectors = $injectors['Custom'];
+ unset($injectors['Custom']); // special case
+ foreach ($injectors as $injector => $b) {
+ // XXX: Fix with a legitimate lookup table of enabled filters
+ if (strpos($injector, '.') !== false) continue;
+ $injector = "HTMLPurifier_Injector_$injector";
+ if (!$b) continue;
+ $this->injectors[] = new $injector;
+ }
+ foreach ($def_injectors as $injector) {
+ // assumed to be objects
+ $this->injectors[] = $injector;
+ }
+ foreach ($custom_injectors as $injector) {
+ if (is_string($injector)) {
+ $injector = "HTMLPurifier_Injector_$injector";
+ $injector = new $injector;
+ }
+ $this->injectors[] = $injector;
+ }
+
+ // give the injectors references to the definition and context
+ // variables for performance reasons
+ foreach ($this->injectors as $ix => $injector) {
+ $error = $injector->prepare($config, $context);
+ if (!$error) continue;
+ array_splice($this->injectors, $ix, 1); // rm the injector
+ trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
+ }
+
+ // -- end INJECTOR --
+
+ // a note on punting:
+ // In order to reduce code duplication, whenever some code needs
+ // to make HTML changes in order to make things "correct", the
+ // new HTML gets sent through the purifier, regardless of its
+ // status. This means that if we add a start token, because it
+ // was totally necessary, we don't have to update nesting; we just
+ // punt ($reprocess = true; continue;) and it does that for us.
+
+ // isset is in loop because $tokens size changes during loop exec
+ for (
+ $t = 0;
+ $t == 0 || isset($tokens[$t - 1]);
+ // only increment if we don't need to reprocess
+ $reprocess ? $reprocess = false : $t++
+ ) {
+
+ // check for a rewind
+ if (is_int($i) && $i >= 0) {
+ // possibility: disable rewinding if the current token has a
+ // rewind set on it already. This would offer protection from
+ // infinite loop, but might hinder some advanced rewinding.
+ $rewind_to = $this->injectors[$i]->getRewind();
+ if (is_int($rewind_to) && $rewind_to < $t) {
+ if ($rewind_to < 0) $rewind_to = 0;
+ while ($t > $rewind_to) {
+ $t--;
+ $prev = $tokens[$t];
+ // indicate that other injectors should not process this token,
+ // but we need to reprocess it
+ unset($prev->skip[$i]);
+ $prev->rewind = $i;
+ if ($prev instanceof HTMLPurifier_Token_Start) array_pop($this->stack);
+ elseif ($prev instanceof HTMLPurifier_Token_End) $this->stack[] = $prev->start;
+ }
+ }
+ $i = false;
+ }
+
+ // handle case of document end
+ if (!isset($tokens[$t])) {
+ // kill processing if stack is empty
+ if (empty($this->stack)) break;
+
+ // peek
+ $top_nesting = array_pop($this->stack);
+ $this->stack[] = $top_nesting;
+
+ // send error
+ if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
+ }
+
+ // append, don't splice, since this is the end
+ $tokens[] = new HTMLPurifier_Token_End($top_nesting->name);
+
+ // punt!
+ $reprocess = true;
+ continue;
+ }
+
+ $token = $tokens[$t];
+
+ //echo '<br>'; printTokens($tokens, $t); printTokens($this->stack);
+
+ // quick-check: if it's not a tag, no need to process
+ if (empty($token->is_tag)) {
+ if ($token instanceof HTMLPurifier_Token_Text) {
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) continue;
+ if ($token->rewind !== null && $token->rewind !== $i) continue;
+ $injector->handleText($token);
+ $this->processToken($token, $i);
+ $reprocess = true;
+ break;
+ }
+ }
+ // another possibility is a comment
+ continue;
+ }
+
+ if (isset($definition->info[$token->name])) {
+ $type = $definition->info[$token->name]->child->type;
+ } else {
+ $type = false; // Type is unknown, treat accordingly
+ }
+
+ // quick tag checks: anything that's *not* an end tag
+ $ok = false;
+ if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
+ // claims to be a start tag but is empty
+ $token = new HTMLPurifier_Token_Empty($token->name, $token->attr);
+ $ok = true;
+ } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
+ // claims to be empty but really is a start tag
+ $this->swap(new HTMLPurifier_Token_End($token->name));
+ $this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr));
+ // punt (since we had to modify the input stream in a non-trivial way)
+ $reprocess = true;
+ continue;
+ } elseif ($token instanceof HTMLPurifier_Token_Empty) {
+ // real empty token
+ $ok = true;
+ } elseif ($token instanceof HTMLPurifier_Token_Start) {
+ // start tag
+
+ // ...unless they also have to close their parent
+ if (!empty($this->stack)) {
+
+ $parent = array_pop($this->stack);
+ $this->stack[] = $parent;
+
+ if (isset($definition->info[$parent->name])) {
+ $elements = $definition->info[$parent->name]->child->getAllowedElements($config);
+ $autoclose = !isset($elements[$token->name]);
+ } else {
+ $autoclose = false;
+ }
+
+ $carryover = false;
+ if ($autoclose && $definition->info[$parent->name]->formatting) {
+ $carryover = true;
+ }
+
+ if ($autoclose) {
+ // errors need to be updated
+ $new_token = new HTMLPurifier_Token_End($parent->name);
+ $new_token->start = $parent;
+ if ($carryover) {
+ $element = clone $parent;
+ $element->armor['MakeWellFormed_TagClosedError'] = true;
+ $element->carryover = true;
+ $this->processToken(array($new_token, $token, $element));
+ } else {
+ $this->insertBefore($new_token);
+ }
+ if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
+ if (!$carryover) {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
+ } else {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
+ }
+ }
+ $reprocess = true;
+ continue;
+ }
+
+ }
+ $ok = true;
+ }
+
+ if ($ok) {
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) continue;
+ if ($token->rewind !== null && $token->rewind !== $i) continue;
+ $injector->handleElement($token);
+ $this->processToken($token, $i);
+ $reprocess = true;
+ break;
+ }
+ if (!$reprocess) {
+ // ah, nothing interesting happened; do normal processing
+ $this->swap($token);
+ if ($token instanceof HTMLPurifier_Token_Start) {
+ $this->stack[] = $token;
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ throw new HTMLPurifier_Exception('Improper handling of end tag in start code; possible error in MakeWellFormed');
+ }
+ }
+ continue;
+ }
+
+ // sanity check: we should be dealing with a closing tag
+ if (!$token instanceof HTMLPurifier_Token_End) {
+ throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
+ }
+
+ // make sure that we have something open
+ if (empty($this->stack)) {
+ if ($escape_invalid_tags) {
+ if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
+ $this->swap(new HTMLPurifier_Token_Text(
+ $generator->generateFromToken($token)
+ ));
+ } else {
+ $this->remove();
+ if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
+ }
+ $reprocess = true;
+ continue;
+ }
+
+ // first, check for the simplest case: everything closes neatly.
+ // Eventually, everything passes through here; if there are problems
+ // we modify the input stream accordingly and then punt, so that
+ // the tokens get processed again.
+ $current_parent = array_pop($this->stack);
+ if ($current_parent->name == $token->name) {
+ $token->start = $current_parent;
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) continue;
+ if ($token->rewind !== null && $token->rewind !== $i) continue;
+ $injector->handleEnd($token);
+ $this->processToken($token, $i);
+ $this->stack[] = $current_parent;
+ $reprocess = true;
+ break;
+ }
+ continue;
+ }
+
+ // okay, so we're trying to close the wrong tag
+
+ // undo the pop previous pop
+ $this->stack[] = $current_parent;
+
+ // scroll back the entire nest, trying to find our tag.
+ // (feature could be to specify how far you'd like to go)
+ $size = count($this->stack);
+ // -2 because -1 is the last element, but we already checked that
+ $skipped_tags = false;
+ for ($j = $size - 2; $j >= 0; $j--) {
+ if ($this->stack[$j]->name == $token->name) {
+ $skipped_tags = array_slice($this->stack, $j);
+ break;
+ }
+ }
+
+ // we didn't find the tag, so remove
+ if ($skipped_tags === false) {
+ if ($escape_invalid_tags) {
+ $this->swap(new HTMLPurifier_Token_Text(
+ $generator->generateFromToken($token)
+ ));
+ if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
+ } else {
+ $this->remove();
+ if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
+ }
+ $reprocess = true;
+ continue;
+ }
+
+ // do errors, in REVERSE $j order: a,b,c with </a></b></c>
+ $c = count($skipped_tags);
+ if ($e) {
+ for ($j = $c - 1; $j > 0; $j--) {
+ // notice we exclude $j == 0, i.e. the current ending tag, from
+ // the errors...
+ if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
+ }
+ }
+ }
+
+ // insert tags, in FORWARD $j order: c,b,a with </a></b></c>
+ $replace = array($token);
+ for ($j = 1; $j < $c; $j++) {
+ // ...as well as from the insertions
+ $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name);
+ $new_token->start = $skipped_tags[$j];
+ array_unshift($replace, $new_token);
+ if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) {
+ $element = clone $skipped_tags[$j];
+ $element->carryover = true;
+ $element->armor['MakeWellFormed_TagClosedError'] = true;
+ $replace[] = $element;
+ }
+ }
+ $this->processToken($replace);
+ $reprocess = true;
+ continue;
+ }
+
+ $context->destroy('CurrentNesting');
+ $context->destroy('InputTokens');
+ $context->destroy('InputIndex');
+ $context->destroy('CurrentToken');
+
+ unset($this->injectors, $this->stack, $this->tokens, $this->t);
+ return $tokens;
+ }
+
+ /**
+ * Processes arbitrary token values for complicated substitution patterns.
+ * In general:
+ *
+ * If $token is an array, it is a list of tokens to substitute for the
+ * current token. These tokens then get individually processed. If there
+ * is a leading integer in the list, that integer determines how many
+ * tokens from the stream should be removed.
+ *
+ * If $token is a regular token, it is swapped with the current token.
+ *
+ * If $token is false, the current token is deleted.
+ *
+ * If $token is an integer, that number of tokens (with the first token
+ * being the current one) will be deleted.
+ *
+ * @param $token Token substitution value
+ * @param $injector Injector that performed the substitution; default is if
+ * this is not an injector related operation.
+ */
+ protected function processToken($token, $injector = -1) {
+
+ // normalize forms of token
+ if (is_object($token)) $token = array(1, $token);
+ if (is_int($token)) $token = array($token);
+ if ($token === false) $token = array(1);
+ if (!is_array($token)) throw new HTMLPurifier_Exception('Invalid token type from injector');
+ if (!is_int($token[0])) array_unshift($token, 1);
+ if ($token[0] === 0) throw new HTMLPurifier_Exception('Deleting zero tokens is not valid');
+
+ // $token is now an array with the following form:
+ // array(number nodes to delete, new node 1, new node 2, ...)
+
+ $delete = array_shift($token);
+ $old = array_splice($this->tokens, $this->t, $delete, $token);
+
+ if ($injector > -1) {
+ // determine appropriate skips
+ $oldskip = isset($old[0]) ? $old[0]->skip : array();
+ foreach ($token as $object) {
+ $object->skip = $oldskip;
+ $object->skip[$injector] = true;
+ }
+ }
+
+ }
+
+ /**
+ * Inserts a token before the current token. Cursor now points to this token
+ */
+ private function insertBefore($token) {
+ array_splice($this->tokens, $this->t, 0, array($token));
+ }
+
+ /**
+ * Removes current token. Cursor now points to new token occupying previously
+ * occupied space.
+ */
+ private function remove() {
+ array_splice($this->tokens, $this->t, 1);
+ }
+
+ /**
+ * Swap current token with new token. Cursor points to new token (no change).
+ */
+ private function swap($token) {
+ $this->tokens[$this->t] = $token;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Strategy/RemoveForeignElements.php b/extlib/HTMLPurifier/HTMLPurifier/Strategy/RemoveForeignElements.php
new file mode 100644
index 000000000..cf3a33e40
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Strategy/RemoveForeignElements.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * Removes all unrecognized tags from the list of tokens.
+ *
+ * This strategy iterates through all the tokens and removes unrecognized
+ * tokens. If a token is not recognized but a TagTransform is defined for
+ * that element, the element will be transformed accordingly.
+ */
+
+class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
+{
+
+ public function execute($tokens, $config, $context) {
+ $definition = $config->getHTMLDefinition();
+ $generator = new HTMLPurifier_Generator($config, $context);
+ $result = array();
+
+ $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
+ $remove_invalid_img = $config->get('Core.RemoveInvalidImg');
+
+ // currently only used to determine if comments should be kept
+ $trusted = $config->get('HTML.Trusted');
+
+ $remove_script_contents = $config->get('Core.RemoveScriptContents');
+ $hidden_elements = $config->get('Core.HiddenElements');
+
+ // remove script contents compatibility
+ if ($remove_script_contents === true) {
+ $hidden_elements['script'] = true;
+ } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) {
+ unset($hidden_elements['script']);
+ }
+
+ $attr_validator = new HTMLPurifier_AttrValidator();
+
+ // removes tokens until it reaches a closing tag with its value
+ $remove_until = false;
+
+ // converts comments into text tokens when this is equal to a tag name
+ $textify_comments = false;
+
+ $token = false;
+ $context->register('CurrentToken', $token);
+
+ $e = false;
+ if ($config->get('Core.CollectErrors')) {
+ $e =& $context->get('ErrorCollector');
+ }
+
+ foreach($tokens as $token) {
+ if ($remove_until) {
+ if (empty($token->is_tag) || $token->name !== $remove_until) {
+ continue;
+ }
+ }
+ if (!empty( $token->is_tag )) {
+ // DEFINITION CALL
+
+ // before any processing, try to transform the element
+ if (
+ isset($definition->info_tag_transform[$token->name])
+ ) {
+ $original_name = $token->name;
+ // there is a transformation for this tag
+ // DEFINITION CALL
+ $token = $definition->
+ info_tag_transform[$token->name]->
+ transform($token, $config, $context);
+ if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name);
+ }
+
+ if (isset($definition->info[$token->name])) {
+
+ // mostly everything's good, but
+ // we need to make sure required attributes are in order
+ if (
+ ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) &&
+ $definition->info[$token->name]->required_attr &&
+ ($token->name != 'img' || $remove_invalid_img) // ensure config option still works
+ ) {
+ $attr_validator->validateToken($token, $config, $context);
+ $ok = true;
+ foreach ($definition->info[$token->name]->required_attr as $name) {
+ if (!isset($token->attr[$name])) {
+ $ok = false;
+ break;
+ }
+ }
+ if (!$ok) {
+ if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Missing required attribute', $name);
+ continue;
+ }
+ $token->armor['ValidateAttributes'] = true;
+ }
+
+ if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) {
+ $textify_comments = $token->name;
+ } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) {
+ $textify_comments = false;
+ }
+
+ } elseif ($escape_invalid_tags) {
+ // invalid tag, generate HTML representation and insert in
+ if ($e) $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text');
+ $token = new HTMLPurifier_Token_Text(
+ $generator->generateFromToken($token)
+ );
+ } else {
+ // check if we need to destroy all of the tag's children
+ // CAN BE GENERICIZED
+ if (isset($hidden_elements[$token->name])) {
+ if ($token instanceof HTMLPurifier_Token_Start) {
+ $remove_until = $token->name;
+ } elseif ($token instanceof HTMLPurifier_Token_Empty) {
+ // do nothing: we're still looking
+ } else {
+ $remove_until = false;
+ }
+ if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed');
+ } else {
+ if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed');
+ }
+ continue;
+ }
+ } elseif ($token instanceof HTMLPurifier_Token_Comment) {
+ // textify comments in script tags when they are allowed
+ if ($textify_comments !== false) {
+ $data = $token->data;
+ $token = new HTMLPurifier_Token_Text($data);
+ } elseif ($trusted) {
+ // keep, but perform comment cleaning
+ if ($e) {
+ // perform check whether or not there's a trailing hyphen
+ if (substr($token->data, -1) == '-') {
+ $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed');
+ }
+ }
+ $token->data = rtrim($token->data, '-');
+ $found_double_hyphen = false;
+ while (strpos($token->data, '--') !== false) {
+ if ($e && !$found_double_hyphen) {
+ $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed');
+ }
+ $found_double_hyphen = true; // prevent double-erroring
+ $token->data = str_replace('--', '-', $token->data);
+ }
+ } else {
+ // strip comments
+ if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed');
+ continue;
+ }
+ } elseif ($token instanceof HTMLPurifier_Token_Text) {
+ } else {
+ continue;
+ }
+ $result[] = $token;
+ }
+ if ($remove_until && $e) {
+ // we removed tokens until the end, throw error
+ $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until);
+ }
+
+ $context->destroy('CurrentToken');
+
+ return $result;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Strategy/ValidateAttributes.php b/extlib/HTMLPurifier/HTMLPurifier/Strategy/ValidateAttributes.php
new file mode 100644
index 000000000..c3328a9d4
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Strategy/ValidateAttributes.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * Validate all attributes in the tokens.
+ */
+
+class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
+{
+
+ public function execute($tokens, $config, $context) {
+
+ // setup validator
+ $validator = new HTMLPurifier_AttrValidator();
+
+ $token = false;
+ $context->register('CurrentToken', $token);
+
+ foreach ($tokens as $key => $token) {
+
+ // only process tokens that have attributes,
+ // namely start and empty tags
+ if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) continue;
+
+ // skip tokens that are armored
+ if (!empty($token->armor['ValidateAttributes'])) continue;
+
+ // note that we have no facilities here for removing tokens
+ $validator->validateToken($token, $config, $context);
+
+ $tokens[$key] = $token; // for PHP 4
+ }
+ $context->destroy('CurrentToken');
+
+ return $tokens;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/StringHash.php b/extlib/HTMLPurifier/HTMLPurifier/StringHash.php
new file mode 100644
index 000000000..62085c5c2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/StringHash.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * This is in almost every respect equivalent to an array except
+ * that it keeps track of which keys were accessed.
+ *
+ * @warning For the sake of backwards compatibility with early versions
+ * of PHP 5, you must not use the $hash[$key] syntax; if you do
+ * our version of offsetGet is never called.
+ */
+class HTMLPurifier_StringHash extends ArrayObject
+{
+ protected $accessed = array();
+
+ /**
+ * Retrieves a value, and logs the access.
+ */
+ public function offsetGet($index) {
+ $this->accessed[$index] = true;
+ return parent::offsetGet($index);
+ }
+
+ /**
+ * Returns a lookup array of all array indexes that have been accessed.
+ * @return Array in form array($index => true).
+ */
+ public function getAccessed() {
+ return $this->accessed;
+ }
+
+ /**
+ * Resets the access array.
+ */
+ public function resetAccessed() {
+ $this->accessed = array();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/StringHashParser.php b/extlib/HTMLPurifier/HTMLPurifier/StringHashParser.php
new file mode 100644
index 000000000..f3e70c712
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/StringHashParser.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * Parses string hash files. File format is as such:
+ *
+ * DefaultKeyValue
+ * KEY: Value
+ * KEY2: Value2
+ * --MULTILINE-KEY--
+ * Multiline
+ * value.
+ *
+ * Which would output something similar to:
+ *
+ * array(
+ * 'ID' => 'DefaultKeyValue',
+ * 'KEY' => 'Value',
+ * 'KEY2' => 'Value2',
+ * 'MULTILINE-KEY' => "Multiline\nvalue.\n",
+ * )
+ *
+ * We use this as an easy to use file-format for configuration schema
+ * files, but the class itself is usage agnostic.
+ *
+ * You can use ---- to forcibly terminate parsing of a single string-hash;
+ * this marker is used in multi string-hashes to delimit boundaries.
+ */
+class HTMLPurifier_StringHashParser
+{
+
+ public $default = 'ID';
+
+ /**
+ * Parses a file that contains a single string-hash.
+ */
+ public function parseFile($file) {
+ if (!file_exists($file)) return false;
+ $fh = fopen($file, 'r');
+ if (!$fh) return false;
+ $ret = $this->parseHandle($fh);
+ fclose($fh);
+ return $ret;
+ }
+
+ /**
+ * Parses a file that contains multiple string-hashes delimited by '----'
+ */
+ public function parseMultiFile($file) {
+ if (!file_exists($file)) return false;
+ $ret = array();
+ $fh = fopen($file, 'r');
+ if (!$fh) return false;
+ while (!feof($fh)) {
+ $ret[] = $this->parseHandle($fh);
+ }
+ fclose($fh);
+ return $ret;
+ }
+
+ /**
+ * Internal parser that acepts a file handle.
+ * @note While it's possible to simulate in-memory parsing by using
+ * custom stream wrappers, if such a use-case arises we should
+ * factor out the file handle into its own class.
+ * @param $fh File handle with pointer at start of valid string-hash
+ * block.
+ */
+ protected function parseHandle($fh) {
+ $state = false;
+ $single = false;
+ $ret = array();
+ do {
+ $line = fgets($fh);
+ if ($line === false) break;
+ $line = rtrim($line, "\n\r");
+ if (!$state && $line === '') continue;
+ if ($line === '----') break;
+ if (strncmp('--#', $line, 3) === 0) {
+ // Comment
+ continue;
+ } elseif (strncmp('--', $line, 2) === 0) {
+ // Multiline declaration
+ $state = trim($line, '- ');
+ if (!isset($ret[$state])) $ret[$state] = '';
+ continue;
+ } elseif (!$state) {
+ $single = true;
+ if (strpos($line, ':') !== false) {
+ // Single-line declaration
+ list($state, $line) = explode(':', $line, 2);
+ $line = trim($line);
+ } else {
+ // Use default declaration
+ $state = $this->default;
+ }
+ }
+ if ($single) {
+ $ret[$state] = $line;
+ $single = false;
+ $state = false;
+ } else {
+ $ret[$state] .= "$line\n";
+ }
+ } while (!feof($fh));
+ return $ret;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/TagTransform.php b/extlib/HTMLPurifier/HTMLPurifier/TagTransform.php
new file mode 100644
index 000000000..210a44721
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/TagTransform.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * Defines a mutation of an obsolete tag into a valid tag.
+ */
+abstract class HTMLPurifier_TagTransform
+{
+
+ /**
+ * Tag name to transform the tag to.
+ */
+ public $transform_to;
+
+ /**
+ * Transforms the obsolete tag into the valid tag.
+ * @param $tag Tag to be transformed.
+ * @param $config Mandatory HTMLPurifier_Config object
+ * @param $context Mandatory HTMLPurifier_Context object
+ */
+ abstract public function transform($tag, $config, $context);
+
+ /**
+ * Prepends CSS properties to the style attribute, creating the
+ * attribute if it doesn't exist.
+ * @warning Copied over from AttrTransform, be sure to keep in sync
+ * @param $attr Attribute array to process (passed by reference)
+ * @param $css CSS to prepend
+ */
+ protected function prependCSS(&$attr, $css) {
+ $attr['style'] = isset($attr['style']) ? $attr['style'] : '';
+ $attr['style'] = $css . $attr['style'];
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/TagTransform/Font.php b/extlib/HTMLPurifier/HTMLPurifier/TagTransform/Font.php
new file mode 100644
index 000000000..ed2463786
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/TagTransform/Font.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * Transforms FONT tags to the proper form (SPAN with CSS styling)
+ *
+ * This transformation takes the three proprietary attributes of FONT and
+ * transforms them into their corresponding CSS attributes. These are color,
+ * face, and size.
+ *
+ * @note Size is an interesting case because it doesn't map cleanly to CSS.
+ * Thanks to
+ * http://style.cleverchimp.com/font_size_intervals/altintervals.html
+ * for reasonable mappings.
+ * @warning This doesn't work completely correctly; specifically, this
+ * TagTransform operates before well-formedness is enforced, so
+ * the "active formatting elements" algorithm doesn't get applied.
+ */
+class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform
+{
+
+ public $transform_to = 'span';
+
+ protected $_size_lookup = array(
+ '0' => 'xx-small',
+ '1' => 'xx-small',
+ '2' => 'small',
+ '3' => 'medium',
+ '4' => 'large',
+ '5' => 'x-large',
+ '6' => 'xx-large',
+ '7' => '300%',
+ '-1' => 'smaller',
+ '-2' => '60%',
+ '+1' => 'larger',
+ '+2' => '150%',
+ '+3' => '200%',
+ '+4' => '300%'
+ );
+
+ public function transform($tag, $config, $context) {
+
+ if ($tag instanceof HTMLPurifier_Token_End) {
+ $new_tag = clone $tag;
+ $new_tag->name = $this->transform_to;
+ return $new_tag;
+ }
+
+ $attr = $tag->attr;
+ $prepend_style = '';
+
+ // handle color transform
+ if (isset($attr['color'])) {
+ $prepend_style .= 'color:' . $attr['color'] . ';';
+ unset($attr['color']);
+ }
+
+ // handle face transform
+ if (isset($attr['face'])) {
+ $prepend_style .= 'font-family:' . $attr['face'] . ';';
+ unset($attr['face']);
+ }
+
+ // handle size transform
+ if (isset($attr['size'])) {
+ // normalize large numbers
+ if ($attr['size']{0} == '+' || $attr['size']{0} == '-') {
+ $size = (int) $attr['size'];
+ if ($size < -2) $attr['size'] = '-2';
+ if ($size > 4) $attr['size'] = '+4';
+ } else {
+ $size = (int) $attr['size'];
+ if ($size > 7) $attr['size'] = '7';
+ }
+ if (isset($this->_size_lookup[$attr['size']])) {
+ $prepend_style .= 'font-size:' .
+ $this->_size_lookup[$attr['size']] . ';';
+ }
+ unset($attr['size']);
+ }
+
+ if ($prepend_style) {
+ $attr['style'] = isset($attr['style']) ?
+ $prepend_style . $attr['style'] :
+ $prepend_style;
+ }
+
+ $new_tag = clone $tag;
+ $new_tag->name = $this->transform_to;
+ $new_tag->attr = $attr;
+
+ return $new_tag;
+
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/TagTransform/Simple.php b/extlib/HTMLPurifier/HTMLPurifier/TagTransform/Simple.php
new file mode 100644
index 000000000..0e36130f2
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/TagTransform/Simple.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Simple transformation, just change tag name to something else,
+ * and possibly add some styling. This will cover most of the deprecated
+ * tag cases.
+ */
+class HTMLPurifier_TagTransform_Simple extends HTMLPurifier_TagTransform
+{
+
+ protected $style;
+
+ /**
+ * @param $transform_to Tag name to transform to.
+ * @param $style CSS style to add to the tag
+ */
+ public function __construct($transform_to, $style = null) {
+ $this->transform_to = $transform_to;
+ $this->style = $style;
+ }
+
+ public function transform($tag, $config, $context) {
+ $new_tag = clone $tag;
+ $new_tag->name = $this->transform_to;
+ if (!is_null($this->style) &&
+ ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty)
+ ) {
+ $this->prependCSS($new_tag->attr, $this->style);
+ }
+ return $new_tag;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Token.php b/extlib/HTMLPurifier/HTMLPurifier/Token.php
new file mode 100644
index 000000000..7900e6cb1
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Token.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * Abstract base token class that all others inherit from.
+ */
+class HTMLPurifier_Token {
+ public $line; /**< Line number node was on in source document. Null if unknown. */
+ public $col; /**< Column of line node was on in source document. Null if unknown. */
+
+ /**
+ * Lookup array of processing that this token is exempt from.
+ * Currently, valid values are "ValidateAttributes" and
+ * "MakeWellFormed_TagClosedError"
+ */
+ public $armor = array();
+
+ /**
+ * Used during MakeWellFormed.
+ */
+ public $skip;
+ public $rewind;
+ public $carryover;
+
+ public function __get($n) {
+ if ($n === 'type') {
+ trigger_error('Deprecated type property called; use instanceof', E_USER_NOTICE);
+ switch (get_class($this)) {
+ case 'HTMLPurifier_Token_Start': return 'start';
+ case 'HTMLPurifier_Token_Empty': return 'empty';
+ case 'HTMLPurifier_Token_End': return 'end';
+ case 'HTMLPurifier_Token_Text': return 'text';
+ case 'HTMLPurifier_Token_Comment': return 'comment';
+ default: return null;
+ }
+ }
+ }
+
+ /**
+ * Sets the position of the token in the source document.
+ */
+ public function position($l = null, $c = null) {
+ $this->line = $l;
+ $this->col = $c;
+ }
+
+ /**
+ * Convenience function for DirectLex settings line/col position.
+ */
+ public function rawPosition($l, $c) {
+ if ($c === -1) $l++;
+ $this->line = $l;
+ $this->col = $c;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Token/Comment.php b/extlib/HTMLPurifier/HTMLPurifier/Token/Comment.php
new file mode 100644
index 000000000..dc6bdcabb
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Token/Comment.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * Concrete comment token class. Generally will be ignored.
+ */
+class HTMLPurifier_Token_Comment extends HTMLPurifier_Token
+{
+ public $data; /**< Character data within comment. */
+ public $is_whitespace = true;
+ /**
+ * Transparent constructor.
+ *
+ * @param $data String comment data.
+ */
+ public function __construct($data, $line = null, $col = null) {
+ $this->data = $data;
+ $this->line = $line;
+ $this->col = $col;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Token/Empty.php b/extlib/HTMLPurifier/HTMLPurifier/Token/Empty.php
new file mode 100644
index 000000000..2a82b47ad
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Token/Empty.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * Concrete empty token class.
+ */
+class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag
+{
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Token/End.php b/extlib/HTMLPurifier/HTMLPurifier/Token/End.php
new file mode 100644
index 000000000..353e79daf
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Token/End.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * Concrete end token class.
+ *
+ * @warning This class accepts attributes even though end tags cannot. This
+ * is for optimization reasons, as under normal circumstances, the Lexers
+ * do not pass attributes.
+ */
+class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag
+{
+ /**
+ * Token that started this node. Added by MakeWellFormed. Please
+ * do not edit this!
+ */
+ public $start;
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Token/Start.php b/extlib/HTMLPurifier/HTMLPurifier/Token/Start.php
new file mode 100644
index 000000000..e0e14fc62
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Token/Start.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * Concrete start token class.
+ */
+class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag
+{
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Token/Tag.php b/extlib/HTMLPurifier/HTMLPurifier/Token/Tag.php
new file mode 100644
index 000000000..798be028e
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Token/Tag.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * Abstract class of a tag token (start, end or empty), and its behavior.
+ */
+class HTMLPurifier_Token_Tag extends HTMLPurifier_Token
+{
+ /**
+ * Static bool marker that indicates the class is a tag.
+ *
+ * This allows us to check objects with <tt>!empty($obj->is_tag)</tt>
+ * without having to use a function call <tt>is_a()</tt>.
+ */
+ public $is_tag = true;
+
+ /**
+ * The lower-case name of the tag, like 'a', 'b' or 'blockquote'.
+ *
+ * @note Strictly speaking, XML tags are case sensitive, so we shouldn't
+ * be lower-casing them, but these tokens cater to HTML tags, which are
+ * insensitive.
+ */
+ public $name;
+
+ /**
+ * Associative array of the tag's attributes.
+ */
+ public $attr = array();
+
+ /**
+ * Non-overloaded constructor, which lower-cases passed tag name.
+ *
+ * @param $name String name.
+ * @param $attr Associative array of attributes.
+ */
+ public function __construct($name, $attr = array(), $line = null, $col = null) {
+ $this->name = ctype_lower($name) ? $name : strtolower($name);
+ foreach ($attr as $key => $value) {
+ // normalization only necessary when key is not lowercase
+ if (!ctype_lower($key)) {
+ $new_key = strtolower($key);
+ if (!isset($attr[$new_key])) {
+ $attr[$new_key] = $attr[$key];
+ }
+ if ($new_key !== $key) {
+ unset($attr[$key]);
+ }
+ }
+ }
+ $this->attr = $attr;
+ $this->line = $line;
+ $this->col = $col;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/Token/Text.php b/extlib/HTMLPurifier/HTMLPurifier/Token/Text.php
new file mode 100644
index 000000000..82efd823d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/Token/Text.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * Concrete text token class.
+ *
+ * Text tokens comprise of regular parsed character data (PCDATA) and raw
+ * character data (from the CDATA sections). Internally, their
+ * data is parsed with all entities expanded. Surprisingly, the text token
+ * does have a "tag name" called #PCDATA, which is how the DTD represents it
+ * in permissible child nodes.
+ */
+class HTMLPurifier_Token_Text extends HTMLPurifier_Token
+{
+
+ public $name = '#PCDATA'; /**< PCDATA tag name compatible with DTD. */
+ public $data; /**< Parsed character data of text. */
+ public $is_whitespace; /**< Bool indicating if node is whitespace. */
+
+ /**
+ * Constructor, accepts data and determines if it is whitespace.
+ *
+ * @param $data String parsed character data.
+ */
+ public function __construct($data, $line = null, $col = null) {
+ $this->data = $data;
+ $this->is_whitespace = ctype_space($data);
+ $this->line = $line;
+ $this->col = $col;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/TokenFactory.php b/extlib/HTMLPurifier/HTMLPurifier/TokenFactory.php
new file mode 100644
index 000000000..7cf48fb41
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/TokenFactory.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * Factory for token generation.
+ *
+ * @note Doing some benchmarking indicates that the new operator is much
+ * slower than the clone operator (even discounting the cost of the
+ * constructor). This class is for that optimization.
+ * Other then that, there's not much point as we don't
+ * maintain parallel HTMLPurifier_Token hierarchies (the main reason why
+ * you'd want to use an abstract factory).
+ * @todo Port DirectLex to use this
+ */
+class HTMLPurifier_TokenFactory
+{
+
+ /**
+ * Prototypes that will be cloned.
+ * @private
+ */
+ // p stands for prototype
+ private $p_start, $p_end, $p_empty, $p_text, $p_comment;
+
+ /**
+ * Generates blank prototypes for cloning.
+ */
+ public function __construct() {
+ $this->p_start = new HTMLPurifier_Token_Start('', array());
+ $this->p_end = new HTMLPurifier_Token_End('');
+ $this->p_empty = new HTMLPurifier_Token_Empty('', array());
+ $this->p_text = new HTMLPurifier_Token_Text('');
+ $this->p_comment= new HTMLPurifier_Token_Comment('');
+ }
+
+ /**
+ * Creates a HTMLPurifier_Token_Start.
+ * @param $name Tag name
+ * @param $attr Associative array of attributes
+ * @return Generated HTMLPurifier_Token_Start
+ */
+ public function createStart($name, $attr = array()) {
+ $p = clone $this->p_start;
+ $p->__construct($name, $attr);
+ return $p;
+ }
+
+ /**
+ * Creates a HTMLPurifier_Token_End.
+ * @param $name Tag name
+ * @return Generated HTMLPurifier_Token_End
+ */
+ public function createEnd($name) {
+ $p = clone $this->p_end;
+ $p->__construct($name);
+ return $p;
+ }
+
+ /**
+ * Creates a HTMLPurifier_Token_Empty.
+ * @param $name Tag name
+ * @param $attr Associative array of attributes
+ * @return Generated HTMLPurifier_Token_Empty
+ */
+ public function createEmpty($name, $attr = array()) {
+ $p = clone $this->p_empty;
+ $p->__construct($name, $attr);
+ return $p;
+ }
+
+ /**
+ * Creates a HTMLPurifier_Token_Text.
+ * @param $data Data of text token
+ * @return Generated HTMLPurifier_Token_Text
+ */
+ public function createText($data) {
+ $p = clone $this->p_text;
+ $p->__construct($data);
+ return $p;
+ }
+
+ /**
+ * Creates a HTMLPurifier_Token_Comment.
+ * @param $data Data of comment token
+ * @return Generated HTMLPurifier_Token_Comment
+ */
+ public function createComment($data) {
+ $p = clone $this->p_comment;
+ $p->__construct($data);
+ return $p;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URI.php b/extlib/HTMLPurifier/HTMLPurifier/URI.php
new file mode 100644
index 000000000..8b50d0d18
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URI.php
@@ -0,0 +1,173 @@
+<?php
+
+/**
+ * HTML Purifier's internal representation of a URI.
+ * @note
+ * Internal data-structures are completely escaped. If the data needs
+ * to be used in a non-URI context (which is very unlikely), be sure
+ * to decode it first. The URI may not necessarily be well-formed until
+ * validate() is called.
+ */
+class HTMLPurifier_URI
+{
+
+ public $scheme, $userinfo, $host, $port, $path, $query, $fragment;
+
+ /**
+ * @note Automatically normalizes scheme and port
+ */
+ public function __construct($scheme, $userinfo, $host, $port, $path, $query, $fragment) {
+ $this->scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme);
+ $this->userinfo = $userinfo;
+ $this->host = $host;
+ $this->port = is_null($port) ? $port : (int) $port;
+ $this->path = $path;
+ $this->query = $query;
+ $this->fragment = $fragment;
+ }
+
+ /**
+ * Retrieves a scheme object corresponding to the URI's scheme/default
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ * @return Scheme object appropriate for validating this URI
+ */
+ public function getSchemeObj($config, $context) {
+ $registry = HTMLPurifier_URISchemeRegistry::instance();
+ if ($this->scheme !== null) {
+ $scheme_obj = $registry->getScheme($this->scheme, $config, $context);
+ if (!$scheme_obj) return false; // invalid scheme, clean it out
+ } else {
+ // no scheme: retrieve the default one
+ $def = $config->getDefinition('URI');
+ $scheme_obj = $registry->getScheme($def->defaultScheme, $config, $context);
+ if (!$scheme_obj) {
+ // something funky happened to the default scheme object
+ trigger_error(
+ 'Default scheme object "' . $def->defaultScheme . '" was not readable',
+ E_USER_WARNING
+ );
+ return false;
+ }
+ }
+ return $scheme_obj;
+ }
+
+ /**
+ * Generic validation method applicable for all schemes. May modify
+ * this URI in order to get it into a compliant form.
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ * @return True if validation/filtering succeeds, false if failure
+ */
+ public function validate($config, $context) {
+
+ // ABNF definitions from RFC 3986
+ $chars_sub_delims = '!$&\'()*+,;=';
+ $chars_gen_delims = ':/?#[]@';
+ $chars_pchar = $chars_sub_delims . ':@';
+
+ // validate scheme (MUST BE FIRST!)
+ if (!is_null($this->scheme) && is_null($this->host)) {
+ $def = $config->getDefinition('URI');
+ if ($def->defaultScheme === $this->scheme) {
+ $this->scheme = null;
+ }
+ }
+
+ // validate host
+ if (!is_null($this->host)) {
+ $host_def = new HTMLPurifier_AttrDef_URI_Host();
+ $this->host = $host_def->validate($this->host, $config, $context);
+ if ($this->host === false) $this->host = null;
+ }
+
+ // validate username
+ if (!is_null($this->userinfo)) {
+ $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':');
+ $this->userinfo = $encoder->encode($this->userinfo);
+ }
+
+ // validate port
+ if (!is_null($this->port)) {
+ if ($this->port < 1 || $this->port > 65535) $this->port = null;
+ }
+
+ // validate path
+ $path_parts = array();
+ $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/');
+ if (!is_null($this->host)) {
+ // path-abempty (hier and relative)
+ $this->path = $segments_encoder->encode($this->path);
+ } elseif ($this->path !== '' && $this->path[0] === '/') {
+ // path-absolute (hier and relative)
+ if (strlen($this->path) >= 2 && $this->path[1] === '/') {
+ // This shouldn't ever happen!
+ $this->path = '';
+ } else {
+ $this->path = $segments_encoder->encode($this->path);
+ }
+ } elseif (!is_null($this->scheme) && $this->path !== '') {
+ // path-rootless (hier)
+ // Short circuit evaluation means we don't need to check nz
+ $this->path = $segments_encoder->encode($this->path);
+ } elseif (is_null($this->scheme) && $this->path !== '') {
+ // path-noscheme (relative)
+ // (once again, not checking nz)
+ $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@');
+ $c = strpos($this->path, '/');
+ if ($c !== false) {
+ $this->path =
+ $segment_nc_encoder->encode(substr($this->path, 0, $c)) .
+ $segments_encoder->encode(substr($this->path, $c));
+ } else {
+ $this->path = $segment_nc_encoder->encode($this->path);
+ }
+ } else {
+ // path-empty (hier and relative)
+ $this->path = ''; // just to be safe
+ }
+
+ // qf = query and fragment
+ $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?');
+
+ if (!is_null($this->query)) {
+ $this->query = $qf_encoder->encode($this->query);
+ }
+
+ if (!is_null($this->fragment)) {
+ $this->fragment = $qf_encoder->encode($this->fragment);
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Convert URI back to string
+ * @return String URI appropriate for output
+ */
+ public function toString() {
+ // reconstruct authority
+ $authority = null;
+ if (!is_null($this->host)) {
+ $authority = '';
+ if(!is_null($this->userinfo)) $authority .= $this->userinfo . '@';
+ $authority .= $this->host;
+ if(!is_null($this->port)) $authority .= ':' . $this->port;
+ }
+
+ // reconstruct the result
+ $result = '';
+ if (!is_null($this->scheme)) $result .= $this->scheme . ':';
+ if (!is_null($authority)) $result .= '//' . $authority;
+ $result .= $this->path;
+ if (!is_null($this->query)) $result .= '?' . $this->query;
+ if (!is_null($this->fragment)) $result .= '#' . $this->fragment;
+
+ return $result;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIDefinition.php b/extlib/HTMLPurifier/HTMLPurifier/URIDefinition.php
new file mode 100644
index 000000000..ea2b8fe24
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIDefinition.php
@@ -0,0 +1,93 @@
+<?php
+
+class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
+{
+
+ public $type = 'URI';
+ protected $filters = array();
+ protected $postFilters = array();
+ protected $registeredFilters = array();
+
+ /**
+ * HTMLPurifier_URI object of the base specified at %URI.Base
+ */
+ public $base;
+
+ /**
+ * String host to consider "home" base, derived off of $base
+ */
+ public $host;
+
+ /**
+ * Name of default scheme based on %URI.DefaultScheme and %URI.Base
+ */
+ public $defaultScheme;
+
+ public function __construct() {
+ $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternal());
+ $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources());
+ $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist());
+ $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute());
+ $this->registerFilter(new HTMLPurifier_URIFilter_Munge());
+ }
+
+ public function registerFilter($filter) {
+ $this->registeredFilters[$filter->name] = $filter;
+ }
+
+ public function addFilter($filter, $config) {
+ $r = $filter->prepare($config);
+ if ($r === false) return; // null is ok, for backwards compat
+ if ($filter->post) {
+ $this->postFilters[$filter->name] = $filter;
+ } else {
+ $this->filters[$filter->name] = $filter;
+ }
+ }
+
+ protected function doSetup($config) {
+ $this->setupMemberVariables($config);
+ $this->setupFilters($config);
+ }
+
+ protected function setupFilters($config) {
+ foreach ($this->registeredFilters as $name => $filter) {
+ $conf = $config->get('URI.' . $name);
+ if ($conf !== false && $conf !== null) {
+ $this->addFilter($filter, $config);
+ }
+ }
+ unset($this->registeredFilters);
+ }
+
+ protected function setupMemberVariables($config) {
+ $this->host = $config->get('URI.Host');
+ $base_uri = $config->get('URI.Base');
+ if (!is_null($base_uri)) {
+ $parser = new HTMLPurifier_URIParser();
+ $this->base = $parser->parse($base_uri);
+ $this->defaultScheme = $this->base->scheme;
+ if (is_null($this->host)) $this->host = $this->base->host;
+ }
+ if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme');
+ }
+
+ public function filter(&$uri, $config, $context) {
+ foreach ($this->filters as $name => $f) {
+ $result = $f->filter($uri, $config, $context);
+ if (!$result) return false;
+ }
+ return true;
+ }
+
+ public function postFilter(&$uri, $config, $context) {
+ foreach ($this->postFilters as $name => $f) {
+ $result = $f->filter($uri, $config, $context);
+ if (!$result) return false;
+ }
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIFilter.php b/extlib/HTMLPurifier/HTMLPurifier/URIFilter.php
new file mode 100644
index 000000000..c116f93df
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIFilter.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * Chainable filters for custom URI processing.
+ *
+ * These filters can perform custom actions on a URI filter object,
+ * including transformation or blacklisting.
+ *
+ * @warning This filter is called before scheme object validation occurs.
+ * Make sure, if you require a specific scheme object, you
+ * you check that it exists. This allows filters to convert
+ * proprietary URI schemes into regular ones.
+ */
+abstract class HTMLPurifier_URIFilter
+{
+
+ /**
+ * Unique identifier of filter
+ */
+ public $name;
+
+ /**
+ * True if this filter should be run after scheme validation.
+ */
+ public $post = false;
+
+ /**
+ * Performs initialization for the filter
+ */
+ public function prepare($config) {return true;}
+
+ /**
+ * Filter a URI object
+ * @param $uri Reference to URI object variable
+ * @param $config Instance of HTMLPurifier_Config
+ * @param $context Instance of HTMLPurifier_Context
+ * @return bool Whether or not to continue processing: false indicates
+ * URL is no good, true indicates continue processing. Note that
+ * all changes are committed directly on the URI object
+ */
+ abstract public function filter(&$uri, $config, $context);
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIFilter/DisableExternal.php b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/DisableExternal.php
new file mode 100644
index 000000000..d8a39a501
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/DisableExternal.php
@@ -0,0 +1,23 @@
+<?php
+
+class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter
+{
+ public $name = 'DisableExternal';
+ protected $ourHostParts = false;
+ public function prepare($config) {
+ $our_host = $config->getDefinition('URI')->host;
+ if ($our_host !== null) $this->ourHostParts = array_reverse(explode('.', $our_host));
+ }
+ public function filter(&$uri, $config, $context) {
+ if (is_null($uri->host)) return true;
+ if ($this->ourHostParts === false) return false;
+ $host_parts = array_reverse(explode('.', $uri->host));
+ foreach ($this->ourHostParts as $i => $x) {
+ if (!isset($host_parts[$i])) return false;
+ if ($host_parts[$i] != $this->ourHostParts[$i]) return false;
+ }
+ return true;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIFilter/DisableExternalResources.php b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/DisableExternalResources.php
new file mode 100644
index 000000000..881abc43c
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/DisableExternalResources.php
@@ -0,0 +1,12 @@
+<?php
+
+class HTMLPurifier_URIFilter_DisableExternalResources extends HTMLPurifier_URIFilter_DisableExternal
+{
+ public $name = 'DisableExternalResources';
+ public function filter(&$uri, $config, $context) {
+ if (!$context->get('EmbeddedURI', true)) return true;
+ return parent::filter($uri, $config, $context);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIFilter/HostBlacklist.php b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/HostBlacklist.php
new file mode 100644
index 000000000..045aa0992
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/HostBlacklist.php
@@ -0,0 +1,21 @@
+<?php
+
+class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
+{
+ public $name = 'HostBlacklist';
+ protected $blacklist = array();
+ public function prepare($config) {
+ $this->blacklist = $config->get('URI.HostBlacklist');
+ return true;
+ }
+ public function filter(&$uri, $config, $context) {
+ foreach($this->blacklist as $blacklisted_host_fragment) {
+ if (strpos($uri->host, $blacklisted_host_fragment) !== false) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIFilter/MakeAbsolute.php b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/MakeAbsolute.php
new file mode 100644
index 000000000..f46ab2630
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/MakeAbsolute.php
@@ -0,0 +1,114 @@
+<?php
+
+// does not support network paths
+
+class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
+{
+ public $name = 'MakeAbsolute';
+ protected $base;
+ protected $basePathStack = array();
+ public function prepare($config) {
+ $def = $config->getDefinition('URI');
+ $this->base = $def->base;
+ if (is_null($this->base)) {
+ trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING);
+ return false;
+ }
+ $this->base->fragment = null; // fragment is invalid for base URI
+ $stack = explode('/', $this->base->path);
+ array_pop($stack); // discard last segment
+ $stack = $this->_collapseStack($stack); // do pre-parsing
+ $this->basePathStack = $stack;
+ return true;
+ }
+ public function filter(&$uri, $config, $context) {
+ if (is_null($this->base)) return true; // abort early
+ if (
+ $uri->path === '' && is_null($uri->scheme) &&
+ is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)
+ ) {
+ // reference to current document
+ $uri = clone $this->base;
+ return true;
+ }
+ if (!is_null($uri->scheme)) {
+ // absolute URI already: don't change
+ if (!is_null($uri->host)) return true;
+ $scheme_obj = $uri->getSchemeObj($config, $context);
+ if (!$scheme_obj) {
+ // scheme not recognized
+ return false;
+ }
+ if (!$scheme_obj->hierarchical) {
+ // non-hierarchal URI with explicit scheme, don't change
+ return true;
+ }
+ // special case: had a scheme but always is hierarchical and had no authority
+ }
+ if (!is_null($uri->host)) {
+ // network path, don't bother
+ return true;
+ }
+ if ($uri->path === '') {
+ $uri->path = $this->base->path;
+ } elseif ($uri->path[0] !== '/') {
+ // relative path, needs more complicated processing
+ $stack = explode('/', $uri->path);
+ $new_stack = array_merge($this->basePathStack, $stack);
+ if ($new_stack[0] !== '' && !is_null($this->base->host)) {
+ array_unshift($new_stack, '');
+ }
+ $new_stack = $this->_collapseStack($new_stack);
+ $uri->path = implode('/', $new_stack);
+ } else {
+ // absolute path, but still we should collapse
+ $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path)));
+ }
+ // re-combine
+ $uri->scheme = $this->base->scheme;
+ if (is_null($uri->userinfo)) $uri->userinfo = $this->base->userinfo;
+ if (is_null($uri->host)) $uri->host = $this->base->host;
+ if (is_null($uri->port)) $uri->port = $this->base->port;
+ return true;
+ }
+
+ /**
+ * Resolve dots and double-dots in a path stack
+ */
+ private function _collapseStack($stack) {
+ $result = array();
+ $is_folder = false;
+ for ($i = 0; isset($stack[$i]); $i++) {
+ $is_folder = false;
+ // absorb an internally duplicated slash
+ if ($stack[$i] == '' && $i && isset($stack[$i+1])) continue;
+ if ($stack[$i] == '..') {
+ if (!empty($result)) {
+ $segment = array_pop($result);
+ if ($segment === '' && empty($result)) {
+ // error case: attempted to back out too far:
+ // restore the leading slash
+ $result[] = '';
+ } elseif ($segment === '..') {
+ $result[] = '..'; // cannot remove .. with ..
+ }
+ } else {
+ // relative path, preserve the double-dots
+ $result[] = '..';
+ }
+ $is_folder = true;
+ continue;
+ }
+ if ($stack[$i] == '.') {
+ // silently absorb
+ $is_folder = true;
+ continue;
+ }
+ $result[] = $stack[$i];
+ }
+ if ($is_folder) $result[] = '';
+ return $result;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIFilter/Munge.php b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/Munge.php
new file mode 100644
index 000000000..efa10a645
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIFilter/Munge.php
@@ -0,0 +1,58 @@
+<?php
+
+class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter
+{
+ public $name = 'Munge';
+ public $post = true;
+ private $target, $parser, $doEmbed, $secretKey;
+
+ protected $replace = array();
+
+ public function prepare($config) {
+ $this->target = $config->get('URI.' . $this->name);
+ $this->parser = new HTMLPurifier_URIParser();
+ $this->doEmbed = $config->get('URI.MungeResources');
+ $this->secretKey = $config->get('URI.MungeSecretKey');
+ return true;
+ }
+ public function filter(&$uri, $config, $context) {
+ if ($context->get('EmbeddedURI', true) && !$this->doEmbed) return true;
+
+ $scheme_obj = $uri->getSchemeObj($config, $context);
+ if (!$scheme_obj) return true; // ignore unknown schemes, maybe another postfilter did it
+ if (is_null($uri->host) || empty($scheme_obj->browsable)) {
+ return true;
+ }
+ // don't redirect if target host is our host
+ if ($uri->host === $config->getDefinition('URI')->host) {
+ return true;
+ }
+
+ $this->makeReplace($uri, $config, $context);
+ $this->replace = array_map('rawurlencode', $this->replace);
+
+ $new_uri = strtr($this->target, $this->replace);
+ $new_uri = $this->parser->parse($new_uri);
+ // don't redirect if the target host is the same as the
+ // starting host
+ if ($uri->host === $new_uri->host) return true;
+ $uri = $new_uri; // overwrite
+ return true;
+ }
+
+ protected function makeReplace($uri, $config, $context) {
+ $string = $uri->toString();
+ // always available
+ $this->replace['%s'] = $string;
+ $this->replace['%r'] = $context->get('EmbeddedURI', true);
+ $token = $context->get('CurrentToken', true);
+ $this->replace['%n'] = $token ? $token->name : null;
+ $this->replace['%m'] = $context->get('CurrentAttr', true);
+ $this->replace['%p'] = $context->get('CurrentCSSProperty', true);
+ // not always available
+ if ($this->secretKey) $this->replace['%t'] = sha1($this->secretKey . ':' . $string);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIParser.php b/extlib/HTMLPurifier/HTMLPurifier/URIParser.php
new file mode 100644
index 000000000..7179e4ab8
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIParser.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * Parses a URI into the components and fragment identifier as specified
+ * by RFC 3986.
+ */
+class HTMLPurifier_URIParser
+{
+
+ /**
+ * Instance of HTMLPurifier_PercentEncoder to do normalization with.
+ */
+ protected $percentEncoder;
+
+ public function __construct() {
+ $this->percentEncoder = new HTMLPurifier_PercentEncoder();
+ }
+
+ /**
+ * Parses a URI.
+ * @param $uri string URI to parse
+ * @return HTMLPurifier_URI representation of URI. This representation has
+ * not been validated yet and may not conform to RFC.
+ */
+ public function parse($uri) {
+
+ $uri = $this->percentEncoder->normalize($uri);
+
+ // Regexp is as per Appendix B.
+ // Note that ["<>] are an addition to the RFC's recommended
+ // characters, because they represent external delimeters.
+ $r_URI = '!'.
+ '(([^:/?#"<>]+):)?'. // 2. Scheme
+ '(//([^/?#"<>]*))?'. // 4. Authority
+ '([^?#"<>]*)'. // 5. Path
+ '(\?([^#"<>]*))?'. // 7. Query
+ '(#([^"<>]*))?'. // 8. Fragment
+ '!';
+
+ $matches = array();
+ $result = preg_match($r_URI, $uri, $matches);
+
+ if (!$result) return false; // *really* invalid URI
+
+ // seperate out parts
+ $scheme = !empty($matches[1]) ? $matches[2] : null;
+ $authority = !empty($matches[3]) ? $matches[4] : null;
+ $path = $matches[5]; // always present, can be empty
+ $query = !empty($matches[6]) ? $matches[7] : null;
+ $fragment = !empty($matches[8]) ? $matches[9] : null;
+
+ // further parse authority
+ if ($authority !== null) {
+ $r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/";
+ $matches = array();
+ preg_match($r_authority, $authority, $matches);
+ $userinfo = !empty($matches[1]) ? $matches[2] : null;
+ $host = !empty($matches[3]) ? $matches[3] : '';
+ $port = !empty($matches[4]) ? (int) $matches[5] : null;
+ } else {
+ $port = $host = $userinfo = null;
+ }
+
+ return new HTMLPurifier_URI(
+ $scheme, $userinfo, $host, $port, $path, $query, $fragment);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIScheme.php b/extlib/HTMLPurifier/HTMLPurifier/URIScheme.php
new file mode 100644
index 000000000..039710fd1
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIScheme.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * Validator for the components of a URI for a specific scheme
+ */
+class HTMLPurifier_URIScheme
+{
+
+ /**
+ * Scheme's default port (integer)
+ */
+ public $default_port = null;
+
+ /**
+ * Whether or not URIs of this schem are locatable by a browser
+ * http and ftp are accessible, while mailto and news are not.
+ */
+ public $browsable = false;
+
+ /**
+ * Whether or not the URI always uses <hier_part>, resolves edge cases
+ * with making relative URIs absolute
+ */
+ public $hierarchical = false;
+
+ /**
+ * Validates the components of a URI
+ * @note This implementation should be called by children if they define
+ * a default port, as it does port processing.
+ * @param $uri Instance of HTMLPurifier_URI
+ * @param $config HTMLPurifier_Config object
+ * @param $context HTMLPurifier_Context object
+ * @return Bool success or failure
+ */
+ public function validate(&$uri, $config, $context) {
+ if ($this->default_port == $uri->port) $uri->port = null;
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIScheme/ftp.php b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/ftp.php
new file mode 100644
index 000000000..5849bf7ff
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/ftp.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Validates ftp (File Transfer Protocol) URIs as defined by generic RFC 1738.
+ */
+class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme {
+
+ public $default_port = 21;
+ public $browsable = true; // usually
+ public $hierarchical = true;
+
+ public function validate(&$uri, $config, $context) {
+ parent::validate($uri, $config, $context);
+ $uri->query = null;
+
+ // typecode check
+ $semicolon_pos = strrpos($uri->path, ';'); // reverse
+ if ($semicolon_pos !== false) {
+ $type = substr($uri->path, $semicolon_pos + 1); // no semicolon
+ $uri->path = substr($uri->path, 0, $semicolon_pos);
+ $type_ret = '';
+ if (strpos($type, '=') !== false) {
+ // figure out whether or not the declaration is correct
+ list($key, $typecode) = explode('=', $type, 2);
+ if ($key !== 'type') {
+ // invalid key, tack it back on encoded
+ $uri->path .= '%3B' . $type;
+ } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') {
+ $type_ret = ";type=$typecode";
+ }
+ } else {
+ $uri->path .= '%3B' . $type;
+ }
+ $uri->path = str_replace(';', '%3B', $uri->path);
+ $uri->path .= $type_ret;
+ }
+
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIScheme/http.php b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/http.php
new file mode 100644
index 000000000..b097a31d6
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/http.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * Validates http (HyperText Transfer Protocol) as defined by RFC 2616
+ */
+class HTMLPurifier_URIScheme_http extends HTMLPurifier_URIScheme {
+
+ public $default_port = 80;
+ public $browsable = true;
+ public $hierarchical = true;
+
+ public function validate(&$uri, $config, $context) {
+ parent::validate($uri, $config, $context);
+ $uri->userinfo = null;
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIScheme/https.php b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/https.php
new file mode 100644
index 000000000..29e380919
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/https.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * Validates https (Secure HTTP) according to http scheme.
+ */
+class HTMLPurifier_URIScheme_https extends HTMLPurifier_URIScheme_http {
+
+ public $default_port = 443;
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIScheme/mailto.php b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/mailto.php
new file mode 100644
index 000000000..c1e2cd5aa
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/mailto.php
@@ -0,0 +1,27 @@
+<?php
+
+// VERY RELAXED! Shouldn't cause problems, not even Firefox checks if the
+// email is valid, but be careful!
+
+/**
+ * Validates mailto (for E-mail) according to RFC 2368
+ * @todo Validate the email address
+ * @todo Filter allowed query parameters
+ */
+
+class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme {
+
+ public $browsable = false;
+
+ public function validate(&$uri, $config, $context) {
+ parent::validate($uri, $config, $context);
+ $uri->userinfo = null;
+ $uri->host = null;
+ $uri->port = null;
+ // we need to validate path against RFC 2368's addr-spec
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIScheme/news.php b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/news.php
new file mode 100644
index 000000000..f5f54f4f5
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/news.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * Validates news (Usenet) as defined by generic RFC 1738
+ */
+class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme {
+
+ public $browsable = false;
+
+ public function validate(&$uri, $config, $context) {
+ parent::validate($uri, $config, $context);
+ $uri->userinfo = null;
+ $uri->host = null;
+ $uri->port = null;
+ $uri->query = null;
+ // typecode check needed on path
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URIScheme/nntp.php b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/nntp.php
new file mode 100644
index 000000000..5bf93ea78
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URIScheme/nntp.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * Validates nntp (Network News Transfer Protocol) as defined by generic RFC 1738
+ */
+class HTMLPurifier_URIScheme_nntp extends HTMLPurifier_URIScheme {
+
+ public $default_port = 119;
+ public $browsable = false;
+
+ public function validate(&$uri, $config, $context) {
+ parent::validate($uri, $config, $context);
+ $uri->userinfo = null;
+ $uri->query = null;
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/URISchemeRegistry.php b/extlib/HTMLPurifier/HTMLPurifier/URISchemeRegistry.php
new file mode 100644
index 000000000..576bf7b6d
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/URISchemeRegistry.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * Registry for retrieving specific URI scheme validator objects.
+ */
+class HTMLPurifier_URISchemeRegistry
+{
+
+ /**
+ * Retrieve sole instance of the registry.
+ * @param $prototype Optional prototype to overload sole instance with,
+ * or bool true to reset to default registry.
+ * @note Pass a registry object $prototype with a compatible interface and
+ * the function will copy it and return it all further times.
+ */
+ public static function instance($prototype = null) {
+ static $instance = null;
+ if ($prototype !== null) {
+ $instance = $prototype;
+ } elseif ($instance === null || $prototype == true) {
+ $instance = new HTMLPurifier_URISchemeRegistry();
+ }
+ return $instance;
+ }
+
+ /**
+ * Cache of retrieved schemes.
+ */
+ protected $schemes = array();
+
+ /**
+ * Retrieves a scheme validator object
+ * @param $scheme String scheme name like http or mailto
+ * @param $config HTMLPurifier_Config object
+ * @param $config HTMLPurifier_Context object
+ */
+ public function getScheme($scheme, $config, $context) {
+ if (!$config) $config = HTMLPurifier_Config::createDefault();
+
+ // important, otherwise attacker could include arbitrary file
+ $allowed_schemes = $config->get('URI.AllowedSchemes');
+ if (!$config->get('URI.OverrideAllowedSchemes') &&
+ !isset($allowed_schemes[$scheme])
+ ) {
+ return;
+ }
+
+ if (isset($this->schemes[$scheme])) return $this->schemes[$scheme];
+ if (!isset($allowed_schemes[$scheme])) return;
+
+ $class = 'HTMLPurifier_URIScheme_' . $scheme;
+ if (!class_exists($class)) return;
+ $this->schemes[$scheme] = new $class();
+ return $this->schemes[$scheme];
+ }
+
+ /**
+ * Registers a custom scheme to the cache, bypassing reflection.
+ * @param $scheme Scheme name
+ * @param $scheme_obj HTMLPurifier_URIScheme object
+ */
+ public function register($scheme, $scheme_obj) {
+ $this->schemes[$scheme] = $scheme_obj;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/UnitConverter.php b/extlib/HTMLPurifier/HTMLPurifier/UnitConverter.php
new file mode 100644
index 000000000..545d42622
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/UnitConverter.php
@@ -0,0 +1,254 @@
+<?php
+
+/**
+ * Class for converting between different unit-lengths as specified by
+ * CSS.
+ */
+class HTMLPurifier_UnitConverter
+{
+
+ const ENGLISH = 1;
+ const METRIC = 2;
+ const DIGITAL = 3;
+
+ /**
+ * Units information array. Units are grouped into measuring systems
+ * (English, Metric), and are assigned an integer representing
+ * the conversion factor between that unit and the smallest unit in
+ * the system. Numeric indexes are actually magical constants that
+ * encode conversion data from one system to the next, with a O(n^2)
+ * constraint on memory (this is generally not a problem, since
+ * the number of measuring systems is small.)
+ */
+ protected static $units = array(
+ self::ENGLISH => array(
+ 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
+ 'pt' => 4,
+ 'pc' => 48,
+ 'in' => 288,
+ self::METRIC => array('pt', '0.352777778', 'mm'),
+ ),
+ self::METRIC => array(
+ 'mm' => 1,
+ 'cm' => 10,
+ self::ENGLISH => array('mm', '2.83464567', 'pt'),
+ ),
+ );
+
+ /**
+ * Minimum bcmath precision for output.
+ */
+ protected $outputPrecision;
+
+ /**
+ * Bcmath precision for internal calculations.
+ */
+ protected $internalPrecision;
+
+ /**
+ * Whether or not BCMath is available
+ */
+ private $bcmath;
+
+ public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) {
+ $this->outputPrecision = $output_precision;
+ $this->internalPrecision = $internal_precision;
+ $this->bcmath = !$force_no_bcmath && function_exists('bcmul');
+ }
+
+ /**
+ * Converts a length object of one unit into another unit.
+ * @param HTMLPurifier_Length $length
+ * Instance of HTMLPurifier_Length to convert. You must validate()
+ * it before passing it here!
+ * @param string $to_unit
+ * Unit to convert to.
+ * @note
+ * About precision: This conversion function pays very special
+ * attention to the incoming precision of values and attempts
+ * to maintain a number of significant figure. Results are
+ * fairly accurate up to nine digits. Some caveats:
+ * - If a number is zero-padded as a result of this significant
+ * figure tracking, the zeroes will be eliminated.
+ * - If a number contains less than four sigfigs ($outputPrecision)
+ * and this causes some decimals to be excluded, those
+ * decimals will be added on.
+ */
+ public function convert($length, $to_unit) {
+
+ if (!$length->isValid()) return false;
+
+ $n = $length->getN();
+ $unit = $length->getUnit();
+
+ if ($n === '0' || $unit === false) {
+ return new HTMLPurifier_Length('0', false);
+ }
+
+ $state = $dest_state = false;
+ foreach (self::$units as $k => $x) {
+ if (isset($x[$unit])) $state = $k;
+ if (isset($x[$to_unit])) $dest_state = $k;
+ }
+ if (!$state || !$dest_state) return false;
+
+ // Some calculations about the initial precision of the number;
+ // this will be useful when we need to do final rounding.
+ $sigfigs = $this->getSigFigs($n);
+ if ($sigfigs < $this->outputPrecision) $sigfigs = $this->outputPrecision;
+
+ // BCMath's internal precision deals only with decimals. Use
+ // our default if the initial number has no decimals, or increase
+ // it by how ever many decimals, thus, the number of guard digits
+ // will always be greater than or equal to internalPrecision.
+ $log = (int) floor(log(abs($n), 10));
+ $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
+
+ for ($i = 0; $i < 2; $i++) {
+
+ // Determine what unit IN THIS SYSTEM we need to convert to
+ if ($dest_state === $state) {
+ // Simple conversion
+ $dest_unit = $to_unit;
+ } else {
+ // Convert to the smallest unit, pending a system shift
+ $dest_unit = self::$units[$state][$dest_state][0];
+ }
+
+ // Do the conversion if necessary
+ if ($dest_unit !== $unit) {
+ $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
+ $n = $this->mul($n, $factor, $cp);
+ $unit = $dest_unit;
+ }
+
+ // Output was zero, so bail out early. Shouldn't ever happen.
+ if ($n === '') {
+ $n = '0';
+ $unit = $to_unit;
+ break;
+ }
+
+ // It was a simple conversion, so bail out
+ if ($dest_state === $state) {
+ break;
+ }
+
+ if ($i !== 0) {
+ // Conversion failed! Apparently, the system we forwarded
+ // to didn't have this unit. This should never happen!
+ return false;
+ }
+
+ // Pre-condition: $i == 0
+
+ // Perform conversion to next system of units
+ $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);
+ $unit = self::$units[$state][$dest_state][2];
+ $state = $dest_state;
+
+ // One more loop around to convert the unit in the new system.
+
+ }
+
+ // Post-condition: $unit == $to_unit
+ if ($unit !== $to_unit) return false;
+
+ // Useful for debugging:
+ //echo "<pre>n";
+ //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n";
+
+ $n = $this->round($n, $sigfigs);
+ if (strpos($n, '.') !== false) $n = rtrim($n, '0');
+ $n = rtrim($n, '.');
+
+ return new HTMLPurifier_Length($n, $unit);
+ }
+
+ /**
+ * Returns the number of significant figures in a string number.
+ * @param string $n Decimal number
+ * @return int number of sigfigs
+ */
+ public function getSigFigs($n) {
+ $n = ltrim($n, '0+-');
+ $dp = strpos($n, '.'); // decimal position
+ if ($dp === false) {
+ $sigfigs = strlen(rtrim($n, '0'));
+ } else {
+ $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character
+ if ($dp !== 0) $sigfigs--;
+ }
+ return $sigfigs;
+ }
+
+ /**
+ * Adds two numbers, using arbitrary precision when available.
+ */
+ private function add($s1, $s2, $scale) {
+ if ($this->bcmath) return bcadd($s1, $s2, $scale);
+ else return $this->scale($s1 + $s2, $scale);
+ }
+
+ /**
+ * Multiples two numbers, using arbitrary precision when available.
+ */
+ private function mul($s1, $s2, $scale) {
+ if ($this->bcmath) return bcmul($s1, $s2, $scale);
+ else return $this->scale($s1 * $s2, $scale);
+ }
+
+ /**
+ * Divides two numbers, using arbitrary precision when available.
+ */
+ private function div($s1, $s2, $scale) {
+ if ($this->bcmath) return bcdiv($s1, $s2, $scale);
+ else return $this->scale($s1 / $s2, $scale);
+ }
+
+ /**
+ * Rounds a number according to the number of sigfigs it should have,
+ * using arbitrary precision when available.
+ */
+ private function round($n, $sigfigs) {
+ $new_log = (int) floor(log(abs($n), 10)); // Number of digits left of decimal - 1
+ $rp = $sigfigs - $new_log - 1; // Number of decimal places needed
+ $neg = $n < 0 ? '-' : ''; // Negative sign
+ if ($this->bcmath) {
+ if ($rp >= 0) {
+ $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1);
+ $n = bcdiv($n, '1', $rp);
+ } else {
+ // This algorithm partially depends on the standardized
+ // form of numbers that comes out of bcmath.
+ $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0);
+ $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1);
+ }
+ return $n;
+ } else {
+ return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);
+ }
+ }
+
+ /**
+ * Scales a float to $scale digits right of decimal point, like BCMath.
+ */
+ private function scale($r, $scale) {
+ if ($scale < 0) {
+ // The f sprintf type doesn't support negative numbers, so we
+ // need to cludge things manually. First get the string.
+ $r = sprintf('%.0f', (float) $r);
+ // Due to floating point precision loss, $r will more than likely
+ // look something like 4652999999999.9234. We grab one more digit
+ // than we need to precise from $r and then use that to round
+ // appropriately.
+ $precise = (string) round(substr($r, 0, strlen($r) + $scale), -1);
+ // Now we return it, truncating the zero that was rounded off.
+ return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);
+ }
+ return sprintf('%.' . $scale . 'f', (float) $r);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/VarParser.php b/extlib/HTMLPurifier/HTMLPurifier/VarParser.php
new file mode 100644
index 000000000..68e72ae86
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/VarParser.php
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ * Parses string representations into their corresponding native PHP
+ * variable type. The base implementation does a simple type-check.
+ */
+class HTMLPurifier_VarParser
+{
+
+ const STRING = 1;
+ const ISTRING = 2;
+ const TEXT = 3;
+ const ITEXT = 4;
+ const INT = 5;
+ const FLOAT = 6;
+ const BOOL = 7;
+ const LOOKUP = 8;
+ const ALIST = 9;
+ const HASH = 10;
+ const MIXED = 11;
+
+ /**
+ * Lookup table of allowed types. Mainly for backwards compatibility, but
+ * also convenient for transforming string type names to the integer constants.
+ */
+ static public $types = array(
+ 'string' => self::STRING,
+ 'istring' => self::ISTRING,
+ 'text' => self::TEXT,
+ 'itext' => self::ITEXT,
+ 'int' => self::INT,
+ 'float' => self::FLOAT,
+ 'bool' => self::BOOL,
+ 'lookup' => self::LOOKUP,
+ 'list' => self::ALIST,
+ 'hash' => self::HASH,
+ 'mixed' => self::MIXED
+ );
+
+ /**
+ * Lookup table of types that are string, and can have aliases or
+ * allowed value lists.
+ */
+ static public $stringTypes = array(
+ self::STRING => true,
+ self::ISTRING => true,
+ self::TEXT => true,
+ self::ITEXT => true,
+ );
+
+ /**
+ * Validate a variable according to type. Throws
+ * HTMLPurifier_VarParserException if invalid.
+ * It may return NULL as a valid type if $allow_null is true.
+ *
+ * @param $var Variable to validate
+ * @param $type Type of variable, see HTMLPurifier_VarParser->types
+ * @param $allow_null Whether or not to permit null as a value
+ * @return Validated and type-coerced variable
+ */
+ final public function parse($var, $type, $allow_null = false) {
+ if (is_string($type)) {
+ if (!isset(HTMLPurifier_VarParser::$types[$type])) {
+ throw new HTMLPurifier_VarParserException("Invalid type '$type'");
+ } else {
+ $type = HTMLPurifier_VarParser::$types[$type];
+ }
+ }
+ $var = $this->parseImplementation($var, $type, $allow_null);
+ if ($allow_null && $var === null) return null;
+ // These are basic checks, to make sure nothing horribly wrong
+ // happened in our implementations.
+ switch ($type) {
+ case (self::STRING):
+ case (self::ISTRING):
+ case (self::TEXT):
+ case (self::ITEXT):
+ if (!is_string($var)) break;
+ if ($type == self::ISTRING || $type == self::ITEXT) $var = strtolower($var);
+ return $var;
+ case (self::INT):
+ if (!is_int($var)) break;
+ return $var;
+ case (self::FLOAT):
+ if (!is_float($var)) break;
+ return $var;
+ case (self::BOOL):
+ if (!is_bool($var)) break;
+ return $var;
+ case (self::LOOKUP):
+ case (self::ALIST):
+ case (self::HASH):
+ if (!is_array($var)) break;
+ if ($type === self::LOOKUP) {
+ foreach ($var as $k) if ($k !== true) $this->error('Lookup table contains value other than true');
+ } elseif ($type === self::ALIST) {
+ $keys = array_keys($var);
+ if (array_keys($keys) !== $keys) $this->error('Indices for list are not uniform');
+ }
+ return $var;
+ case (self::MIXED):
+ return $var;
+ default:
+ $this->errorInconsistent(get_class($this), $type);
+ }
+ $this->errorGeneric($var, $type);
+ }
+
+ /**
+ * Actually implements the parsing. Base implementation is to not
+ * do anything to $var. Subclasses should overload this!
+ */
+ protected function parseImplementation($var, $type, $allow_null) {
+ return $var;
+ }
+
+ /**
+ * Throws an exception.
+ */
+ protected function error($msg) {
+ throw new HTMLPurifier_VarParserException($msg);
+ }
+
+ /**
+ * Throws an inconsistency exception.
+ * @note This should not ever be called. It would be called if we
+ * extend the allowed values of HTMLPurifier_VarParser without
+ * updating subclasses.
+ */
+ protected function errorInconsistent($class, $type) {
+ throw new HTMLPurifier_Exception("Inconsistency in $class: ".HTMLPurifier_VarParser::getTypeName($type)." not implemented");
+ }
+
+ /**
+ * Generic error for if a type didn't work.
+ */
+ protected function errorGeneric($var, $type) {
+ $vtype = gettype($var);
+ $this->error("Expected type ".HTMLPurifier_VarParser::getTypeName($type).", got $vtype");
+ }
+
+ static public function getTypeName($type) {
+ static $lookup;
+ if (!$lookup) {
+ // Lazy load the alternative lookup table
+ $lookup = array_flip(HTMLPurifier_VarParser::$types);
+ }
+ if (!isset($lookup[$type])) return 'unknown';
+ return $lookup[$type];
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/VarParser/Flexible.php b/extlib/HTMLPurifier/HTMLPurifier/VarParser/Flexible.php
new file mode 100644
index 000000000..c954250e9
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/VarParser/Flexible.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * Performs safe variable parsing based on types which can be used by
+ * users. This may not be able to represent all possible data inputs,
+ * however.
+ */
+class HTMLPurifier_VarParser_Flexible extends HTMLPurifier_VarParser
+{
+
+ protected function parseImplementation($var, $type, $allow_null) {
+ if ($allow_null && $var === null) return null;
+ switch ($type) {
+ // Note: if code "breaks" from the switch, it triggers a generic
+ // exception to be thrown. Specific errors can be specifically
+ // done here.
+ case self::MIXED :
+ case self::ISTRING :
+ case self::STRING :
+ case self::TEXT :
+ case self::ITEXT :
+ return $var;
+ case self::INT :
+ if (is_string($var) && ctype_digit($var)) $var = (int) $var;
+ return $var;
+ case self::FLOAT :
+ if ((is_string($var) && is_numeric($var)) || is_int($var)) $var = (float) $var;
+ return $var;
+ case self::BOOL :
+ if (is_int($var) && ($var === 0 || $var === 1)) {
+ $var = (bool) $var;
+ } elseif (is_string($var)) {
+ if ($var == 'on' || $var == 'true' || $var == '1') {
+ $var = true;
+ } elseif ($var == 'off' || $var == 'false' || $var == '0') {
+ $var = false;
+ } else {
+ throw new HTMLPurifier_VarParserException("Unrecognized value '$var' for $type");
+ }
+ }
+ return $var;
+ case self::ALIST :
+ case self::HASH :
+ case self::LOOKUP :
+ if (is_string($var)) {
+ // special case: technically, this is an array with
+ // a single empty string item, but having an empty
+ // array is more intuitive
+ if ($var == '') return array();
+ if (strpos($var, "\n") === false && strpos($var, "\r") === false) {
+ // simplistic string to array method that only works
+ // for simple lists of tag names or alphanumeric characters
+ $var = explode(',',$var);
+ } else {
+ $var = preg_split('/(,|[\n\r]+)/', $var);
+ }
+ // remove spaces
+ foreach ($var as $i => $j) $var[$i] = trim($j);
+ if ($type === self::HASH) {
+ // key:value,key2:value2
+ $nvar = array();
+ foreach ($var as $keypair) {
+ $c = explode(':', $keypair, 2);
+ if (!isset($c[1])) continue;
+ $nvar[$c[0]] = $c[1];
+ }
+ $var = $nvar;
+ }
+ }
+ if (!is_array($var)) break;
+ $keys = array_keys($var);
+ if ($keys === array_keys($keys)) {
+ if ($type == self::ALIST) return $var;
+ elseif ($type == self::LOOKUP) {
+ $new = array();
+ foreach ($var as $key) {
+ $new[$key] = true;
+ }
+ return $new;
+ } else break;
+ }
+ if ($type === self::LOOKUP) {
+ foreach ($var as $key => $value) {
+ $var[$key] = true;
+ }
+ }
+ return $var;
+ default:
+ $this->errorInconsistent(__CLASS__, $type);
+ }
+ $this->errorGeneric($var, $type);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/VarParser/Native.php b/extlib/HTMLPurifier/HTMLPurifier/VarParser/Native.php
new file mode 100644
index 000000000..b02a6de54
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/VarParser/Native.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * This variable parser uses PHP's internal code engine. Because it does
+ * this, it can represent all inputs; however, it is dangerous and cannot
+ * be used by users.
+ */
+class HTMLPurifier_VarParser_Native extends HTMLPurifier_VarParser
+{
+
+ protected function parseImplementation($var, $type, $allow_null) {
+ return $this->evalExpression($var);
+ }
+
+ protected function evalExpression($expr) {
+ $var = null;
+ $result = eval("\$var = $expr;");
+ if ($result === false) {
+ throw new HTMLPurifier_VarParserException("Fatal error in evaluated code");
+ }
+ return $var;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/HTMLPurifier/VarParserException.php b/extlib/HTMLPurifier/HTMLPurifier/VarParserException.php
new file mode 100644
index 000000000..5df341495
--- /dev/null
+++ b/extlib/HTMLPurifier/HTMLPurifier/VarParserException.php
@@ -0,0 +1,11 @@
+<?php
+
+/**
+ * Exception type for HTMLPurifier_VarParser
+ */
+class HTMLPurifier_VarParserException extends HTMLPurifier_Exception
+{
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/extlib/HTMLPurifier/LICENSE b/extlib/HTMLPurifier/LICENSE
new file mode 100644
index 000000000..8c88a20d4
--- /dev/null
+++ b/extlib/HTMLPurifier/LICENSE
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+ vim: et sw=4 sts=4
diff --git a/js/jquery.form.js b/js/jquery.form.js
index 936b847ab..dde394270 100644
--- a/js/jquery.form.js
+++ b/js/jquery.form.js
@@ -1,632 +1,660 @@
-/*
- * jQuery Form Plugin
- * version: 2.17 (06-NOV-2008)
- * @requires jQuery v1.2.2 or later
- *
- * Examples and documentation at: http://malsup.com/jquery/form/
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- *
- * Revision: $Id$
- */
-;(function($) {
-
-/*
- Usage Note:
- -----------
- Do not use both ajaxSubmit and ajaxForm on the same form. These
- functions are intended to be exclusive. Use ajaxSubmit if you want
- to bind your own submit handler to the form. For example,
-
- $(document).ready(function() {
- $('#myForm').bind('submit', function() {
- $(this).ajaxSubmit({
- target: '#output'
- });
- return false; // <-- important!
- });
- });
-
- Use ajaxForm when you want the plugin to manage all the event binding
- for you. For example,
-
- $(document).ready(function() {
- $('#myForm').ajaxForm({
- target: '#output'
- });
- });
-
- When using ajaxForm, the ajaxSubmit function will be invoked for you
- at the appropriate time.
-*/
-
-/**
- * ajaxSubmit() provides a mechanism for immediately submitting
- * an HTML form using AJAX.
- */
-$.fn.ajaxSubmit = function(options) {
- // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
- if (!this.length) {
- log('ajaxSubmit: skipping submit process - no element selected');
- return this;
- }
-
- if (typeof options == 'function')
- options = { success: options };
-
- options = $.extend({
- url: this.attr('action') || window.location.toString(),
- type: this.attr('method') || 'GET'
- }, options || {});
-
- // hook for manipulating the form data before it is extracted;
- // convenient for use with rich editors like tinyMCE or FCKEditor
- var veto = {};
- this.trigger('form-pre-serialize', [this, options, veto]);
- if (veto.veto) {
- log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
- return this;
- }
-
- // provide opportunity to alter form data before it is serialized
- if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
- log('ajaxSubmit: submit aborted via beforeSerialize callback');
- return this;
- }
-
- var a = this.formToArray(options.semantic);
- if (options.data) {
- options.extraData = options.data;
- for (var n in options.data) {
- if(options.data[n] instanceof Array) {
- for (var k in options.data[n])
- a.push( { name: n, value: options.data[n][k] } )
- }
- else
- a.push( { name: n, value: options.data[n] } );
- }
- }
-
- // give pre-submit callback an opportunity to abort the submit
- if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
- log('ajaxSubmit: submit aborted via beforeSubmit callback');
- return this;
- }
-
- // fire vetoable 'validate' event
- this.trigger('form-submit-validate', [a, this, options, veto]);
- if (veto.veto) {
- log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
- return this;
- }
-
- var q = $.param(a);
-
- if (options.type.toUpperCase() == 'GET') {
- options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
- options.data = null; // data is null for 'get'
- }
- else
- options.data = q; // data is the query string for 'post'
-
- var $form = this, callbacks = [];
- if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
- if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
-
- // perform a load on the target only if dataType is not provided
- if (!options.dataType && options.target) {
- var oldSuccess = options.success || function(){};
- callbacks.push(function(data) {
- $(options.target).html(data).each(oldSuccess, arguments);
- });
- }
- else if (options.success)
- callbacks.push(options.success);
-
- options.success = function(data, status) {
- for (var i=0, max=callbacks.length; i < max; i++)
- callbacks[i].apply(options, [data, status, $form]);
- };
-
- // are there files to upload?
- var files = $('input:file', this).fieldValue();
- var found = false;
- for (var j=0; j < files.length; j++)
- if (files[j])
- found = true;
-
- // options.iframe allows user to force iframe mode
- if (options.iframe || found) {
- // hack to fix Safari hang (thanks to Tim Molendijk for this)
- // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
- if ($.browser.safari && options.closeKeepAlive)
- $.get(options.closeKeepAlive, fileUpload);
- else
- fileUpload();
- }
- else
- $.ajax(options);
-
- // fire 'notify' event
- this.trigger('form-submit-notify', [this, options]);
- return this;
-
-
- // private function for handling file uploads (hat tip to YAHOO!)
- function fileUpload() {
- var form = $form[0];
-
- if ($(':input[name=submit]', form).length) {
- alert('Error: Form elements must not be named "submit".');
- return;
- }
-
- var opts = $.extend({}, $.ajaxSettings, options);
- var s = jQuery.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
-
- var id = 'jqFormIO' + (new Date().getTime());
- var $io = $('<iframe id="' + id + '" name="' + id + '" />');
- var io = $io[0];
-
- if ($.browser.msie || $.browser.opera)
- io.src = 'javascript:false;document.write("");';
- $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
-
- var xhr = { // mock object
- aborted: 0,
- responseText: null,
- responseXML: null,
- status: 0,
- statusText: 'n/a',
- getAllResponseHeaders: function() {},
- getResponseHeader: function() {},
- setRequestHeader: function() {},
- abort: function() {
- this.aborted = 1;
- $io.attr('src','about:blank'); // abort op in progress
- }
- };
-
- var g = opts.global;
- // trigger ajax global events so that activity/block indicators work like normal
- if (g && ! $.active++) $.event.trigger("ajaxStart");
- if (g) $.event.trigger("ajaxSend", [xhr, opts]);
-
- if (s.beforeSend && s.beforeSend(xhr, s) === false) {
- s.global && jQuery.active--;
- return;
- }
- if (xhr.aborted)
- return;
-
- var cbInvoked = 0;
- var timedOut = 0;
-
- // add submitting element to data if we know it
- var sub = form.clk;
- if (sub) {
- var n = sub.name;
- if (n && !sub.disabled) {
- options.extraData = options.extraData || {};
- options.extraData[n] = sub.value;
- if (sub.type == "image") {
- options.extraData[name+'.x'] = form.clk_x;
- options.extraData[name+'.y'] = form.clk_y;
- }
- }
- }
-
- // take a breath so that pending repaints get some cpu time before the upload starts
- setTimeout(function() {
- // make sure form attrs are set
- var t = $form.attr('target'), a = $form.attr('action');
- $form.attr({
- target: id,
- method: 'POST',
- action: opts.url
- });
-
- // ie borks in some cases when setting encoding
- if (! options.skipEncodingOverride) {
- $form.attr({
- encoding: 'multipart/form-data',
- enctype: 'multipart/form-data'
- });
- }
-
- // support timout
- if (opts.timeout)
- setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
-
- // add "extra" data to form if provided in options
- var extraInputs = [];
- try {
- if (options.extraData)
- for (var n in options.extraData)
- extraInputs.push(
- $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
- .appendTo(form)[0]);
-
- // add iframe to doc and submit the form
- $io.appendTo('body');
- io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
- form.submit();
- }
- finally {
- // reset attrs and remove "extra" input elements
- $form.attr('action', a);
- t ? $form.attr('target', t) : $form.removeAttr('target');
- $(extraInputs).remove();
- }
- }, 10);
-
- function cb() {
- if (cbInvoked++) return;
-
- io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
-
- var operaHack = 0;
- var ok = true;
- try {
- if (timedOut) throw 'timeout';
- // extract the server response from the iframe
- var data, doc;
-
- doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
-
- if (doc.body == null && !operaHack && $.browser.opera) {
- // In Opera 9.2.x the iframe DOM is not always traversable when
- // the onload callback fires so we give Opera 100ms to right itself
- operaHack = 1;
- cbInvoked--;
- setTimeout(cb, 100);
- return;
- }
-
- xhr.responseText = doc.body ? doc.body.innerHTML : null;
- xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
- xhr.getResponseHeader = function(header){
- var headers = {'content-type': opts.dataType};
- return headers[header];
- };
-
- if (opts.dataType == 'json' || opts.dataType == 'script') {
- var ta = doc.getElementsByTagName('textarea')[0];
- xhr.responseText = ta ? ta.value : xhr.responseText;
- }
- else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
- xhr.responseXML = toXml(xhr.responseText);
- }
- data = $.httpData(xhr, opts.dataType);
- }
- catch(e){
- ok = false;
- $.handleError(opts, xhr, 'error', e);
- }
-
- // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
- if (ok) {
- opts.success(data, 'success');
- if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
- }
- if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
- if (g && ! --$.active) $.event.trigger("ajaxStop");
- if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
-
- // clean up
- setTimeout(function() {
- $io.remove();
- xhr.responseXML = null;
- }, 100);
- };
-
- function toXml(s, doc) {
- if (window.ActiveXObject) {
- doc = new ActiveXObject('Microsoft.XMLDOM');
- doc.async = 'false';
- doc.loadXML(s);
- }
- else
- doc = (new DOMParser()).parseFromString(s, 'text/xml');
- return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
- };
- };
-};
-
-/**
- * ajaxForm() provides a mechanism for fully automating form submission.
- *
- * The advantages of using this method instead of ajaxSubmit() are:
- *
- * 1: This method will include coordinates for <input type="image" /> elements (if the element
- * is used to submit the form).
- * 2. This method will include the submit element's name/value data (for the element that was
- * used to submit the form).
- * 3. This method binds the submit() method to the form for you.
- *
- * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
- * passes the options argument along after properly binding events for submit elements and
- * the form itself.
- */
-$.fn.ajaxForm = function(options) {
- return this.ajaxFormUnbind().bind('submit.form-plugin',function() {
- $(this).ajaxSubmit(options);
- return false;
- }).each(function() {
- // store options in hash
- $(":submit,input:image", this).bind('click.form-plugin',function(e) {
- var form = this.form;
- form.clk = this;
- if (this.type == 'image') {
- if (e.offsetX != undefined) {
- form.clk_x = e.offsetX;
- form.clk_y = e.offsetY;
- } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
- var offset = $(this).offset();
- form.clk_x = e.pageX - offset.left;
- form.clk_y = e.pageY - offset.top;
- } else {
- form.clk_x = e.pageX - this.offsetLeft;
- form.clk_y = e.pageY - this.offsetTop;
- }
- }
- // clear form vars
- setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10);
- });
- });
-};
-
-// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
-$.fn.ajaxFormUnbind = function() {
- this.unbind('submit.form-plugin');
- return this.each(function() {
- $(":submit,input:image", this).unbind('click.form-plugin');
- });
-
-};
-
-/**
- * formToArray() gathers form element data into an array of objects that can
- * be passed to any of the following ajax functions: $.get, $.post, or load.
- * Each object in the array has both a 'name' and 'value' property. An example of
- * an array for a simple login form might be:
- *
- * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
- *
- * It is this array that is passed to pre-submit callback functions provided to the
- * ajaxSubmit() and ajaxForm() methods.
- */
-$.fn.formToArray = function(semantic) {
- var a = [];
- if (this.length == 0) return a;
-
- var form = this[0];
- var els = semantic ? form.getElementsByTagName('*') : form.elements;
- if (!els) return a;
- for(var i=0, max=els.length; i < max; i++) {
- var el = els[i];
- var n = el.name;
- if (!n) continue;
-
- if (semantic && form.clk && el.type == "image") {
- // handle image inputs on the fly when semantic == true
- if(!el.disabled && form.clk == el)
- a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
- continue;
- }
-
- var v = $.fieldValue(el, true);
- if (v && v.constructor == Array) {
- for(var j=0, jmax=v.length; j < jmax; j++)
- a.push({name: n, value: v[j]});
- }
- else if (v !== null && typeof v != 'undefined')
- a.push({name: n, value: v});
- }
-
- if (!semantic && form.clk) {
- // input type=='image' are not found in elements array! handle them here
- var inputs = form.getElementsByTagName("input");
- for(var i=0, max=inputs.length; i < max; i++) {
- var input = inputs[i];
- var n = input.name;
- if(n && !input.disabled && input.type == "image" && form.clk == input)
- a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
- }
- }
- return a;
-};
-
-/**
- * Serializes form data into a 'submittable' string. This method will return a string
- * in the format: name1=value1&amp;name2=value2
- */
-$.fn.formSerialize = function(semantic) {
- //hand off to jQuery.param for proper encoding
- return $.param(this.formToArray(semantic));
-};
-
-/**
- * Serializes all field elements in the jQuery object into a query string.
- * This method will return a string in the format: name1=value1&amp;name2=value2
- */
-$.fn.fieldSerialize = function(successful) {
- var a = [];
- this.each(function() {
- var n = this.name;
- if (!n) return;
- var v = $.fieldValue(this, successful);
- if (v && v.constructor == Array) {
- for (var i=0,max=v.length; i < max; i++)
- a.push({name: n, value: v[i]});
- }
- else if (v !== null && typeof v != 'undefined')
- a.push({name: this.name, value: v});
- });
- //hand off to jQuery.param for proper encoding
- return $.param(a);
-};
-
-/**
- * Returns the value(s) of the element in the matched set. For example, consider the following form:
- *
- * <form><fieldset>
- * <input name="A" type="text" />
- * <input name="A" type="text" />
- * <input name="B" type="checkbox" value="B1" />
- * <input name="B" type="checkbox" value="B2"/>
- * <input name="C" type="radio" value="C1" />
- * <input name="C" type="radio" value="C2" />
- * </fieldset></form>
- *
- * var v = $(':text').fieldValue();
- * // if no values are entered into the text inputs
- * v == ['','']
- * // if values entered into the text inputs are 'foo' and 'bar'
- * v == ['foo','bar']
- *
- * var v = $(':checkbox').fieldValue();
- * // if neither checkbox is checked
- * v === undefined
- * // if both checkboxes are checked
- * v == ['B1', 'B2']
- *
- * var v = $(':radio').fieldValue();
- * // if neither radio is checked
- * v === undefined
- * // if first radio is checked
- * v == ['C1']
- *
- * The successful argument controls whether or not the field element must be 'successful'
- * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
- * The default value of the successful argument is true. If this value is false the value(s)
- * for each element is returned.
- *
- * Note: This method *always* returns an array. If no valid value can be determined the
- * array will be empty, otherwise it will contain one or more values.
- */
-$.fn.fieldValue = function(successful) {
- for (var val=[], i=0, max=this.length; i < max; i++) {
- var el = this[i];
- var v = $.fieldValue(el, successful);
- if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
- continue;
- v.constructor == Array ? $.merge(val, v) : val.push(v);
- }
- return val;
-};
-
-/**
- * Returns the value of the field element.
- */
-$.fieldValue = function(el, successful) {
- var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
- if (typeof successful == 'undefined') successful = true;
-
- if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
- (t == 'checkbox' || t == 'radio') && !el.checked ||
- (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
- tag == 'select' && el.selectedIndex == -1))
- return null;
-
- if (tag == 'select') {
- var index = el.selectedIndex;
- if (index < 0) return null;
- var a = [], ops = el.options;
- var one = (t == 'select-one');
- var max = (one ? index+1 : ops.length);
- for(var i=(one ? index : 0); i < max; i++) {
- var op = ops[i];
- if (op.selected) {
- // extra pain for IE...
- var v = $.browser.msie && !(op.attributes['value'].specified) ? op.text : op.value;
- if (one) return v;
- a.push(v);
- }
- }
- return a;
- }
- return el.value;
-};
-
-/**
- * Clears the form data. Takes the following actions on the form's input fields:
- * - input text fields will have their 'value' property set to the empty string
- * - select elements will have their 'selectedIndex' property set to -1
- * - checkbox and radio inputs will have their 'checked' property set to false
- * - inputs of type submit, button, reset, and hidden will *not* be effected
- * - button elements will *not* be effected
- */
-$.fn.clearForm = function() {
- return this.each(function() {
- $('input,select,textarea', this).clearFields();
- });
-};
-
-/**
- * Clears the selected form elements.
- */
-$.fn.clearFields = $.fn.clearInputs = function() {
- return this.each(function() {
- var t = this.type, tag = this.tagName.toLowerCase();
- if (t == 'file' || t == 'text' || t == 'password' || tag == 'textarea')
- this.value = '';
- else if (t == 'checkbox' || t == 'radio')
- this.checked = false;
- else if (tag == 'select')
- this.selectedIndex = -1;
- });
-};
-
-/**
- * Resets the form data. Causes all form elements to be reset to their original value.
- */
-$.fn.resetForm = function() {
- return this.each(function() {
- // guard against an input with the name of 'reset'
- // note that IE reports the reset function as an 'object'
- if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
- this.reset();
- });
-};
-
-/**
- * Enables or disables any matching elements.
- */
-$.fn.enable = function(b) {
- if (b == undefined) b = true;
- return this.each(function() {
- this.disabled = !b
- });
-};
-
-/**
- * Checks/unchecks any matching checkboxes or radio buttons and
- * selects/deselects and matching option elements.
- */
-$.fn.selected = function(select) {
- if (select == undefined) select = true;
- return this.each(function() {
- var t = this.type;
- if (t == 'checkbox' || t == 'radio')
- this.checked = select;
- else if (this.tagName.toLowerCase() == 'option') {
- var $sel = $(this).parent('select');
- if (select && $sel[0] && $sel[0].type == 'select-one') {
- // deselect all other options
- $sel.find('option').selected(false);
- }
- this.selected = select;
- }
- });
-};
-
-// helper fn for console logging
-// set $.fn.ajaxSubmit.debug to true to enable debug logging
-function log() {
- if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
- window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
-};
-
-})(jQuery);
+/*
+ * jQuery Form Plugin
+ * version: 2.36 (07-NOV-2009)
+ * @requires jQuery v1.2.6 or later
+ *
+ * Examples and documentation at: http://malsup.com/jquery/form/
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ */
+;(function($) {
+
+/*
+ Usage Note:
+ -----------
+ Do not use both ajaxSubmit and ajaxForm on the same form. These
+ functions are intended to be exclusive. Use ajaxSubmit if you want
+ to bind your own submit handler to the form. For example,
+
+ $(document).ready(function() {
+ $('#myForm').bind('submit', function() {
+ $(this).ajaxSubmit({
+ target: '#output'
+ });
+ return false; // <-- important!
+ });
+ });
+
+ Use ajaxForm when you want the plugin to manage all the event binding
+ for you. For example,
+
+ $(document).ready(function() {
+ $('#myForm').ajaxForm({
+ target: '#output'
+ });
+ });
+
+ When using ajaxForm, the ajaxSubmit function will be invoked for you
+ at the appropriate time.
+*/
+
+/**
+ * ajaxSubmit() provides a mechanism for immediately submitting
+ * an HTML form using AJAX.
+ */
+$.fn.ajaxSubmit = function(options) {
+ // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
+ if (!this.length) {
+ log('ajaxSubmit: skipping submit process - no element selected');
+ return this;
+ }
+
+ if (typeof options == 'function')
+ options = { success: options };
+
+ var url = $.trim(this.attr('action'));
+ if (url) {
+ // clean url (don't include hash vaue)
+ url = (url.match(/^([^#]+)/)||[])[1];
+ }
+ url = url || window.location.href || '';
+
+ options = $.extend({
+ url: url,
+ type: this.attr('method') || 'GET',
+ iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
+ }, options || {});
+
+ // hook for manipulating the form data before it is extracted;
+ // convenient for use with rich editors like tinyMCE or FCKEditor
+ var veto = {};
+ this.trigger('form-pre-serialize', [this, options, veto]);
+ if (veto.veto) {
+ log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
+ return this;
+ }
+
+ // provide opportunity to alter form data before it is serialized
+ if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
+ log('ajaxSubmit: submit aborted via beforeSerialize callback');
+ return this;
+ }
+
+ var a = this.formToArray(options.semantic);
+ if (options.data) {
+ options.extraData = options.data;
+ for (var n in options.data) {
+ if(options.data[n] instanceof Array) {
+ for (var k in options.data[n])
+ a.push( { name: n, value: options.data[n][k] } );
+ }
+ else
+ a.push( { name: n, value: options.data[n] } );
+ }
+ }
+
+ // give pre-submit callback an opportunity to abort the submit
+ if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
+ log('ajaxSubmit: submit aborted via beforeSubmit callback');
+ return this;
+ }
+
+ // fire vetoable 'validate' event
+ this.trigger('form-submit-validate', [a, this, options, veto]);
+ if (veto.veto) {
+ log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
+ return this;
+ }
+
+ var q = $.param(a);
+
+ if (options.type.toUpperCase() == 'GET') {
+ options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
+ options.data = null; // data is null for 'get'
+ }
+ else
+ options.data = q; // data is the query string for 'post'
+
+ var $form = this, callbacks = [];
+ if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
+ if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
+
+ // perform a load on the target only if dataType is not provided
+ if (!options.dataType && options.target) {
+ var oldSuccess = options.success || function(){};
+ callbacks.push(function(data) {
+ $(options.target).html(data).each(oldSuccess, arguments);
+ });
+ }
+ else if (options.success)
+ callbacks.push(options.success);
+
+ options.success = function(data, status) {
+ for (var i=0, max=callbacks.length; i < max; i++)
+ callbacks[i].apply(options, [data, status, $form]);
+ };
+
+ // are there files to upload?
+ var files = $('input:file', this).fieldValue();
+ var found = false;
+ for (var j=0; j < files.length; j++)
+ if (files[j])
+ found = true;
+
+ var multipart = false;
+// var mp = 'multipart/form-data';
+// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
+
+ // options.iframe allows user to force iframe mode
+ // 06-NOV-09: now defaulting to iframe mode if file input is detected
+ if ((files.length && options.iframe !== false) || options.iframe || found || multipart) {
+ // hack to fix Safari hang (thanks to Tim Molendijk for this)
+ // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
+ if (options.closeKeepAlive)
+ $.get(options.closeKeepAlive, fileUpload);
+ else
+ fileUpload();
+ }
+ else
+ $.ajax(options);
+
+ // fire 'notify' event
+ this.trigger('form-submit-notify', [this, options]);
+ return this;
+
+
+ // private function for handling file uploads (hat tip to YAHOO!)
+ function fileUpload() {
+ var form = $form[0];
+
+ if ($(':input[name=submit]', form).length) {
+ alert('Error: Form elements must not be named "submit".');
+ return;
+ }
+
+ var opts = $.extend({}, $.ajaxSettings, options);
+ var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
+
+ var id = 'jqFormIO' + (new Date().getTime());
+ var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ opts.iframeSrc +'" />');
+ var io = $io[0];
+
+ $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
+
+ var xhr = { // mock object
+ aborted: 0,
+ responseText: null,
+ responseXML: null,
+ status: 0,
+ statusText: 'n/a',
+ getAllResponseHeaders: function() {},
+ getResponseHeader: function() {},
+ setRequestHeader: function() {},
+ abort: function() {
+ this.aborted = 1;
+ $io.attr('src', opts.iframeSrc); // abort op in progress
+ }
+ };
+
+ var g = opts.global;
+ // trigger ajax global events so that activity/block indicators work like normal
+ if (g && ! $.active++) $.event.trigger("ajaxStart");
+ if (g) $.event.trigger("ajaxSend", [xhr, opts]);
+
+ if (s.beforeSend && s.beforeSend(xhr, s) === false) {
+ s.global && $.active--;
+ return;
+ }
+ if (xhr.aborted)
+ return;
+
+ var cbInvoked = 0;
+ var timedOut = 0;
+
+ // add submitting element to data if we know it
+ var sub = form.clk;
+ if (sub) {
+ var n = sub.name;
+ if (n && !sub.disabled) {
+ options.extraData = options.extraData || {};
+ options.extraData[n] = sub.value;
+ if (sub.type == "image") {
+ options.extraData[name+'.x'] = form.clk_x;
+ options.extraData[name+'.y'] = form.clk_y;
+ }
+ }
+ }
+
+ // take a breath so that pending repaints get some cpu time before the upload starts
+ setTimeout(function() {
+ // make sure form attrs are set
+ var t = $form.attr('target'), a = $form.attr('action');
+
+ // update form attrs in IE friendly way
+ form.setAttribute('target',id);
+ if (form.getAttribute('method') != 'POST')
+ form.setAttribute('method', 'POST');
+ if (form.getAttribute('action') != opts.url)
+ form.setAttribute('action', opts.url);
+
+ // ie borks in some cases when setting encoding
+ if (! options.skipEncodingOverride) {
+ $form.attr({
+ encoding: 'multipart/form-data',
+ enctype: 'multipart/form-data'
+ });
+ }
+
+ // support timout
+ if (opts.timeout)
+ setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
+
+ // add "extra" data to form if provided in options
+ var extraInputs = [];
+ try {
+ if (options.extraData)
+ for (var n in options.extraData)
+ extraInputs.push(
+ $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
+ .appendTo(form)[0]);
+
+ // add iframe to doc and submit the form
+ $io.appendTo('body');
+ io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
+ form.submit();
+ }
+ finally {
+ // reset attrs and remove "extra" input elements
+ form.setAttribute('action',a);
+ t ? form.setAttribute('target', t) : $form.removeAttr('target');
+ $(extraInputs).remove();
+ }
+ }, 10);
+
+ var domCheckCount = 50;
+
+ function cb() {
+ if (cbInvoked++) return;
+
+ io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
+
+ var ok = true;
+ try {
+ if (timedOut) throw 'timeout';
+ // extract the server response from the iframe
+ var data, doc;
+
+ doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
+
+ var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
+ log('isXml='+isXml);
+ if (!isXml && (doc.body == null || doc.body.innerHTML == '')) {
+ if (--domCheckCount) {
+ // in some browsers (Opera) the iframe DOM is not always traversable when
+ // the onload callback fires, so we loop a bit to accommodate
+ cbInvoked = 0;
+ setTimeout(cb, 100);
+ return;
+ }
+ log('Could not access iframe DOM after 50 tries.');
+ return;
+ }
+
+ xhr.responseText = doc.body ? doc.body.innerHTML : null;
+ xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
+ xhr.getResponseHeader = function(header){
+ var headers = {'content-type': opts.dataType};
+ return headers[header];
+ };
+
+ if (opts.dataType == 'json' || opts.dataType == 'script') {
+ // see if user embedded response in textarea
+ var ta = doc.getElementsByTagName('textarea')[0];
+ if (ta)
+ xhr.responseText = ta.value;
+ else {
+ // account for browsers injecting pre around json response
+ var pre = doc.getElementsByTagName('pre')[0];
+ if (pre)
+ xhr.responseText = pre.innerHTML;
+ }
+ }
+ else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
+ xhr.responseXML = toXml(xhr.responseText);
+ }
+ data = $.httpData(xhr, opts.dataType);
+ }
+ catch(e){
+ ok = false;
+ $.handleError(opts, xhr, 'error', e);
+ }
+
+ // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
+ if (ok) {
+ opts.success(data, 'success');
+ if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
+ }
+ if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
+ if (g && ! --$.active) $.event.trigger("ajaxStop");
+ if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
+
+ // clean up
+ setTimeout(function() {
+ $io.remove();
+ xhr.responseXML = null;
+ }, 100);
+ };
+
+ function toXml(s, doc) {
+ if (window.ActiveXObject) {
+ doc = new ActiveXObject('Microsoft.XMLDOM');
+ doc.async = 'false';
+ doc.loadXML(s);
+ }
+ else
+ doc = (new DOMParser()).parseFromString(s, 'text/xml');
+ return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
+ };
+ };
+};
+
+/**
+ * ajaxForm() provides a mechanism for fully automating form submission.
+ *
+ * The advantages of using this method instead of ajaxSubmit() are:
+ *
+ * 1: This method will include coordinates for <input type="image" /> elements (if the element
+ * is used to submit the form).
+ * 2. This method will include the submit element's name/value data (for the element that was
+ * used to submit the form).
+ * 3. This method binds the submit() method to the form for you.
+ *
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
+ * passes the options argument along after properly binding events for submit elements and
+ * the form itself.
+ */
+$.fn.ajaxForm = function(options) {
+ return this.ajaxFormUnbind().bind('submit.form-plugin', function() {
+ $(this).ajaxSubmit(options);
+ return false;
+ }).bind('click.form-plugin', function(e) {
+ var target = e.target;
+ var $el = $(target);
+ if (!($el.is(":submit,input:image"))) {
+ // is this a child element of the submit el? (ex: a span within a button)
+ var t = $el.closest(':submit');
+ if (t.length == 0)
+ return;
+ target = t[0];
+ }
+ var form = this;
+ form.clk = target;
+ if (target.type == 'image') {
+ if (e.offsetX != undefined) {
+ form.clk_x = e.offsetX;
+ form.clk_y = e.offsetY;
+ } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
+ var offset = $el.offset();
+ form.clk_x = e.pageX - offset.left;
+ form.clk_y = e.pageY - offset.top;
+ } else {
+ form.clk_x = e.pageX - target.offsetLeft;
+ form.clk_y = e.pageY - target.offsetTop;
+ }
+ }
+ // clear form vars
+ setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
+ });
+};
+
+// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
+$.fn.ajaxFormUnbind = function() {
+ return this.unbind('submit.form-plugin click.form-plugin');
+};
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
+ * Each object in the array has both a 'name' and 'value' property. An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ */
+$.fn.formToArray = function(semantic) {
+ var a = [];
+ if (this.length == 0) return a;
+
+ var form = this[0];
+ var els = semantic ? form.getElementsByTagName('*') : form.elements;
+ if (!els) return a;
+ for(var i=0, max=els.length; i < max; i++) {
+ var el = els[i];
+ var n = el.name;
+ if (!n) continue;
+
+ if (semantic && form.clk && el.type == "image") {
+ // handle image inputs on the fly when semantic == true
+ if(!el.disabled && form.clk == el) {
+ a.push({name: n, value: $(el).val()});
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ continue;
+ }
+
+ var v = $.fieldValue(el, true);
+ if (v && v.constructor == Array) {
+ for(var j=0, jmax=v.length; j < jmax; j++)
+ a.push({name: n, value: v[j]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: n, value: v});
+ }
+
+ if (!semantic && form.clk) {
+ // input type=='image' are not found in elements array! handle it here
+ var $input = $(form.clk), input = $input[0], n = input.name;
+ if (n && !input.disabled && input.type == 'image') {
+ a.push({name: n, value: $input.val()});
+ a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+ }
+ }
+ return a;
+};
+
+/**
+ * Serializes form data into a 'submittable' string. This method will return a string
+ * in the format: name1=value1&amp;name2=value2
+ */
+$.fn.formSerialize = function(semantic) {
+ //hand off to jQuery.param for proper encoding
+ return $.param(this.formToArray(semantic));
+};
+
+/**
+ * Serializes all field elements in the jQuery object into a query string.
+ * This method will return a string in the format: name1=value1&amp;name2=value2
+ */
+$.fn.fieldSerialize = function(successful) {
+ var a = [];
+ this.each(function() {
+ var n = this.name;
+ if (!n) return;
+ var v = $.fieldValue(this, successful);
+ if (v && v.constructor == Array) {
+ for (var i=0,max=v.length; i < max; i++)
+ a.push({name: n, value: v[i]});
+ }
+ else if (v !== null && typeof v != 'undefined')
+ a.push({name: this.name, value: v});
+ });
+ //hand off to jQuery.param for proper encoding
+ return $.param(a);
+};
+
+/**
+ * Returns the value(s) of the element in the matched set. For example, consider the following form:
+ *
+ * <form><fieldset>
+ * <input name="A" type="text" />
+ * <input name="A" type="text" />
+ * <input name="B" type="checkbox" value="B1" />
+ * <input name="B" type="checkbox" value="B2"/>
+ * <input name="C" type="radio" value="C1" />
+ * <input name="C" type="radio" value="C2" />
+ * </fieldset></form>
+ *
+ * var v = $(':text').fieldValue();
+ * // if no values are entered into the text inputs
+ * v == ['','']
+ * // if values entered into the text inputs are 'foo' and 'bar'
+ * v == ['foo','bar']
+ *
+ * var v = $(':checkbox').fieldValue();
+ * // if neither checkbox is checked
+ * v === undefined
+ * // if both checkboxes are checked
+ * v == ['B1', 'B2']
+ *
+ * var v = $(':radio').fieldValue();
+ * // if neither radio is checked
+ * v === undefined
+ * // if first radio is checked
+ * v == ['C1']
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true. If this value is false the value(s)
+ * for each element is returned.
+ *
+ * Note: This method *always* returns an array. If no valid value can be determined the
+ * array will be empty, otherwise it will contain one or more values.
+ */
+$.fn.fieldValue = function(successful) {
+ for (var val=[], i=0, max=this.length; i < max; i++) {
+ var el = this[i];
+ var v = $.fieldValue(el, successful);
+ if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
+ continue;
+ v.constructor == Array ? $.merge(val, v) : val.push(v);
+ }
+ return val;
+};
+
+/**
+ * Returns the value of the field element.
+ */
+$.fieldValue = function(el, successful) {
+ var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+ if (typeof successful == 'undefined') successful = true;
+
+ if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+ (t == 'checkbox' || t == 'radio') && !el.checked ||
+ (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+ tag == 'select' && el.selectedIndex == -1))
+ return null;
+
+ if (tag == 'select') {
+ var index = el.selectedIndex;
+ if (index < 0) return null;
+ var a = [], ops = el.options;
+ var one = (t == 'select-one');
+ var max = (one ? index+1 : ops.length);
+ for(var i=(one ? index : 0); i < max; i++) {
+ var op = ops[i];
+ if (op.selected) {
+ var v = op.value;
+ if (!v) // extra pain for IE...
+ v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
+ if (one) return v;
+ a.push(v);
+ }
+ }
+ return a;
+ }
+ return el.value;
+};
+
+/**
+ * Clears the form data. Takes the following actions on the form's input fields:
+ * - input text fields will have their 'value' property set to the empty string
+ * - select elements will have their 'selectedIndex' property set to -1
+ * - checkbox and radio inputs will have their 'checked' property set to false
+ * - inputs of type submit, button, reset, and hidden will *not* be effected
+ * - button elements will *not* be effected
+ */
+$.fn.clearForm = function() {
+ return this.each(function() {
+ $('input,select,textarea', this).clearFields();
+ });
+};
+
+/**
+ * Clears the selected form elements.
+ */
+$.fn.clearFields = $.fn.clearInputs = function() {
+ return this.each(function() {
+ var t = this.type, tag = this.tagName.toLowerCase();
+ if (t == 'text' || t == 'password' || tag == 'textarea')
+ this.value = '';
+ else if (t == 'checkbox' || t == 'radio')
+ this.checked = false;
+ else if (tag == 'select')
+ this.selectedIndex = -1;
+ });
+};
+
+/**
+ * Resets the form data. Causes all form elements to be reset to their original value.
+ */
+$.fn.resetForm = function() {
+ return this.each(function() {
+ // guard against an input with the name of 'reset'
+ // note that IE reports the reset function as an 'object'
+ if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
+ this.reset();
+ });
+};
+
+/**
+ * Enables or disables any matching elements.
+ */
+$.fn.enable = function(b) {
+ if (b == undefined) b = true;
+ return this.each(function() {
+ this.disabled = !b;
+ });
+};
+
+/**
+ * Checks/unchecks any matching checkboxes or radio buttons and
+ * selects/deselects and matching option elements.
+ */
+$.fn.selected = function(select) {
+ if (select == undefined) select = true;
+ return this.each(function() {
+ var t = this.type;
+ if (t == 'checkbox' || t == 'radio')
+ this.checked = select;
+ else if (this.tagName.toLowerCase() == 'option') {
+ var $sel = $(this).parent('select');
+ if (select && $sel[0] && $sel[0].type == 'select-one') {
+ // deselect all other options
+ $sel.find('option').selected(false);
+ }
+ this.selected = select;
+ }
+ });
+};
+
+// helper fn for console logging
+// set $.fn.ajaxSubmit.debug to true to enable debug logging
+function log() {
+ if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
+ window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
+};
+
+})(jQuery);
diff --git a/js/jquery.js b/js/jquery.js
index 926357433..237e1b908 100644
--- a/js/jquery.js
+++ b/js/jquery.js
@@ -1,136 +1,221 @@
/*!
- * jQuery JavaScript Library v1.3.2
+ * jQuery JavaScript Library v1.4.1
* http://jquery.com/
*
- * Copyright (c) 2009 John Resig
- * Dual licensed under the MIT and GPL licenses.
- * http://docs.jquery.com/License
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
*
- * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
- * Revision: 6246
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2010, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Mon Jan 25 19:43:33 2010 -0500
*/
-(function(){
+(function( window, undefined ) {
+
+// Define a local copy of jQuery
+var jQuery = function( selector, context ) {
+ // The jQuery object is actually just the init constructor 'enhanced'
+ return new jQuery.fn.init( selector, context );
+ },
-var
- // Will speed up references to window, and allows munging its name.
- window = this,
- // Will speed up references to undefined, and allows munging its name.
- undefined,
// Map over jQuery in case of overwrite
_jQuery = window.jQuery,
+
// Map over the $ in case of overwrite
_$ = window.$,
- jQuery = window.jQuery = window.$ = function( selector, context ) {
- // The jQuery object is actually just the init constructor 'enhanced'
- return new jQuery.fn.init( selector, context );
- },
+ // Use the correct document accordingly with window argument (sandbox)
+ document = window.document,
+
+ // A central reference to the root jQuery(document)
+ rootjQuery,
// A simple way to check for HTML strings or ID strings
// (both of which we optimize for)
- quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,
+ quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,
+
// Is it a simple selector
- isSimple = /^.[^:#\[\.,]*$/;
+ isSimple = /^.[^:#\[\.,]*$/,
+
+ // Check if a string has a non-whitespace character in it
+ rnotwhite = /\S/,
+
+ // Used for trimming whitespace
+ rtrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g,
+
+ // Match a standalone tag
+ rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
+
+ // Keep a UserAgent string for use with jQuery.browser
+ userAgent = navigator.userAgent,
+
+ // For matching the engine and version of the browser
+ browserMatch,
+
+ // Has the ready events already been bound?
+ readyBound = false,
+
+ // The functions to execute on DOM ready
+ readyList = [],
+
+ // The ready event handler
+ DOMContentLoaded,
+
+ // Save a reference to some core methods
+ toString = Object.prototype.toString,
+ hasOwnProperty = Object.prototype.hasOwnProperty,
+ push = Array.prototype.push,
+ slice = Array.prototype.slice,
+ indexOf = Array.prototype.indexOf;
jQuery.fn = jQuery.prototype = {
init: function( selector, context ) {
- // Make sure that a selection was provided
- selector = selector || document;
+ var match, elem, ret, doc;
+
+ // Handle $(""), $(null), or $(undefined)
+ if ( !selector ) {
+ return this;
+ }
// Handle $(DOMElement)
if ( selector.nodeType ) {
- this[0] = selector;
+ this.context = this[0] = selector;
this.length = 1;
- this.context = selector;
return this;
}
+
// Handle HTML strings
if ( typeof selector === "string" ) {
// Are we dealing with HTML string or an ID?
- var match = quickExpr.exec( selector );
+ match = quickExpr.exec( selector );
// Verify a match, and that no context was specified for #id
if ( match && (match[1] || !context) ) {
// HANDLE: $(html) -> $(array)
- if ( match[1] )
- selector = jQuery.clean( [ match[1] ], context );
+ if ( match[1] ) {
+ doc = (context ? context.ownerDocument || context : document);
+
+ // If a single string is passed in and it's a single tag
+ // just do a createElement and skip the rest
+ ret = rsingleTag.exec( selector );
+
+ if ( ret ) {
+ if ( jQuery.isPlainObject( context ) ) {
+ selector = [ document.createElement( ret[1] ) ];
+ jQuery.fn.attr.call( selector, context, true );
+
+ } else {
+ selector = [ doc.createElement( ret[1] ) ];
+ }
+
+ } else {
+ ret = buildFragment( [ match[1] ], [ doc ] );
+ selector = (ret.cacheable ? ret.fragment.cloneNode(true) : ret.fragment).childNodes;
+ }
// HANDLE: $("#id")
- else {
- var elem = document.getElementById( match[3] );
-
- // Handle the case where IE and Opera return items
- // by name instead of ID
- if ( elem && elem.id != match[3] )
- return jQuery().find( selector );
-
- // Otherwise, we inject the element directly into the jQuery object
- var ret = jQuery( elem || [] );
- ret.context = document;
- ret.selector = selector;
- return ret;
+ } else {
+ elem = document.getElementById( match[2] );
+
+ if ( elem ) {
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem.id !== match[2] ) {
+ return rootjQuery.find( selector );
+ }
+
+ // Otherwise, we inject the element directly into the jQuery object
+ this.length = 1;
+ this[0] = elem;
+ }
+
+ this.context = document;
+ this.selector = selector;
+ return this;
}
- // HANDLE: $(expr, [context])
- // (which is just equivalent to: $(content).find(expr)
- } else
+ // HANDLE: $("TAG")
+ } else if ( !context && /^\w+$/.test( selector ) ) {
+ this.selector = selector;
+ this.context = document;
+ selector = document.getElementsByTagName( selector );
+
+ // HANDLE: $(expr, $(...))
+ } else if ( !context || context.jquery ) {
+ return (context || rootjQuery).find( selector );
+
+ // HANDLE: $(expr, context)
+ // (which is just equivalent to: $(context).find(expr)
+ } else {
return jQuery( context ).find( selector );
+ }
// HANDLE: $(function)
// Shortcut for document ready
- } else if ( jQuery.isFunction( selector ) )
- return jQuery( document ).ready( selector );
+ } else if ( jQuery.isFunction( selector ) ) {
+ return rootjQuery.ready( selector );
+ }
- // Make sure that old selector state is passed along
- if ( selector.selector && selector.context ) {
+ if (selector.selector !== undefined) {
this.selector = selector.selector;
this.context = selector.context;
}
- return this.setArray(jQuery.isArray( selector ) ?
- selector :
- jQuery.makeArray(selector));
+ return jQuery.isArray( selector ) ?
+ this.setArray( selector ) :
+ jQuery.makeArray( selector, this );
},
// Start with an empty selector
selector: "",
// The current version of jQuery being used
- jquery: "1.3.2",
+ jquery: "1.4.1",
+
+ // The default length of a jQuery object is 0
+ length: 0,
// The number of elements contained in the matched element set
size: function() {
return this.length;
},
+ toArray: function() {
+ return slice.call( this, 0 );
+ },
+
// Get the Nth element in the matched element set OR
// Get the whole matched element set as a clean array
get: function( num ) {
- return num === undefined ?
+ return num == null ?
// Return a 'clean' array
- Array.prototype.slice.call( this ) :
+ this.toArray() :
// Return just the object
- this[ num ];
+ ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] );
},
// Take an array of elements and push it onto the stack
// (returning the new matched element set)
pushStack: function( elems, name, selector ) {
// Build a new jQuery matched element set
- var ret = jQuery( elems );
+ var ret = jQuery( elems || null );
// Add the old object onto the stack (as a reference)
ret.prevObject = this;
ret.context = this.context;
- if ( name === "find" )
+ if ( name === "find" ) {
ret.selector = this.selector + (this.selector ? " " : "") + selector;
- else if ( name )
+ } else if ( name ) {
ret.selector = this.selector + "." + name + "(" + selector + ")";
+ }
// Return the newly-formed element set
return ret;
@@ -143,7 +228,7 @@ jQuery.fn = jQuery.prototype = {
// Resetting the length to 0, then using the native Array push
// is a super-fast way to populate an object with array-like properties
this.length = 0;
- Array.prototype.push.apply( this, elems );
+ push.apply( this, elems );
return this;
},
@@ -154,822 +239,1208 @@ jQuery.fn = jQuery.prototype = {
each: function( callback, args ) {
return jQuery.each( this, callback, args );
},
+
+ ready: function( fn ) {
+ // Attach the listeners
+ jQuery.bindReady();
- // Determine the position of an element within
- // the matched set of elements
- index: function( elem ) {
- // Locate the position of the desired element
- return jQuery.inArray(
- // If it receives a jQuery object, the first element is used
- elem && elem.jquery ? elem[0] : elem
- , this );
- },
+ // If the DOM is already ready
+ if ( jQuery.isReady ) {
+ // Execute the function immediately
+ fn.call( document, jQuery );
- attr: function( name, value, type ) {
- var options = name;
+ // Otherwise, remember the function for later
+ } else if ( readyList ) {
+ // Add the function to the wait list
+ readyList.push( fn );
+ }
- // Look for the case where we're accessing a style value
- if ( typeof name === "string" )
- if ( value === undefined )
- return this[0] && jQuery[ type || "attr" ]( this[0], name );
+ return this;
+ },
+
+ eq: function( i ) {
+ return i === -1 ?
+ this.slice( i ) :
+ this.slice( i, +i + 1 );
+ },
- else {
- options = {};
- options[ name ] = value;
- }
+ first: function() {
+ return this.eq( 0 );
+ },
- // Check to see if we're setting style values
- return this.each(function(i){
- // Set all the styles
- for ( name in options )
- jQuery.attr(
- type ?
- this.style :
- this,
- name, jQuery.prop( this, options[ name ], type, i, name )
- );
- });
+ last: function() {
+ return this.eq( -1 );
},
- css: function( key, value ) {
- // ignore negative width and height values
- if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
- value = undefined;
- return this.attr( key, value, "curCSS" );
+ slice: function() {
+ return this.pushStack( slice.apply( this, arguments ),
+ "slice", slice.call(arguments).join(",") );
},
- text: function( text ) {
- if ( typeof text !== "object" && text != null )
- return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+ map: function( callback ) {
+ return this.pushStack( jQuery.map(this, function( elem, i ) {
+ return callback.call( elem, i, elem );
+ }));
+ },
+
+ end: function() {
+ return this.prevObject || jQuery(null);
+ },
+
+ // For internal use only.
+ // Behaves like an Array's method, not like a jQuery method.
+ push: push,
+ sort: [].sort,
+ splice: [].splice
+};
- var ret = "";
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
- jQuery.each( text || this, function(){
- jQuery.each( this.childNodes, function(){
- if ( this.nodeType != 8 )
- ret += this.nodeType != 1 ?
- this.nodeValue :
- jQuery.fn.text( [ this ] );
- });
- });
+jQuery.extend = jQuery.fn.extend = function() {
+ // copy reference to target object
+ var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy;
- return ret;
- },
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
- wrapAll: function( html ) {
- if ( this[0] ) {
- // The elements to wrap the target around
- var wrap = jQuery( html, this[0].ownerDocument ).clone();
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+ target = {};
+ }
- if ( this[0].parentNode )
- wrap.insertBefore( this[0] );
+ // extend jQuery itself if only one argument is passed
+ if ( length === i ) {
+ target = this;
+ --i;
+ }
- wrap.map(function(){
- var elem = this;
+ for ( ; i < length; i++ ) {
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null ) {
+ // Extend the base object
+ for ( name in options ) {
+ src = target[ name ];
+ copy = options[ name ];
- while ( elem.firstChild )
- elem = elem.firstChild;
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
- return elem;
- }).append(this);
+ // Recurse if we're merging object literal values or arrays
+ if ( deep && copy && ( jQuery.isPlainObject(copy) || jQuery.isArray(copy) ) ) {
+ var clone = src && ( jQuery.isPlainObject(src) || jQuery.isArray(src) ) ? src
+ : jQuery.isArray(copy) ? [] : {};
+
+ // Never move original objects, clone them
+ target[ name ] = jQuery.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
}
+ }
- return this;
- },
+ // Return the modified object
+ return target;
+};
- wrapInner: function( html ) {
- return this.each(function(){
- jQuery( this ).contents().wrapAll( html );
- });
+jQuery.extend({
+ noConflict: function( deep ) {
+ window.$ = _$;
+
+ if ( deep ) {
+ window.jQuery = _jQuery;
+ }
+
+ return jQuery;
},
+
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+
+ // Handle when the DOM is ready
+ ready: function() {
+ // Make sure that the DOM is not already loaded
+ if ( !jQuery.isReady ) {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !document.body ) {
+ return setTimeout( jQuery.ready, 13 );
+ }
- wrap: function( html ) {
- return this.each(function(){
- jQuery( this ).wrapAll( html );
- });
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If there are functions bound, to execute
+ if ( readyList ) {
+ // Execute all of them
+ var fn, i = 0;
+ while ( (fn = readyList[ i++ ]) ) {
+ fn.call( document, jQuery );
+ }
+
+ // Reset the list of functions
+ readyList = null;
+ }
+
+ // Trigger any bound ready events
+ if ( jQuery.fn.triggerHandler ) {
+ jQuery( document ).triggerHandler( "ready" );
+ }
+ }
},
+
+ bindReady: function() {
+ if ( readyBound ) {
+ return;
+ }
- append: function() {
- return this.domManip(arguments, true, function(elem){
- if (this.nodeType == 1)
- this.appendChild( elem );
- });
+ readyBound = true;
+
+ // Catch cases where $(document).ready() is called after the
+ // browser event has already occurred.
+ if ( document.readyState === "complete" ) {
+ return jQuery.ready();
+ }
+
+ // Mozilla, Opera and webkit nightlies currently support this event
+ if ( document.addEventListener ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", jQuery.ready, false );
+
+ // If IE event model is used
+ } else if ( document.attachEvent ) {
+ // ensure firing before onload,
+ // maybe late but safe also for iframes
+ document.attachEvent("onreadystatechange", DOMContentLoaded);
+
+ // A fallback to window.onload, that will always work
+ window.attachEvent( "onload", jQuery.ready );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ var toplevel = false;
+
+ try {
+ toplevel = window.frameElement == null;
+ } catch(e) {}
+
+ if ( document.documentElement.doScroll && toplevel ) {
+ doScrollCheck();
+ }
+ }
},
- prepend: function() {
- return this.domManip(arguments, true, function(elem){
- if (this.nodeType == 1)
- this.insertBefore( elem, this.firstChild );
- });
+ // See test/unit/core.js for details concerning isFunction.
+ // Since version 1.3, DOM methods and functions like alert
+ // aren't supported. They return false on IE (#2968).
+ isFunction: function( obj ) {
+ return toString.call(obj) === "[object Function]";
},
- before: function() {
- return this.domManip(arguments, false, function(elem){
- this.parentNode.insertBefore( elem, this );
- });
+ isArray: function( obj ) {
+ return toString.call(obj) === "[object Array]";
},
- after: function() {
- return this.domManip(arguments, false, function(elem){
- this.parentNode.insertBefore( elem, this.nextSibling );
- });
+ isPlainObject: function( obj ) {
+ // Must be an Object.
+ // Because of IE, we also have to check the presence of the constructor property.
+ // Make sure that DOM nodes and window objects don't pass through, as well
+ if ( !obj || toString.call(obj) !== "[object Object]" || obj.nodeType || obj.setInterval ) {
+ return false;
+ }
+
+ // Not own constructor property must be Object
+ if ( obj.constructor
+ && !hasOwnProperty.call(obj, "constructor")
+ && !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf") ) {
+ return false;
+ }
+
+ // Own properties are enumerated firstly, so to speed up,
+ // if last one is own, then all properties are own.
+
+ var key;
+ for ( key in obj ) {}
+
+ return key === undefined || hasOwnProperty.call( obj, key );
},
- end: function() {
- return this.prevObject || jQuery( [] );
+ isEmptyObject: function( obj ) {
+ for ( var name in obj ) {
+ return false;
+ }
+ return true;
},
+
+ error: function( msg ) {
+ throw msg;
+ },
+
+ parseJSON: function( data ) {
+ if ( typeof data !== "string" || !data ) {
+ return null;
+ }
+
+ // Make sure the incoming data is actual JSON
+ // Logic borrowed from http://json.org/json2.js
+ if ( /^[\],:{}\s]*$/.test(data.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@")
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]")
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, "")) ) {
- // For internal use only.
- // Behaves like an Array's method, not like a jQuery method.
- push: [].push,
- sort: [].sort,
- splice: [].splice,
+ // Try to use the native JSON parser first
+ return window.JSON && window.JSON.parse ?
+ window.JSON.parse( data ) :
+ (new Function("return " + data))();
- find: function( selector ) {
- if ( this.length === 1 ) {
- var ret = this.pushStack( [], "find", selector );
- ret.length = 0;
- jQuery.find( selector, this[0], ret );
- return ret;
} else {
- return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){
- return jQuery.find( selector, elem );
- })), "find", selector );
+ jQuery.error( "Invalid JSON: " + data );
}
},
- clone: function( events ) {
- // Do the clone
- var ret = this.map(function(){
- if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
- // IE copies events bound via attachEvent when
- // using cloneNode. Calling detachEvent on the
- // clone will also remove the events from the orignal
- // In order to get around this, we use innerHTML.
- // Unfortunately, this means some modifications to
- // attributes in IE that are actually only stored
- // as properties will not be copied (such as the
- // the name attribute on an input).
- var html = this.outerHTML;
- if ( !html ) {
- var div = this.ownerDocument.createElement("div");
- div.appendChild( this.cloneNode(true) );
- html = div.innerHTML;
- }
+ noop: function() {},
- return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0];
- } else
- return this.cloneNode(true);
- });
+ // Evalulates a script in a global context
+ globalEval: function( data ) {
+ if ( data && rnotwhite.test(data) ) {
+ // Inspired by code by Andrea Giammarchi
+ // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
+ var head = document.getElementsByTagName("head")[0] || document.documentElement,
+ script = document.createElement("script");
- // Copy the events from the original to the clone
- if ( events === true ) {
- var orig = this.find("*").andSelf(), i = 0;
+ script.type = "text/javascript";
- ret.find("*").andSelf().each(function(){
- if ( this.nodeName !== orig[i].nodeName )
- return;
+ if ( jQuery.support.scriptEval ) {
+ script.appendChild( document.createTextNode( data ) );
+ } else {
+ script.text = data;
+ }
- var events = jQuery.data( orig[i], "events" );
+ // Use insertBefore instead of appendChild to circumvent an IE6 bug.
+ // This arises when a base node is used (#2709).
+ head.insertBefore( script, head.firstChild );
+ head.removeChild( script );
+ }
+ },
- for ( var type in events ) {
- for ( var handler in events[ type ] ) {
- jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
+ nodeName: function( elem, name ) {
+ return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
+ },
+
+ // args is for internal usage only
+ each: function( object, callback, args ) {
+ var name, i = 0,
+ length = object.length,
+ isObj = length === undefined || jQuery.isFunction(object);
+
+ if ( args ) {
+ if ( isObj ) {
+ for ( name in object ) {
+ if ( callback.apply( object[ name ], args ) === false ) {
+ break;
}
}
+ } else {
+ for ( ; i < length; ) {
+ if ( callback.apply( object[ i++ ], args ) === false ) {
+ break;
+ }
+ }
+ }
- i++;
- });
+ // A special, fast, case for the most common use of each
+ } else {
+ if ( isObj ) {
+ for ( name in object ) {
+ if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( var value = object[0];
+ i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {}
+ }
}
- // Return the cloned set
- return ret;
+ return object;
},
- filter: function( selector ) {
- return this.pushStack(
- jQuery.isFunction( selector ) &&
- jQuery.grep(this, function(elem, i){
- return selector.call( elem, i );
- }) ||
-
- jQuery.multiFilter( selector, jQuery.grep(this, function(elem){
- return elem.nodeType === 1;
- }) ), "filter", selector );
- },
-
- closest: function( selector ) {
- var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null,
- closer = 0;
-
- return this.map(function(){
- var cur = this;
- while ( cur && cur.ownerDocument ) {
- if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) {
- jQuery.data(cur, "closest", closer);
- return cur;
- }
- cur = cur.parentNode;
- closer++;
- }
- });
+ trim: function( text ) {
+ return (text || "").replace( rtrim, "" );
},
- not: function( selector ) {
- if ( typeof selector === "string" )
- // test special case where just one selector is passed in
- if ( isSimple.test( selector ) )
- return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector );
- else
- selector = jQuery.multiFilter( selector, this );
-
- var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType;
- return this.filter(function() {
- return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector;
- });
- },
+ // results is for internal usage only
+ makeArray: function( array, results ) {
+ var ret = results || [];
- add: function( selector ) {
- return this.pushStack( jQuery.unique( jQuery.merge(
- this.get(),
- typeof selector === "string" ?
- jQuery( selector ) :
- jQuery.makeArray( selector )
- )));
- },
+ if ( array != null ) {
+ // The window, strings (and functions) also have 'length'
+ // The extra typeof function check is to prevent crashes
+ // in Safari 2 (See: #3039)
+ if ( array.length == null || typeof array === "string" || jQuery.isFunction(array) || (typeof array !== "function" && array.setInterval) ) {
+ push.call( ret, array );
+ } else {
+ jQuery.merge( ret, array );
+ }
+ }
- is: function( selector ) {
- return !!selector && jQuery.multiFilter( selector, this ).length > 0;
+ return ret;
},
- hasClass: function( selector ) {
- return !!selector && this.is( "." + selector );
+ inArray: function( elem, array ) {
+ if ( array.indexOf ) {
+ return array.indexOf( elem );
+ }
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ if ( array[ i ] === elem ) {
+ return i;
+ }
+ }
+
+ return -1;
},
- val: function( value ) {
- if ( value === undefined ) {
- var elem = this[0];
+ merge: function( first, second ) {
+ var i = first.length, j = 0;
- if ( elem ) {
- if( jQuery.nodeName( elem, 'option' ) )
- return (elem.attributes.value || {}).specified ? elem.value : elem.text;
-
- // We need to handle select boxes special
- if ( jQuery.nodeName( elem, "select" ) ) {
- var index = elem.selectedIndex,
- values = [],
- options = elem.options,
- one = elem.type == "select-one";
+ if ( typeof second.length === "number" ) {
+ for ( var l = second.length; j < l; j++ ) {
+ first[ i++ ] = second[ j ];
+ }
+ } else {
+ while ( second[j] !== undefined ) {
+ first[ i++ ] = second[ j++ ];
+ }
+ }
- // Nothing was selected
- if ( index < 0 )
- return null;
+ first.length = i;
- // Loop through all the selected options
- for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
- var option = options[ i ];
+ return first;
+ },
- if ( option.selected ) {
- // Get the specifc value for the option
- value = jQuery(option).val();
+ grep: function( elems, callback, inv ) {
+ var ret = [];
- // We don't need an array for one selects
- if ( one )
- return value;
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( var i = 0, length = elems.length; i < length; i++ ) {
+ if ( !inv !== !callback( elems[ i ], i ) ) {
+ ret.push( elems[ i ] );
+ }
+ }
- // Multi-Selects return an array
- values.push( value );
- }
- }
+ return ret;
+ },
- return values;
- }
+ // arg is for internal usage only
+ map: function( elems, callback, arg ) {
+ var ret = [], value;
- // Everything else, we just grab the value
- return (elem.value || "").replace(/\r/g, "");
+ // Go through the array, translating each of the items to their
+ // new value (or values).
+ for ( var i = 0, length = elems.length; i < length; i++ ) {
+ value = callback( elems[ i ], i, arg );
+ if ( value != null ) {
+ ret[ ret.length ] = value;
}
-
- return undefined;
}
- if ( typeof value === "number" )
- value += '';
+ return ret.concat.apply( [], ret );
+ },
- return this.each(function(){
- if ( this.nodeType != 1 )
- return;
+ // A global GUID counter for objects
+ guid: 1,
- if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) )
- this.checked = (jQuery.inArray(this.value, value) >= 0 ||
- jQuery.inArray(this.name, value) >= 0);
+ proxy: function( fn, proxy, thisObject ) {
+ if ( arguments.length === 2 ) {
+ if ( typeof proxy === "string" ) {
+ thisObject = fn;
+ fn = thisObject[ proxy ];
+ proxy = undefined;
- else if ( jQuery.nodeName( this, "select" ) ) {
- var values = jQuery.makeArray(value);
+ } else if ( proxy && !jQuery.isFunction( proxy ) ) {
+ thisObject = proxy;
+ proxy = undefined;
+ }
+ }
- jQuery( "option", this ).each(function(){
- this.selected = (jQuery.inArray( this.value, values ) >= 0 ||
- jQuery.inArray( this.text, values ) >= 0);
- });
+ if ( !proxy && fn ) {
+ proxy = function() {
+ return fn.apply( thisObject || this, arguments );
+ };
+ }
- if ( !values.length )
- this.selectedIndex = -1;
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ if ( fn ) {
+ proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
+ }
- } else
- this.value = value;
- });
+ // So proxy can be declared as an argument
+ return proxy;
},
- html: function( value ) {
- return value === undefined ?
- (this[0] ?
- this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") :
- null) :
- this.empty().append( value );
- },
+ // Use of jQuery.browser is frowned upon.
+ // More details: http://docs.jquery.com/Utilities/jQuery.browser
+ uaMatch: function( ua ) {
+ ua = ua.toLowerCase();
- replaceWith: function( value ) {
- return this.after( value ).remove();
- },
+ var match = /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
+ /(opera)(?:.*version)?[ \/]([\w.]+)/.exec( ua ) ||
+ /(msie) ([\w.]+)/.exec( ua ) ||
+ !/compatible/.test( ua ) && /(mozilla)(?:.*? rv:([\w.]+))?/.exec( ua ) ||
+ [];
- eq: function( i ) {
- return this.slice( i, +i + 1 );
+ return { browser: match[1] || "", version: match[2] || "0" };
},
- slice: function() {
- return this.pushStack( Array.prototype.slice.apply( this, arguments ),
- "slice", Array.prototype.slice.call(arguments).join(",") );
- },
+ browser: {}
+});
- map: function( callback ) {
- return this.pushStack( jQuery.map(this, function(elem, i){
- return callback.call( elem, i, elem );
- }));
- },
+browserMatch = jQuery.uaMatch( userAgent );
+if ( browserMatch.browser ) {
+ jQuery.browser[ browserMatch.browser ] = true;
+ jQuery.browser.version = browserMatch.version;
+}
- andSelf: function() {
- return this.add( this.prevObject );
- },
+// Deprecated, use jQuery.browser.webkit instead
+if ( jQuery.browser.webkit ) {
+ jQuery.browser.safari = true;
+}
- domManip: function( args, table, callback ) {
- if ( this[0] ) {
- var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(),
- scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ),
- first = fragment.firstChild;
-
- if ( first )
- for ( var i = 0, l = this.length; i < l; i++ )
- callback.call( root(this[i], first), this.length > 1 || i > 0 ?
- fragment.cloneNode(true) : fragment );
-
- if ( scripts )
- jQuery.each( scripts, evalScript );
- }
+if ( indexOf ) {
+ jQuery.inArray = function( elem, array ) {
+ return indexOf.call( array, elem );
+ };
+}
- return this;
-
- function root( elem, cur ) {
- return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ?
- (elem.getElementsByTagName("tbody")[0] ||
- elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
- elem;
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+
+// Cleanup functions for the document ready method
+if ( document.addEventListener ) {
+ DOMContentLoaded = function() {
+ document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+ jQuery.ready();
+ };
+
+} else if ( document.attachEvent ) {
+ DOMContentLoaded = function() {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( document.readyState === "complete" ) {
+ document.detachEvent( "onreadystatechange", DOMContentLoaded );
+ jQuery.ready();
}
+ };
+}
+
+// The DOM ready check for Internet Explorer
+function doScrollCheck() {
+ if ( jQuery.isReady ) {
+ return;
}
-};
-// Give the init function the jQuery prototype for later instantiation
-jQuery.fn.init.prototype = jQuery.fn;
+ try {
+ // If IE is used, use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ document.documentElement.doScroll("left");
+ } catch( error ) {
+ setTimeout( doScrollCheck, 1 );
+ return;
+ }
+
+ // and execute any waiting functions
+ jQuery.ready();
+}
function evalScript( i, elem ) {
- if ( elem.src )
+ if ( elem.src ) {
jQuery.ajax({
url: elem.src,
async: false,
dataType: "script"
});
-
- else
+ } else {
jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
+ }
- if ( elem.parentNode )
+ if ( elem.parentNode ) {
elem.parentNode.removeChild( elem );
+ }
}
-function now(){
- return +new Date;
+// Mutifunctional method to get and set values to a collection
+// The value/s can be optionally by executed if its a function
+function access( elems, key, value, exec, fn, pass ) {
+ var length = elems.length;
+
+ // Setting many attributes
+ if ( typeof key === "object" ) {
+ for ( var k in key ) {
+ access( elems, k, key[k], exec, fn, value );
+ }
+ return elems;
+ }
+
+ // Setting one attribute
+ if ( value !== undefined ) {
+ // Optionally, function values get executed if exec is true
+ exec = !pass && exec && jQuery.isFunction(value);
+
+ for ( var i = 0; i < length; i++ ) {
+ fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
+ }
+
+ return elems;
+ }
+
+ // Getting an attribute
+ return length ? fn( elems[0], key ) : null;
}
-jQuery.extend = jQuery.fn.extend = function() {
- // copy reference to target object
- var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
+function now() {
+ return (new Date).getTime();
+}
+(function() {
- // Handle a deep copy situation
- if ( typeof target === "boolean" ) {
- deep = target;
- target = arguments[1] || {};
- // skip the boolean and the target
- i = 2;
- }
+ jQuery.support = {};
- // Handle case when target is a string or something (possible in deep copy)
- if ( typeof target !== "object" && !jQuery.isFunction(target) )
- target = {};
+ var root = document.documentElement,
+ script = document.createElement("script"),
+ div = document.createElement("div"),
+ id = "script" + now();
- // extend jQuery itself if only one argument is passed
- if ( length == i ) {
- target = this;
- --i;
+ div.style.display = "none";
+ div.innerHTML = " <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
+
+ var all = div.getElementsByTagName("*"),
+ a = div.getElementsByTagName("a")[0];
+
+ // Can't get basic test support
+ if ( !all || !all.length || !a ) {
+ return;
}
- for ( ; i < length; i++ )
- // Only deal with non-null/undefined values
- if ( (options = arguments[ i ]) != null )
- // Extend the base object
- for ( var name in options ) {
- var src = target[ name ], copy = options[ name ];
+ jQuery.support = {
+ // IE strips leading whitespace when .innerHTML is used
+ leadingWhitespace: div.firstChild.nodeType === 3,
- // Prevent never-ending loop
- if ( target === copy )
- continue;
+ // Make sure that tbody elements aren't automatically inserted
+ // IE will insert them into empty tables
+ tbody: !div.getElementsByTagName("tbody").length,
- // Recurse if we're merging object values
- if ( deep && copy && typeof copy === "object" && !copy.nodeType )
- target[ name ] = jQuery.extend( deep,
- // Never move original objects, clone them
- src || ( copy.length != null ? [ ] : { } )
- , copy );
+ // Make sure that link elements get serialized correctly by innerHTML
+ // This requires a wrapper element in IE
+ htmlSerialize: !!div.getElementsByTagName("link").length,
- // Don't bring in undefined values
- else if ( copy !== undefined )
- target[ name ] = copy;
+ // Get the style information from getAttribute
+ // (IE uses .cssText insted)
+ style: /red/.test( a.getAttribute("style") ),
- }
+ // Make sure that URLs aren't manipulated
+ // (IE normalizes it by default)
+ hrefNormalized: a.getAttribute("href") === "/a",
- // Return the modified object
- return target;
-};
+ // Make sure that element opacity exists
+ // (IE uses filter instead)
+ // Use a regex to work around a WebKit issue. See #5145
+ opacity: /^0.55$/.test( a.style.opacity ),
-// exclude the following css properties to add px
-var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
- // cache defaultView
- defaultView = document.defaultView || {},
- toString = Object.prototype.toString;
+ // Verify style float existence
+ // (IE uses styleFloat instead of cssFloat)
+ cssFloat: !!a.style.cssFloat,
-jQuery.extend({
- noConflict: function( deep ) {
- window.$ = _$;
+ // Make sure that if no value is specified for a checkbox
+ // that it defaults to "on".
+ // (WebKit defaults to "" instead)
+ checkOn: div.getElementsByTagName("input")[0].value === "on",
- if ( deep )
- window.jQuery = _jQuery;
+ // Make sure that a selected-by-default option has a working selected property.
+ // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+ optSelected: document.createElement("select").appendChild( document.createElement("option") ).selected,
- return jQuery;
- },
+ // Will be defined later
+ checkClone: false,
+ scriptEval: false,
+ noCloneEvent: true,
+ boxModel: null
+ };
- // See test/unit/core.js for details concerning isFunction.
- // Since version 1.3, DOM methods and functions like alert
- // aren't supported. They return false on IE (#2968).
- isFunction: function( obj ) {
- return toString.call(obj) === "[object Function]";
- },
+ script.type = "text/javascript";
+ try {
+ script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
+ } catch(e) {}
- isArray: function( obj ) {
- return toString.call(obj) === "[object Array]";
- },
+ root.insertBefore( script, root.firstChild );
- // check if an element is in a (or is an) XML document
- isXMLDoc: function( elem ) {
- return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
- !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument );
- },
+ // Make sure that the execution of code works by injecting a script
+ // tag with appendChild/createTextNode
+ // (IE doesn't support this, fails, and uses .text instead)
+ if ( window[ id ] ) {
+ jQuery.support.scriptEval = true;
+ delete window[ id ];
+ }
- // Evalulates a script in a global context
- globalEval: function( data ) {
- if ( data && /\S/.test(data) ) {
- // Inspired by code by Andrea Giammarchi
- // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
- var head = document.getElementsByTagName("head")[0] || document.documentElement,
- script = document.createElement("script");
+ root.removeChild( script );
- script.type = "text/javascript";
- if ( jQuery.support.scriptEval )
- script.appendChild( document.createTextNode( data ) );
- else
- script.text = data;
+ if ( div.attachEvent && div.fireEvent ) {
+ div.attachEvent("onclick", function click() {
+ // Cloning a node shouldn't copy over any
+ // bound event handlers (IE does this)
+ jQuery.support.noCloneEvent = false;
+ div.detachEvent("onclick", click);
+ });
+ div.cloneNode(true).fireEvent("onclick");
+ }
- // Use insertBefore instead of appendChild to circumvent an IE6 bug.
- // This arises when a base node is used (#2709).
- head.insertBefore( script, head.firstChild );
- head.removeChild( script );
- }
- },
+ div = document.createElement("div");
+ div.innerHTML = "<input type='radio' name='radiotest' checked='checked'/>";
- nodeName: function( elem, name ) {
- return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
+ var fragment = document.createDocumentFragment();
+ fragment.appendChild( div.firstChild );
+
+ // WebKit doesn't clone checked state correctly in fragments
+ jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked;
+
+ // Figure out if the W3C box model works as expected
+ // document.body must exist before we can do this
+ jQuery(function() {
+ var div = document.createElement("div");
+ div.style.width = div.style.paddingLeft = "1px";
+
+ document.body.appendChild( div );
+ jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
+ document.body.removeChild( div ).style.display = 'none';
+ div = null;
+ });
+
+ // Technique from Juriy Zaytsev
+ // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
+ var eventSupported = function( eventName ) {
+ var el = document.createElement("div");
+ eventName = "on" + eventName;
+
+ var isSupported = (eventName in el);
+ if ( !isSupported ) {
+ el.setAttribute(eventName, "return;");
+ isSupported = typeof el[eventName] === "function";
+ }
+ el = null;
+
+ return isSupported;
+ };
+
+ jQuery.support.submitBubbles = eventSupported("submit");
+ jQuery.support.changeBubbles = eventSupported("change");
+
+ // release memory in IE
+ root = script = div = all = a = null;
+})();
+
+jQuery.props = {
+ "for": "htmlFor",
+ "class": "className",
+ readonly: "readOnly",
+ maxlength: "maxLength",
+ cellspacing: "cellSpacing",
+ rowspan: "rowSpan",
+ colspan: "colSpan",
+ tabindex: "tabIndex",
+ usemap: "useMap",
+ frameborder: "frameBorder"
+};
+var expando = "jQuery" + now(), uuid = 0, windowData = {};
+var emptyObject = {};
+
+jQuery.extend({
+ cache: {},
+
+ expando:expando,
+
+ // The following elements throw uncatchable exceptions if you
+ // attempt to add expando properties to them.
+ noData: {
+ "embed": true,
+ "object": true,
+ "applet": true
},
- // args is for internal usage only
- each: function( object, callback, args ) {
- var name, i = 0, length = object.length;
+ data: function( elem, name, data ) {
+ if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
+ return;
+ }
- if ( args ) {
- if ( length === undefined ) {
- for ( name in object )
- if ( callback.apply( object[ name ], args ) === false )
- break;
- } else
- for ( ; i < length; )
- if ( callback.apply( object[ i++ ], args ) === false )
- break;
+ elem = elem == window ?
+ windowData :
+ elem;
- // A special, fast, case for the most common use of each
+ var id = elem[ expando ], cache = jQuery.cache, thisCache;
+
+ // Handle the case where there's no name immediately
+ if ( !name && !id ) {
+ return null;
+ }
+
+ // Compute a unique ID for the element
+ if ( !id ) {
+ id = ++uuid;
+ }
+
+ // Avoid generating a new cache unless none exists and we
+ // want to manipulate it.
+ if ( typeof name === "object" ) {
+ elem[ expando ] = id;
+ thisCache = cache[ id ] = jQuery.extend(true, {}, name);
+ } else if ( cache[ id ] ) {
+ thisCache = cache[ id ];
+ } else if ( typeof data === "undefined" ) {
+ thisCache = emptyObject;
} else {
- if ( length === undefined ) {
- for ( name in object )
- if ( callback.call( object[ name ], name, object[ name ] ) === false )
- break;
- } else
- for ( var value = object[0];
- i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
+ thisCache = cache[ id ] = {};
}
- return object;
+ // Prevent overriding the named cache with undefined values
+ if ( data !== undefined ) {
+ elem[ expando ] = id;
+ thisCache[ name ] = data;
+ }
+
+ return typeof name === "string" ? thisCache[ name ] : thisCache;
},
- prop: function( elem, value, type, i, name ) {
- // Handle executable functions
- if ( jQuery.isFunction( value ) )
- value = value.call( elem, i );
+ removeData: function( elem, name ) {
+ if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
+ return;
+ }
- // Handle passing in a number to a CSS property
- return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ?
- value + "px" :
- value;
- },
+ elem = elem == window ?
+ windowData :
+ elem;
+
+ var id = elem[ expando ], cache = jQuery.cache, thisCache = cache[ id ];
+
+ // If we want to remove a specific section of the element's data
+ if ( name ) {
+ if ( thisCache ) {
+ // Remove the section of cache data
+ delete thisCache[ name ];
+
+ // If we've removed all the data, remove the element's cache
+ if ( jQuery.isEmptyObject(thisCache) ) {
+ jQuery.removeData( elem );
+ }
+ }
+
+ // Otherwise, we want to remove all of the element's data
+ } else {
+ // Clean up the element expando
+ try {
+ delete elem[ expando ];
+ } catch( e ) {
+ // IE has trouble directly removing the expando
+ // but it's ok with using removeAttribute
+ if ( elem.removeAttribute ) {
+ elem.removeAttribute( expando );
+ }
+ }
+
+ // Completely remove the data cache
+ delete cache[ id ];
+ }
+ }
+});
- className: {
- // internal only, use addClass("class")
- add: function( elem, classNames ) {
- jQuery.each((classNames || "").split(/\s+/), function(i, className){
- if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) )
- elem.className += (elem.className ? " " : "") + className;
+jQuery.fn.extend({
+ data: function( key, value ) {
+ if ( typeof key === "undefined" && this.length ) {
+ return jQuery.data( this[0] );
+
+ } else if ( typeof key === "object" ) {
+ return this.each(function() {
+ jQuery.data( this, key );
});
- },
+ }
- // internal only, use removeClass("class")
- remove: function( elem, classNames ) {
- if (elem.nodeType == 1)
- elem.className = classNames !== undefined ?
- jQuery.grep(elem.className.split(/\s+/), function(className){
- return !jQuery.className.has( classNames, className );
- }).join(" ") :
- "";
- },
+ var parts = key.split(".");
+ parts[1] = parts[1] ? "." + parts[1] : "";
- // internal only, use hasClass("class")
- has: function( elem, className ) {
- return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1;
+ if ( value === undefined ) {
+ var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
+
+ if ( data === undefined && this.length ) {
+ data = jQuery.data( this[0], key );
+ }
+ return data === undefined && parts[1] ?
+ this.data( parts[0] ) :
+ data;
+ } else {
+ return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function() {
+ jQuery.data( this, key, value );
+ });
}
},
- // A method for quickly swapping in/out CSS properties to get correct calculations
- swap: function( elem, options, callback ) {
- var old = {};
- // Remember the old values, and insert the new ones
- for ( var name in options ) {
- old[ name ] = elem.style[ name ];
- elem.style[ name ] = options[ name ];
+ removeData: function( key ) {
+ return this.each(function() {
+ jQuery.removeData( this, key );
+ });
+ }
+});
+jQuery.extend({
+ queue: function( elem, type, data ) {
+ if ( !elem ) {
+ return;
}
- callback.call( elem );
+ type = (type || "fx") + "queue";
+ var q = jQuery.data( elem, type );
- // Revert the old values
- for ( var name in options )
- elem.style[ name ] = old[ name ];
+ // Speed up dequeue by getting out quickly if this is just a lookup
+ if ( !data ) {
+ return q || [];
+ }
+
+ if ( !q || jQuery.isArray(data) ) {
+ q = jQuery.data( elem, type, jQuery.makeArray(data) );
+
+ } else {
+ q.push( data );
+ }
+
+ return q;
},
- css: function( elem, name, force, extra ) {
- if ( name == "width" || name == "height" ) {
- var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];
+ dequeue: function( elem, type ) {
+ type = type || "fx";
- function getWH() {
- val = name == "width" ? elem.offsetWidth : elem.offsetHeight;
+ var queue = jQuery.queue( elem, type ), fn = queue.shift();
- if ( extra === "border" )
- return;
+ // If the fx queue is dequeued, always remove the progress sentinel
+ if ( fn === "inprogress" ) {
+ fn = queue.shift();
+ }
- jQuery.each( which, function() {
- if ( !extra )
- val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
- if ( extra === "margin" )
- val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0;
- else
- val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
- });
+ if ( fn ) {
+ // Add a progress sentinel to prevent the fx queue from being
+ // automatically dequeued
+ if ( type === "fx" ) {
+ queue.unshift("inprogress");
}
- if ( elem.offsetWidth !== 0 )
- getWH();
- else
- jQuery.swap( elem, props, getWH );
+ fn.call(elem, function() {
+ jQuery.dequeue(elem, type);
+ });
+ }
+ }
+});
- return Math.max(0, Math.round(val));
+jQuery.fn.extend({
+ queue: function( type, data ) {
+ if ( typeof type !== "string" ) {
+ data = type;
+ type = "fx";
}
- return jQuery.curCSS( elem, name, force );
- },
+ if ( data === undefined ) {
+ return jQuery.queue( this[0], type );
+ }
+ return this.each(function( i, elem ) {
+ var queue = jQuery.queue( this, type, data );
- curCSS: function( elem, name, force ) {
- var ret, style = elem.style;
+ if ( type === "fx" && queue[0] !== "inprogress" ) {
+ jQuery.dequeue( this, type );
+ }
+ });
+ },
+ dequeue: function( type ) {
+ return this.each(function() {
+ jQuery.dequeue( this, type );
+ });
+ },
- // We need to handle opacity special in IE
- if ( name == "opacity" && !jQuery.support.opacity ) {
- ret = jQuery.attr( style, "opacity" );
+ // Based off of the plugin by Clint Helfers, with permission.
+ // http://blindsignals.com/index.php/2009/07/jquery-delay/
+ delay: function( time, type ) {
+ time = jQuery.fx ? jQuery.fx.speeds[time] || time : time;
+ type = type || "fx";
+
+ return this.queue( type, function() {
+ var elem = this;
+ setTimeout(function() {
+ jQuery.dequeue( elem, type );
+ }, time );
+ });
+ },
- return ret == "" ?
- "1" :
- ret;
- }
+ clearQueue: function( type ) {
+ return this.queue( type || "fx", [] );
+ }
+});
+var rclass = /[\n\t]/g,
+ rspace = /\s+/,
+ rreturn = /\r/g,
+ rspecialurl = /href|src|style/,
+ rtype = /(button|input)/i,
+ rfocusable = /(button|input|object|select|textarea)/i,
+ rclickable = /^(a|area)$/i,
+ rradiocheck = /radio|checkbox/;
- // Make sure we're using the right name for getting the float value
- if ( name.match( /float/i ) )
- name = styleFloat;
+jQuery.fn.extend({
+ attr: function( name, value ) {
+ return access( this, name, value, true, jQuery.attr );
+ },
- if ( !force && style && style[ name ] )
- ret = style[ name ];
+ removeAttr: function( name, fn ) {
+ return this.each(function(){
+ jQuery.attr( this, name, "" );
+ if ( this.nodeType === 1 ) {
+ this.removeAttribute( name );
+ }
+ });
+ },
- else if ( defaultView.getComputedStyle ) {
+ addClass: function( value ) {
+ if ( jQuery.isFunction(value) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ self.addClass( value.call(this, i, self.attr("class")) );
+ });
+ }
- // Only "float" is needed here
- if ( name.match( /float/i ) )
- name = "float";
+ if ( value && typeof value === "string" ) {
+ var classNames = (value || "").split( rspace );
- name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ var elem = this[i];
- var computedStyle = defaultView.getComputedStyle( elem, null );
+ if ( elem.nodeType === 1 ) {
+ if ( !elem.className ) {
+ elem.className = value;
- if ( computedStyle )
- ret = computedStyle.getPropertyValue( name );
+ } else {
+ var className = " " + elem.className + " ";
+ for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
+ if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) {
+ elem.className += " " + classNames[c];
+ }
+ }
+ }
+ }
+ }
+ }
- // We should always get a number back from opacity
- if ( name == "opacity" && ret == "" )
- ret = "1";
+ return this;
+ },
- } else if ( elem.currentStyle ) {
- var camelCase = name.replace(/\-(\w)/g, function(all, letter){
- return letter.toUpperCase();
+ removeClass: function( value ) {
+ if ( jQuery.isFunction(value) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ self.removeClass( value.call(this, i, self.attr("class")) );
});
+ }
- ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
+ if ( (value && typeof value === "string") || value === undefined ) {
+ var classNames = (value || "").split(rspace);
- // From the awesome hack by Dean Edwards
- // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ var elem = this[i];
- // If we're not dealing with a regular pixel number
- // but a number that has a weird ending, we need to convert it to pixels
- if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
- // Remember the original values
- var left = style.left, rsLeft = elem.runtimeStyle.left;
-
- // Put in the new values to get a computed value out
- elem.runtimeStyle.left = elem.currentStyle.left;
- style.left = ret || 0;
- ret = style.pixelLeft + "px";
+ if ( elem.nodeType === 1 && elem.className ) {
+ if ( value ) {
+ var className = (" " + elem.className + " ").replace(rclass, " ");
+ for ( var c = 0, cl = classNames.length; c < cl; c++ ) {
+ className = className.replace(" " + classNames[c] + " ", " ");
+ }
+ elem.className = className.substring(1, className.length - 1);
- // Revert the changed values
- style.left = left;
- elem.runtimeStyle.left = rsLeft;
+ } else {
+ elem.className = "";
+ }
+ }
}
}
- return ret;
+ return this;
},
- clean: function( elems, context, fragment ) {
- context = context || document;
-
- // !context.createElement fails in IE with an error but returns typeof 'object'
- if ( typeof context.createElement === "undefined" )
- context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+ toggleClass: function( value, stateVal ) {
+ var type = typeof value, isBool = typeof stateVal === "boolean";
- // If a single string is passed in and it's a single tag
- // just do a createElement and skip the rest
- if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) {
- var match = /^<(\w+)\s*\/?>$/.exec(elems[0]);
- if ( match )
- return [ context.createElement( match[1] ) ];
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal );
+ });
}
- var ret = [], scripts = [], div = context.createElement("div");
+ return this.each(function() {
+ if ( type === "string" ) {
+ // toggle individual class names
+ var className, i = 0, self = jQuery(this),
+ state = stateVal,
+ classNames = value.split( rspace );
+
+ while ( (className = classNames[ i++ ]) ) {
+ // check each className given, space seperated list
+ state = isBool ? state : !self.hasClass( className );
+ self[ state ? "addClass" : "removeClass" ]( className );
+ }
- jQuery.each(elems, function(i, elem){
- if ( typeof elem === "number" )
- elem += '';
+ } else if ( type === "undefined" || type === "boolean" ) {
+ if ( this.className ) {
+ // store className if set
+ jQuery.data( this, "__className__", this.className );
+ }
- if ( !elem )
- return;
+ // toggle whole className
+ this.className = this.className || value === false ? "" : jQuery.data( this, "__className__" ) || "";
+ }
+ });
+ },
- // Convert html string into DOM nodes
- if ( typeof elem === "string" ) {
- // Fix "XHTML"-style tags in all browsers
- elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){
- return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ?
- all :
- front + "></" + tag + ">";
- });
+ hasClass: function( selector ) {
+ var className = " " + selector + " ";
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
+ return true;
+ }
+ }
- // Trim whitespace, otherwise indexOf won't work as expected
- var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase();
+ return false;
+ },
- var wrap =
- // option or optgroup
- !tags.indexOf("<opt") &&
- [ 1, "<select multiple='multiple'>", "</select>" ] ||
+ val: function( value ) {
+ if ( value === undefined ) {
+ var elem = this[0];
- !tags.indexOf("<leg") &&
- [ 1, "<fieldset>", "</fieldset>" ] ||
+ if ( elem ) {
+ if ( jQuery.nodeName( elem, "option" ) ) {
+ return (elem.attributes.value || {}).specified ? elem.value : elem.text;
+ }
- tags.match(/^<(thead|tbody|tfoot|colg|cap)/) &&
- [ 1, "<table>", "</table>" ] ||
+ // We need to handle select boxes special
+ if ( jQuery.nodeName( elem, "select" ) ) {
+ var index = elem.selectedIndex,
+ values = [],
+ options = elem.options,
+ one = elem.type === "select-one";
- !tags.indexOf("<tr") &&
- [ 2, "<table><tbody>", "</tbody></table>" ] ||
+ // Nothing was selected
+ if ( index < 0 ) {
+ return null;
+ }
- // <thead> matched above
- (!tags.indexOf("<td") || !tags.indexOf("<th")) &&
- [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ] ||
+ // Loop through all the selected options
+ for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
+ var option = options[ i ];
- !tags.indexOf("<col") &&
- [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ] ||
+ if ( option.selected ) {
+ // Get the specifc value for the option
+ value = jQuery(option).val();
- // IE can't serialize <link> and <script> tags normally
- !jQuery.support.htmlSerialize &&
- [ 1, "div<div>", "</div>" ] ||
+ // We don't need an array for one selects
+ if ( one ) {
+ return value;
+ }
- [ 0, "", "" ];
+ // Multi-Selects return an array
+ values.push( value );
+ }
+ }
- // Go to html and back, then peel off extra wrappers
- div.innerHTML = wrap[1] + elem + wrap[2];
+ return values;
+ }
- // Move to the right depth
- while ( wrap[0]-- )
- div = div.lastChild;
+ // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+ if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) {
+ return elem.getAttribute("value") === null ? "on" : elem.value;
+ }
+
- // Remove IE's autoinserted <tbody> from table fragments
- if ( !jQuery.support.tbody ) {
+ // Everything else, we just grab the value
+ return (elem.value || "").replace(rreturn, "");
- // String was a <table>, *may* have spurious <tbody>
- var hasBody = /<tbody/i.test(elem),
- tbody = !tags.indexOf("<table") && !hasBody ?
- div.firstChild && div.firstChild.childNodes :
+ }
- // String was a bare <thead> or <tfoot>
- wrap[1] == "<table>" && !hasBody ?
- div.childNodes :
- [];
+ return undefined;
+ }
- for ( var j = tbody.length - 1; j >= 0 ; --j )
- if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length )
- tbody[ j ].parentNode.removeChild( tbody[ j ] );
+ var isFunction = jQuery.isFunction(value);
- }
+ return this.each(function(i) {
+ var self = jQuery(this), val = value;
- // IE completely kills leading whitespace when innerHTML is used
- if ( !jQuery.support.leadingWhitespace && /^\s/.test( elem ) )
- div.insertBefore( context.createTextNode( elem.match(/^\s*/)[0] ), div.firstChild );
-
- elem = jQuery.makeArray( div.childNodes );
+ if ( this.nodeType !== 1 ) {
+ return;
}
- if ( elem.nodeType )
- ret.push( elem );
- else
- ret = jQuery.merge( ret, elem );
+ if ( isFunction ) {
+ val = value.call(this, i, self.val());
+ }
- });
+ // Typecast each time if the value is a Function and the appended
+ // value is therefore different each time.
+ if ( typeof val === "number" ) {
+ val += "";
+ }
- if ( fragment ) {
- for ( var i = 0; ret[i]; i++ ) {
- if ( jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
- scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
- } else {
- if ( ret[i].nodeType === 1 )
- ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
- fragment.appendChild( ret[i] );
+ if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) {
+ this.checked = jQuery.inArray( self.val(), val ) >= 0;
+
+ } else if ( jQuery.nodeName( this, "select" ) ) {
+ var values = jQuery.makeArray(val);
+
+ jQuery( "option", this ).each(function() {
+ this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+ });
+
+ if ( !values.length ) {
+ this.selectedIndex = -1;
}
+
+ } else {
+ this.value = val;
}
-
- return scripts;
- }
+ });
+ }
+});
- return ret;
+jQuery.extend({
+ attrFn: {
+ val: true,
+ css: true,
+ html: true,
+ text: true,
+ data: true,
+ width: true,
+ height: true,
+ offset: true
},
-
- attr: function( elem, name, value ) {
+
+ attr: function( elem, name, value, pass ) {
// don't set attributes on text and comment nodes
- if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
return undefined;
+ }
+
+ if ( pass && name in jQuery.attrFn ) {
+ return jQuery(elem)[name](value);
+ }
- var notxml = !jQuery.isXMLDoc( elem ),
+ var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ),
// Whether we are setting (or getting)
set = value !== undefined;
@@ -977,472 +1448,1120 @@ jQuery.extend({
name = notxml && jQuery.props[ name ] || name;
// Only do all the following if this is a node (faster for style)
- // IE elem.getAttribute passes even for style
- if ( elem.tagName ) {
-
+ if ( elem.nodeType === 1 ) {
// These attributes require special treatment
- var special = /href|src|style/.test( name );
+ var special = rspecialurl.test( name );
- // Safari mis-reports the default selected property of a hidden option
+ // Safari mis-reports the default selected property of an option
// Accessing the parent's selectedIndex property fixes it
- if ( name == "selected" && elem.parentNode )
- elem.parentNode.selectedIndex;
+ if ( name === "selected" && !jQuery.support.optSelected ) {
+ var parent = elem.parentNode;
+ if ( parent ) {
+ parent.selectedIndex;
+
+ // Make sure that it also works with optgroups, see #5701
+ if ( parent.parentNode ) {
+ parent.parentNode.selectedIndex;
+ }
+ }
+ }
// If applicable, access the attribute via the DOM 0 way
if ( name in elem && notxml && !special ) {
- if ( set ){
+ if ( set ) {
// We can't allow the type property to be changed (since it causes problems in IE)
- if ( name == "type" && jQuery.nodeName( elem, "input" ) && elem.parentNode )
- throw "type property can't be changed";
+ if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) {
+ jQuery.error( "type property can't be changed" );
+ }
elem[ name ] = value;
}
// browsers index elements by id/name on forms, give priority to attributes.
- if( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) )
+ if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) {
return elem.getAttributeNode( name ).nodeValue;
+ }
// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
- if ( name == "tabIndex" ) {
+ if ( name === "tabIndex" ) {
var attributeNode = elem.getAttributeNode( "tabIndex" );
- return attributeNode && attributeNode.specified
- ? attributeNode.value
- : elem.nodeName.match(/(button|input|object|select|textarea)/i)
- ? 0
- : elem.nodeName.match(/^(a|area)$/i) && elem.href
- ? 0
- : undefined;
+
+ return attributeNode && attributeNode.specified ?
+ attributeNode.value :
+ rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+ 0 :
+ undefined;
}
return elem[ name ];
}
- if ( !jQuery.support.style && notxml && name == "style" )
- return jQuery.attr( elem.style, "cssText", value );
+ if ( !jQuery.support.style && notxml && name === "style" ) {
+ if ( set ) {
+ elem.style.cssText = "" + value;
+ }
+
+ return elem.style.cssText;
+ }
- if ( set )
+ if ( set ) {
// convert the value to a string (all browsers do this but IE) see #1070
elem.setAttribute( name, "" + value );
+ }
- var attr = !jQuery.support.hrefNormalized && notxml && special
+ var attr = !jQuery.support.hrefNormalized && notxml && special ?
// Some attributes require a special call on IE
- ? elem.getAttribute( name, 2 )
- : elem.getAttribute( name );
+ elem.getAttribute( name, 2 ) :
+ elem.getAttribute( name );
// Non-existent attributes return null, we normalize to undefined
return attr === null ? undefined : attr;
}
// elem is actually elem.style ... set the style
+ // Using attr for specific style information is now deprecated. Use style insead.
+ return jQuery.style( elem, name, value );
+ }
+});
+var fcleanup = function( nm ) {
+ return nm.replace(/[^\w\s\.\|`]/g, function( ch ) {
+ return "\\" + ch;
+ });
+};
- // IE uses filters for opacity
- if ( !jQuery.support.opacity && name == "opacity" ) {
- if ( set ) {
- // IE has trouble with opacity if it does not have layout
- // Force it by setting the zoom level
- elem.zoom = 1;
+/*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code originated from
+ * Dean Edwards' addEvent library.
+ */
+jQuery.event = {
- // Set the alpha filter to set the opacity
- elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
- (parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
- }
+ // Bind an event to an element
+ // Original by Dean Edwards
+ add: function( elem, types, handler, data ) {
+ if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return;
+ }
- return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
- (parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
- "";
+ // For whatever reason, IE has trouble passing the window object
+ // around, causing it to be cloned in the process
+ if ( elem.setInterval && ( elem !== window && !elem.frameElement ) ) {
+ elem = window;
}
- name = name.replace(/-([a-z])/ig, function(all, letter){
- return letter.toUpperCase();
- });
+ // Make sure that the function being executed has a unique ID
+ if ( !handler.guid ) {
+ handler.guid = jQuery.guid++;
+ }
- if ( set )
- elem[ name ] = value;
+ // if data is passed, bind to handler
+ if ( data !== undefined ) {
+ // Create temporary function pointer to original handler
+ var fn = handler;
- return elem[ name ];
- },
+ // Create unique handler function, wrapped around original handler
+ handler = jQuery.proxy( fn );
- trim: function( text ) {
- return (text || "").replace( /^\s+|\s+$/g, "" );
- },
+ // Store data in unique handler
+ handler.data = data;
+ }
- makeArray: function( array ) {
- var ret = [];
+ // Init the element's event structure
+ var events = jQuery.data( elem, "events" ) || jQuery.data( elem, "events", {} ),
+ handle = jQuery.data( elem, "handle" ), eventHandle;
- if( array != null ){
- var i = array.length;
- // The window, strings (and functions) also have 'length'
- if( i == null || typeof array === "string" || jQuery.isFunction(array) || array.setInterval )
- ret[0] = array;
- else
- while( i )
- ret[--i] = array[i];
+ if ( !handle ) {
+ eventHandle = function() {
+ // Handle the second event of a trigger and when
+ // an event is called after a page has unloaded
+ return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
+ jQuery.event.handle.apply( eventHandle.elem, arguments ) :
+ undefined;
+ };
+
+ handle = jQuery.data( elem, "handle", eventHandle );
}
- return ret;
- },
+ // If no handle is found then we must be trying to bind to one of the
+ // banned noData elements
+ if ( !handle ) {
+ return;
+ }
- inArray: function( elem, array ) {
- for ( var i = 0, length = array.length; i < length; i++ )
- // Use === because on IE, window == document
- if ( array[ i ] === elem )
- return i;
+ // Add elem as a property of the handle function
+ // This is to prevent a memory leak with non-native
+ // event in IE.
+ handle.elem = elem;
- return -1;
+ // Handle multiple events separated by a space
+ // jQuery(...).bind("mouseover mouseout", fn);
+ types = types.split( /\s+/ );
+
+ var type, i = 0;
+
+ while ( (type = types[ i++ ]) ) {
+ // Namespaced event handlers
+ var namespaces = type.split(".");
+ type = namespaces.shift();
+
+ if ( i > 1 ) {
+ handler = jQuery.proxy( handler );
+
+ if ( data !== undefined ) {
+ handler.data = data;
+ }
+ }
+
+ handler.type = namespaces.slice(0).sort().join(".");
+
+ // Get the current list of functions bound to this event
+ var handlers = events[ type ],
+ special = this.special[ type ] || {};
+
+ // Init the event handler queue
+ if ( !handlers ) {
+ handlers = events[ type ] = {};
+
+ // Check for a special event handler
+ // Only use addEventListener/attachEvent if the special
+ // events handler returns false
+ if ( !special.setup || special.setup.call( elem, data, namespaces, handler) === false ) {
+ // Bind the global event handler to the element
+ if ( elem.addEventListener ) {
+ elem.addEventListener( type, handle, false );
+ } else if ( elem.attachEvent ) {
+ elem.attachEvent( "on" + type, handle );
+ }
+ }
+ }
+
+ if ( special.add ) {
+ var modifiedHandler = special.add.call( elem, handler, data, namespaces, handlers );
+ if ( modifiedHandler && jQuery.isFunction( modifiedHandler ) ) {
+ modifiedHandler.guid = modifiedHandler.guid || handler.guid;
+ modifiedHandler.data = modifiedHandler.data || handler.data;
+ modifiedHandler.type = modifiedHandler.type || handler.type;
+ handler = modifiedHandler;
+ }
+ }
+
+ // Add the function to the element's handler list
+ handlers[ handler.guid ] = handler;
+
+ // Keep track of which events have been used, for global triggering
+ this.global[ type ] = true;
+ }
+
+ // Nullify elem to prevent memory leaks in IE
+ elem = null;
},
- merge: function( first, second ) {
- // We have to loop this way because IE & Opera overwrite the length
- // expando of getElementsByTagName
- var i = 0, elem, pos = first.length;
- // Also, we need to make sure that the correct elements are being returned
- // (IE returns comment nodes in a '*' query)
- if ( !jQuery.support.getAll ) {
- while ( (elem = second[ i++ ]) != null )
- if ( elem.nodeType != 8 )
- first[ pos++ ] = elem;
-
- } else
- while ( (elem = second[ i++ ]) != null )
- first[ pos++ ] = elem;
+ global: {},
- return first;
+ // Detach an event or set of events from an element
+ remove: function( elem, types, handler ) {
+ // don't do events on text and comment nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return;
+ }
+
+ var events = jQuery.data( elem, "events" ), ret, type, fn;
+
+ if ( events ) {
+ // Unbind all events for the element
+ if ( types === undefined || (typeof types === "string" && types.charAt(0) === ".") ) {
+ for ( type in events ) {
+ this.remove( elem, type + (types || "") );
+ }
+ } else {
+ // types is actually an event object here
+ if ( types.type ) {
+ handler = types.handler;
+ types = types.type;
+ }
+
+ // Handle multiple events separated by a space
+ // jQuery(...).unbind("mouseover mouseout", fn);
+ types = types.split(/\s+/);
+ var i = 0;
+ while ( (type = types[ i++ ]) ) {
+ // Namespaced event handlers
+ var namespaces = type.split(".");
+ type = namespaces.shift();
+ var all = !namespaces.length,
+ cleaned = jQuery.map( namespaces.slice(0).sort(), fcleanup ),
+ namespace = new RegExp("(^|\\.)" + cleaned.join("\\.(?:.*\\.)?") + "(\\.|$)"),
+ special = this.special[ type ] || {};
+
+ if ( events[ type ] ) {
+ // remove the given handler for the given type
+ if ( handler ) {
+ fn = events[ type ][ handler.guid ];
+ delete events[ type ][ handler.guid ];
+
+ // remove all handlers for the given type
+ } else {
+ for ( var handle in events[ type ] ) {
+ // Handle the removal of namespaced events
+ if ( all || namespace.test( events[ type ][ handle ].type ) ) {
+ delete events[ type ][ handle ];
+ }
+ }
+ }
+
+ if ( special.remove ) {
+ special.remove.call( elem, namespaces, fn);
+ }
+
+ // remove generic event handler if no more handlers exist
+ for ( ret in events[ type ] ) {
+ break;
+ }
+ if ( !ret ) {
+ if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
+ if ( elem.removeEventListener ) {
+ elem.removeEventListener( type, jQuery.data( elem, "handle" ), false );
+ } else if ( elem.detachEvent ) {
+ elem.detachEvent( "on" + type, jQuery.data( elem, "handle" ) );
+ }
+ }
+ ret = null;
+ delete events[ type ];
+ }
+ }
+ }
+ }
+
+ // Remove the expando if it's no longer used
+ for ( ret in events ) {
+ break;
+ }
+ if ( !ret ) {
+ var handle = jQuery.data( elem, "handle" );
+ if ( handle ) {
+ handle.elem = null;
+ }
+ jQuery.removeData( elem, "events" );
+ jQuery.removeData( elem, "handle" );
+ }
+ }
},
- unique: function( array ) {
- var ret = [], done = {};
+ // bubbling is internal
+ trigger: function( event, data, elem /*, bubbling */ ) {
+ // Event object or event type
+ var type = event.type || event,
+ bubbling = arguments[3];
+
+ if ( !bubbling ) {
+ event = typeof event === "object" ?
+ // jQuery.Event object
+ event[expando] ? event :
+ // Object literal
+ jQuery.extend( jQuery.Event(type), event ) :
+ // Just the event type (string)
+ jQuery.Event(type);
+
+ if ( type.indexOf("!") >= 0 ) {
+ event.type = type = type.slice(0, -1);
+ event.exclusive = true;
+ }
+
+ // Handle a global trigger
+ if ( !elem ) {
+ // Don't bubble custom events when global (to avoid too much overhead)
+ event.stopPropagation();
+
+ // Only trigger if we've ever bound an event for it
+ if ( this.global[ type ] ) {
+ jQuery.each( jQuery.cache, function() {
+ if ( this.events && this.events[type] ) {
+ jQuery.event.trigger( event, data, this.handle.elem );
+ }
+ });
+ }
+ }
+
+ // Handle triggering a single element
+
+ // don't do events on text and comment nodes
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return undefined;
+ }
+
+ // Clean up in case it is reused
+ event.result = undefined;
+ event.target = elem;
+
+ // Clone the incoming data, if any
+ data = jQuery.makeArray( data );
+ data.unshift( event );
+ }
+
+ event.currentTarget = elem;
+
+ // Trigger the event, it is assumed that "handle" is a function
+ var handle = jQuery.data( elem, "handle" );
+ if ( handle ) {
+ handle.apply( elem, data );
+ }
+
+ var parent = elem.parentNode || elem.ownerDocument;
+ // Trigger an inline bound script
try {
+ if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) {
+ if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) {
+ event.result = false;
+ }
+ }
+
+ // prevent IE from throwing an error for some elements with some event types, see #3533
+ } catch (e) {}
- for ( var i = 0, length = array.length; i < length; i++ ) {
- var id = jQuery.data( array[ i ] );
+ if ( !event.isPropagationStopped() && parent ) {
+ jQuery.event.trigger( event, data, parent, true );
- if ( !done[ id ] ) {
- done[ id ] = true;
- ret.push( array[ i ] );
+ } else if ( !event.isDefaultPrevented() ) {
+ var target = event.target, old,
+ isClick = jQuery.nodeName(target, "a") && type === "click";
+
+ if ( !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) {
+ try {
+ if ( target[ type ] ) {
+ // Make sure that we don't accidentally re-trigger the onFOO events
+ old = target[ "on" + type ];
+
+ if ( old ) {
+ target[ "on" + type ] = null;
+ }
+
+ this.triggered = true;
+ target[ type ]();
+ }
+
+ // prevent IE from throwing an error for some elements with some event types, see #3533
+ } catch (e) {}
+
+ if ( old ) {
+ target[ "on" + type ] = old;
}
+
+ this.triggered = false;
}
+ }
+ },
+
+ handle: function( event ) {
+ // returned undefined or false
+ var all, handlers;
+
+ event = arguments[0] = jQuery.event.fix( event || window.event );
+ event.currentTarget = this;
+
+ // Namespaced event handlers
+ var namespaces = event.type.split(".");
+ event.type = namespaces.shift();
+
+ // Cache this now, all = true means, any handler
+ all = !namespaces.length && !event.exclusive;
+
+ var namespace = new RegExp("(^|\\.)" + namespaces.slice(0).sort().join("\\.(?:.*\\.)?") + "(\\.|$)");
+
+ handlers = ( jQuery.data(this, "events") || {} )[ event.type ];
+
+ for ( var j in handlers ) {
+ var handler = handlers[ j ];
+
+ // Filter the functions by class
+ if ( all || namespace.test(handler.type) ) {
+ // Pass in a reference to the handler function itself
+ // So that we can later remove it
+ event.handler = handler;
+ event.data = handler.data;
+
+ var ret = handler.apply( this, arguments );
+
+ if ( ret !== undefined ) {
+ event.result = ret;
+ if ( ret === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
- } catch( e ) {
- ret = array;
+ if ( event.isImmediatePropagationStopped() ) {
+ break;
+ }
+
+ }
}
- return ret;
+ return event.result;
},
- grep: function( elems, callback, inv ) {
- var ret = [];
+ props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
- // Go through the array, only saving the items
- // that pass the validator function
- for ( var i = 0, length = elems.length; i < length; i++ )
- if ( !inv != !callback( elems[ i ], i ) )
- ret.push( elems[ i ] );
+ fix: function( event ) {
+ if ( event[ expando ] ) {
+ return event;
+ }
- return ret;
+ // store a copy of the original event object
+ // and "clone" to set read-only properties
+ var originalEvent = event;
+ event = jQuery.Event( originalEvent );
+
+ for ( var i = this.props.length, prop; i; ) {
+ prop = this.props[ --i ];
+ event[ prop ] = originalEvent[ prop ];
+ }
+
+ // Fix target property, if necessary
+ if ( !event.target ) {
+ event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
+ }
+
+ // check if target is a textnode (safari)
+ if ( event.target.nodeType === 3 ) {
+ event.target = event.target.parentNode;
+ }
+
+ // Add relatedTarget, if necessary
+ if ( !event.relatedTarget && event.fromElement ) {
+ event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
+ }
+
+ // Calculate pageX/Y if missing and clientX/Y available
+ if ( event.pageX == null && event.clientX != null ) {
+ var doc = document.documentElement, body = document.body;
+ event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
+ event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
+ }
+
+ // Add which for key events
+ if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) ) {
+ event.which = event.charCode || event.keyCode;
+ }
+
+ // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
+ if ( !event.metaKey && event.ctrlKey ) {
+ event.metaKey = event.ctrlKey;
+ }
+
+ // Add which for click: 1 === left; 2 === middle; 3 === right
+ // Note: button is not normalized, so don't use it
+ if ( !event.which && event.button !== undefined ) {
+ event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
+ }
+
+ return event;
},
- map: function( elems, callback ) {
- var ret = [];
+ // Deprecated, use jQuery.guid instead
+ guid: 1E8,
- // Go through the array, translating each of the items to their
- // new value (or values).
- for ( var i = 0, length = elems.length; i < length; i++ ) {
- var value = callback( elems[ i ], i );
+ // Deprecated, use jQuery.proxy instead
+ proxy: jQuery.proxy,
- if ( value != null )
- ret[ ret.length ] = value;
+ special: {
+ ready: {
+ // Make sure the ready event is setup
+ setup: jQuery.bindReady,
+ teardown: jQuery.noop
+ },
+
+ live: {
+ add: function( proxy, data, namespaces, live ) {
+ jQuery.extend( proxy, data || {} );
+
+ proxy.guid += data.selector + data.live;
+ data.liveProxy = proxy;
+
+ jQuery.event.add( this, data.live, liveHandler, data );
+
+ },
+
+ remove: function( namespaces ) {
+ if ( namespaces.length ) {
+ var remove = 0, name = new RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
+
+ jQuery.each( (jQuery.data(this, "events").live || {}), function() {
+ if ( name.test(this.type) ) {
+ remove++;
+ }
+ });
+
+ if ( remove < 1 ) {
+ jQuery.event.remove( this, namespaces[0], liveHandler );
+ }
+ }
+ },
+ special: {}
+ },
+ beforeunload: {
+ setup: function( data, namespaces, fn ) {
+ // We only want to do this special case on windows
+ if ( this.setInterval ) {
+ this.onbeforeunload = fn;
+ }
+
+ return false;
+ },
+ teardown: function( namespaces, fn ) {
+ if ( this.onbeforeunload === fn ) {
+ this.onbeforeunload = null;
+ }
+ }
}
+ }
+};
- return ret.concat.apply( [], ret );
+jQuery.Event = function( src ) {
+ // Allow instantiation without the 'new' keyword
+ if ( !this.preventDefault ) {
+ return new jQuery.Event( src );
}
-});
-// Use of jQuery.browser is deprecated.
-// It's included for backwards compatibility and plugins,
-// although they should work to migrate away.
+ // Event object
+ if ( src && src.type ) {
+ this.originalEvent = src;
+ this.type = src.type;
+ // Event type
+ } else {
+ this.type = src;
+ }
-var userAgent = navigator.userAgent.toLowerCase();
+ // timeStamp is buggy for some events on Firefox(#3843)
+ // So we won't rely on the native value
+ this.timeStamp = now();
-// Figure out what browser is being used
-jQuery.browser = {
- version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1],
- safari: /webkit/.test( userAgent ),
- opera: /opera/.test( userAgent ),
- msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
- mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
+ // Mark it as fixed
+ this[ expando ] = true;
};
-jQuery.each({
- parent: function(elem){return elem.parentNode;},
- parents: function(elem){return jQuery.dir(elem,"parentNode");},
- next: function(elem){return jQuery.nth(elem,2,"nextSibling");},
- prev: function(elem){return jQuery.nth(elem,2,"previousSibling");},
- nextAll: function(elem){return jQuery.dir(elem,"nextSibling");},
- prevAll: function(elem){return jQuery.dir(elem,"previousSibling");},
- siblings: function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},
- children: function(elem){return jQuery.sibling(elem.firstChild);},
- contents: function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}
-}, function(name, fn){
- jQuery.fn[ name ] = function( selector ) {
- var ret = jQuery.map( this, fn );
+function returnFalse() {
+ return false;
+}
+function returnTrue() {
+ return true;
+}
- if ( selector && typeof selector == "string" )
- ret = jQuery.multiFilter( selector, ret );
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+ preventDefault: function() {
+ this.isDefaultPrevented = returnTrue;
- return this.pushStack( jQuery.unique( ret ), name, selector );
- };
-});
+ var e = this.originalEvent;
+ if ( !e ) {
+ return;
+ }
+
+ // if preventDefault exists run it on the original event
+ if ( e.preventDefault ) {
+ e.preventDefault();
+ }
+ // otherwise set the returnValue property of the original event to false (IE)
+ e.returnValue = false;
+ },
+ stopPropagation: function() {
+ this.isPropagationStopped = returnTrue;
-jQuery.each({
- appendTo: "append",
- prependTo: "prepend",
- insertBefore: "before",
- insertAfter: "after",
- replaceAll: "replaceWith"
-}, function(name, original){
- jQuery.fn[ name ] = function( selector ) {
- var ret = [], insert = jQuery( selector );
+ var e = this.originalEvent;
+ if ( !e ) {
+ return;
+ }
+ // if stopPropagation exists run it on the original event
+ if ( e.stopPropagation ) {
+ e.stopPropagation();
+ }
+ // otherwise set the cancelBubble property of the original event to true (IE)
+ e.cancelBubble = true;
+ },
+ stopImmediatePropagation: function() {
+ this.isImmediatePropagationStopped = returnTrue;
+ this.stopPropagation();
+ },
+ isDefaultPrevented: returnFalse,
+ isPropagationStopped: returnFalse,
+ isImmediatePropagationStopped: returnFalse
+};
- for ( var i = 0, l = insert.length; i < l; i++ ) {
- var elems = (i > 0 ? this.clone(true) : this).get();
- jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
- ret = ret.concat( elems );
+// Checks if an event happened on an element within another element
+// Used in jQuery.event.special.mouseenter and mouseleave handlers
+var withinElement = function( event ) {
+ // Check if mouse(over|out) are still within the same parent element
+ var parent = event.relatedTarget;
+
+ // Traverse up the tree
+ while ( parent && parent !== this ) {
+ // Firefox sometimes assigns relatedTarget a XUL element
+ // which we cannot access the parentNode property of
+ try {
+ parent = parent.parentNode;
+
+ // assuming we've left the element since we most likely mousedover a xul element
+ } catch(e) {
+ break;
}
+ }
+
+ if ( parent !== this ) {
+ // set the correct event type
+ event.type = event.data;
+
+ // handle event if we actually just moused on to a non sub-element
+ jQuery.event.handle.apply( this, arguments );
+ }
+
+},
+
+// In case of event delegation, we only need to rename the event.type,
+// liveHandler will take care of the rest.
+delegate = function( event ) {
+ event.type = event.data;
+ jQuery.event.handle.apply( this, arguments );
+};
- return this.pushStack( ret, name, selector );
+// Create mouseenter and mouseleave events
+jQuery.each({
+ mouseenter: "mouseover",
+ mouseleave: "mouseout"
+}, function( orig, fix ) {
+ jQuery.event.special[ orig ] = {
+ setup: function( data ) {
+ jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig );
+ },
+ teardown: function( data ) {
+ jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement );
+ }
};
});
-jQuery.each({
- removeAttr: function( name ) {
- jQuery.attr( this, name, "" );
- if (this.nodeType == 1)
- this.removeAttribute( name );
- },
+// submit delegation
+if ( !jQuery.support.submitBubbles ) {
- addClass: function( classNames ) {
- jQuery.className.add( this, classNames );
+jQuery.event.special.submit = {
+ setup: function( data, namespaces, fn ) {
+ if ( this.nodeName.toLowerCase() !== "form" ) {
+ jQuery.event.add(this, "click.specialSubmit." + fn.guid, function( e ) {
+ var elem = e.target, type = elem.type;
+
+ if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) {
+ return trigger( "submit", this, arguments );
+ }
+ });
+
+ jQuery.event.add(this, "keypress.specialSubmit." + fn.guid, function( e ) {
+ var elem = e.target, type = elem.type;
+
+ if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) {
+ return trigger( "submit", this, arguments );
+ }
+ });
+
+ } else {
+ return false;
+ }
},
- removeClass: function( classNames ) {
- jQuery.className.remove( this, classNames );
+ remove: function( namespaces, fn ) {
+ jQuery.event.remove( this, "click.specialSubmit" + (fn ? "."+fn.guid : "") );
+ jQuery.event.remove( this, "keypress.specialSubmit" + (fn ? "."+fn.guid : "") );
+ }
+};
+
+}
+
+// change delegation, happens here so we have bind.
+if ( !jQuery.support.changeBubbles ) {
+
+var formElems = /textarea|input|select/i;
+
+function getVal( elem ) {
+ var type = elem.type, val = elem.value;
+
+ if ( type === "radio" || type === "checkbox" ) {
+ val = elem.checked;
+
+ } else if ( type === "select-multiple" ) {
+ val = elem.selectedIndex > -1 ?
+ jQuery.map( elem.options, function( elem ) {
+ return elem.selected;
+ }).join("-") :
+ "";
+
+ } else if ( elem.nodeName.toLowerCase() === "select" ) {
+ val = elem.selectedIndex;
+ }
+
+ return val;
+}
+
+function testChange( e ) {
+ var elem = e.target, data, val;
+
+ if ( !formElems.test( elem.nodeName ) || elem.readOnly ) {
+ return;
+ }
+
+ data = jQuery.data( elem, "_change_data" );
+ val = getVal(elem);
+
+ // the current data will be also retrieved by beforeactivate
+ if ( e.type !== "focusout" || elem.type !== "radio" ) {
+ jQuery.data( elem, "_change_data", val );
+ }
+
+ if ( data === undefined || val === data ) {
+ return;
+ }
+
+ if ( data != null || val ) {
+ e.type = "change";
+ return jQuery.event.trigger( e, arguments[1], elem );
+ }
+}
+
+jQuery.event.special.change = {
+ filters: {
+ focusout: testChange,
+
+ click: function( e ) {
+ var elem = e.target, type = elem.type;
+
+ if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) {
+ return testChange.call( this, e );
+ }
+ },
+
+ // Change has to be called before submit
+ // Keydown will be called before keypress, which is used in submit-event delegation
+ keydown: function( e ) {
+ var elem = e.target, type = elem.type;
+
+ if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") ||
+ (e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
+ type === "select-multiple" ) {
+ return testChange.call( this, e );
+ }
+ },
+
+ // Beforeactivate happens also before the previous element is blurred
+ // with this event you can't trigger a change event, but you can store
+ // information/focus[in] is not needed anymore
+ beforeactivate: function( e ) {
+ var elem = e.target;
+
+ if ( elem.nodeName.toLowerCase() === "input" && elem.type === "radio" ) {
+ jQuery.data( elem, "_change_data", getVal(elem) );
+ }
+ }
},
+ setup: function( data, namespaces, fn ) {
+ for ( var type in changeFilters ) {
+ jQuery.event.add( this, type + ".specialChange." + fn.guid, changeFilters[type] );
+ }
- toggleClass: function( classNames, state ) {
- if( typeof state !== "boolean" )
- state = !jQuery.className.has( this, classNames );
- jQuery.className[ state ? "add" : "remove" ]( this, classNames );
+ return formElems.test( this.nodeName );
},
+ remove: function( namespaces, fn ) {
+ for ( var type in changeFilters ) {
+ jQuery.event.remove( this, type + ".specialChange" + (fn ? "."+fn.guid : ""), changeFilters[type] );
+ }
- remove: function( selector ) {
- if ( !selector || jQuery.filter( selector, [ this ] ).length ) {
- // Prevent memory leaks
- jQuery( "*", this ).add([this]).each(function(){
- jQuery.event.remove(this);
- jQuery.removeData(this);
+ return formElems.test( this.nodeName );
+ }
+};
+
+var changeFilters = jQuery.event.special.change.filters;
+
+}
+
+function trigger( type, elem, args ) {
+ args[0].type = type;
+ return jQuery.event.handle.apply( elem, args );
+}
+
+// Create "bubbling" focus and blur events
+if ( document.addEventListener ) {
+ jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+ jQuery.event.special[ fix ] = {
+ setup: function() {
+ this.addEventListener( orig, handler, true );
+ },
+ teardown: function() {
+ this.removeEventListener( orig, handler, true );
+ }
+ };
+
+ function handler( e ) {
+ e = jQuery.event.fix( e );
+ e.type = fix;
+ return jQuery.event.handle.call( this, e );
+ }
+ });
+}
+
+jQuery.each(["bind", "one"], function( i, name ) {
+ jQuery.fn[ name ] = function( type, data, fn ) {
+ // Handle object literals
+ if ( typeof type === "object" ) {
+ for ( var key in type ) {
+ this[ name ](key, data, type[key], fn);
+ }
+ return this;
+ }
+
+ if ( jQuery.isFunction( data ) ) {
+ fn = data;
+ data = undefined;
+ }
+
+ var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
+ jQuery( this ).unbind( event, handler );
+ return fn.apply( this, arguments );
+ }) : fn;
+
+ return type === "unload" && name !== "one" ?
+ this.one( type, data, fn ) :
+ this.each(function() {
+ jQuery.event.add( this, type, handler, data );
});
- if (this.parentNode)
- this.parentNode.removeChild( this );
+ };
+});
+
+jQuery.fn.extend({
+ unbind: function( type, fn ) {
+ // Handle object literals
+ if ( typeof type === "object" && !type.preventDefault ) {
+ for ( var key in type ) {
+ this.unbind(key, type[key]);
+ }
+ return this;
+ }
+
+ return this.each(function() {
+ jQuery.event.remove( this, type, fn );
+ });
+ },
+ trigger: function( type, data ) {
+ return this.each(function() {
+ jQuery.event.trigger( type, data, this );
+ });
+ },
+
+ triggerHandler: function( type, data ) {
+ if ( this[0] ) {
+ var event = jQuery.Event( type );
+ event.preventDefault();
+ event.stopPropagation();
+ jQuery.event.trigger( event, data, this[0] );
+ return event.result;
}
},
- empty: function() {
- // Remove element nodes and prevent memory leaks
- jQuery(this).children().remove();
+ toggle: function( fn ) {
+ // Save reference to arguments for access in closure
+ var args = arguments, i = 1;
- // Remove any remaining nodes
- while ( this.firstChild )
- this.removeChild( this.firstChild );
+ // link all the functions, so any of them can unbind this click handler
+ while ( i < args.length ) {
+ jQuery.proxy( fn, args[ i++ ] );
+ }
+
+ return this.click( jQuery.proxy( fn, function( event ) {
+ // Figure out which function to execute
+ var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+ jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+
+ // Make sure that clicks stop
+ event.preventDefault();
+
+ // and execute the function
+ return args[ lastToggle ].apply( this, arguments ) || false;
+ }));
+ },
+
+ hover: function( fnOver, fnOut ) {
+ return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
}
-}, function(name, fn){
- jQuery.fn[ name ] = function(){
- return this.each( fn, arguments );
+});
+
+jQuery.each(["live", "die"], function( i, name ) {
+ jQuery.fn[ name ] = function( types, data, fn ) {
+ var type, i = 0;
+
+ if ( jQuery.isFunction( data ) ) {
+ fn = data;
+ data = undefined;
+ }
+
+ types = (types || "").split( /\s+/ );
+
+ while ( (type = types[ i++ ]) != null ) {
+ type = type === "focus" ? "focusin" : // focus --> focusin
+ type === "blur" ? "focusout" : // blur --> focusout
+ type === "hover" ? types.push("mouseleave") && "mouseenter" : // hover support
+ type;
+
+ if ( name === "live" ) {
+ // bind live handler
+ jQuery( this.context ).bind( liveConvert( type, this.selector ), {
+ data: data, selector: this.selector, live: type
+ }, fn );
+
+ } else {
+ // unbind live handler
+ jQuery( this.context ).unbind( liveConvert( type, this.selector ), fn ? { guid: fn.guid + this.selector + type } : null );
+ }
+ }
+
+ return this;
+ }
+});
+
+function liveHandler( event ) {
+ var stop, elems = [], selectors = [], args = arguments,
+ related, match, fn, elem, j, i, l, data,
+ live = jQuery.extend({}, jQuery.data( this, "events" ).live);
+
+ // Make sure we avoid non-left-click bubbling in Firefox (#3861)
+ if ( event.button && event.type === "click" ) {
+ return;
+ }
+
+ for ( j in live ) {
+ fn = live[j];
+ if ( fn.live === event.type ||
+ fn.altLive && jQuery.inArray(event.type, fn.altLive) > -1 ) {
+
+ data = fn.data;
+ if ( !(data.beforeFilter && data.beforeFilter[event.type] &&
+ !data.beforeFilter[event.type](event)) ) {
+ selectors.push( fn.selector );
+ }
+ } else {
+ delete live[j];
+ }
+ }
+
+ match = jQuery( event.target ).closest( selectors, event.currentTarget );
+
+ for ( i = 0, l = match.length; i < l; i++ ) {
+ for ( j in live ) {
+ fn = live[j];
+ elem = match[i].elem;
+ related = null;
+
+ if ( match[i].selector === fn.selector ) {
+ // Those two events require additional checking
+ if ( fn.live === "mouseenter" || fn.live === "mouseleave" ) {
+ related = jQuery( event.relatedTarget ).closest( fn.selector )[0];
+ }
+
+ if ( !related || related !== elem ) {
+ elems.push({ elem: elem, fn: fn });
+ }
+ }
+ }
+ }
+
+ for ( i = 0, l = elems.length; i < l; i++ ) {
+ match = elems[i];
+ event.currentTarget = match.elem;
+ event.data = match.fn.data;
+ if ( match.fn.apply( match.elem, args ) === false ) {
+ stop = false;
+ break;
+ }
+ }
+
+ return stop;
+}
+
+function liveConvert( type, selector ) {
+ return "live." + (type ? type + "." : "") + selector.replace(/\./g, "`").replace(/ /g, "&");
+}
+
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+ "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+ "change select submit keydown keypress keyup error").split(" "), function( i, name ) {
+
+ // Handle event binding
+ jQuery.fn[ name ] = function( fn ) {
+ return fn ? this.bind( name, fn ) : this.trigger( name );
};
+
+ if ( jQuery.attrFn ) {
+ jQuery.attrFn[ name ] = true;
+ }
});
-// Helper function used by the dimensions and offset modules
-function num(elem, prop) {
- return elem[0] && parseInt( jQuery.curCSS(elem[0], prop, true), 10 ) || 0;
+// Prevent memory leaks in IE
+// Window isn't included so as not to unbind existing unload events
+// More info:
+// - http://isaacschlueter.com/2006/10/msie-memory-leaks/
+if ( window.attachEvent && !window.addEventListener ) {
+ window.attachEvent("onunload", function() {
+ for ( var id in jQuery.cache ) {
+ if ( jQuery.cache[ id ].handle ) {
+ // Try/Catch is to handle iframes being unloaded, see #4280
+ try {
+ jQuery.event.remove( jQuery.cache[ id ].handle.elem );
+ } catch(e) {}
+ }
+ }
+ });
}
-var expando = "jQuery" + now(), uuid = 0, windowData = {};
-
-jQuery.extend({
- cache: {},
-
- data: function( elem, name, data ) {
- elem = elem == window ?
- windowData :
- elem;
-
- var id = elem[ expando ];
-
- // Compute a unique ID for the element
- if ( !id )
- id = elem[ expando ] = ++uuid;
-
- // Only generate the data cache if we're
- // trying to access or manipulate it
- if ( name && !jQuery.cache[ id ] )
- jQuery.cache[ id ] = {};
-
- // Prevent overriding the named cache with undefined values
- if ( data !== undefined )
- jQuery.cache[ id ][ name ] = data;
-
- // Return the named cache data, or the ID for the element
- return name ?
- jQuery.cache[ id ][ name ] :
- id;
- },
-
- removeData: function( elem, name ) {
- elem = elem == window ?
- windowData :
- elem;
-
- var id = elem[ expando ];
-
- // If we want to remove a specific section of the element's data
- if ( name ) {
- if ( jQuery.cache[ id ] ) {
- // Remove the section of cache data
- delete jQuery.cache[ id ][ name ];
-
- // If we've removed all the data, remove the element's cache
- name = "";
-
- for ( name in jQuery.cache[ id ] )
- break;
-
- if ( !name )
- jQuery.removeData( elem );
- }
-
- // Otherwise, we want to remove all of the element's data
- } else {
- // Clean up the element expando
- try {
- delete elem[ expando ];
- } catch(e){
- // IE has trouble directly removing the expando
- // but it's ok with using removeAttribute
- if ( elem.removeAttribute )
- elem.removeAttribute( expando );
- }
-
- // Completely remove the data cache
- delete jQuery.cache[ id ];
- }
- },
- queue: function( elem, type, data ) {
- if ( elem ){
-
- type = (type || "fx") + "queue";
-
- var q = jQuery.data( elem, type );
-
- if ( !q || jQuery.isArray(data) )
- q = jQuery.data( elem, type, jQuery.makeArray(data) );
- else if( data )
- q.push( data );
-
- }
- return q;
- },
-
- dequeue: function( elem, type ){
- var queue = jQuery.queue( elem, type ),
- fn = queue.shift();
-
- if( !type || type === "fx" )
- fn = queue[0];
-
- if( fn !== undefined )
- fn.call(elem);
- }
-});
-
-jQuery.fn.extend({
- data: function( key, value ){
- var parts = key.split(".");
- parts[1] = parts[1] ? "." + parts[1] : "";
-
- if ( value === undefined ) {
- var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
-
- if ( data === undefined && this.length )
- data = jQuery.data( this[0], key );
-
- return data === undefined && parts[1] ?
- this.data( parts[0] ) :
- data;
- } else
- return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){
- jQuery.data( this, key, value );
- });
- },
-
- removeData: function( key ){
- return this.each(function(){
- jQuery.removeData( this, key );
- });
- },
- queue: function(type, data){
- if ( typeof type !== "string" ) {
- data = type;
- type = "fx";
- }
-
- if ( data === undefined )
- return jQuery.queue( this[0], type );
-
- return this.each(function(){
- var queue = jQuery.queue( this, type, data );
-
- if( type == "fx" && queue.length == 1 )
- queue[0].call(this);
- });
- },
- dequeue: function(type){
- return this.each(function(){
- jQuery.dequeue( this, type );
- });
- }
-});/*!
- * Sizzle CSS Selector Engine - v0.9.3
+/*!
+ * Sizzle CSS Selector Engine - v1.0
* Copyright 2009, The Dojo Foundation
* Released under the MIT, BSD, and GPL Licenses.
* More information: http://sizzlejs.com/
*/
(function(){
-var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,
+var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
done = 0,
- toString = Object.prototype.toString;
+ toString = Object.prototype.toString,
+ hasDuplicate = false,
+ baseHasDuplicate = true;
+
+// Here we check if the JavaScript engine is using some sort of
+// optimization where it does not always call our comparision
+// function. If that is the case, discard the hasDuplicate value.
+// Thus far that includes Google Chrome.
+[0, 0].sort(function(){
+ baseHasDuplicate = false;
+ return 0;
+});
var Sizzle = function(selector, context, results, seed) {
results = results || [];
- context = context || document;
+ var origContext = context = context || document;
- if ( context.nodeType !== 1 && context.nodeType !== 9 )
+ if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
return [];
+ }
if ( !selector || typeof selector !== "string" ) {
return results;
}
- var parts = [], m, set, checkSet, check, mode, extra, prune = true;
+ var parts = [], m, set, checkSet, extra, prune = true, contextXML = isXML(context),
+ soFar = selector;
// Reset the position of the chunker regexp (start from head)
- chunker.lastIndex = 0;
-
- while ( (m = chunker.exec(selector)) !== null ) {
+ while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) {
+ soFar = m[3];
+
parts.push( m[1] );
if ( m[2] ) {
- extra = RegExp.rightContext;
+ extra = m[3];
break;
}
}
@@ -1458,38 +2577,51 @@ var Sizzle = function(selector, context, results, seed) {
while ( parts.length ) {
selector = parts.shift();
- if ( Expr.relative[ selector ] )
+ if ( Expr.relative[ selector ] ) {
selector += parts.shift();
-
+ }
+
set = posProcess( selector, set );
}
}
} else {
- var ret = seed ?
- { expr: parts.pop(), set: makeArray(seed) } :
- Sizzle.find( parts.pop(), parts.length === 1 && context.parentNode ? context.parentNode : context, isXML(context) );
- set = Sizzle.filter( ret.expr, ret.set );
-
- if ( parts.length > 0 ) {
- checkSet = makeArray(set);
- } else {
- prune = false;
+ // Take a shortcut and set the context if the root selector is an ID
+ // (but not if it'll be faster if the inner selector is an ID)
+ if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
+ Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
+ var ret = Sizzle.find( parts.shift(), context, contextXML );
+ context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0];
}
- while ( parts.length ) {
- var cur = parts.pop(), pop = cur;
+ if ( context ) {
+ var ret = seed ?
+ { expr: parts.pop(), set: makeArray(seed) } :
+ Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
+ set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;
- if ( !Expr.relative[ cur ] ) {
- cur = "";
+ if ( parts.length > 0 ) {
+ checkSet = makeArray(set);
} else {
- pop = parts.pop();
+ prune = false;
}
- if ( pop == null ) {
- pop = context;
- }
+ while ( parts.length ) {
+ var cur = parts.pop(), pop = cur;
+
+ if ( !Expr.relative[ cur ] ) {
+ cur = "";
+ } else {
+ pop = parts.pop();
+ }
+
+ if ( pop == null ) {
+ pop = context;
+ }
- Expr.relative[ cur ]( checkSet, pop, isXML(context) );
+ Expr.relative[ cur ]( checkSet, pop, contextXML );
+ }
+ } else {
+ checkSet = parts = [];
}
}
@@ -1498,13 +2630,13 @@ var Sizzle = function(selector, context, results, seed) {
}
if ( !checkSet ) {
- throw "Syntax error, unrecognized expression: " + (cur || selector);
+ Sizzle.error( cur || selector );
}
if ( toString.call(checkSet) === "[object Array]" ) {
if ( !prune ) {
results.push.apply( results, checkSet );
- } else if ( context.nodeType === 1 ) {
+ } else if ( context && context.nodeType === 1 ) {
for ( var i = 0; checkSet[i] != null; i++ ) {
if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) {
results.push( set[i] );
@@ -1522,17 +2654,22 @@ var Sizzle = function(selector, context, results, seed) {
}
if ( extra ) {
- Sizzle( extra, context, results, seed );
+ Sizzle( extra, origContext, results, seed );
+ Sizzle.uniqueSort( results );
+ }
- if ( sortOrder ) {
- hasDuplicate = false;
- results.sort(sortOrder);
+ return results;
+};
- if ( hasDuplicate ) {
- for ( var i = 1; i < results.length; i++ ) {
- if ( results[i] === results[i-1] ) {
- results.splice(i--, 1);
- }
+Sizzle.uniqueSort = function(results){
+ if ( sortOrder ) {
+ hasDuplicate = baseHasDuplicate;
+ results.sort(sortOrder);
+
+ if ( hasDuplicate ) {
+ for ( var i = 1; i < results.length; i++ ) {
+ if ( results[i] === results[i-1] ) {
+ results.splice(i--, 1);
}
}
}
@@ -1555,8 +2692,9 @@ Sizzle.find = function(expr, context, isXML){
for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
var type = Expr.order[i], match;
- if ( (match = Expr.match[ type ].exec( expr )) ) {
- var left = RegExp.leftContext;
+ if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
+ var left = match[1];
+ match.splice(1,1);
if ( left.substr( left.length - 1 ) !== "\\" ) {
match[1] = (match[1] || "").replace(/\\/g, "");
@@ -1582,11 +2720,17 @@ Sizzle.filter = function(expr, set, inplace, not){
while ( expr && set.length ) {
for ( var type in Expr.filter ) {
- if ( (match = Expr.match[ type ].exec( expr )) != null ) {
- var filter = Expr.filter[ type ], found, item;
+ if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
+ var filter = Expr.filter[ type ], found, item, left = match[1];
anyFound = false;
- if ( curLoop == result ) {
+ match.splice(1,1);
+
+ if ( left.substr( left.length - 1 ) === "\\" ) {
+ continue;
+ }
+
+ if ( curLoop === result ) {
result = [];
}
@@ -1637,9 +2781,9 @@ Sizzle.filter = function(expr, set, inplace, not){
}
// Improper expression
- if ( expr == old ) {
+ if ( expr === old ) {
if ( anyFound == null ) {
- throw "Syntax error, unrecognized expression: " + expr;
+ Sizzle.error( expr );
} else {
break;
}
@@ -1651,18 +2795,23 @@ Sizzle.filter = function(expr, set, inplace, not){
return curLoop;
};
+Sizzle.error = function( msg ) {
+ throw "Syntax error, unrecognized expression: " + msg;
+};
+
var Expr = Sizzle.selectors = {
order: [ "ID", "NAME", "TAG" ],
match: {
- ID: /#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
- CLASS: /\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
- NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,
- ATTR: /\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
- TAG: /^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,
+ ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+ CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+ NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,
+ ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
+ TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,
CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,
POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,
- PSEUDO: /:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/
+ PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
},
+ leftMatch: {},
attrMap: {
"class": "className",
"for": "htmlFor"
@@ -1673,20 +2822,20 @@ var Expr = Sizzle.selectors = {
}
},
relative: {
- "+": function(checkSet, part, isXML){
+ "+": function(checkSet, part){
var isPartStr = typeof part === "string",
isTag = isPartStr && !/\W/.test(part),
isPartStrNotTag = isPartStr && !isTag;
- if ( isTag && !isXML ) {
- part = part.toUpperCase();
+ if ( isTag ) {
+ part = part.toLowerCase();
}
for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
if ( (elem = checkSet[i]) ) {
while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
- checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ?
+ checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
elem || false :
elem === part;
}
@@ -1696,17 +2845,17 @@ var Expr = Sizzle.selectors = {
Sizzle.filter( part, checkSet, true );
}
},
- ">": function(checkSet, part, isXML){
+ ">": function(checkSet, part){
var isPartStr = typeof part === "string";
if ( isPartStr && !/\W/.test(part) ) {
- part = isXML ? part : part.toUpperCase();
+ part = part.toLowerCase();
for ( var i = 0, l = checkSet.length; i < l; i++ ) {
var elem = checkSet[i];
if ( elem ) {
var parent = elem.parentNode;
- checkSet[i] = parent.nodeName === part ? parent : false;
+ checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
}
}
} else {
@@ -1727,8 +2876,8 @@ var Expr = Sizzle.selectors = {
"": function(checkSet, part, isXML){
var doneName = done++, checkFn = dirCheck;
- if ( !part.match(/\W/) ) {
- var nodeCheck = part = isXML ? part : part.toUpperCase();
+ if ( typeof part === "string" && !/\W/.test(part) ) {
+ var nodeCheck = part = part.toLowerCase();
checkFn = dirNodeCheck;
}
@@ -1737,8 +2886,8 @@ var Expr = Sizzle.selectors = {
"~": function(checkSet, part, isXML){
var doneName = done++, checkFn = dirCheck;
- if ( typeof part === "string" && !part.match(/\W/) ) {
- var nodeCheck = part = isXML ? part : part.toUpperCase();
+ if ( typeof part === "string" && !/\W/.test(part) ) {
+ var nodeCheck = part = part.toLowerCase();
checkFn = dirNodeCheck;
}
@@ -1752,7 +2901,7 @@ var Expr = Sizzle.selectors = {
return m ? [m] : [];
}
},
- NAME: function(match, context, isXML){
+ NAME: function(match, context){
if ( typeof context.getElementsByName !== "undefined" ) {
var ret = [], results = context.getElementsByName(match[1]);
@@ -1779,9 +2928,10 @@ var Expr = Sizzle.selectors = {
for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
if ( elem ) {
- if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) {
- if ( !inplace )
+ if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n]/g, " ").indexOf(match) >= 0) ) {
+ if ( !inplace ) {
result.push( elem );
+ }
} else if ( inplace ) {
curLoop[i] = false;
}
@@ -1794,14 +2944,13 @@ var Expr = Sizzle.selectors = {
return match[1].replace(/\\/g, "");
},
TAG: function(match, curLoop){
- for ( var i = 0; curLoop[i] === false; i++ ){}
- return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase();
+ return match[1].toLowerCase();
},
CHILD: function(match){
- if ( match[1] == "nth" ) {
+ if ( match[1] === "nth" ) {
// parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
- match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" ||
+ match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
!/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
// calculate the numbers (first)n+(last) including if they are negative
@@ -1830,7 +2979,7 @@ var Expr = Sizzle.selectors = {
PSEUDO: function(match, curLoop, inplace, result, not){
if ( match[1] === "not" ) {
// If we're dealing with a complex expression, or a simple one
- if ( match[3].match(chunker).length > 1 || /^\w/.test(match[3]) ) {
+ if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
match[3] = Sizzle(match[3], null, null, curLoop);
} else {
var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
@@ -1903,7 +3052,7 @@ var Expr = Sizzle.selectors = {
return "reset" === elem.type;
},
button: function(elem){
- return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON";
+ return "button" === elem.type || elem.nodeName.toLowerCase() === "button";
},
input: function(elem){
return /input|select|textarea|button/i.test(elem.nodeName);
@@ -1929,10 +3078,10 @@ var Expr = Sizzle.selectors = {
return i > match[3] - 0;
},
nth: function(elem, i, match){
- return match[3] - 0 == i;
+ return match[3] - 0 === i;
},
eq: function(elem, i, match){
- return match[3] - 0 == i;
+ return match[3] - 0 === i;
}
},
filter: {
@@ -1942,7 +3091,7 @@ var Expr = Sizzle.selectors = {
if ( filter ) {
return filter( elem, i, match, array );
} else if ( name === "contains" ) {
- return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0;
+ return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0;
} else if ( name === "not" ) {
var not = match[3];
@@ -1953,6 +3102,8 @@ var Expr = Sizzle.selectors = {
}
return true;
+ } else {
+ Sizzle.error( "Syntax error, unrecognized expression: " + name );
}
},
CHILD: function(elem, match){
@@ -1960,20 +3111,26 @@ var Expr = Sizzle.selectors = {
switch (type) {
case 'only':
case 'first':
- while (node = node.previousSibling) {
- if ( node.nodeType === 1 ) return false;
+ while ( (node = node.previousSibling) ) {
+ if ( node.nodeType === 1 ) {
+ return false;
+ }
+ }
+ if ( type === "first" ) {
+ return true;
}
- if ( type == 'first') return true;
node = elem;
case 'last':
- while (node = node.nextSibling) {
- if ( node.nodeType === 1 ) return false;
+ while ( (node = node.nextSibling) ) {
+ if ( node.nodeType === 1 ) {
+ return false;
+ }
}
return true;
case 'nth':
var first = match[2], last = match[3];
- if ( first == 1 && last == 0 ) {
+ if ( first === 1 && last === 0 ) {
return true;
}
@@ -1991,10 +3148,10 @@ var Expr = Sizzle.selectors = {
}
var diff = elem.nodeIndex - last;
- if ( first == 0 ) {
- return diff == 0;
+ if ( first === 0 ) {
+ return diff === 0;
} else {
- return ( diff % first == 0 && diff / first >= 0 );
+ return ( diff % first === 0 && diff / first >= 0 );
}
}
},
@@ -2002,7 +3159,7 @@ var Expr = Sizzle.selectors = {
return elem.nodeType === 1 && elem.getAttribute("id") === match;
},
TAG: function(elem, match){
- return (match === "*" && elem.nodeType === 1) || elem.nodeName === match;
+ return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
},
CLASS: function(elem, match){
return (" " + (elem.className || elem.getAttribute("class")) + " ")
@@ -2030,7 +3187,7 @@ var Expr = Sizzle.selectors = {
!check ?
value && result !== false :
type === "!=" ?
- value != check :
+ value !== check :
type === "^=" ?
value.indexOf(check) === 0 :
type === "$=" ?
@@ -2052,11 +3209,14 @@ var Expr = Sizzle.selectors = {
var origPOS = Expr.match.POS;
for ( var type in Expr.match ) {
- Expr.match[ type ] = RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
+ Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
+ Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, function(all, num){
+ return "\\" + (num - 0 + 1);
+ }));
}
var makeArray = function(array, results) {
- array = Array.prototype.slice.call( array );
+ array = Array.prototype.slice.call( array, 0 );
if ( results ) {
results.push.apply( results, array );
@@ -2069,7 +3229,7 @@ var makeArray = function(array, results) {
// Perform a simple check to determine if the browser is capable of
// converting a NodeList to an array using builtin methods.
try {
- Array.prototype.slice.call( document.documentElement.childNodes );
+ Array.prototype.slice.call( document.documentElement.childNodes, 0 );
// Provide a fallback method if it does not work
} catch(e){
@@ -2098,6 +3258,13 @@ var sortOrder;
if ( document.documentElement.compareDocumentPosition ) {
sortOrder = function( a, b ) {
+ if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
+ if ( a == b ) {
+ hasDuplicate = true;
+ }
+ return a.compareDocumentPosition ? -1 : 1;
+ }
+
var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
if ( ret === 0 ) {
hasDuplicate = true;
@@ -2106,6 +3273,13 @@ if ( document.documentElement.compareDocumentPosition ) {
};
} else if ( "sourceIndex" in document.documentElement ) {
sortOrder = function( a, b ) {
+ if ( !a.sourceIndex || !b.sourceIndex ) {
+ if ( a == b ) {
+ hasDuplicate = true;
+ }
+ return a.sourceIndex ? -1 : 1;
+ }
+
var ret = a.sourceIndex - b.sourceIndex;
if ( ret === 0 ) {
hasDuplicate = true;
@@ -2114,11 +3288,18 @@ if ( document.documentElement.compareDocumentPosition ) {
};
} else if ( document.createRange ) {
sortOrder = function( a, b ) {
+ if ( !a.ownerDocument || !b.ownerDocument ) {
+ if ( a == b ) {
+ hasDuplicate = true;
+ }
+ return a.ownerDocument ? -1 : 1;
+ }
+
var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
- aRange.selectNode(a);
- aRange.collapse(true);
- bRange.selectNode(b);
- bRange.collapse(true);
+ aRange.setStart(a, 0);
+ aRange.setEnd(a, 0);
+ bRange.setStart(b, 0);
+ bRange.setEnd(b, 0);
var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
if ( ret === 0 ) {
hasDuplicate = true;
@@ -2127,13 +3308,33 @@ if ( document.documentElement.compareDocumentPosition ) {
};
}
+// Utility function for retreiving the text value of an array of DOM nodes
+function getText( elems ) {
+ var ret = "", elem;
+
+ for ( var i = 0; elems[i]; i++ ) {
+ elem = elems[i];
+
+ // Get the text from text nodes and CDATA nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
+ ret += elem.nodeValue;
+
+ // Traverse everything else, except comment nodes
+ } else if ( elem.nodeType !== 8 ) {
+ ret += getText( elem.childNodes );
+ }
+ }
+
+ return ret;
+}
+
// Check to see if the browser returns elements by name when
// querying by getElementById (and provide a workaround)
(function(){
// We're going to inject a fake input element with a specified name
- var form = document.createElement("form"),
+ var form = document.createElement("div"),
id = "script" + (new Date).getTime();
- form.innerHTML = "<input name='" + id + "'/>";
+ form.innerHTML = "<a name='" + id + "'/>";
// Inject it into the root element, check its status, and remove it quickly
var root = document.documentElement;
@@ -2141,7 +3342,7 @@ if ( document.documentElement.compareDocumentPosition ) {
// The workaround has to do additional checks after a getElementById
// Which slows things down for other browsers (hence the branching)
- if ( !!document.getElementById( id ) ) {
+ if ( document.getElementById( id ) ) {
Expr.find.ID = function(match, context, isXML){
if ( typeof context.getElementById !== "undefined" && !isXML ) {
var m = context.getElementById(match[1]);
@@ -2156,6 +3357,7 @@ if ( document.documentElement.compareDocumentPosition ) {
}
root.removeChild( form );
+ root = form = null; // release memory in IE
})();
(function(){
@@ -2196,69 +3398,75 @@ if ( document.documentElement.compareDocumentPosition ) {
return elem.getAttribute("href", 2);
};
}
+
+ div = null; // release memory in IE
})();
-if ( document.querySelectorAll ) (function(){
- var oldSizzle = Sizzle, div = document.createElement("div");
- div.innerHTML = "<p class='TEST'></p>";
+if ( document.querySelectorAll ) {
+ (function(){
+ var oldSizzle = Sizzle, div = document.createElement("div");
+ div.innerHTML = "<p class='TEST'></p>";
- // Safari can't handle uppercase or unicode characters when
- // in quirks mode.
- if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
- return;
- }
+ // Safari can't handle uppercase or unicode characters when
+ // in quirks mode.
+ if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
+ return;
+ }
- Sizzle = function(query, context, extra, seed){
- context = context || document;
+ Sizzle = function(query, context, extra, seed){
+ context = context || document;
+
+ // Only use querySelectorAll on non-XML documents
+ // (ID selectors don't work in non-HTML documents)
+ if ( !seed && context.nodeType === 9 && !isXML(context) ) {
+ try {
+ return makeArray( context.querySelectorAll(query), extra );
+ } catch(e){}
+ }
+
+ return oldSizzle(query, context, extra, seed);
+ };
- // Only use querySelectorAll on non-XML documents
- // (ID selectors don't work in non-HTML documents)
- if ( !seed && context.nodeType === 9 && !isXML(context) ) {
- try {
- return makeArray( context.querySelectorAll(query), extra );
- } catch(e){}
+ for ( var prop in oldSizzle ) {
+ Sizzle[ prop ] = oldSizzle[ prop ];
}
-
- return oldSizzle(query, context, extra, seed);
- };
- Sizzle.find = oldSizzle.find;
- Sizzle.filter = oldSizzle.filter;
- Sizzle.selectors = oldSizzle.selectors;
- Sizzle.matches = oldSizzle.matches;
-})();
+ div = null; // release memory in IE
+ })();
+}
-if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){
+(function(){
var div = document.createElement("div");
+
div.innerHTML = "<div class='test e'></div><div class='test'></div>";
// Opera can't find a second classname (in 9.6)
- if ( div.getElementsByClassName("e").length === 0 )
+ // Also, make sure that getElementsByClassName actually exists
+ if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
return;
+ }
// Safari caches class attributes, doesn't catch changes (in 3.2)
div.lastChild.className = "e";
- if ( div.getElementsByClassName("e").length === 1 )
+ if ( div.getElementsByClassName("e").length === 1 ) {
return;
-
+ }
+
Expr.order.splice(1, 0, "CLASS");
Expr.find.CLASS = function(match, context, isXML) {
if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
return context.getElementsByClassName(match[1]);
}
};
+
+ div = null; // release memory in IE
})();
function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
- var sibDir = dir == "previousSibling" && !isXML;
for ( var i = 0, l = checkSet.length; i < l; i++ ) {
var elem = checkSet[i];
if ( elem ) {
- if ( sibDir && elem.nodeType === 1 ){
- elem.sizcache = doneName;
- elem.sizset = i;
- }
elem = elem[dir];
var match = false;
@@ -2273,7 +3481,7 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
elem.sizset = i;
}
- if ( elem.nodeName === cur ) {
+ if ( elem.nodeName.toLowerCase() === cur ) {
match = elem;
break;
}
@@ -2287,14 +3495,9 @@ function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
}
function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
- var sibDir = dir == "previousSibling" && !isXML;
for ( var i = 0, l = checkSet.length; i < l; i++ ) {
var elem = checkSet[i];
if ( elem ) {
- if ( sibDir && elem.nodeType === 1 ) {
- elem.sizcache = doneName;
- elem.sizset = i;
- }
elem = elem[dir];
var match = false;
@@ -2329,15 +3532,17 @@ function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
}
}
-var contains = document.compareDocumentPosition ? function(a, b){
+var contains = document.compareDocumentPosition ? function(a, b){
return a.compareDocumentPosition(b) & 16;
} : function(a, b){
return a !== b && (a.contains ? a.contains(b) : true);
};
var isXML = function(elem){
- return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
- !!elem.ownerDocument && isXML( elem.ownerDocument );
+ // documentElement is verified for cases where it doesn't yet exist
+ // (such as loading iframes in IE - #4833)
+ var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
+ return documentElement ? documentElement.nodeName !== "HTML" : false;
};
var posProcess = function(selector, context){
@@ -2362,873 +3567,1068 @@ var posProcess = function(selector, context){
// EXPOSE
jQuery.find = Sizzle;
-jQuery.filter = Sizzle.filter;
jQuery.expr = Sizzle.selectors;
jQuery.expr[":"] = jQuery.expr.filters;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.getText = getText;
+jQuery.isXMLDoc = isXML;
+jQuery.contains = contains;
-Sizzle.selectors.filters.hidden = function(elem){
- return elem.offsetWidth === 0 || elem.offsetHeight === 0;
-};
+return;
-Sizzle.selectors.filters.visible = function(elem){
- return elem.offsetWidth > 0 || elem.offsetHeight > 0;
-};
+window.Sizzle = Sizzle;
-Sizzle.selectors.filters.animated = function(elem){
- return jQuery.grep(jQuery.timers, function(fn){
- return elem === fn.elem;
- }).length;
-};
+})();
+var runtil = /Until$/,
+ rparentsprev = /^(?:parents|prevUntil|prevAll)/,
+ // Note: This RegExp should be improved, or likely pulled from Sizzle
+ rmultiselector = /,/,
+ slice = Array.prototype.slice;
+
+// Implement the identical functionality for filter and not
+var winnow = function( elements, qualifier, keep ) {
+ if ( jQuery.isFunction( qualifier ) ) {
+ return jQuery.grep(elements, function( elem, i ) {
+ return !!qualifier.call( elem, i, elem ) === keep;
+ });
-jQuery.multiFilter = function( expr, elems, not ) {
- if ( not ) {
- expr = ":not(" + expr + ")";
- }
+ } else if ( qualifier.nodeType ) {
+ return jQuery.grep(elements, function( elem, i ) {
+ return (elem === qualifier) === keep;
+ });
- return Sizzle.matches(expr, elems);
-};
+ } else if ( typeof qualifier === "string" ) {
+ var filtered = jQuery.grep(elements, function( elem ) {
+ return elem.nodeType === 1;
+ });
-jQuery.dir = function( elem, dir ){
- var matched = [], cur = elem[dir];
- while ( cur && cur != document ) {
- if ( cur.nodeType == 1 )
- matched.push( cur );
- cur = cur[dir];
+ if ( isSimple.test( qualifier ) ) {
+ return jQuery.filter(qualifier, filtered, !keep);
+ } else {
+ qualifier = jQuery.filter( qualifier, filtered );
+ }
}
- return matched;
+
+ return jQuery.grep(elements, function( elem, i ) {
+ return (jQuery.inArray( elem, qualifier ) >= 0) === keep;
+ });
};
-jQuery.nth = function(cur, result, dir, elem){
- result = result || 1;
- var num = 0;
+jQuery.fn.extend({
+ find: function( selector ) {
+ var ret = this.pushStack( "", "find", selector ), length = 0;
+
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ length = ret.length;
+ jQuery.find( selector, this[i], ret );
+
+ if ( i > 0 ) {
+ // Make sure that the results are unique
+ for ( var n = length; n < ret.length; n++ ) {
+ for ( var r = 0; r < length; r++ ) {
+ if ( ret[r] === ret[n] ) {
+ ret.splice(n--, 1);
+ break;
+ }
+ }
+ }
+ }
+ }
- for ( ; cur; cur = cur[dir] )
- if ( cur.nodeType == 1 && ++num == result )
- break;
+ return ret;
+ },
- return cur;
-};
+ has: function( target ) {
+ var targets = jQuery( target );
+ return this.filter(function() {
+ for ( var i = 0, l = targets.length; i < l; i++ ) {
+ if ( jQuery.contains( this, targets[i] ) ) {
+ return true;
+ }
+ }
+ });
+ },
-jQuery.sibling = function(n, elem){
- var r = [];
+ not: function( selector ) {
+ return this.pushStack( winnow(this, selector, false), "not", selector);
+ },
- for ( ; n; n = n.nextSibling ) {
- if ( n.nodeType == 1 && n != elem )
- r.push( n );
- }
+ filter: function( selector ) {
+ return this.pushStack( winnow(this, selector, true), "filter", selector );
+ },
+
+ is: function( selector ) {
+ return !!selector && jQuery.filter( selector, this ).length > 0;
+ },
- return r;
-};
+ closest: function( selectors, context ) {
+ if ( jQuery.isArray( selectors ) ) {
+ var ret = [], cur = this[0], match, matches = {}, selector;
-return;
+ if ( cur && selectors.length ) {
+ for ( var i = 0, l = selectors.length; i < l; i++ ) {
+ selector = selectors[i];
-window.Sizzle = Sizzle;
+ if ( !matches[selector] ) {
+ matches[selector] = jQuery.expr.match.POS.test( selector ) ?
+ jQuery( selector, context || this.context ) :
+ selector;
+ }
+ }
-})();
-/*
- * A number of helper functions used for managing events.
- * Many of the ideas behind this code originated from
- * Dean Edwards' addEvent library.
- */
-jQuery.event = {
+ while ( cur && cur.ownerDocument && cur !== context ) {
+ for ( selector in matches ) {
+ match = matches[selector];
- // Bind an event to an element
- // Original by Dean Edwards
- add: function(elem, types, handler, data) {
- if ( elem.nodeType == 3 || elem.nodeType == 8 )
- return;
+ if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) {
+ ret.push({ selector: selector, elem: cur });
+ delete matches[selector];
+ }
+ }
+ cur = cur.parentNode;
+ }
+ }
- // For whatever reason, IE has trouble passing the window object
- // around, causing it to be cloned in the process
- if ( elem.setInterval && elem != window )
- elem = window;
+ return ret;
+ }
- // Make sure that the function being executed has a unique ID
- if ( !handler.guid )
- handler.guid = this.guid++;
+ var pos = jQuery.expr.match.POS.test( selectors ) ?
+ jQuery( selectors, context || this.context ) : null;
- // if data is passed, bind to handler
- if ( data !== undefined ) {
- // Create temporary function pointer to original handler
- var fn = handler;
+ return this.map(function( i, cur ) {
+ while ( cur && cur.ownerDocument && cur !== context ) {
+ if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selectors) ) {
+ return cur;
+ }
+ cur = cur.parentNode;
+ }
+ return null;
+ });
+ },
+
+ // Determine the position of an element within
+ // the matched set of elements
+ index: function( elem ) {
+ if ( !elem || typeof elem === "string" ) {
+ return jQuery.inArray( this[0],
+ // If it receives a string, the selector is used
+ // If it receives nothing, the siblings are used
+ elem ? jQuery( elem ) : this.parent().children() );
+ }
+ // Locate the position of the desired element
+ return jQuery.inArray(
+ // If it receives a jQuery object, the first element is used
+ elem.jquery ? elem[0] : elem, this );
+ },
- // Create unique handler function, wrapped around original handler
- handler = this.proxy( fn );
+ add: function( selector, context ) {
+ var set = typeof selector === "string" ?
+ jQuery( selector, context || this.context ) :
+ jQuery.makeArray( selector ),
+ all = jQuery.merge( this.get(), set );
- // Store data in unique handler
- handler.data = data;
+ return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
+ all :
+ jQuery.unique( all ) );
+ },
+
+ andSelf: function() {
+ return this.add( this.prevObject );
+ }
+});
+
+// A painfully simple check to see if an element is disconnected
+// from a document (should be improved, where feasible).
+function isDisconnected( node ) {
+ return !node || !node.parentNode || node.parentNode.nodeType === 11;
+}
+
+jQuery.each({
+ parent: function( elem ) {
+ var parent = elem.parentNode;
+ return parent && parent.nodeType !== 11 ? parent : null;
+ },
+ parents: function( elem ) {
+ return jQuery.dir( elem, "parentNode" );
+ },
+ parentsUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "parentNode", until );
+ },
+ next: function( elem ) {
+ return jQuery.nth( elem, 2, "nextSibling" );
+ },
+ prev: function( elem ) {
+ return jQuery.nth( elem, 2, "previousSibling" );
+ },
+ nextAll: function( elem ) {
+ return jQuery.dir( elem, "nextSibling" );
+ },
+ prevAll: function( elem ) {
+ return jQuery.dir( elem, "previousSibling" );
+ },
+ nextUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "nextSibling", until );
+ },
+ prevUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "previousSibling", until );
+ },
+ siblings: function( elem ) {
+ return jQuery.sibling( elem.parentNode.firstChild, elem );
+ },
+ children: function( elem ) {
+ return jQuery.sibling( elem.firstChild );
+ },
+ contents: function( elem ) {
+ return jQuery.nodeName( elem, "iframe" ) ?
+ elem.contentDocument || elem.contentWindow.document :
+ jQuery.makeArray( elem.childNodes );
+ }
+}, function( name, fn ) {
+ jQuery.fn[ name ] = function( until, selector ) {
+ var ret = jQuery.map( this, fn, until );
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
}
- // Init the element's event structure
- var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
- handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
- // Handle the second event of a trigger and when
- // an event is called after a page has unloaded
- return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
- jQuery.event.handle.apply(arguments.callee.elem, arguments) :
- undefined;
- });
- // Add elem as a property of the handle function
- // This is to prevent a memory leak with non-native
- // event in IE.
- handle.elem = elem;
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
- // Handle multiple events separated by a space
- // jQuery(...).bind("mouseover mouseout", fn);
- jQuery.each(types.split(/\s+/), function(index, type) {
- // Namespaced event handlers
- var namespaces = type.split(".");
- type = namespaces.shift();
- handler.type = namespaces.slice().sort().join(".");
+ ret = this.length > 1 ? jQuery.unique( ret ) : ret;
- // Get the current list of functions bound to this event
- var handlers = events[type];
-
- if ( jQuery.event.specialAll[type] )
- jQuery.event.specialAll[type].setup.call(elem, data, namespaces);
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
- // Init the event handler queue
- if (!handlers) {
- handlers = events[type] = {};
+ return this.pushStack( ret, name, slice.call(arguments).join(",") );
+ };
+});
- // Check for a special event handler
- // Only use addEventListener/attachEvent if the special
- // events handler returns false
- if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) {
- // Bind the global event handler to the element
- if (elem.addEventListener)
- elem.addEventListener(type, handle, false);
- else if (elem.attachEvent)
- elem.attachEvent("on" + type, handle);
- }
+jQuery.extend({
+ filter: function( expr, elems, not ) {
+ if ( not ) {
+ expr = ":not(" + expr + ")";
+ }
+
+ return jQuery.find.matches(expr, elems);
+ },
+
+ dir: function( elem, dir, until ) {
+ var matched = [], cur = elem[dir];
+ while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+ if ( cur.nodeType === 1 ) {
+ matched.push( cur );
}
+ cur = cur[dir];
+ }
+ return matched;
+ },
- // Add the function to the element's handler list
- handlers[handler.guid] = handler;
+ nth: function( cur, result, dir, elem ) {
+ result = result || 1;
+ var num = 0;
- // Keep track of which events have been used, for global triggering
- jQuery.event.global[type] = true;
- });
+ for ( ; cur; cur = cur[dir] ) {
+ if ( cur.nodeType === 1 && ++num === result ) {
+ break;
+ }
+ }
- // Nullify elem to prevent memory leaks in IE
- elem = null;
+ return cur;
},
- guid: 1,
- global: {},
+ sibling: function( n, elem ) {
+ var r = [];
- // Detach an event or set of events from an element
- remove: function(elem, types, handler) {
- // don't do events on text and comment nodes
- if ( elem.nodeType == 3 || elem.nodeType == 8 )
- return;
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType === 1 && n !== elem ) {
+ r.push( n );
+ }
+ }
- var events = jQuery.data(elem, "events"), ret, index;
+ return r;
+ }
+});
+var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
+ rleadingWhitespace = /^\s+/,
+ rxhtmlTag = /(<([\w:]+)[^>]*?)\/>/g,
+ rselfClosing = /^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,
+ rtagName = /<([\w:]+)/,
+ rtbody = /<tbody/i,
+ rhtml = /<|&\w+;/,
+ rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, // checked="checked" or checked (html5)
+ fcloseTag = function( all, front, tag ) {
+ return rselfClosing.test( tag ) ?
+ all :
+ front + "></" + tag + ">";
+ },
+ wrapMap = {
+ option: [ 1, "<select multiple='multiple'>", "</select>" ],
+ legend: [ 1, "<fieldset>", "</fieldset>" ],
+ thead: [ 1, "<table>", "</table>" ],
+ tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+ td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+ col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+ area: [ 1, "<map>", "</map>" ],
+ _default: [ 0, "", "" ]
+ };
- if ( events ) {
- // Unbind all events for the element
- if ( types === undefined || (typeof types === "string" && types.charAt(0) == ".") )
- for ( var type in events )
- this.remove( elem, type + (types || "") );
- else {
- // types is actually an event object here
- if ( types.type ) {
- handler = types.handler;
- types = types.type;
- }
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
- // Handle multiple events seperated by a space
- // jQuery(...).unbind("mouseover mouseout", fn);
- jQuery.each(types.split(/\s+/), function(index, type){
- // Namespaced event handlers
- var namespaces = type.split(".");
- type = namespaces.shift();
- var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
+// IE can't serialize <link> and <script> tags normally
+if ( !jQuery.support.htmlSerialize ) {
+ wrapMap._default = [ 1, "div<div>", "</div>" ];
+}
- if ( events[type] ) {
- // remove the given handler for the given type
- if ( handler )
- delete events[type][handler.guid];
+jQuery.fn.extend({
+ text: function( text ) {
+ if ( jQuery.isFunction(text) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ self.text( text.call(this, i, self.text()) );
+ });
+ }
- // remove all handlers for the given type
- else
- for ( var handle in events[type] )
- // Handle the removal of namespaced events
- if ( namespace.test(events[type][handle].type) )
- delete events[type][handle];
-
- if ( jQuery.event.specialAll[type] )
- jQuery.event.specialAll[type].teardown.call(elem, namespaces);
+ if ( typeof text !== "object" && text !== undefined ) {
+ return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+ }
- // remove generic event handler if no more handlers exist
- for ( ret in events[type] ) break;
- if ( !ret ) {
- if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem, namespaces) === false ) {
- if (elem.removeEventListener)
- elem.removeEventListener(type, jQuery.data(elem, "handle"), false);
- else if (elem.detachEvent)
- elem.detachEvent("on" + type, jQuery.data(elem, "handle"));
- }
- ret = null;
- delete events[type];
- }
- }
- });
- }
+ return jQuery.getText( this );
+ },
- // Remove the expando if it's no longer used
- for ( ret in events ) break;
- if ( !ret ) {
- var handle = jQuery.data( elem, "handle" );
- if ( handle ) handle.elem = null;
- jQuery.removeData( elem, "events" );
- jQuery.removeData( elem, "handle" );
+ wrapAll: function( html ) {
+ if ( jQuery.isFunction( html ) ) {
+ return this.each(function(i) {
+ jQuery(this).wrapAll( html.call(this, i) );
+ });
+ }
+
+ if ( this[0] ) {
+ // The elements to wrap the target around
+ var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+ if ( this[0].parentNode ) {
+ wrap.insertBefore( this[0] );
}
+
+ wrap.map(function() {
+ var elem = this;
+
+ while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+ elem = elem.firstChild;
+ }
+
+ return elem;
+ }).append(this);
}
+
+ return this;
},
- // bubbling is internal
- trigger: function( event, data, elem, bubbling ) {
- // Event object or event type
- var type = event.type || event;
+ wrapInner: function( html ) {
+ if ( jQuery.isFunction( html ) ) {
+ return this.each(function(i) {
+ jQuery(this).wrapInner( html.call(this, i) );
+ });
+ }
- if( !bubbling ){
- event = typeof event === "object" ?
- // jQuery.Event object
- event[expando] ? event :
- // Object literal
- jQuery.extend( jQuery.Event(type), event ) :
- // Just the event type (string)
- jQuery.Event(type);
+ return this.each(function() {
+ var self = jQuery( this ), contents = self.contents();
- if ( type.indexOf("!") >= 0 ) {
- event.type = type = type.slice(0, -1);
- event.exclusive = true;
- }
+ if ( contents.length ) {
+ contents.wrapAll( html );
- // Handle a global trigger
- if ( !elem ) {
- // Don't bubble custom events when global (to avoid too much overhead)
- event.stopPropagation();
- // Only trigger if we've ever bound an event for it
- if ( this.global[type] )
- jQuery.each( jQuery.cache, function(){
- if ( this.events && this.events[type] )
- jQuery.event.trigger( event, data, this.handle.elem );
- });
+ } else {
+ self.append( html );
}
+ });
+ },
- // Handle triggering a single element
-
- // don't do events on text and comment nodes
- if ( !elem || elem.nodeType == 3 || elem.nodeType == 8 )
- return undefined;
-
- // Clean up in case it is reused
- event.result = undefined;
- event.target = elem;
-
- // Clone the incoming data, if any
- data = jQuery.makeArray(data);
- data.unshift( event );
- }
+ wrap: function( html ) {
+ return this.each(function() {
+ jQuery( this ).wrapAll( html );
+ });
+ },
- event.currentTarget = elem;
+ unwrap: function() {
+ return this.parent().each(function() {
+ if ( !jQuery.nodeName( this, "body" ) ) {
+ jQuery( this ).replaceWith( this.childNodes );
+ }
+ }).end();
+ },
- // Trigger the event, it is assumed that "handle" is a function
- var handle = jQuery.data(elem, "handle");
- if ( handle )
- handle.apply( elem, data );
+ append: function() {
+ return this.domManip(arguments, true, function( elem ) {
+ if ( this.nodeType === 1 ) {
+ this.appendChild( elem );
+ }
+ });
+ },
- // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)
- if ( (!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )
- event.result = false;
+ prepend: function() {
+ return this.domManip(arguments, true, function( elem ) {
+ if ( this.nodeType === 1 ) {
+ this.insertBefore( elem, this.firstChild );
+ }
+ });
+ },
- // Trigger the native events (except for clicks on links)
- if ( !bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
- this.triggered = true;
- try {
- elem[ type ]();
- // prevent IE from throwing an error for some hidden elements
- } catch (e) {}
+ before: function() {
+ if ( this[0] && this[0].parentNode ) {
+ return this.domManip(arguments, false, function( elem ) {
+ this.parentNode.insertBefore( elem, this );
+ });
+ } else if ( arguments.length ) {
+ var set = jQuery(arguments[0]);
+ set.push.apply( set, this.toArray() );
+ return this.pushStack( set, "before", arguments );
}
+ },
- this.triggered = false;
-
- if ( !event.isPropagationStopped() ) {
- var parent = elem.parentNode || elem.ownerDocument;
- if ( parent )
- jQuery.event.trigger(event, data, parent, true);
+ after: function() {
+ if ( this[0] && this[0].parentNode ) {
+ return this.domManip(arguments, false, function( elem ) {
+ this.parentNode.insertBefore( elem, this.nextSibling );
+ });
+ } else if ( arguments.length ) {
+ var set = this.pushStack( this, "after", arguments );
+ set.push.apply( set, jQuery(arguments[0]).toArray() );
+ return set;
}
},
- handle: function(event) {
- // returned undefined or false
- var all, handlers;
+ clone: function( events ) {
+ // Do the clone
+ var ret = this.map(function() {
+ if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) {
+ // IE copies events bound via attachEvent when
+ // using cloneNode. Calling detachEvent on the
+ // clone will also remove the events from the orignal
+ // In order to get around this, we use innerHTML.
+ // Unfortunately, this means some modifications to
+ // attributes in IE that are actually only stored
+ // as properties will not be copied (such as the
+ // the name attribute on an input).
+ var html = this.outerHTML, ownerDocument = this.ownerDocument;
+ if ( !html ) {
+ var div = ownerDocument.createElement("div");
+ div.appendChild( this.cloneNode(true) );
+ html = div.innerHTML;
+ }
- event = arguments[0] = jQuery.event.fix( event || window.event );
- event.currentTarget = this;
-
- // Namespaced event handlers
- var namespaces = event.type.split(".");
- event.type = namespaces.shift();
+ return jQuery.clean([html.replace(rinlinejQuery, "")
+ .replace(rleadingWhitespace, "")], ownerDocument)[0];
+ } else {
+ return this.cloneNode(true);
+ }
+ });
- // Cache this now, all = true means, any handler
- all = !namespaces.length && !event.exclusive;
-
- var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
+ // Copy the events from the original to the clone
+ if ( events === true ) {
+ cloneCopyEvent( this, ret );
+ cloneCopyEvent( this.find("*"), ret.find("*") );
+ }
- handlers = ( jQuery.data(this, "events") || {} )[event.type];
+ // Return the cloned set
+ return ret;
+ },
- for ( var j in handlers ) {
- var handler = handlers[j];
+ html: function( value ) {
+ if ( value === undefined ) {
+ return this[0] && this[0].nodeType === 1 ?
+ this[0].innerHTML.replace(rinlinejQuery, "") :
+ null;
- // Filter the functions by class
- if ( all || namespace.test(handler.type) ) {
- // Pass in a reference to the handler function itself
- // So that we can later remove it
- event.handler = handler;
- event.data = handler.data;
+ // See if we can take a shortcut and just use innerHTML
+ } else if ( typeof value === "string" && !/<script/i.test( value ) &&
+ (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
+ !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
- var ret = handler.apply(this, arguments);
+ value = value.replace(rxhtmlTag, fcloseTag);
- if( ret !== undefined ){
- event.result = ret;
- if ( ret === false ) {
- event.preventDefault();
- event.stopPropagation();
+ try {
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ // Remove element nodes and prevent memory leaks
+ if ( this[i].nodeType === 1 ) {
+ jQuery.cleanData( this[i].getElementsByTagName("*") );
+ this[i].innerHTML = value;
}
}
- if( event.isImmediatePropagationStopped() )
- break;
-
+ // If using innerHTML throws an exception, use the fallback method
+ } catch(e) {
+ this.empty().append( value );
}
+
+ } else if ( jQuery.isFunction( value ) ) {
+ this.each(function(i){
+ var self = jQuery(this), old = self.html();
+ self.empty().append(function(){
+ return value.call( this, i, old );
+ });
+ });
+
+ } else {
+ this.empty().append( value );
}
+
+ return this;
},
- props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+ replaceWith: function( value ) {
+ if ( this[0] && this[0].parentNode ) {
+ // Make sure that the elements are removed from the DOM before they are inserted
+ // this can help fix replacing a parent with child elements
+ if ( !jQuery.isFunction( value ) ) {
+ value = jQuery( value ).detach();
- fix: function(event) {
- if ( event[expando] )
- return event;
+ } else {
+ return this.each(function(i) {
+ var self = jQuery(this), old = self.html();
+ self.replaceWith( value.call( this, i, old ) );
+ });
+ }
- // store a copy of the original event object
- // and "clone" to set read-only properties
- var originalEvent = event;
- event = jQuery.Event( originalEvent );
+ return this.each(function() {
+ var next = this.nextSibling, parent = this.parentNode;
- for ( var i = this.props.length, prop; i; ){
- prop = this.props[ --i ];
- event[ prop ] = originalEvent[ prop ];
+ jQuery(this).remove();
+
+ if ( next ) {
+ jQuery(next).before( value );
+ } else {
+ jQuery(parent).append( value );
+ }
+ });
+ } else {
+ return this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value );
}
+ },
- // Fix target property, if necessary
- if ( !event.target )
- event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either
+ detach: function( selector ) {
+ return this.remove( selector, true );
+ },
- // check if target is a textnode (safari)
- if ( event.target.nodeType == 3 )
- event.target = event.target.parentNode;
+ domManip: function( args, table, callback ) {
+ var results, first, value = args[0], scripts = [];
- // Add relatedTarget, if necessary
- if ( !event.relatedTarget && event.fromElement )
- event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement;
+ // We can't cloneNode fragments that contain checked, in WebKit
+ if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
+ return this.each(function() {
+ jQuery(this).domManip( args, table, callback, true );
+ });
+ }
- // Calculate pageX/Y if missing and clientX/Y available
- if ( event.pageX == null && event.clientX != null ) {
- var doc = document.documentElement, body = document.body;
- event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
- event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
+ if ( jQuery.isFunction(value) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ args[0] = value.call(this, i, table ? self.html() : undefined);
+ self.domManip( args, table, callback );
+ });
}
- // Add which for key events
- if ( !event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode) )
- event.which = event.charCode || event.keyCode;
+ if ( this[0] ) {
+ // If we're in a fragment, just use that instead of building a new one
+ if ( args[0] && args[0].parentNode && args[0].parentNode.nodeType === 11 ) {
+ results = { fragment: args[0].parentNode };
+ } else {
+ results = buildFragment( args, this, scripts );
+ }
- // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
- if ( !event.metaKey && event.ctrlKey )
- event.metaKey = event.ctrlKey;
+ first = results.fragment.firstChild;
- // Add which for click: 1 == left; 2 == middle; 3 == right
- // Note: button is not normalized, so don't use it
- if ( !event.which && event.button )
- event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
+ if ( first ) {
+ table = table && jQuery.nodeName( first, "tr" );
- return event;
- },
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ callback.call(
+ table ?
+ root(this[i], first) :
+ this[i],
+ results.cacheable || this.length > 1 || i > 0 ?
+ results.fragment.cloneNode(true) :
+ results.fragment
+ );
+ }
+ }
- proxy: function( fn, proxy ){
- proxy = proxy || function(){ return fn.apply(this, arguments); };
- // Set the guid of unique handler to the same of original handler, so it can be removed
- proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
- // So proxy can be declared as an argument
- return proxy;
- },
+ if ( scripts ) {
+ jQuery.each( scripts, evalScript );
+ }
+ }
- special: {
- ready: {
- // Make sure the ready event is setup
- setup: bindReady,
- teardown: function() {}
+ return this;
+
+ function root( elem, cur ) {
+ return jQuery.nodeName(elem, "table") ?
+ (elem.getElementsByTagName("tbody")[0] ||
+ elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
+ elem;
}
- },
-
- specialAll: {
- live: {
- setup: function( selector, namespaces ){
- jQuery.event.add( this, namespaces[0], liveHandler );
- },
- teardown: function( namespaces ){
- if ( namespaces.length ) {
- var remove = 0, name = RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)");
-
- jQuery.each( (jQuery.data(this, "events").live || {}), function(){
- if ( name.test(this.type) )
- remove++;
- });
-
- if ( remove < 1 )
- jQuery.event.remove( this, namespaces[0], liveHandler );
+ }
+});
+
+function cloneCopyEvent(orig, ret) {
+ var i = 0;
+
+ ret.each(function() {
+ if ( this.nodeName !== (orig[i] && orig[i].nodeName) ) {
+ return;
+ }
+
+ var oldData = jQuery.data( orig[i++] ), curData = jQuery.data( this, oldData ), events = oldData && oldData.events;
+
+ if ( events ) {
+ delete curData.handle;
+ curData.events = {};
+
+ for ( var type in events ) {
+ for ( var handler in events[ type ] ) {
+ jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data );
}
}
}
+ });
+}
+
+function buildFragment( args, nodes, scripts ) {
+ var fragment, cacheable, cacheresults, doc;
+
+ // webkit does not clone 'checked' attribute of radio inputs on cloneNode, so don't cache if string has a checked
+ if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && args[0].indexOf("<option") < 0 && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) {
+ cacheable = true;
+ cacheresults = jQuery.fragments[ args[0] ];
+ if ( cacheresults ) {
+ if ( cacheresults !== 1 ) {
+ fragment = cacheresults;
+ }
+ }
}
-};
-jQuery.Event = function( src ){
- // Allow instantiation without the 'new' keyword
- if( !this.preventDefault )
- return new jQuery.Event(src);
-
- // Event object
- if( src && src.type ){
- this.originalEvent = src;
- this.type = src.type;
- // Event type
- }else
- this.type = src;
+ if ( !fragment ) {
+ doc = (nodes && nodes[0] ? nodes[0].ownerDocument || nodes[0] : document);
+ fragment = doc.createDocumentFragment();
+ jQuery.clean( args, doc, fragment, scripts );
+ }
- // timeStamp is buggy for some events on Firefox(#3843)
- // So we won't rely on the native value
- this.timeStamp = now();
-
- // Mark it as fixed
- this[expando] = true;
-};
+ if ( cacheable ) {
+ jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1;
+ }
-function returnFalse(){
- return false;
-}
-function returnTrue(){
- return true;
+ return { fragment: fragment, cacheable: cacheable };
}
-// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
-// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
-jQuery.Event.prototype = {
- preventDefault: function() {
- this.isDefaultPrevented = returnTrue;
+jQuery.fragments = {};
- var e = this.originalEvent;
- if( !e )
- return;
- // if preventDefault exists run it on the original event
- if (e.preventDefault)
- e.preventDefault();
- // otherwise set the returnValue property of the original event to false (IE)
- e.returnValue = false;
- },
- stopPropagation: function() {
- this.isPropagationStopped = returnTrue;
+jQuery.each({
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+}, function( name, original ) {
+ jQuery.fn[ name ] = function( selector ) {
+ var ret = [], insert = jQuery( selector );
- var e = this.originalEvent;
- if( !e )
- return;
- // if stopPropagation exists run it on the original event
- if (e.stopPropagation)
- e.stopPropagation();
- // otherwise set the cancelBubble property of the original event to true (IE)
- e.cancelBubble = true;
- },
- stopImmediatePropagation:function(){
- this.isImmediatePropagationStopped = returnTrue;
- this.stopPropagation();
- },
- isDefaultPrevented: returnFalse,
- isPropagationStopped: returnFalse,
- isImmediatePropagationStopped: returnFalse
-};
-// Checks if an event happened on an element within another element
-// Used in jQuery.event.special.mouseenter and mouseleave handlers
-var withinElement = function(event) {
- // Check if mouse(over|out) are still within the same parent element
- var parent = event.relatedTarget;
- // Traverse up the tree
- while ( parent && parent != this )
- try { parent = parent.parentNode; }
- catch(e) { parent = this; }
-
- if( parent != this ){
- // set the correct event type
- event.type = event.data;
- // handle event if we actually just moused on to a non sub-element
- jQuery.event.handle.apply( this, arguments );
- }
-};
-
-jQuery.each({
- mouseover: 'mouseenter',
- mouseout: 'mouseleave'
-}, function( orig, fix ){
- jQuery.event.special[ fix ] = {
- setup: function(){
- jQuery.event.add( this, orig, withinElement, fix );
- },
- teardown: function(){
- jQuery.event.remove( this, orig, withinElement );
+ for ( var i = 0, l = insert.length; i < l; i++ ) {
+ var elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
+ ret = ret.concat( elems );
}
- };
+ return this.pushStack( ret, name, insert.selector );
+ };
});
-jQuery.fn.extend({
- bind: function( type, data, fn ) {
- return type == "unload" ? this.one(type, data, fn) : this.each(function(){
- jQuery.event.add( this, type, fn || data, fn && data );
- });
- },
+jQuery.each({
+ // keepData is for internal use only--do not document
+ remove: function( selector, keepData ) {
+ if ( !selector || jQuery.filter( selector, [ this ] ).length ) {
+ if ( !keepData && this.nodeType === 1 ) {
+ jQuery.cleanData( this.getElementsByTagName("*") );
+ jQuery.cleanData( [ this ] );
+ }
- one: function( type, data, fn ) {
- var one = jQuery.event.proxy( fn || data, function(event) {
- jQuery(this).unbind(event, one);
- return (fn || data).apply( this, arguments );
- });
- return this.each(function(){
- jQuery.event.add( this, type, one, fn && data);
- });
+ if ( this.parentNode ) {
+ this.parentNode.removeChild( this );
+ }
+ }
},
- unbind: function( type, fn ) {
- return this.each(function(){
- jQuery.event.remove( this, type, fn );
- });
- },
+ empty: function() {
+ // Remove element nodes and prevent memory leaks
+ if ( this.nodeType === 1 ) {
+ jQuery.cleanData( this.getElementsByTagName("*") );
+ }
- trigger: function( type, data ) {
- return this.each(function(){
- jQuery.event.trigger( type, data, this );
- });
- },
+ // Remove any remaining nodes
+ while ( this.firstChild ) {
+ this.removeChild( this.firstChild );
+ }
+ }
+}, function( name, fn ) {
+ jQuery.fn[ name ] = function() {
+ return this.each( fn, arguments );
+ };
+});
- triggerHandler: function( type, data ) {
- if( this[0] ){
- var event = jQuery.Event(type);
- event.preventDefault();
- event.stopPropagation();
- jQuery.event.trigger( event, data, this[0] );
- return event.result;
- }
- },
+jQuery.extend({
+ clean: function( elems, context, fragment, scripts ) {
+ context = context || document;
- toggle: function( fn ) {
- // Save reference to arguments for access in closure
- var args = arguments, i = 1;
+ // !context.createElement fails in IE with an error but returns typeof 'object'
+ if ( typeof context.createElement === "undefined" ) {
+ context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+ }
- // link all the functions, so any of them can unbind this click handler
- while( i < args.length )
- jQuery.event.proxy( fn, args[i++] );
+ var ret = [];
- return this.click( jQuery.event.proxy( fn, function(event) {
- // Figure out which function to execute
- this.lastToggle = ( this.lastToggle || 0 ) % i;
+ jQuery.each(elems, function( i, elem ) {
+ if ( typeof elem === "number" ) {
+ elem += "";
+ }
- // Make sure that clicks stop
- event.preventDefault();
+ if ( !elem ) {
+ return;
+ }
- // and execute the function
- return args[ this.lastToggle++ ].apply( this, arguments ) || false;
- }));
- },
+ // Convert html string into DOM nodes
+ if ( typeof elem === "string" && !rhtml.test( elem ) ) {
+ elem = context.createTextNode( elem );
- hover: function(fnOver, fnOut) {
- return this.mouseenter(fnOver).mouseleave(fnOut);
- },
+ } else if ( typeof elem === "string" ) {
+ // Fix "XHTML"-style tags in all browsers
+ elem = elem.replace(rxhtmlTag, fcloseTag);
- ready: function(fn) {
- // Attach the listeners
- bindReady();
+ // Trim whitespace, otherwise indexOf won't work as expected
+ var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
+ wrap = wrapMap[ tag ] || wrapMap._default,
+ depth = wrap[0],
+ div = context.createElement("div");
- // If the DOM is already ready
- if ( jQuery.isReady )
- // Execute the function immediately
- fn.call( document, jQuery );
+ // Go to html and back, then peel off extra wrappers
+ div.innerHTML = wrap[1] + elem + wrap[2];
- // Otherwise, remember the function for later
- else
- // Add the function to the wait list
- jQuery.readyList.push( fn );
+ // Move to the right depth
+ while ( depth-- ) {
+ div = div.lastChild;
+ }
- return this;
- },
-
- live: function( type, fn ){
- var proxy = jQuery.event.proxy( fn );
- proxy.guid += this.selector + type;
+ // Remove IE's autoinserted <tbody> from table fragments
+ if ( !jQuery.support.tbody ) {
+
+ // String was a <table>, *may* have spurious <tbody>
+ var hasBody = rtbody.test(elem),
+ tbody = tag === "table" && !hasBody ?
+ div.firstChild && div.firstChild.childNodes :
- jQuery(document).bind( liveConvert(type, this.selector), this.selector, proxy );
+ // String was a bare <thead> or <tfoot>
+ wrap[1] === "<table>" && !hasBody ?
+ div.childNodes :
+ [];
- return this;
+ for ( var j = tbody.length - 1; j >= 0 ; --j ) {
+ if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
+ tbody[ j ].parentNode.removeChild( tbody[ j ] );
+ }
+ }
+
+ }
+
+ // IE completely kills leading whitespace when innerHTML is used
+ if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+ div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
+ }
+
+ elem = jQuery.makeArray( div.childNodes );
+ }
+
+ if ( elem.nodeType ) {
+ ret.push( elem );
+ } else {
+ ret = jQuery.merge( ret, elem );
+ }
+
+ });
+
+ if ( fragment ) {
+ for ( var i = 0; ret[i]; i++ ) {
+ if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
+ scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
+ } else {
+ if ( ret[i].nodeType === 1 ) {
+ ret.splice.apply( ret, [i + 1, 0].concat(jQuery.makeArray(ret[i].getElementsByTagName("script"))) );
+ }
+ fragment.appendChild( ret[i] );
+ }
+ }
+ }
+
+ return ret;
},
- die: function( type, fn ){
- jQuery(document).unbind( liveConvert(type, this.selector), fn ? { guid: fn.guid + this.selector + type } : null );
- return this;
+ cleanData: function( elems ) {
+ for ( var i = 0, elem, id; (elem = elems[i]) != null; i++ ) {
+ jQuery.event.remove( elem );
+ jQuery.removeData( elem );
+ }
}
});
+// exclude the following css properties to add px
+var rexclude = /z-?index|font-?weight|opacity|zoom|line-?height/i,
+ ralpha = /alpha\([^)]*\)/,
+ ropacity = /opacity=([^)]*)/,
+ rfloat = /float/i,
+ rdashAlpha = /-([a-z])/ig,
+ rupper = /([A-Z])/g,
+ rnumpx = /^-?\d+(?:px)?$/i,
+ rnum = /^-?\d/,
+
+ cssShow = { position: "absolute", visibility: "hidden", display:"block" },
+ cssWidth = [ "Left", "Right" ],
+ cssHeight = [ "Top", "Bottom" ],
+
+ // cache check for defaultView.getComputedStyle
+ getComputedStyle = document.defaultView && document.defaultView.getComputedStyle,
+ // normalize float css property
+ styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat",
+ fcamelCase = function( all, letter ) {
+ return letter.toUpperCase();
+ };
-function liveHandler( event ){
- var check = RegExp("(^|\\.)" + event.type + "(\\.|$)"),
- stop = true,
- elems = [];
-
- jQuery.each(jQuery.data(this, "events").live || [], function(i, fn){
- if ( check.test(fn.type) ) {
- var elem = jQuery(event.target).closest(fn.data)[0];
- if ( elem )
- elems.push({ elem: elem, fn: fn });
+jQuery.fn.css = function( name, value ) {
+ return access( this, name, value, true, function( elem, name, value ) {
+ if ( value === undefined ) {
+ return jQuery.curCSS( elem, name );
+ }
+
+ if ( typeof value === "number" && !rexclude.test(name) ) {
+ value += "px";
}
- });
- elems.sort(function(a,b) {
- return jQuery.data(a.elem, "closest") - jQuery.data(b.elem, "closest");
- });
-
- jQuery.each(elems, function(){
- if ( this.fn.call(this.elem, event, this.fn.data) === false )
- return (stop = false);
+ jQuery.style( elem, name, value );
});
+};
- return stop;
-}
+jQuery.extend({
+ style: function( elem, name, value ) {
+ // don't set styles on text and comment nodes
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return undefined;
+ }
-function liveConvert(type, selector){
- return ["live", type, selector.replace(/\./g, "`").replace(/ /g, "|")].join(".");
-}
+ // ignore negative width and height values #1599
+ if ( (name === "width" || name === "height") && parseFloat(value) < 0 ) {
+ value = undefined;
+ }
-jQuery.extend({
- isReady: false,
- readyList: [],
- // Handle when the DOM is ready
- ready: function() {
- // Make sure that the DOM is not already loaded
- if ( !jQuery.isReady ) {
- // Remember that the DOM is ready
- jQuery.isReady = true;
+ var style = elem.style || elem, set = value !== undefined;
- // If there are functions bound, to execute
- if ( jQuery.readyList ) {
- // Execute all of them
- jQuery.each( jQuery.readyList, function(){
- this.call( document, jQuery );
- });
+ // IE uses filters for opacity
+ if ( !jQuery.support.opacity && name === "opacity" ) {
+ if ( set ) {
+ // IE has trouble with opacity if it does not have layout
+ // Force it by setting the zoom level
+ style.zoom = 1;
- // Reset the list of functions
- jQuery.readyList = null;
+ // Set the alpha filter to set the opacity
+ var opacity = parseInt( value, 10 ) + "" === "NaN" ? "" : "alpha(opacity=" + value * 100 + ")";
+ var filter = style.filter || jQuery.curCSS( elem, "filter" ) || "";
+ style.filter = ralpha.test(filter) ? filter.replace(ralpha, opacity) : opacity;
}
- // Trigger any bound ready events
- jQuery(document).triggerHandler("ready");
+ return style.filter && style.filter.indexOf("opacity=") >= 0 ?
+ (parseFloat( ropacity.exec(style.filter)[1] ) / 100) + "":
+ "";
}
- }
-});
-var readyBound = false;
+ // Make sure we're using the right name for getting the float value
+ if ( rfloat.test( name ) ) {
+ name = styleFloat;
+ }
-function bindReady(){
- if ( readyBound ) return;
- readyBound = true;
+ name = name.replace(rdashAlpha, fcamelCase);
- // Mozilla, Opera and webkit nightlies currently support this event
- if ( document.addEventListener ) {
- // Use the handy event callback
- document.addEventListener( "DOMContentLoaded", function(){
- document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
- jQuery.ready();
- }, false );
+ if ( set ) {
+ style[ name ] = value;
+ }
+
+ return style[ name ];
+ },
+
+ css: function( elem, name, force, extra ) {
+ if ( name === "width" || name === "height" ) {
+ var val, props = cssShow, which = name === "width" ? cssWidth : cssHeight;
+
+ function getWH() {
+ val = name === "width" ? elem.offsetWidth : elem.offsetHeight;
+
+ if ( extra === "border" ) {
+ return;
+ }
- // If IE event model is used
- } else if ( document.attachEvent ) {
- // ensure firing before onload,
- // maybe late but safe also for iframes
- document.attachEvent("onreadystatechange", function(){
- if ( document.readyState === "complete" ) {
- document.detachEvent( "onreadystatechange", arguments.callee );
- jQuery.ready();
+ jQuery.each( which, function() {
+ if ( !extra ) {
+ val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0;
+ }
+
+ if ( extra === "margin" ) {
+ val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0;
+ } else {
+ val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0;
+ }
+ });
}
- });
- // If IE and not an iframe
- // continually check to see if the document is ready
- if ( document.documentElement.doScroll && window == window.top ) (function(){
- if ( jQuery.isReady ) return;
+ if ( elem.offsetWidth !== 0 ) {
+ getWH();
+ } else {
+ jQuery.swap( elem, props, getWH );
+ }
- try {
- // If IE is used, use the trick by Diego Perini
- // http://javascript.nwbox.com/IEContentLoaded/
- document.documentElement.doScroll("left");
- } catch( error ) {
- setTimeout( arguments.callee, 0 );
- return;
+ return Math.max(0, Math.round(val));
+ }
+
+ return jQuery.curCSS( elem, name, force );
+ },
+
+ curCSS: function( elem, name, force ) {
+ var ret, style = elem.style, filter;
+
+ // IE uses filters for opacity
+ if ( !jQuery.support.opacity && name === "opacity" && elem.currentStyle ) {
+ ret = ropacity.test(elem.currentStyle.filter || "") ?
+ (parseFloat(RegExp.$1) / 100) + "" :
+ "";
+
+ return ret === "" ?
+ "1" :
+ ret;
+ }
+
+ // Make sure we're using the right name for getting the float value
+ if ( rfloat.test( name ) ) {
+ name = styleFloat;
+ }
+
+ if ( !force && style && style[ name ] ) {
+ ret = style[ name ];
+
+ } else if ( getComputedStyle ) {
+
+ // Only "float" is needed here
+ if ( rfloat.test( name ) ) {
+ name = "float";
}
- // and execute any waiting functions
- jQuery.ready();
- })();
- }
+ name = name.replace( rupper, "-$1" ).toLowerCase();
- // A fallback to window.onload, that will always work
- jQuery.event.add( window, "load", jQuery.ready );
-}
+ var defaultView = elem.ownerDocument.defaultView;
-jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
- "mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
- "change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){
+ if ( !defaultView ) {
+ return null;
+ }
- // Handle event binding
- jQuery.fn[name] = function(fn){
- return fn ? this.bind(name, fn) : this.trigger(name);
- };
-});
+ var computedStyle = defaultView.getComputedStyle( elem, null );
-// Prevent memory leaks in IE
-// And prevent errors on refresh with events like mouseover in other browsers
-// Window isn't included so as not to unbind existing unload events
-jQuery( window ).bind( 'unload', function(){
- for ( var id in jQuery.cache )
- // Skip the window
- if ( id != 1 && jQuery.cache[ id ].handle )
- jQuery.event.remove( jQuery.cache[ id ].handle.elem );
-});
-(function(){
+ if ( computedStyle ) {
+ ret = computedStyle.getPropertyValue( name );
+ }
- jQuery.support = {};
+ // We should always get a number back from opacity
+ if ( name === "opacity" && ret === "" ) {
+ ret = "1";
+ }
- var root = document.documentElement,
- script = document.createElement("script"),
- div = document.createElement("div"),
- id = "script" + (new Date).getTime();
+ } else if ( elem.currentStyle ) {
+ var camelCase = name.replace(rdashAlpha, fcamelCase);
- div.style.display = "none";
- div.innerHTML = ' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';
+ ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];
- var all = div.getElementsByTagName("*"),
- a = div.getElementsByTagName("a")[0];
+ // From the awesome hack by Dean Edwards
+ // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
- // Can't get basic test support
- if ( !all || !all.length || !a ) {
- return;
- }
+ // If we're not dealing with a regular pixel number
+ // but a number that has a weird ending, we need to convert it to pixels
+ if ( !rnumpx.test( ret ) && rnum.test( ret ) ) {
+ // Remember the original values
+ var left = style.left, rsLeft = elem.runtimeStyle.left;
- jQuery.support = {
- // IE strips leading whitespace when .innerHTML is used
- leadingWhitespace: div.firstChild.nodeType == 3,
-
- // Make sure that tbody elements aren't automatically inserted
- // IE will insert them into empty tables
- tbody: !div.getElementsByTagName("tbody").length,
-
- // Make sure that you can get all elements in an <object> element
- // IE 7 always returns no results
- objectAll: !!div.getElementsByTagName("object")[0]
- .getElementsByTagName("*").length,
-
- // Make sure that link elements get serialized correctly by innerHTML
- // This requires a wrapper element in IE
- htmlSerialize: !!div.getElementsByTagName("link").length,
-
- // Get the style information from getAttribute
- // (IE uses .cssText insted)
- style: /red/.test( a.getAttribute("style") ),
-
- // Make sure that URLs aren't manipulated
- // (IE normalizes it by default)
- hrefNormalized: a.getAttribute("href") === "/a",
-
- // Make sure that element opacity exists
- // (IE uses filter instead)
- opacity: a.style.opacity === "0.5",
-
- // Verify style float existence
- // (IE uses styleFloat instead of cssFloat)
- cssFloat: !!a.style.cssFloat,
+ // Put in the new values to get a computed value out
+ elem.runtimeStyle.left = elem.currentStyle.left;
+ style.left = camelCase === "fontSize" ? "1em" : (ret || 0);
+ ret = style.pixelLeft + "px";
- // Will be defined later
- scriptEval: false,
- noCloneEvent: true,
- boxModel: null
- };
-
- script.type = "text/javascript";
- try {
- script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
- } catch(e){}
+ // Revert the changed values
+ style.left = left;
+ elem.runtimeStyle.left = rsLeft;
+ }
+ }
- root.insertBefore( script, root.firstChild );
-
- // Make sure that the execution of code works by injecting a script
- // tag with appendChild/createTextNode
- // (IE doesn't support this, fails, and uses .text instead)
- if ( window[ id ] ) {
- jQuery.support.scriptEval = true;
- delete window[ id ];
- }
+ return ret;
+ },
- root.removeChild( script );
+ // A method for quickly swapping in/out CSS properties to get correct calculations
+ swap: function( elem, options, callback ) {
+ var old = {};
- if ( div.attachEvent && div.fireEvent ) {
- div.attachEvent("onclick", function(){
- // Cloning a node shouldn't copy over any
- // bound event handlers (IE does this)
- jQuery.support.noCloneEvent = false;
- div.detachEvent("onclick", arguments.callee);
- });
- div.cloneNode(true).fireEvent("onclick");
+ // Remember the old values, and insert the new ones
+ for ( var name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ callback.call( elem );
+
+ // Revert the old values
+ for ( var name in options ) {
+ elem.style[ name ] = old[ name ];
+ }
}
+});
- // Figure out if the W3C box model works as expected
- // document.body must exist before we can do this
- jQuery(function(){
- var div = document.createElement("div");
- div.style.width = div.style.paddingLeft = "1px";
+if ( jQuery.expr && jQuery.expr.filters ) {
+ jQuery.expr.filters.hidden = function( elem ) {
+ var width = elem.offsetWidth, height = elem.offsetHeight,
+ skip = elem.nodeName.toLowerCase() === "tr";
- document.body.appendChild( div );
- jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2;
- document.body.removeChild( div ).style.display = 'none';
- });
-})();
+ return width === 0 && height === 0 && !skip ?
+ true :
+ width > 0 && height > 0 && !skip ?
+ false :
+ jQuery.curCSS(elem, "display") === "none";
+ };
-var styleFloat = jQuery.support.cssFloat ? "cssFloat" : "styleFloat";
+ jQuery.expr.filters.visible = function( elem ) {
+ return !jQuery.expr.filters.hidden( elem );
+ };
+}
+var jsc = now(),
+ rscript = /<script(.|\s)*?\/script>/gi,
+ rselectTextarea = /select|textarea/i,
+ rinput = /color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,
+ jsre = /=\?(&|$)/,
+ rquery = /\?/,
+ rts = /(\?|&)_=.*?(&|$)/,
+ rurl = /^(\w+:)?\/\/([^\/?#]+)/,
+ r20 = /%20/g;
-jQuery.props = {
- "for": "htmlFor",
- "class": "className",
- "float": styleFloat,
- cssFloat: styleFloat,
- styleFloat: styleFloat,
- readonly: "readOnly",
- maxlength: "maxLength",
- cellspacing: "cellSpacing",
- rowspan: "rowSpan",
- tabindex: "tabIndex"
-};
jQuery.fn.extend({
// Keep a copy of the old load
_load: jQuery.fn.load,
load: function( url, params, callback ) {
- if ( typeof url !== "string" )
+ if ( typeof url !== "string" ) {
return this._load( url );
+ // Don't do a request if no elements are being requested
+ } else if ( !this.length ) {
+ return this;
+ }
+
var off = url.indexOf(" ");
if ( off >= 0 ) {
var selector = url.slice(off, url.length);
@@ -3239,7 +4639,7 @@ jQuery.fn.extend({
var type = "GET";
// If the second parameter was provided
- if ( params )
+ if ( params ) {
// If it's a function
if ( jQuery.isFunction( params ) ) {
// We assume that it's the callback
@@ -3247,10 +4647,11 @@ jQuery.fn.extend({
params = null;
// Otherwise, build a param string
- } else if( typeof params === "object" ) {
- params = jQuery.param( params );
+ } else if ( typeof params === "object" ) {
+ params = jQuery.param( params, jQuery.ajaxSettings.traditional );
type = "POST";
}
+ }
var self = this;
@@ -3260,27 +4661,30 @@ jQuery.fn.extend({
type: type,
dataType: "html",
data: params,
- complete: function(res, status){
+ complete: function( res, status ) {
// If successful, inject the HTML into all the matched elements
- if ( status == "success" || status == "notmodified" )
+ if ( status === "success" || status === "notmodified" ) {
// See if a selector was specified
self.html( selector ?
// Create a dummy div to hold the results
- jQuery("<div/>")
+ jQuery("<div />")
// inject the contents of the document in, removing the scripts
// to avoid any 'Permission Denied' errors in IE
- .append(res.responseText.replace(/<script(.|\s)*?\/script>/g, ""))
+ .append(res.responseText.replace(rscript, ""))
// Locate the specified elements
.find(selector) :
// If not, just inject the full result
res.responseText );
+ }
- if( callback )
+ if ( callback ) {
self.each( callback, [res.responseText, status, res] );
+ }
}
});
+
return this;
},
@@ -3288,40 +4692,41 @@ jQuery.fn.extend({
return jQuery.param(this.serializeArray());
},
serializeArray: function() {
- return this.map(function(){
+ return this.map(function() {
return this.elements ? jQuery.makeArray(this.elements) : this;
})
- .filter(function(){
+ .filter(function() {
return this.name && !this.disabled &&
- (this.checked || /select|textarea/i.test(this.nodeName) ||
- /text|hidden|password|search/i.test(this.type));
+ (this.checked || rselectTextarea.test(this.nodeName) ||
+ rinput.test(this.type));
})
- .map(function(i, elem){
+ .map(function( i, elem ) {
var val = jQuery(this).val();
- return val == null ? null :
+
+ return val == null ?
+ null :
jQuery.isArray(val) ?
- jQuery.map( val, function(val, i){
- return {name: elem.name, value: val};
+ jQuery.map( val, function( val, i ) {
+ return { name: elem.name, value: val };
}) :
- {name: elem.name, value: val};
+ { name: elem.name, value: val };
}).get();
}
});
// Attach a bunch of functions for handling common AJAX events
-jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){
- jQuery.fn[o] = function(f){
+jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function( i, o ) {
+ jQuery.fn[o] = function( f ) {
return this.bind(o, f);
};
});
-var jsc = now();
-
jQuery.extend({
-
+
get: function( url, data, callback, type ) {
- // shift arguments if data argument was ommited
+ // shift arguments if data argument was omited
if ( jQuery.isFunction( data ) ) {
+ type = type || callback;
callback = data;
data = null;
}
@@ -3344,7 +4749,9 @@ jQuery.extend({
},
post: function( url, data, callback, type ) {
+ // shift arguments if data argument was omited
if ( jQuery.isFunction( data ) ) {
+ type = type || callback;
callback = data;
data = {};
}
@@ -3374,13 +4781,21 @@ jQuery.extend({
data: null,
username: null,
password: null,
+ traditional: false,
*/
// Create the request object; Microsoft failed to properly
- // implement the XMLHttpRequest in IE7, so we use the ActiveXObject when it is available
+ // implement the XMLHttpRequest in IE7 (can't request local files),
+ // so we use the ActiveXObject when it is available
// This function can be overriden by calling jQuery.ajaxSetup
- xhr:function(){
- return window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
- },
+ xhr: window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject) ?
+ function() {
+ return new window.XMLHttpRequest();
+ } :
+ function() {
+ try {
+ return new window.ActiveXObject("Microsoft.XMLHTTP");
+ } catch(e) {}
+ },
accepts: {
xml: "application/xml, text/xml",
html: "text/html",
@@ -3393,36 +4808,41 @@ jQuery.extend({
// Last-Modified header cache for next request
lastModified: {},
+ etag: {},
- ajax: function( s ) {
- // Extend the settings, but re-extend 's' so that it can be
- // checked again later (in the test suite, specifically)
- s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s));
-
- var jsonp, jsre = /=\?(&|$)/g, status, data,
+ ajax: function( origSettings ) {
+ var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings);
+
+ var jsonp, status, data,
+ callbackContext = origSettings && origSettings.context || s,
type = s.type.toUpperCase();
// convert data if not already a string
- if ( s.data && s.processData && typeof s.data !== "string" )
- s.data = jQuery.param(s.data);
+ if ( s.data && s.processData && typeof s.data !== "string" ) {
+ s.data = jQuery.param( s.data, s.traditional );
+ }
// Handle JSONP Parameter Callbacks
- if ( s.dataType == "jsonp" ) {
- if ( type == "GET" ) {
- if ( !s.url.match(jsre) )
- s.url += (s.url.match(/\?/) ? "&" : "?") + (s.jsonp || "callback") + "=?";
- } else if ( !s.data || !s.data.match(jsre) )
+ if ( s.dataType === "jsonp" ) {
+ if ( type === "GET" ) {
+ if ( !jsre.test( s.url ) ) {
+ s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?";
+ }
+ } else if ( !s.data || !jsre.test(s.data) ) {
s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
+ }
s.dataType = "json";
}
// Build temporary JSONP function
- if ( s.dataType == "json" && (s.data && s.data.match(jsre) || s.url.match(jsre)) ) {
- jsonp = "jsonp" + jsc++;
+ if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) {
+ jsonp = s.jsonpCallback || ("jsonp" + jsc++);
// Replace the =? sequence both in the query string and the data
- if ( s.data )
+ if ( s.data ) {
s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
+ }
+
s.url = s.url.replace(jsre, "=" + jsonp + "$1");
// We need to make sure
@@ -3430,75 +4850,85 @@ jQuery.extend({
s.dataType = "script";
// Handle JSONP-style loading
- window[ jsonp ] = function(tmp){
+ window[ jsonp ] = window[ jsonp ] || function( tmp ) {
data = tmp;
success();
complete();
// Garbage collect
window[ jsonp ] = undefined;
- try{ delete window[ jsonp ]; } catch(e){}
- if ( head )
+
+ try {
+ delete window[ jsonp ];
+ } catch(e) {}
+
+ if ( head ) {
head.removeChild( script );
+ }
};
}
- if ( s.dataType == "script" && s.cache == null )
+ if ( s.dataType === "script" && s.cache === null ) {
s.cache = false;
+ }
- if ( s.cache === false && type == "GET" ) {
+ if ( s.cache === false && type === "GET" ) {
var ts = now();
+
// try replacing _= if it is there
- var ret = s.url.replace(/(\?|&)_=.*?(&|$)/, "$1_=" + ts + "$2");
+ var ret = s.url.replace(rts, "$1_=" + ts + "$2");
+
// if nothing was replaced, add timestamp to the end
- s.url = ret + ((ret == s.url) ? (s.url.match(/\?/) ? "&" : "?") + "_=" + ts : "");
+ s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : "");
}
// If data is available, append data to url for get requests
- if ( s.data && type == "GET" ) {
- s.url += (s.url.match(/\?/) ? "&" : "?") + s.data;
-
- // IE likes to send both get and post data, prevent this
- s.data = null;
+ if ( s.data && type === "GET" ) {
+ s.url += (rquery.test(s.url) ? "&" : "?") + s.data;
}
// Watch for a new set of requests
- if ( s.global && ! jQuery.active++ )
+ if ( s.global && ! jQuery.active++ ) {
jQuery.event.trigger( "ajaxStart" );
+ }
// Matches an absolute URL, and saves the domain
- var parts = /^(\w+:)?\/\/([^\/?#]+)/.exec( s.url );
+ var parts = rurl.exec( s.url ),
+ remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
// If we're requesting a remote document
// and trying to load JSON or Script with a GET
- if ( s.dataType == "script" && type == "GET" && parts
- && ( parts[1] && parts[1] != location.protocol || parts[2] != location.host )){
-
- var head = document.getElementsByTagName("head")[0];
+ if ( s.dataType === "script" && type === "GET" && remote ) {
+ var head = document.getElementsByTagName("head")[0] || document.documentElement;
var script = document.createElement("script");
script.src = s.url;
- if (s.scriptCharset)
+ if ( s.scriptCharset ) {
script.charset = s.scriptCharset;
+ }
// Handle Script loading
if ( !jsonp ) {
var done = false;
// Attach handlers for all browsers
- script.onload = script.onreadystatechange = function(){
+ script.onload = script.onreadystatechange = function() {
if ( !done && (!this.readyState ||
- this.readyState == "loaded" || this.readyState == "complete") ) {
+ this.readyState === "loaded" || this.readyState === "complete") ) {
done = true;
success();
complete();
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
- head.removeChild( script );
+ if ( head && script.parentNode ) {
+ head.removeChild( script );
+ }
}
};
}
- head.appendChild(script);
+ // Use insertBefore instead of appendChild to circumvent an IE6 bug.
+ // This arises when a base node is used (#2709 and #4378).
+ head.insertBefore( script, head.firstChild );
// We handle everything using the script element injection
return undefined;
@@ -3509,158 +4939,197 @@ jQuery.extend({
// Create the request object
var xhr = s.xhr();
+ if ( !xhr ) {
+ return;
+ }
+
// Open the socket
// Passing null username, generates a login popup on Opera (#2865)
- if( s.username )
+ if ( s.username ) {
xhr.open(type, s.url, s.async, s.username, s.password);
- else
+ } else {
xhr.open(type, s.url, s.async);
+ }
// Need an extra try/catch for cross domain requests in Firefox 3
try {
// Set the correct header, if data is being sent
- if ( s.data )
+ if ( s.data || origSettings && origSettings.contentType ) {
xhr.setRequestHeader("Content-Type", s.contentType);
+ }
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ if ( jQuery.lastModified[s.url] ) {
+ xhr.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url]);
+ }
- // Set the If-Modified-Since header, if ifModified mode.
- if ( s.ifModified )
- xhr.setRequestHeader("If-Modified-Since",
- jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
+ if ( jQuery.etag[s.url] ) {
+ xhr.setRequestHeader("If-None-Match", jQuery.etag[s.url]);
+ }
+ }
// Set header so the called script knows that it's an XMLHttpRequest
- xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ // Only send the header if it's not a remote XHR
+ if ( !remote ) {
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ }
// Set the Accepts header for the server, depending on the dataType
xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
s.accepts[ s.dataType ] + ", */*" :
s.accepts._default );
- } catch(e){}
+ } catch(e) {}
// Allow custom headers/mimetypes and early abort
- if ( s.beforeSend && s.beforeSend(xhr, s) === false ) {
+ if ( s.beforeSend && s.beforeSend.call(callbackContext, xhr, s) === false ) {
// Handle the global AJAX counter
- if ( s.global && ! --jQuery.active )
+ if ( s.global && ! --jQuery.active ) {
jQuery.event.trigger( "ajaxStop" );
+ }
+
// close opended socket
xhr.abort();
return false;
}
- if ( s.global )
- jQuery.event.trigger("ajaxSend", [xhr, s]);
+ if ( s.global ) {
+ trigger("ajaxSend", [xhr, s]);
+ }
// Wait for a response to come back
- var onreadystatechange = function(isTimeout){
- // The request was aborted, clear the interval and decrement jQuery.active
- if (xhr.readyState == 0) {
- if (ival) {
- // clear poll interval
- clearInterval(ival);
- ival = null;
- // Handle the global AJAX counter
- if ( s.global && ! --jQuery.active )
- jQuery.event.trigger( "ajaxStop" );
+ var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) {
+ // The request was aborted
+ if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" ) {
+ // Opera doesn't call onreadystatechange before this point
+ // so we simulate the call
+ if ( !requestDone ) {
+ complete();
+ }
+
+ requestDone = true;
+ if ( xhr ) {
+ xhr.onreadystatechange = jQuery.noop;
}
+
// The transfer is complete and the data is available, or the request timed out
- } else if ( !requestDone && xhr && (xhr.readyState == 4 || isTimeout == "timeout") ) {
+ } else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) {
requestDone = true;
+ xhr.onreadystatechange = jQuery.noop;
- // clear poll interval
- if (ival) {
- clearInterval(ival);
- ival = null;
- }
+ status = isTimeout === "timeout" ?
+ "timeout" :
+ !jQuery.httpSuccess( xhr ) ?
+ "error" :
+ s.ifModified && jQuery.httpNotModified( xhr, s.url ) ?
+ "notmodified" :
+ "success";
- status = isTimeout == "timeout" ? "timeout" :
- !jQuery.httpSuccess( xhr ) ? "error" :
- s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? "notmodified" :
- "success";
+ var errMsg;
- if ( status == "success" ) {
+ if ( status === "success" ) {
// Watch for, and catch, XML document parse errors
try {
// process the data (runs the xml through httpData regardless of callback)
data = jQuery.httpData( xhr, s.dataType, s );
- } catch(e) {
+ } catch(err) {
status = "parsererror";
+ errMsg = err;
}
}
// Make sure that the request was successful or notmodified
- if ( status == "success" ) {
- // Cache Last-Modified header, if ifModified mode.
- var modRes;
- try {
- modRes = xhr.getResponseHeader("Last-Modified");
- } catch(e) {} // swallow exception thrown by FF if header is not available
-
- if ( s.ifModified && modRes )
- jQuery.lastModified[s.url] = modRes;
-
+ if ( status === "success" || status === "notmodified" ) {
// JSONP handles its own success callback
- if ( !jsonp )
+ if ( !jsonp ) {
success();
- } else
- jQuery.handleError(s, xhr, status);
+ }
+ } else {
+ jQuery.handleError(s, xhr, status, errMsg);
+ }
// Fire the complete handlers
complete();
- if ( isTimeout )
+ if ( isTimeout === "timeout" ) {
xhr.abort();
+ }
// Stop memory leaks
- if ( s.async )
+ if ( s.async ) {
xhr = null;
+ }
}
};
- if ( s.async ) {
- // don't attach the handler to the request, just poll it instead
- var ival = setInterval(onreadystatechange, 13);
+ // Override the abort handler, if we can (IE doesn't allow it, but that's OK)
+ // Opera doesn't fire onreadystatechange at all on abort
+ try {
+ var oldAbort = xhr.abort;
+ xhr.abort = function() {
+ if ( xhr ) {
+ oldAbort.call( xhr );
+ }
- // Timeout checker
- if ( s.timeout > 0 )
- setTimeout(function(){
- // Check to see if the request is still happening
- if ( xhr && !requestDone )
- onreadystatechange( "timeout" );
- }, s.timeout);
+ onreadystatechange( "abort" );
+ };
+ } catch(e) { }
+
+ // Timeout checker
+ if ( s.async && s.timeout > 0 ) {
+ setTimeout(function() {
+ // Check to see if the request is still happening
+ if ( xhr && !requestDone ) {
+ onreadystatechange( "timeout" );
+ }
+ }, s.timeout);
}
// Send the data
try {
- xhr.send(s.data);
+ xhr.send( type === "POST" || type === "PUT" || type === "DELETE" ? s.data : null );
} catch(e) {
jQuery.handleError(s, xhr, null, e);
+ // Fire the complete handlers
+ complete();
}
// firefox 1.5 doesn't fire statechange for sync requests
- if ( !s.async )
+ if ( !s.async ) {
onreadystatechange();
+ }
- function success(){
+ function success() {
// If a local callback was specified, fire it and pass it the data
- if ( s.success )
- s.success( data, status );
+ if ( s.success ) {
+ s.success.call( callbackContext, data, status, xhr );
+ }
// Fire the global callback
- if ( s.global )
- jQuery.event.trigger( "ajaxSuccess", [xhr, s] );
+ if ( s.global ) {
+ trigger( "ajaxSuccess", [xhr, s] );
+ }
}
- function complete(){
+ function complete() {
// Process result
- if ( s.complete )
- s.complete(xhr, status);
+ if ( s.complete ) {
+ s.complete.call( callbackContext, xhr, status);
+ }
// The request was completed
- if ( s.global )
- jQuery.event.trigger( "ajaxComplete", [xhr, s] );
+ if ( s.global ) {
+ trigger( "ajaxComplete", [xhr, s] );
+ }
// Handle the global AJAX counter
- if ( s.global && ! --jQuery.active )
+ if ( s.global && ! --jQuery.active ) {
jQuery.event.trigger( "ajaxStop" );
+ }
+ }
+
+ function trigger(type, args) {
+ (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
}
// return XMLHttpRequest to allow aborting the request etc.
@@ -3669,11 +5138,14 @@ jQuery.extend({
handleError: function( s, xhr, status, e ) {
// If a local callback was specified, fire it
- if ( s.error ) s.error( xhr, status, e );
+ if ( s.error ) {
+ s.error.call( s.context || s, xhr, status, e );
+ }
// Fire the global callback
- if ( s.global )
- jQuery.event.trigger( "ajaxError", [xhr, s, e] );
+ if ( s.global ) {
+ (s.context ? jQuery(s.context) : jQuery.event).trigger( "ajaxError", [xhr, s, e] );
+ }
},
// Counter for holding the number of active queries
@@ -3683,86 +5155,131 @@ jQuery.extend({
httpSuccess: function( xhr ) {
try {
// IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
- return !xhr.status && location.protocol == "file:" ||
- ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status == 304 || xhr.status == 1223;
- } catch(e){}
+ return !xhr.status && location.protocol === "file:" ||
+ // Opera returns 0 when status is 304
+ ( xhr.status >= 200 && xhr.status < 300 ) ||
+ xhr.status === 304 || xhr.status === 1223 || xhr.status === 0;
+ } catch(e) {}
+
return false;
},
// Determines if an XMLHttpRequest returns NotModified
httpNotModified: function( xhr, url ) {
- try {
- var xhrRes = xhr.getResponseHeader("Last-Modified");
+ var lastModified = xhr.getResponseHeader("Last-Modified"),
+ etag = xhr.getResponseHeader("Etag");
- // Firefox always returns 200. check Last-Modified date
- return xhr.status == 304 || xhrRes == jQuery.lastModified[url];
- } catch(e){}
- return false;
+ if ( lastModified ) {
+ jQuery.lastModified[url] = lastModified;
+ }
+
+ if ( etag ) {
+ jQuery.etag[url] = etag;
+ }
+
+ // Opera returns 0 when status is 304
+ return xhr.status === 304 || xhr.status === 0;
},
httpData: function( xhr, type, s ) {
- var ct = xhr.getResponseHeader("content-type"),
- xml = type == "xml" || !type && ct && ct.indexOf("xml") >= 0,
+ var ct = xhr.getResponseHeader("content-type") || "",
+ xml = type === "xml" || !type && ct.indexOf("xml") >= 0,
data = xml ? xhr.responseXML : xhr.responseText;
- if ( xml && data.documentElement.tagName == "parsererror" )
- throw "parsererror";
-
+ if ( xml && data.documentElement.nodeName === "parsererror" ) {
+ jQuery.error( "parsererror" );
+ }
+
// Allow a pre-filtering function to sanitize the response
- // s != null is checked to keep backwards compatibility
- if( s && s.dataFilter )
+ // s is checked to keep backwards compatibility
+ if ( s && s.dataFilter ) {
data = s.dataFilter( data, type );
+ }
// The filter can actually parse the response
- if( typeof data === "string" ){
+ if ( typeof data === "string" ) {
+ // Get the JavaScript object, if JSON is used.
+ if ( type === "json" || !type && ct.indexOf("json") >= 0 ) {
+ data = jQuery.parseJSON( data );
// If the type is "script", eval it in global context
- if ( type == "script" )
+ } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) {
jQuery.globalEval( data );
-
- // Get the JavaScript object, if JSON is used.
- if ( type == "json" )
- data = window["eval"]("(" + data + ")");
+ }
}
-
+
return data;
},
// Serialize an array of form elements or a set of
// key/values into a query string
- param: function( a ) {
- var s = [ ];
-
- function add( key, value ){
- s[ s.length ] = encodeURIComponent(key) + '=' + encodeURIComponent(value);
- };
-
- // If an array was passed in, assume that it is an array
- // of form elements
- if ( jQuery.isArray(a) || a.jquery )
+ param: function( a, traditional ) {
+ var s = [];
+
+ // Set traditional to true for jQuery <= 1.3.2 behavior.
+ if ( traditional === undefined ) {
+ traditional = jQuery.ajaxSettings.traditional;
+ }
+
+ // If an array was passed in, assume that it is an array of form elements.
+ if ( jQuery.isArray(a) || a.jquery ) {
// Serialize the form elements
- jQuery.each( a, function(){
+ jQuery.each( a, function() {
add( this.name, this.value );
});
-
- // Otherwise, assume that it's an object of key/value pairs
- else
- // Serialize the key/values
- for ( var j in a )
- // If the value is an array then the key names need to be repeated
- if ( jQuery.isArray(a[j]) )
- jQuery.each( a[j], function(){
- add( j, this );
- });
- else
- add( j, jQuery.isFunction(a[j]) ? a[j]() : a[j] );
+
+ } else {
+ // If traditional, encode the "old" way (the way 1.3.2 or older
+ // did it), otherwise encode params recursively.
+ for ( var prefix in a ) {
+ buildParams( prefix, a[prefix] );
+ }
+ }
// Return the resulting serialization
- return s.join("&").replace(/%20/g, "+");
- }
+ return s.join("&").replace(r20, "+");
+
+ function buildParams( prefix, obj ) {
+ if ( jQuery.isArray(obj) ) {
+ // Serialize array item.
+ jQuery.each( obj, function( i, v ) {
+ if ( traditional ) {
+ // Treat each array item as a scalar.
+ add( prefix, v );
+ } else {
+ // If array item is non-scalar (array or object), encode its
+ // numeric index to resolve deserialization ambiguity issues.
+ // Note that rack (as of 1.0.0) can't currently deserialize
+ // nested arrays properly, and attempting to do so may cause
+ // a server error. Possible fixes are to modify rack's
+ // deserialization algorithm or to provide an option or flag
+ // to force array serialization to be shallow.
+ buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v );
+ }
+ });
+
+ } else if ( !traditional && obj != null && typeof obj === "object" ) {
+ // Serialize object item.
+ jQuery.each( obj, function( k, v ) {
+ buildParams( prefix + "[" + k + "]", v );
+ });
+
+ } else {
+ // Serialize scalar item.
+ add( prefix, obj );
+ }
+ }
+ function add( key, value ) {
+ // If value is a function, invoke it and return its value
+ value = jQuery.isFunction(value) ? value() : value;
+ s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
+ }
+ }
});
var elemdisplay = {},
+ rfxtypes = /toggle|show|hide/,
+ rfxnum = /^([+-]=)?([\d+-.]+)(.*)$/,
timerId,
fxAttrs = [
// height animations
@@ -3773,69 +5290,67 @@ var elemdisplay = {},
[ "opacity" ]
];
-function genFx( type, num ){
- var obj = {};
- jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function(){
- obj[ this ] = type;
- });
- return obj;
-}
-
jQuery.fn.extend({
- show: function(speed,callback){
- if ( speed ) {
+ show: function( speed, callback ) {
+ if ( speed || speed === 0) {
return this.animate( genFx("show", 3), speed, callback);
+
} else {
- for ( var i = 0, l = this.length; i < l; i++ ){
+ for ( var i = 0, l = this.length; i < l; i++ ) {
var old = jQuery.data(this[i], "olddisplay");
-
+
this[i].style.display = old || "";
-
+
if ( jQuery.css(this[i], "display") === "none" ) {
- var tagName = this[i].tagName, display;
-
- if ( elemdisplay[ tagName ] ) {
- display = elemdisplay[ tagName ];
+ var nodeName = this[i].nodeName, display;
+
+ if ( elemdisplay[ nodeName ] ) {
+ display = elemdisplay[ nodeName ];
+
} else {
- var elem = jQuery("<" + tagName + " />").appendTo("body");
-
+ var elem = jQuery("<" + nodeName + " />").appendTo("body");
+
display = elem.css("display");
- if ( display === "none" )
+
+ if ( display === "none" ) {
display = "block";
-
+ }
+
elem.remove();
-
- elemdisplay[ tagName ] = display;
+
+ elemdisplay[ nodeName ] = display;
}
-
+
jQuery.data(this[i], "olddisplay", display);
}
}
// Set the display of the elements in a second loop
// to avoid the constant reflow
- for ( var i = 0, l = this.length; i < l; i++ ){
- this[i].style.display = jQuery.data(this[i], "olddisplay") || "";
+ for ( var j = 0, k = this.length; j < k; j++ ) {
+ this[j].style.display = jQuery.data(this[j], "olddisplay") || "";
}
-
+
return this;
}
},
- hide: function(speed,callback){
- if ( speed ) {
+ hide: function( speed, callback ) {
+ if ( speed || speed === 0 ) {
return this.animate( genFx("hide", 3), speed, callback);
+
} else {
- for ( var i = 0, l = this.length; i < l; i++ ){
+ for ( var i = 0, l = this.length; i < l; i++ ) {
var old = jQuery.data(this[i], "olddisplay");
- if ( !old && old !== "none" )
+ if ( !old && old !== "none" ) {
jQuery.data(this[i], "olddisplay", jQuery.css(this[i], "display"));
+ }
}
// Set the display of the elements in a second loop
// to avoid the constant reflow
- for ( var i = 0, l = this.length; i < l; i++ ){
- this[i].style.display = "none";
+ for ( var j = 0, k = this.length; j < k; j++ ) {
+ this[j].style.display = "none";
}
return this;
@@ -3845,77 +5360,107 @@ jQuery.fn.extend({
// Save the old toggle function
_toggle: jQuery.fn.toggle,
- toggle: function( fn, fn2 ){
+ toggle: function( fn, fn2 ) {
var bool = typeof fn === "boolean";
- return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
- this._toggle.apply( this, arguments ) :
- fn == null || bool ?
- this.each(function(){
- var state = bool ? fn : jQuery(this).is(":hidden");
- jQuery(this)[ state ? "show" : "hide" ]();
- }) :
- this.animate(genFx("toggle", 3), fn, fn2);
+ if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {
+ this._toggle.apply( this, arguments );
+
+ } else if ( fn == null || bool ) {
+ this.each(function() {
+ var state = bool ? fn : jQuery(this).is(":hidden");
+ jQuery(this)[ state ? "show" : "hide" ]();
+ });
+
+ } else {
+ this.animate(genFx("toggle", 3), fn, fn2);
+ }
+
+ return this;
},
- fadeTo: function(speed,to,callback){
- return this.animate({opacity: to}, speed, callback);
+ fadeTo: function( speed, to, callback ) {
+ return this.filter(":hidden").css("opacity", 0).show().end()
+ .animate({opacity: to}, speed, callback);
},
animate: function( prop, speed, easing, callback ) {
var optall = jQuery.speed(speed, easing, callback);
- return this[ optall.queue === false ? "each" : "queue" ](function(){
-
+ if ( jQuery.isEmptyObject( prop ) ) {
+ return this.each( optall.complete );
+ }
+
+ return this[ optall.queue === false ? "each" : "queue" ](function() {
var opt = jQuery.extend({}, optall), p,
- hidden = this.nodeType == 1 && jQuery(this).is(":hidden"),
+ hidden = this.nodeType === 1 && jQuery(this).is(":hidden"),
self = this;
-
+
for ( p in prop ) {
- if ( prop[p] == "hide" && hidden || prop[p] == "show" && !hidden )
+ var name = p.replace(rdashAlpha, fcamelCase);
+
+ if ( p !== name ) {
+ prop[ name ] = prop[ p ];
+ delete prop[ p ];
+ p = name;
+ }
+
+ if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) {
return opt.complete.call(this);
+ }
- if ( ( p == "height" || p == "width" ) && this.style ) {
+ if ( ( p === "height" || p === "width" ) && this.style ) {
// Store display property
opt.display = jQuery.css(this, "display");
// Make sure that nothing sneaks out
opt.overflow = this.style.overflow;
}
+
+ if ( jQuery.isArray( prop[p] ) ) {
+ // Create (if needed) and add to specialEasing
+ (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1];
+ prop[p] = prop[p][0];
+ }
}
- if ( opt.overflow != null )
+ if ( opt.overflow != null ) {
this.style.overflow = "hidden";
+ }
opt.curAnim = jQuery.extend({}, prop);
- jQuery.each( prop, function(name, val){
+ jQuery.each( prop, function( name, val ) {
var e = new jQuery.fx( self, opt, name );
- if ( /toggle|show|hide/.test(val) )
- e[ val == "toggle" ? hidden ? "show" : "hide" : val ]( prop );
- else {
- var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),
+ if ( rfxtypes.test(val) ) {
+ e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop );
+
+ } else {
+ var parts = rfxnum.exec(val),
start = e.cur(true) || 0;
if ( parts ) {
- var end = parseFloat(parts[2]),
+ var end = parseFloat( parts[2] ),
unit = parts[3] || "px";
// We need to compute starting value
- if ( unit != "px" ) {
+ if ( unit !== "px" ) {
self.style[ name ] = (end || 1) + unit;
start = ((end || 1) / e.cur(true)) * start;
self.style[ name ] = start + unit;
}
// If a +=/-= token was provided, we're doing a relative animation
- if ( parts[1] )
- end = ((parts[1] == "-=" ? -1 : 1) * end) + start;
+ if ( parts[1] ) {
+ end = ((parts[1] === "-=" ? -1 : 1) * end) + start;
+ }
e.custom( start, end, unit );
- } else
+
+ } else {
e.custom( start, val, "" );
+ }
}
});
@@ -3924,26 +5469,31 @@ jQuery.fn.extend({
});
},
- stop: function(clearQueue, gotoEnd){
+ stop: function( clearQueue, gotoEnd ) {
var timers = jQuery.timers;
- if (clearQueue)
+ if ( clearQueue ) {
this.queue([]);
+ }
- this.each(function(){
+ this.each(function() {
// go in reverse order so anything added to the queue during the loop is ignored
- for ( var i = timers.length - 1; i >= 0; i-- )
- if ( timers[i].elem == this ) {
- if (gotoEnd)
+ for ( var i = timers.length - 1; i >= 0; i-- ) {
+ if ( timers[i].elem === this ) {
+ if (gotoEnd) {
// force the next step to be the last
timers[i](true);
+ }
+
timers.splice(i, 1);
}
+ }
});
// start the next in the queue if the last step wasn't forced
- if (!gotoEnd)
+ if ( !gotoEnd ) {
this.dequeue();
+ }
return this;
}
@@ -3957,16 +5507,15 @@ jQuery.each({
slideToggle: genFx("toggle", 1),
fadeIn: { opacity: "show" },
fadeOut: { opacity: "hide" }
-}, function( name, props ){
- jQuery.fn[ name ] = function( speed, callback ){
+}, function( name, props ) {
+ jQuery.fn[ name ] = function( speed, callback ) {
return this.animate( props, speed, callback );
};
});
jQuery.extend({
-
- speed: function(speed, easing, fn) {
- var opt = typeof speed === "object" ? speed : {
+ speed: function( speed, easing, fn ) {
+ var opt = speed && typeof speed === "object" ? speed : {
complete: fn || !fn && easing ||
jQuery.isFunction( speed ) && speed,
duration: speed,
@@ -3978,11 +5527,13 @@ jQuery.extend({
// Queueing
opt.old = opt.complete;
- opt.complete = function(){
- if ( opt.queue !== false )
+ opt.complete = function() {
+ if ( opt.queue !== false ) {
jQuery(this).dequeue();
- if ( jQuery.isFunction( opt.old ) )
+ }
+ if ( jQuery.isFunction( opt.old ) ) {
opt.old.call( this );
+ }
};
return opt;
@@ -3999,42 +5550,45 @@ jQuery.extend({
timers: [],
- fx: function( elem, options, prop ){
+ fx: function( elem, options, prop ) {
this.options = options;
this.elem = elem;
this.prop = prop;
- if ( !options.orig )
+ if ( !options.orig ) {
options.orig = {};
+ }
}
});
jQuery.fx.prototype = {
-
// Simple function for setting a style value
- update: function(){
- if ( this.options.step )
+ update: function() {
+ if ( this.options.step ) {
this.options.step.call( this.elem, this.now, this );
+ }
(jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
// Set display property to block for height/width animations
- if ( ( this.prop == "height" || this.prop == "width" ) && this.elem.style )
+ if ( ( this.prop === "height" || this.prop === "width" ) && this.elem.style ) {
this.elem.style.display = "block";
+ }
},
// Get the current size
- cur: function(force){
- if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) )
+ cur: function( force ) {
+ if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) {
return this.elem[ this.prop ];
+ }
var r = parseFloat(jQuery.css(this.elem, this.prop, force));
return r && r > -10000 ? r : parseFloat(jQuery.curCSS(this.elem, this.prop)) || 0;
},
// Start an animation from one number to another
- custom: function(from, to, unit){
+ custom: function( from, to, unit ) {
this.startTime = now();
this.start = from;
this.end = to;
@@ -4043,47 +5597,36 @@ jQuery.fx.prototype = {
this.pos = this.state = 0;
var self = this;
- function t(gotoEnd){
+ function t( gotoEnd ) {
return self.step(gotoEnd);
}
t.elem = this.elem;
if ( t() && jQuery.timers.push(t) && !timerId ) {
- timerId = setInterval(function(){
- var timers = jQuery.timers;
-
- for ( var i = 0; i < timers.length; i++ )
- if ( !timers[i]() )
- timers.splice(i--, 1);
-
- if ( !timers.length ) {
- clearInterval( timerId );
- timerId = undefined;
- }
- }, 13);
+ timerId = setInterval(jQuery.fx.tick, 13);
}
},
// Simple 'show' function
- show: function(){
+ show: function() {
// Remember where we started, so that we can go back to it later
- this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+ this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
this.options.show = true;
// Begin the animation
// Make sure that we start at a small width/height to avoid any
// flash of content
- this.custom(this.prop == "width" || this.prop == "height" ? 1 : 0, this.cur());
+ this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur());
// Start by showing the element
- jQuery(this.elem).show();
+ jQuery( this.elem ).show();
},
// Simple 'hide' function
- hide: function(){
+ hide: function() {
// Remember where we started, so that we can go back to it later
- this.options.orig[this.prop] = jQuery.attr( this.elem.style, this.prop );
+ this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
this.options.hide = true;
// Begin the animation
@@ -4091,8 +5634,8 @@ jQuery.fx.prototype = {
},
// Each step of an animation
- step: function(gotoEnd){
- var t = now();
+ step: function( gotoEnd ) {
+ var t = now(), done = true;
if ( gotoEnd || t >= this.options.duration + this.startTime ) {
this.now = this.end;
@@ -4101,10 +5644,11 @@ jQuery.fx.prototype = {
this.options.curAnim[ this.prop ] = true;
- var done = true;
- for ( var i in this.options.curAnim )
- if ( this.options.curAnim[i] !== true )
+ for ( var i in this.options.curAnim ) {
+ if ( this.options.curAnim[i] !== true ) {
done = false;
+ }
+ }
if ( done ) {
if ( this.options.display != null ) {
@@ -4112,31 +5656,40 @@ jQuery.fx.prototype = {
this.elem.style.overflow = this.options.overflow;
// Reset the display
- this.elem.style.display = this.options.display;
- if ( jQuery.css(this.elem, "display") == "none" )
+ var old = jQuery.data(this.elem, "olddisplay");
+ this.elem.style.display = old ? old : this.options.display;
+
+ if ( jQuery.css(this.elem, "display") === "none" ) {
this.elem.style.display = "block";
+ }
}
// Hide the element if the "hide" operation was done
- if ( this.options.hide )
+ if ( this.options.hide ) {
jQuery(this.elem).hide();
+ }
// Reset the properties, if the item has been hidden or shown
- if ( this.options.hide || this.options.show )
- for ( var p in this.options.curAnim )
- jQuery.attr(this.elem.style, p, this.options.orig[p]);
-
+ if ( this.options.hide || this.options.show ) {
+ for ( var p in this.options.curAnim ) {
+ jQuery.style(this.elem, p, this.options.orig[p]);
+ }
+ }
+
// Execute the complete function
this.options.complete.call( this.elem );
}
return false;
+
} else {
var n = t - this.startTime;
this.state = n / this.options.duration;
// Perform the easing function, defaults to swing
- this.pos = jQuery.easing[this.options.easing || (jQuery.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);
+ var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop];
+ var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear");
+ this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration);
this.now = this.start + ((this.end - this.start) * this.pos);
// Perform the next step of the animation
@@ -4145,232 +5698,382 @@ jQuery.fx.prototype = {
return true;
}
-
};
jQuery.extend( jQuery.fx, {
- speeds:{
+ tick: function() {
+ var timers = jQuery.timers;
+
+ for ( var i = 0; i < timers.length; i++ ) {
+ if ( !timers[i]() ) {
+ timers.splice(i--, 1);
+ }
+ }
+
+ if ( !timers.length ) {
+ jQuery.fx.stop();
+ }
+ },
+
+ stop: function() {
+ clearInterval( timerId );
+ timerId = null;
+ },
+
+ speeds: {
slow: 600,
fast: 200,
// Default speed
_default: 400
},
- step: {
- opacity: function(fx){
- jQuery.attr(fx.elem.style, "opacity", fx.now);
+ step: {
+ opacity: function( fx ) {
+ jQuery.style(fx.elem, "opacity", fx.now);
},
- _default: function(fx){
- if ( fx.elem.style && fx.elem.style[ fx.prop ] != null )
- fx.elem.style[ fx.prop ] = fx.now + fx.unit;
- else
+ _default: function( fx ) {
+ if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {
+ fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit;
+ } else {
fx.elem[ fx.prop ] = fx.now;
+ }
}
}
});
-if ( document.documentElement["getBoundingClientRect"] )
- jQuery.fn.offset = function() {
- if ( !this[0] ) return { top: 0, left: 0 };
- if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] );
- var box = this[0].getBoundingClientRect(), doc = this[0].ownerDocument, body = doc.body, docElem = doc.documentElement,
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+ jQuery.expr.filters.animated = function( elem ) {
+ return jQuery.grep(jQuery.timers, function( fn ) {
+ return elem === fn.elem;
+ }).length;
+ };
+}
+
+function genFx( type, num ) {
+ var obj = {};
+
+ jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() {
+ obj[ this ] = type;
+ });
+
+ return obj;
+}
+if ( "getBoundingClientRect" in document.documentElement ) {
+ jQuery.fn.offset = function( options ) {
+ var elem = this[0];
+
+ if ( options ) {
+ return this.each(function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ });
+ }
+
+ if ( !elem || !elem.ownerDocument ) {
+ return null;
+ }
+
+ if ( elem === elem.ownerDocument.body ) {
+ return jQuery.offset.bodyOffset( elem );
+ }
+
+ var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement,
clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0,
- top = box.top + (self.pageYOffset || jQuery.boxModel && docElem.scrollTop || body.scrollTop ) - clientTop,
- left = box.left + (self.pageXOffset || jQuery.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
+ top = box.top + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop ) - clientTop,
+ left = box.left + (self.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft;
+
return { top: top, left: left };
};
-else
- jQuery.fn.offset = function() {
- if ( !this[0] ) return { top: 0, left: 0 };
- if ( this[0] === this[0].ownerDocument.body ) return jQuery.offset.bodyOffset( this[0] );
- jQuery.offset.initialized || jQuery.offset.initialize();
- var elem = this[0], offsetParent = elem.offsetParent, prevOffsetParent = elem,
+} else {
+ jQuery.fn.offset = function( options ) {
+ var elem = this[0];
+
+ if ( options ) {
+ return this.each(function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ });
+ }
+
+ if ( !elem || !elem.ownerDocument ) {
+ return null;
+ }
+
+ if ( elem === elem.ownerDocument.body ) {
+ return jQuery.offset.bodyOffset( elem );
+ }
+
+ jQuery.offset.initialize();
+
+ var offsetParent = elem.offsetParent, prevOffsetParent = elem,
doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement,
body = doc.body, defaultView = doc.defaultView,
- prevComputedStyle = defaultView.getComputedStyle(elem, null),
+ prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle,
top = elem.offsetTop, left = elem.offsetLeft;
while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
- computedStyle = defaultView.getComputedStyle(elem, null);
- top -= elem.scrollTop, left -= elem.scrollLeft;
+ if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
+ break;
+ }
+
+ computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;
+ top -= elem.scrollTop;
+ left -= elem.scrollLeft;
+
if ( elem === offsetParent ) {
- top += elem.offsetTop, left += elem.offsetLeft;
- if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.tagName)) )
- top += parseInt( computedStyle.borderTopWidth, 10) || 0,
- left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
+ top += elem.offsetTop;
+ left += elem.offsetLeft;
+
+ if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.nodeName)) ) {
+ top += parseFloat( computedStyle.borderTopWidth ) || 0;
+ left += parseFloat( computedStyle.borderLeftWidth ) || 0;
+ }
+
prevOffsetParent = offsetParent, offsetParent = elem.offsetParent;
}
- if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" )
- top += parseInt( computedStyle.borderTopWidth, 10) || 0,
- left += parseInt( computedStyle.borderLeftWidth, 10) || 0;
+
+ if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) {
+ top += parseFloat( computedStyle.borderTopWidth ) || 0;
+ left += parseFloat( computedStyle.borderLeftWidth ) || 0;
+ }
+
prevComputedStyle = computedStyle;
}
- if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" )
- top += body.offsetTop,
+ if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) {
+ top += body.offsetTop;
left += body.offsetLeft;
+ }
- if ( prevComputedStyle.position === "fixed" )
- top += Math.max(docElem.scrollTop, body.scrollTop),
- left += Math.max(docElem.scrollLeft, body.scrollLeft);
+ if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
+ top += Math.max( docElem.scrollTop, body.scrollTop );
+ left += Math.max( docElem.scrollLeft, body.scrollLeft );
+ }
return { top: top, left: left };
};
+}
jQuery.offset = {
initialize: function() {
- if ( this.initialized ) return;
- var body = document.body, container = document.createElement('div'), innerDiv, checkDiv, table, td, rules, prop, bodyMarginTop = body.style.marginTop,
- html = '<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';
+ var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.curCSS(body, "marginTop", true) ) || 0,
+ html = "<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
- rules = { position: 'absolute', top: 0, left: 0, margin: 0, border: 0, width: '1px', height: '1px', visibility: 'hidden' };
- for ( prop in rules ) container.style[prop] = rules[prop];
+ jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } );
container.innerHTML = html;
- body.insertBefore(container, body.firstChild);
- innerDiv = container.firstChild, checkDiv = innerDiv.firstChild, td = innerDiv.nextSibling.firstChild.firstChild;
+ body.insertBefore( container, body.firstChild );
+ innerDiv = container.firstChild;
+ checkDiv = innerDiv.firstChild;
+ td = innerDiv.nextSibling.firstChild.firstChild;
this.doesNotAddBorder = (checkDiv.offsetTop !== 5);
this.doesAddBorderForTableAndCells = (td.offsetTop === 5);
- innerDiv.style.overflow = 'hidden', innerDiv.style.position = 'relative';
+ checkDiv.style.position = "fixed", checkDiv.style.top = "20px";
+ // safari subtracts parent border width here which is 5px
+ this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15);
+ checkDiv.style.position = checkDiv.style.top = "";
+
+ innerDiv.style.overflow = "hidden", innerDiv.style.position = "relative";
this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);
- body.style.marginTop = '1px';
- this.doesNotIncludeMarginInBodyOffset = (body.offsetTop === 0);
- body.style.marginTop = bodyMarginTop;
+ this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop);
- body.removeChild(container);
- this.initialized = true;
+ body.removeChild( container );
+ body = container = innerDiv = checkDiv = table = td = null;
+ jQuery.offset.initialize = jQuery.noop;
},
- bodyOffset: function(body) {
- jQuery.offset.initialized || jQuery.offset.initialize();
+ bodyOffset: function( body ) {
var top = body.offsetTop, left = body.offsetLeft;
- if ( jQuery.offset.doesNotIncludeMarginInBodyOffset )
- top += parseInt( jQuery.curCSS(body, 'marginTop', true), 10 ) || 0,
- left += parseInt( jQuery.curCSS(body, 'marginLeft', true), 10 ) || 0;
+
+ jQuery.offset.initialize();
+
+ if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) {
+ top += parseFloat( jQuery.curCSS(body, "marginTop", true) ) || 0;
+ left += parseFloat( jQuery.curCSS(body, "marginLeft", true) ) || 0;
+ }
+
return { top: top, left: left };
+ },
+
+ setOffset: function( elem, options, i ) {
+ // set position first, in-case top/left are set even on static elem
+ if ( /static/.test( jQuery.curCSS( elem, "position" ) ) ) {
+ elem.style.position = "relative";
+ }
+ var curElem = jQuery( elem ),
+ curOffset = curElem.offset(),
+ curTop = parseInt( jQuery.curCSS( elem, "top", true ), 10 ) || 0,
+ curLeft = parseInt( jQuery.curCSS( elem, "left", true ), 10 ) || 0;
+
+ if ( jQuery.isFunction( options ) ) {
+ options = options.call( elem, i, curOffset );
+ }
+
+ var props = {
+ top: (options.top - curOffset.top) + curTop,
+ left: (options.left - curOffset.left) + curLeft
+ };
+
+ if ( "using" in options ) {
+ options.using.call( elem, props );
+ } else {
+ curElem.css( props );
+ }
}
};
jQuery.fn.extend({
position: function() {
- var left = 0, top = 0, results;
-
- if ( this[0] ) {
- // Get *real* offsetParent
- var offsetParent = this.offsetParent(),
-
- // Get correct offsets
- offset = this.offset(),
- parentOffset = /^body|html$/i.test(offsetParent[0].tagName) ? { top: 0, left: 0 } : offsetParent.offset();
-
- // Subtract element margins
- // note: when an element has margin: auto the offsetLeft and marginLeft
- // are the same in Safari causing offset.left to incorrectly be 0
- offset.top -= num( this, 'marginTop' );
- offset.left -= num( this, 'marginLeft' );
-
- // Add offsetParent borders
- parentOffset.top += num( offsetParent, 'borderTopWidth' );
- parentOffset.left += num( offsetParent, 'borderLeftWidth' );
-
- // Subtract the two offsets
- results = {
- top: offset.top - parentOffset.top,
- left: offset.left - parentOffset.left
- };
+ if ( !this[0] ) {
+ return null;
}
- return results;
+ var elem = this[0],
+
+ // Get *real* offsetParent
+ offsetParent = this.offsetParent(),
+
+ // Get correct offsets
+ offset = this.offset(),
+ parentOffset = /^body|html$/i.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+ // Subtract element margins
+ // note: when an element has margin: auto the offsetLeft and marginLeft
+ // are the same in Safari causing offset.left to incorrectly be 0
+ offset.top -= parseFloat( jQuery.curCSS(elem, "marginTop", true) ) || 0;
+ offset.left -= parseFloat( jQuery.curCSS(elem, "marginLeft", true) ) || 0;
+
+ // Add offsetParent borders
+ parentOffset.top += parseFloat( jQuery.curCSS(offsetParent[0], "borderTopWidth", true) ) || 0;
+ parentOffset.left += parseFloat( jQuery.curCSS(offsetParent[0], "borderLeftWidth", true) ) || 0;
+
+ // Subtract the two offsets
+ return {
+ top: offset.top - parentOffset.top,
+ left: offset.left - parentOffset.left
+ };
},
offsetParent: function() {
- var offsetParent = this[0].offsetParent || document.body;
- while ( offsetParent && (!/^body|html$/i.test(offsetParent.tagName) && jQuery.css(offsetParent, 'position') == 'static') )
- offsetParent = offsetParent.offsetParent;
- return jQuery(offsetParent);
+ return this.map(function() {
+ var offsetParent = this.offsetParent || document.body;
+ while ( offsetParent && (!/^body|html$/i.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
+ offsetParent = offsetParent.offsetParent;
+ }
+ return offsetParent;
+ });
}
});
// Create scrollLeft and scrollTop methods
-jQuery.each( ['Left', 'Top'], function(i, name) {
- var method = 'scroll' + name;
-
- jQuery.fn[ method ] = function(val) {
- if (!this[0]) return null;
+jQuery.each( ["Left", "Top"], function( i, name ) {
+ var method = "scroll" + name;
- return val !== undefined ?
+ jQuery.fn[ method ] = function(val) {
+ var elem = this[0], win;
+
+ if ( !elem ) {
+ return null;
+ }
+ if ( val !== undefined ) {
// Set the scroll offset
- this.each(function() {
- this == window || this == document ?
- window.scrollTo(
- !i ? val : jQuery(window).scrollLeft(),
- i ? val : jQuery(window).scrollTop()
- ) :
+ return this.each(function() {
+ win = getWindow( this );
+
+ if ( win ) {
+ win.scrollTo(
+ !i ? val : jQuery(win).scrollLeft(),
+ i ? val : jQuery(win).scrollTop()
+ );
+
+ } else {
this[ method ] = val;
- }) :
+ }
+ });
+ } else {
+ win = getWindow( elem );
// Return the scroll offset
- this[0] == window || this[0] == document ?
- self[ i ? 'pageYOffset' : 'pageXOffset' ] ||
- jQuery.boxModel && document.documentElement[ method ] ||
- document.body[ method ] :
- this[0][ method ];
+ return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
+ jQuery.support.boxModel && win.document.documentElement[ method ] ||
+ win.document.body[ method ] :
+ elem[ method ];
+ }
};
});
+
+function getWindow( elem ) {
+ return ("scrollTo" in elem && elem.document) ?
+ elem :
+ elem.nodeType === 9 ?
+ elem.defaultView || elem.parentWindow :
+ false;
+}
// Create innerHeight, innerWidth, outerHeight and outerWidth methods
-jQuery.each([ "Height", "Width" ], function(i, name){
+jQuery.each([ "Height", "Width" ], function( i, name ) {
- var tl = i ? "Left" : "Top", // top or left
- br = i ? "Right" : "Bottom", // bottom or right
- lower = name.toLowerCase();
+ var type = name.toLowerCase();
// innerHeight and innerWidth
- jQuery.fn["inner" + name] = function(){
+ jQuery.fn["inner" + name] = function() {
return this[0] ?
- jQuery.css( this[0], lower, false, "padding" ) :
+ jQuery.css( this[0], type, false, "padding" ) :
null;
};
// outerHeight and outerWidth
- jQuery.fn["outer" + name] = function(margin) {
+ jQuery.fn["outer" + name] = function( margin ) {
return this[0] ?
- jQuery.css( this[0], lower, false, margin ? "margin" : "border" ) :
+ jQuery.css( this[0], type, false, margin ? "margin" : "border" ) :
null;
};
-
- var type = name.toLowerCase();
jQuery.fn[ type ] = function( size ) {
// Get window width or height
- return this[0] == window ?
+ var elem = this[0];
+ if ( !elem ) {
+ return size == null ? null : this;
+ }
+
+ if ( jQuery.isFunction( size ) ) {
+ return this.each(function( i ) {
+ var self = jQuery( this );
+ self[ type ]( size.call( this, i, self[ type ]() ) );
+ });
+ }
+
+ return ("scrollTo" in elem && elem.document) ? // does it walk and quack like a window?
// Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
- document.compatMode == "CSS1Compat" && document.documentElement[ "client" + name ] ||
- document.body[ "client" + name ] :
+ elem.document.compatMode === "CSS1Compat" && elem.document.documentElement[ "client" + name ] ||
+ elem.document.body[ "client" + name ] :
// Get document width or height
- this[0] == document ?
+ (elem.nodeType === 9) ? // is it a document
// Either scroll[Width/Height] or offset[Width/Height], whichever is greater
Math.max(
- document.documentElement["client" + name],
- document.body["scroll" + name], document.documentElement["scroll" + name],
- document.body["offset" + name], document.documentElement["offset" + name]
+ elem.documentElement["client" + name],
+ elem.body["scroll" + name], elem.documentElement["scroll" + name],
+ elem.body["offset" + name], elem.documentElement["offset" + name]
) :
// Get or set width or height on the element
size === undefined ?
// Get width or height on the element
- (this.length ? jQuery.css( this[0], type ) : null) :
+ jQuery.css( elem, type ) :
// Set the width or height on the element (default to pixels if value is unitless)
this.css( type, typeof size === "string" ? size : size + "px" );
};
});
-})();
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+
+})(window);
+
diff --git a/js/jquery.min.js b/js/jquery.min.js
index b1ae21d8b..950198f47 100644
--- a/js/jquery.min.js
+++ b/js/jquery.min.js
@@ -1,19 +1,153 @@
-/*
- * jQuery JavaScript Library v1.3.2
+/*!
+ * jQuery JavaScript Library v1.4.1
* http://jquery.com/
*
- * Copyright (c) 2009 John Resig
- * Dual licensed under the MIT and GPL licenses.
- * http://docs.jquery.com/License
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
*
- * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
- * Revision: 6246
- */
-(function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F<J;F++){var G=M[F];if(G.selected){K=o(G).val();if(H){return K}L.push(K)}}return L}return(E.value||"").replace(/\r/g,"")}return g}if(typeof K==="number"){K+=""}return this.each(function(){if(this.nodeType!=1){return}if(o.isArray(K)&&/radio|checkbox/.test(this.type)){this.checked=(o.inArray(this.value,K)>=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G<E;G++){L.call(K(this[G],H),this.length>1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H<I;H++){if((G=arguments[H])!=null){for(var F in G){var K=J[F],L=G[F];if(J===L){continue}if(E&&L&&typeof L==="object"&&!L.nodeType){J[F]=o.extend(E,K||(L.length!=null?[]:{}),L)}else{if(L!==g){J[F]=L}}}}}return J};var b=/z-?index|font-?weight|opacity|zoom|line-?height/i,q=document.defaultView||{},s=Object.prototype.toString;o.extend({noConflict:function(E){l.$=p;if(E){l.jQuery=y}return o},isFunction:function(E){return s.call(E)==="[object Function]"},isArray:function(E){return s.call(E)==="[object Array]"},isXMLDoc:function(E){return E.nodeType===9&&E.documentElement.nodeName!=="HTML"||!!E.ownerDocument&&o.isXMLDoc(E.ownerDocument)},globalEval:function(G){if(G&&/\S/.test(G)){var F=document.getElementsByTagName("head")[0]||document.documentElement,E=document.createElement("script");E.type="text/javascript";if(o.support.scriptEval){E.appendChild(document.createTextNode(G))}else{E.text=G}F.insertBefore(E,F.firstChild);F.removeChild(E)}},nodeName:function(F,E){return F.nodeName&&F.nodeName.toUpperCase()==E.toUpperCase()},each:function(G,K,F){var E,H=0,I=G.length;if(F){if(I===g){for(E in G){if(K.apply(G[E],F)===false){break}}}else{for(;H<I;){if(K.apply(G[H++],F)===false){break}}}}else{if(I===g){for(E in G){if(K.call(G[E],E,G[E])===false){break}}}else{for(var J=G[0];H<I&&K.call(J,H,J)!==false;J=G[++H]){}}}return G},prop:function(H,I,G,F,E){if(o.isFunction(I)){I=I.call(H,F)}return typeof I==="number"&&G=="curCSS"&&!b.test(E)?I+"px":I},className:{add:function(E,F){o.each((F||"").split(/\s+/),function(G,H){if(E.nodeType==1&&!o.className.has(E.className,H)){E.className+=(E.className?" ":"")+H}})},remove:function(E,F){if(E.nodeType==1){E.className=F!==g?o.grep(E.className.split(/\s+/),function(G){return !o.className.has(F,G)}).join(" "):""}},has:function(F,E){return F&&o.inArray(E,(F.className||F).toString().split(/\s+/))>-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+"></"+T+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!O.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!O.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!O.indexOf("<td")||!O.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!O.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||!o.support.htmlSerialize&&[1,"div<div>","</div>"]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/<tbody/i.test(S),N=!O.indexOf("<table")&&!R?L.firstChild&&L.firstChild.childNodes:Q[1]=="<table>"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E<F;E++){if(H[E]===G){return E}}return -1},merge:function(H,E){var F=0,G,I=H.length;if(!o.support.getAll){while((G=E[F++])!=null){if(G.nodeType!=8){H[I++]=G}}}else{while((G=E[F++])!=null){H[I++]=G}}return H},unique:function(K){var F=[],E={};try{for(var G=0,H=K.length;G<H;G++){var J=o.data(K[G]);if(!E[J]){E[J]=true;F.push(K[G])}}}catch(I){F=K}return F},grep:function(F,J,E){var G=[];for(var H=0,I=F.length;H<I;H++){if(!E!=!J(F[H],H)){G.push(F[H])}}return G},map:function(E,J){var F=[];for(var G=0,H=E.length;G<H;G++){var I=J(E[G],G);if(I!=null){F[F.length]=I}}return F.concat.apply([],F)}});var C=navigator.userAgent.toLowerCase();o.browser={version:(C.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[0,"0"])[1],safari:/webkit/.test(C),opera:/opera/.test(C),msie:/msie/.test(C)&&!/opera/.test(C),mozilla:/mozilla/.test(C)&&!/(compatible|webkit)/.test(C)};o.each({parent:function(E){return E.parentNode},parents:function(E){return o.dir(E,"parentNode")},next:function(E){return o.nth(E,2,"nextSibling")},prev:function(E){return o.nth(E,2,"previousSibling")},nextAll:function(E){return o.dir(E,"nextSibling")},prevAll:function(E){return o.dir(E,"previousSibling")},siblings:function(E){return o.sibling(E.parentNode.firstChild,E)},children:function(E){return o.sibling(E.firstChild)},contents:function(E){return o.nodeName(E,"iframe")?E.contentDocument||E.contentWindow.document:o.makeArray(E.childNodes)}},function(E,F){o.fn[E]=function(G){var H=o.map(this,F);if(G&&typeof G=="string"){H=o.multiFilter(G,H)}return this.pushStack(o.unique(H),E,G)}});o.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(E,F){o.fn[E]=function(G){var J=[],L=o(G);for(var K=0,H=L.length;K<H;K++){var I=(K>0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}});
-/*
- * Sizzle CSS Selector Engine - v0.9.3
- * Copyright 2009, The Dojo Foundation
- * Released under the MIT, BSD, and GPL Licenses.
- * More information: http://sizzlejs.com/
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2010, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Mon Jan 25 19:43:33 2010 -0500
*/
-(function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa<ab.length;aa++){if(ab[aa]===ab[aa-1]){ab.splice(aa--,1)}}}}}return ab};F.matches=function(T,U){return F(T,null,null,U)};F.find=function(aa,T,ab){var Z,X;if(!aa){return[]}for(var W=0,V=I.order.length;W<V;W++){var Y=I.order[W],X;if((X=I.match[Y].exec(aa))){var U=RegExp.leftContext;if(U.substr(U.length-1)!=="\\"){X[1]=(X[1]||"").replace(/\\/g,"");Z=I.find[Y](X,T,ab);if(Z!=null){aa=aa.replace(I.match[Y],"");break}}}}if(!Z){Z=T.getElementsByTagName("*")}return{set:Z,expr:aa}};F.filter=function(ad,ac,ag,W){var V=ad,ai=[],aa=ac,Y,T,Z=ac&&ac[0]&&Q(ac[0]);while(ad&&ac.length){for(var ab in I.filter){if((Y=I.match[ab].exec(ad))!=null){var U=I.filter[ab],ah,af;T=false;if(aa==ai){ai=[]}if(I.preFilter[ab]){Y=I.preFilter[ab](Y,aa,ag,ai,W,Z);if(!Y){T=ah=true}else{if(Y===true){continue}}}if(Y){for(var X=0;(af=aa[X])!=null;X++){if(af){ah=U(af,Y,X,aa);var ae=W^!!ah;if(ag&&ah!=null){if(ae){T=true}else{aa[X]=false}}else{if(ae){ai.push(af);T=true}}}}}if(ah!==g){if(!ag){aa=ai}ad=ad.replace(I.match[ab],"");if(!T){return[]}break}}}if(ad==V){if(T==null){throw"Syntax error, unrecognized expression: "+ad}else{break}}V=ad}return aa};var I=F.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(T){return T.getAttribute("href")}},relative:{"+":function(aa,T,Z){var X=typeof T==="string",ab=X&&!/\W/.test(T),Y=X&&!ab;if(ab&&!Z){T=T.toUpperCase()}for(var W=0,V=aa.length,U;W<V;W++){if((U=aa[W])){while((U=U.previousSibling)&&U.nodeType!==1){}aa[W]=Y||U&&U.nodeName===T?U||false:U===T}}if(Y){F.filter(T,aa,true)}},">":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V<T;V++){var Y=Z[V];if(Y){var W=Y.parentNode;Z[V]=W.nodeName===U?W:false}}}else{for(var V=0,T=Z.length;V<T;V++){var Y=Z[V];if(Y){Z[V]=X?Y.parentNode:Y.parentNode===U}}if(X){F.filter(U,Z,true)}}},"":function(W,U,Y){var V=L++,T=S;if(!U.match(/\W/)){var X=U=Y?U:U.toUpperCase();T=P}T("parentNode",U,V,W,X,Y)},"~":function(W,U,Y){var V=L++,T=S;if(typeof U==="string"&&!U.match(/\W/)){var X=U=Y?U:U.toUpperCase();T=P}T("previousSibling",U,V,W,X,Y)}},find:{ID:function(U,V,W){if(typeof V.getElementById!=="undefined"&&!W){var T=V.getElementById(U[1]);return T?[T]:[]}},NAME:function(V,Y,Z){if(typeof Y.getElementsByName!=="undefined"){var U=[],X=Y.getElementsByName(V[1]);for(var W=0,T=X.length;W<T;W++){if(X[W].getAttribute("name")===V[1]){U.push(X[W])}}return U.length===0?null:U}},TAG:function(T,U){return U.getElementsByTagName(T[1])}},preFilter:{CLASS:function(W,U,V,T,Z,aa){W=" "+W[1].replace(/\\/g,"")+" ";if(aa){return W}for(var X=0,Y;(Y=U[X])!=null;X++){if(Y){if(Z^(Y.className&&(" "+Y.className+" ").indexOf(W)>=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return U<T[3]-0},gt:function(V,U,T){return U>T[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W<T;W++){if(Y[W]===Z){return false}}return true}}}},CHILD:function(T,W){var Z=W[1],U=T;switch(Z){case"only":case"first":while(U=U.previousSibling){if(U.nodeType===1){return false}}if(Z=="first"){return true}U=T;case"last":while(U=U.nextSibling){if(U.nodeType===1){return false}}return true;case"nth":var V=W[2],ac=W[3];if(V==1&&ac==0){return true}var Y=W[0],ab=T.parentNode;if(ab&&(ab.sizcache!==Y||!T.nodeIndex)){var X=0;for(U=ab.firstChild;U;U=U.nextSibling){if(U.nodeType===1){U.nodeIndex=++X}}ab.sizcache=Y}var aa=T.nodeIndex-ac;if(V==0){return aa==0}else{return(aa%V==0&&aa/V>=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V<T;V++){U.push(X[V])}}else{for(var V=0;X[V];V++){U.push(X[V])}}}return U}}var G;if(document.documentElement.compareDocumentPosition){G=function(U,T){var V=U.compareDocumentPosition(T)&4?-1:U===T?0:1;if(V===0){hasDuplicate=true}return V}}else{if("sourceIndex" in document.documentElement){G=function(U,T){var V=U.sourceIndex-T.sourceIndex;if(V===0){hasDuplicate=true}return V}}else{if(document.createRange){G=function(W,U){var V=W.ownerDocument.createRange(),T=U.ownerDocument.createRange();V.selectNode(W);V.collapse(true);T.selectNode(U);T.collapse(true);var X=V.compareBoundaryPoints(Range.START_TO_END,T);if(X===0){hasDuplicate=true}return X}}}}(function(){var U=document.createElement("form"),V="script"+(new Date).getTime();U.innerHTML="<input name='"+V+"'/>";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML="<a href='#'></a>";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="<p class='TEST'></p>";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="<div class='test e'></div><div class='test'></div>";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W<V;W++){var T=ad[W];if(T){if(ab&&T.nodeType===1){T.sizcache=Y;T.sizset=W}T=T[U];var X=false;while(T){if(T.sizcache===Y){X=ad[T.sizset];break}if(T.nodeType===1&&!ac){T.sizcache=Y;T.sizset=W}if(T.nodeName===Z){X=T;break}T=T[U]}ad[W]=X}}}function S(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W<V;W++){var T=ad[W];if(T){if(ab&&T.nodeType===1){T.sizcache=Y;T.sizset=W}T=T[U];var X=false;while(T){if(T.sizcache===Y){X=ad[T.sizset];break}if(T.nodeType===1){if(!ac){T.sizcache=Y;T.sizset=W}if(typeof Z!=="string"){if(T===Z){X=true;break}}else{if(F.filter(Z,[T]).length>0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z<U;Z++){F(T,V[Z],W)}return F.filter(X,W)};o.find=F;o.filter=F.filter;o.expr=F.selectors;o.expr[":"]=o.expr.filters;F.selectors.filters.hidden=function(T){return T.offsetWidth===0||T.offsetHeight===0};F.selectors.filters.visible=function(T){return T.offsetWidth>0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F<E.length){o.event.proxy(G,E[F++])}return this.click(o.event.proxy(G,function(H){this.lastToggle=(this.lastToggle||0)%F;H.preventDefault();return E[this.lastToggle++].apply(this,arguments)||false}))},hover:function(E,F){return this.mouseenter(E).mouseleave(F)},ready:function(E){B();if(o.isReady){E.call(document,o)}else{o.readyList.push(E)}return this},live:function(G,F){var E=o.event.proxy(F);E.guid+=this.selector+G;o(document).bind(i(G,this.selector),this.selector,E);return this},die:function(F,E){o(document).unbind(i(F,this.selector),E?{guid:E.guid+this.selector+F}:null);return this}});function c(H){var E=RegExp("(^|\\.)"+H.type+"(\\.|$)"),G=true,F=[];o.each(o.data(this,"events").live||[],function(I,J){if(E.test(J.type)){var K=o(H.target).closest(J.data)[0];if(K){F.push({elem:K,fn:J})}}});F.sort(function(J,I){return o.data(J.elem,"closest")-o.data(I.elem,"closest")});o.each(F,function(){if(this.fn.call(this.elem,H,this.fn.data)===false){return(G=false)}});return G}function i(F,E){return["live",F,E.replace(/\./g,"`").replace(/ /g,"|")].join(".")}o.extend({isReady:false,readyList:[],ready:function(){if(!o.isReady){o.isReady=true;if(o.readyList){o.each(o.readyList,function(){this.call(document,o)});o.readyList=null}o(document).triggerHandler("ready")}}});var x=false;function B(){if(x){return}x=true;if(document.addEventListener){document.addEventListener("DOMContentLoaded",function(){document.removeEventListener("DOMContentLoaded",arguments.callee,false);o.ready()},false)}else{if(document.attachEvent){document.attachEvent("onreadystatechange",function(){if(document.readyState==="complete"){document.detachEvent("onreadystatechange",arguments.callee);o.ready()}});if(document.documentElement.doScroll&&l==l.top){(function(){if(o.isReady){return}try{document.documentElement.doScroll("left")}catch(E){setTimeout(arguments.callee,0);return}o.ready()})()}}}o.event.add(l,"load",o.ready)}o.each(("blur,focus,load,resize,scroll,unload,click,dblclick,mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave,change,select,submit,keydown,keypress,keyup,error").split(","),function(F,E){o.fn[E]=function(G){return G?this.bind(E,G):this.trigger(E)}});o(l).bind("unload",function(){for(var E in o.cache){if(E!=1&&o.cache[E].handle){o.event.remove(o.cache[E].handle.elem)}}});(function(){o.support={};var F=document.documentElement,G=document.createElement("script"),K=document.createElement("div"),J="script"+(new Date).getTime();K.style.display="none";K.innerHTML=' <link/><table></table><a href="/a" style="color:red;float:left;opacity:.5;">a</a><select><option>text</option></select><object><param/></object>';var H=K.getElementsByTagName("*"),E=K.getElementsByTagName("a")[0];if(!H||!H.length||!E){return}o.support={leadingWhitespace:K.firstChild.nodeType==3,tbody:!K.getElementsByTagName("tbody").length,objectAll:!!K.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!K.getElementsByTagName("link").length,style:/red/.test(E.getAttribute("style")),hrefNormalized:E.getAttribute("href")==="/a",opacity:E.style.opacity==="0.5",cssFloat:!!E.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};G.type="text/javascript";try{G.appendChild(document.createTextNode("window."+J+"=1;"))}catch(I){}F.insertBefore(G,F.firstChild);if(l[J]){o.support.scriptEval=true;delete l[J]}F.removeChild(G);if(K.attachEvent&&K.fireEvent){K.attachEvent("onclick",function(){o.support.noCloneEvent=false;K.detachEvent("onclick",arguments.callee)});K.cloneNode(true).fireEvent("onclick")}o(function(){var L=document.createElement("div");L.style.width=L.style.paddingLeft="1px";document.body.appendChild(L);o.boxModel=o.support.boxModel=L.offsetWidth===2;document.body.removeChild(L).style.display="none"})})();var w=o.support.cssFloat?"cssFloat":"styleFloat";o.props={"for":"htmlFor","class":"className","float":w,cssFloat:w,styleFloat:w,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};o.fn.extend({_load:o.fn.load,load:function(G,J,K){if(typeof G!=="string"){return this._load(G)}var I=G.indexOf(" ");if(I>=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("<div/>").append(M.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password|search/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();T.onload=T.onreadystatechange=null;H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H<F;H++){var E=o.data(this[H],"olddisplay");this[H].style.display=E||"";if(o.css(this[H],"display")==="none"){var G=this[H].tagName,K;if(m[G]){K=m[G]}else{var I=o("<"+G+" />").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}o.data(this[H],"olddisplay",K)}}for(var H=0,F=this.length;H<F;H++){this[H].style.display=o.data(this[H],"olddisplay")||""}return this}},hide:function(H,I){if(H){return this.animate(t("hide",3),H,I)}else{for(var G=0,F=this.length;G<F;G++){var E=o.data(this[G],"olddisplay");if(!E&&E!=="none"){o.data(this[G],"olddisplay",o.css(this[G],"display"))}}for(var G=0,F=this.length;G<F;G++){this[G].style.display="none"}return this}},_toggle:o.fn.toggle,toggle:function(G,F){var E=typeof G==="boolean";return o.isFunction(G)&&o.isFunction(F)?this._toggle.apply(this,arguments):G==null||E?this.each(function(){var H=E?G:o(this).is(":hidden");o(this)[H?"show":"hide"]()}):this.animate(t("toggle",3),G,F)},fadeTo:function(E,G,F){return this.animate({opacity:G},E,F)},animate:function(I,F,H,G){var E=o.speed(F,H,G);return this[E.queue===false?"each":"queue"](function(){var K=o.extend({},E),M,L=this.nodeType==1&&o(this).is(":hidden"),J=this;for(M in I){if(I[M]=="hide"&&L||I[M]=="show"&&!L){return K.complete.call(this)}if((M=="height"||M=="width")&&this.style){K.display=o.css(this,"display");K.overflow=this.style.overflow}}if(K.overflow!=null){this.style.overflow="hidden"}K.curAnim=o.extend({},I);o.each(I,function(O,S){var R=new o.fx(J,K,O);if(/toggle|show|hide/.test(S)){R[S=="toggle"?L?"show":"hide":S](I)}else{var Q=S.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),T=R.cur(true)||0;if(Q){var N=parseFloat(Q[2]),P=Q[3]||"px";if(P!="px"){J.style[O]=(N||1)+P;T=((N||1)/R.cur(true))*T;J.style[O]=T+P}if(Q[1]){N=((Q[1]=="-="?-1:1)*N)+T}R.custom(T,N,P)}else{R.custom(T,S,"")}}});return true})},stop:function(F,E){var G=o.timers;if(F){this.queue([])}this.each(function(){for(var H=G.length-1;H>=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)&&!n){n=setInterval(function(){var K=o.timers;for(var J=0;J<K.length;J++){if(!K[J]()){K.splice(J--,1)}}if(!K.length){clearInterval(n);n=g}},13)}},show:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.show=true;this.custom(this.prop=="width"||this.prop=="height"?1:0,this.cur());o(this.elem).show()},hide:function(){this.options.orig[this.prop]=o.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(H){var G=e();if(H||G>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(I,G){var E=I?"Left":"Top",H=I?"Right":"Bottom",F=G.toLowerCase();o.fn["inner"+G]=function(){return this[0]?o.css(this[0],F,false,"padding"):null};o.fn["outer"+G]=function(K){return this[0]?o.css(this[0],F,false,K?"margin":"border"):null};var J=G.toLowerCase();o.fn[J]=function(K){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+G]||document.body["client"+G]:this[0]==document?Math.max(document.documentElement["client"+G],document.body["scroll"+G],document.documentElement["scroll"+G],document.body["offset"+G],document.documentElement["offset"+G]):K===g?(this.length?o.css(this[0],J):null):this.css(J,typeof K==="string"?K:K+"px")}})})(); \ No newline at end of file
+(function(z,v){function la(){if(!c.isReady){try{r.documentElement.doScroll("left")}catch(a){setTimeout(la,1);return}c.ready()}}function Ma(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,i){var j=a.length;if(typeof b==="object"){for(var n in b)X(a,n,b[n],f,e,d);return a}if(d!==v){f=!i&&f&&c.isFunction(d);for(n=0;n<j;n++)e(a[n],b,f?d.call(a[n],n,e(a[n],b)):d,i);return a}return j?
+e(a[0],b):null}function J(){return(new Date).getTime()}function Y(){return false}function Z(){return true}function ma(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function na(a){var b,d=[],f=[],e=arguments,i,j,n,o,m,s,x=c.extend({},c.data(this,"events").live);if(!(a.button&&a.type==="click")){for(o in x){j=x[o];if(j.live===a.type||j.altLive&&c.inArray(a.type,j.altLive)>-1){i=j.data;i.beforeFilter&&i.beforeFilter[a.type]&&!i.beforeFilter[a.type](a)||f.push(j.selector)}else delete x[o]}i=c(a.target).closest(f,
+a.currentTarget);m=0;for(s=i.length;m<s;m++)for(o in x){j=x[o];n=i[m].elem;f=null;if(i[m].selector===j.selector){if(j.live==="mouseenter"||j.live==="mouseleave")f=c(a.relatedTarget).closest(j.selector)[0];if(!f||f!==n)d.push({elem:n,fn:j})}}m=0;for(s=d.length;m<s;m++){i=d[m];a.currentTarget=i.elem;a.data=i.fn.data;if(i.fn.apply(i.elem,e)===false){b=false;break}}return b}}function oa(a,b){return"live."+(a?a+".":"")+b.replace(/\./g,"`").replace(/ /g,"&")}function pa(a){return!a||!a.parentNode||a.parentNode.nodeType===
+11}function qa(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var i in f)for(var j in f[i])c.event.add(this,i,f[i][j],f[i][j].data)}}})}function ra(a,b,d){var f,e,i;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&a[0].indexOf("<option")<0&&(c.support.checkClone||!sa.test(a[0]))){e=true;if(i=c.fragments[a[0]])if(i!==1)f=i}if(!f){b=b&&b[0]?b[0].ownerDocument||b[0]:r;f=b.createDocumentFragment();
+c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=i?f:1;return{fragment:f,cacheable:e}}function K(a,b){var d={};c.each(ta.concat.apply([],ta.slice(0,b)),function(){d[this]=a});return d}function ua(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Na=z.jQuery,Oa=z.$,r=z.document,S,Pa=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Qa=/^.[^:#\[\.,]*$/,Ra=/\S/,Sa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Ta=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,O=navigator.userAgent,
+va=false,P=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,Q=Array.prototype.slice,wa=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(typeof a==="string")if((d=Pa.exec(a))&&(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:r;if(a=Ta.exec(a))if(c.isPlainObject(b)){a=[r.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=ra([d[1]],
+[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}}else{if(b=r.getElementById(d[2])){if(b.id!==d[2])return S.find(a);this.length=1;this[0]=b}this.context=r;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=r;a=r.getElementsByTagName(a)}else return!b||b.jquery?(b||S).find(a):c(b).find(a);else if(c.isFunction(a))return S.ready(a);if(a.selector!==v){this.selector=a.selector;this.context=a.context}return c.isArray(a)?this.setArray(a):c.makeArray(a,
+this)},selector:"",jquery:"1.4.1",length:0,size:function(){return this.length},toArray:function(){return Q.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){a=c(a||null);a.prevObject=this;a.context=this.context;if(b==="find")a.selector=this.selector+(this.selector?" ":"")+d;else if(b)a.selector=this.selector+"."+b+"("+d+")";return a},setArray:function(a){this.length=0;ba.apply(this,a);return this},each:function(a,b){return c.each(this,
+a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(r,c);else P&&P.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(Q.apply(this,arguments),"slice",Q.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};
+c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,i,j,n;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(i in e){j=a[i];n=e[i];if(a!==n)if(f&&n&&(c.isPlainObject(n)||c.isArray(n))){j=j&&(c.isPlainObject(j)||c.isArray(j))?j:c.isArray(n)?[]:{};a[i]=c.extend(f,j,n)}else if(n!==v)a[i]=n}return a};c.extend({noConflict:function(a){z.$=
+Oa;if(a)z.jQuery=Na;return c},isReady:false,ready:function(){if(!c.isReady){if(!r.body)return setTimeout(c.ready,13);c.isReady=true;if(P){for(var a,b=0;a=P[b++];)a.call(r,c);P=null}c.fn.triggerHandler&&c(r).triggerHandler("ready")}},bindReady:function(){if(!va){va=true;if(r.readyState==="complete")return c.ready();if(r.addEventListener){r.addEventListener("DOMContentLoaded",L,false);z.addEventListener("load",c.ready,false)}else if(r.attachEvent){r.attachEvent("onreadystatechange",L);z.attachEvent("onload",
+c.ready);var a=false;try{a=z.frameElement==null}catch(b){}r.documentElement.doScroll&&a&&la()}}},isFunction:function(a){return $.call(a)==="[object Function]"},isArray:function(a){return $.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||$.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!aa.call(a,"constructor")&&!aa.call(a.constructor.prototype,"isPrototypeOf"))return false;var b;for(b in a);return b===v||aa.call(a,b)},isEmptyObject:function(a){for(var b in a)return false;
+return true},error:function(a){throw a;},parseJSON:function(a){if(typeof a!=="string"||!a)return null;if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return z.JSON&&z.JSON.parse?z.JSON.parse(a):(new Function("return "+a))();else c.error("Invalid JSON: "+a)},noop:function(){},globalEval:function(a){if(a&&Ra.test(a)){var b=r.getElementsByTagName("head")[0]||
+r.documentElement,d=r.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(r.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,i=a.length,j=i===v||c.isFunction(a);if(d)if(j)for(f in a){if(b.apply(a[f],d)===false)break}else for(;e<i;){if(b.apply(a[e++],d)===false)break}else if(j)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=
+a[0];e<i&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Sa,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ba.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=0,f=b.length;d<f;d++)if(b[d]===a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==
+v;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,i=a.length;e<i;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,i=0,j=a.length;i<j;i++){e=b(a[i],i,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b==="string"){d=a;a=d[b];b=v}else if(b&&!c.isFunction(b)){d=b;b=v}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},
+uaMatch:function(a){a=a.toLowerCase();a=/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||!/compatible/.test(a)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}},browser:{}});O=c.uaMatch(O);if(O.browser){c.browser[O.browser]=true;c.browser.version=O.version}if(c.browser.webkit)c.browser.safari=true;if(wa)c.inArray=function(a,b){return wa.call(b,a)};S=c(r);if(r.addEventListener)L=function(){r.removeEventListener("DOMContentLoaded",
+L,false);c.ready()};else if(r.attachEvent)L=function(){if(r.readyState==="complete"){r.detachEvent("onreadystatechange",L);c.ready()}};(function(){c.support={};var a=r.documentElement,b=r.createElement("script"),d=r.createElement("div"),f="script"+J();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";var e=d.getElementsByTagName("*"),i=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!i)){c.support=
+{leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(i.getAttribute("style")),hrefNormalized:i.getAttribute("href")==="/a",opacity:/^0.55$/.test(i.style.opacity),cssFloat:!!i.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:r.createElement("select").appendChild(r.createElement("option")).selected,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};
+b.type="text/javascript";try{b.appendChild(r.createTextNode("window."+f+"=1;"))}catch(j){}a.insertBefore(b,a.firstChild);if(z[f]){c.support.scriptEval=true;delete z[f]}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function n(){c.support.noCloneEvent=false;d.detachEvent("onclick",n)});d.cloneNode(true).fireEvent("onclick")}d=r.createElement("div");d.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";a=r.createDocumentFragment();a.appendChild(d.firstChild);
+c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var n=r.createElement("div");n.style.width=n.style.paddingLeft="1px";r.body.appendChild(n);c.boxModel=c.support.boxModel=n.offsetWidth===2;r.body.removeChild(n).style.display="none"});a=function(n){var o=r.createElement("div");n="on"+n;var m=n in o;if(!m){o.setAttribute(n,"return;");m=typeof o[n]==="function"}return m};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=i=null}})();c.props=
+{"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ua=0,xa={},Va={};c.extend({cache:{},expando:G,noData:{embed:true,object:true,applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==z?xa:a;var f=a[G],e=c.cache;if(!b&&!f)return null;f||(f=++Ua);if(typeof b==="object"){a[G]=f;e=e[f]=c.extend(true,
+{},b)}else e=e[f]?e[f]:typeof d==="undefined"?Va:(e[f]={});if(d!==v){a[G]=f;e[b]=d}return typeof b==="string"?e[b]:e}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==z?xa:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{try{delete a[G]}catch(i){a.removeAttribute&&a.removeAttribute(G)}delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,
+a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===v){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===v&&this.length)f=c.data(this[0],a);return f===v&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);
+return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===v)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||
+a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var ya=/[\n\t]/g,ca=/\s+/,Wa=/\r/g,Xa=/href|src|style/,Ya=/(button|input)/i,Za=/(button|input|object|select|textarea)/i,$a=/^(a|area)$/i,za=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(o){var m=
+c(this);m.addClass(a.call(this,o,m.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className)for(var i=" "+e.className+" ",j=0,n=b.length;j<n;j++){if(i.indexOf(" "+b[j]+" ")<0)e.className+=" "+b[j]}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(o){var m=c(this);m.removeClass(a.call(this,o,m.attr("class")))});if(a&&typeof a==="string"||a===v)for(var b=(a||"").split(ca),
+d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var i=(" "+e.className+" ").replace(ya," "),j=0,n=b.length;j<n;j++)i=i.replace(" "+b[j]+" "," ");e.className=i.substring(1,i.length-1)}else e.className=""}return this},toggleClass:function(a,b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var i=c(this);i.toggleClass(a.call(this,e,i.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,i=0,j=c(this),n=b,o=
+a.split(ca);e=o[i++];){n=f?n:!j.hasClass(e);j[n?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(ya," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===v){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||
+{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var i=b?d:0;for(d=b?d+1:e.length;i<d;i++){var j=e[i];if(j.selected){a=c(j).val();if(b)return a;f.push(a)}}return f}if(za.test(b.type)&&!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Wa,"")}return v}var n=c.isFunction(a);return this.each(function(o){var m=c(this),s=a;if(this.nodeType===1){if(n)s=a.call(this,o,m.val());
+if(typeof s==="number")s+="";if(c.isArray(s)&&za.test(this.type))this.checked=c.inArray(m.val(),s)>=0;else if(c.nodeName(this,"select")){var x=c.makeArray(s);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),x)>=0});if(!x.length)this.selectedIndex=-1}else this.value=s}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return v;if(f&&b in c.attrFn)return c(a)[b](d);
+f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==v;b=f&&c.props[b]||b;if(a.nodeType===1){var i=Xa.test(b);if(b in a&&f&&!i){if(e){b==="type"&&Ya.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Za.test(a.nodeName)||$a.test(a.nodeName)&&a.href?0:v;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=
+""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&i?a.getAttribute(b,2):a.getAttribute(b);return a===null?v:a}return c.style(a,b,d)}});var ab=function(a){return a.replace(/[^\w\s\.\|`]/g,function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==z&&!a.frameElement)a=z;if(!d.guid)d.guid=c.guid++;if(f!==v){d=c.proxy(d);d.data=f}var e=c.data(a,"events")||c.data(a,"events",{}),i=c.data(a,"handle"),j;if(!i){j=
+function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(j.elem,arguments):v};i=c.data(a,"handle",j)}if(i){i.elem=a;b=b.split(/\s+/);for(var n,o=0;n=b[o++];){var m=n.split(".");n=m.shift();if(o>1){d=c.proxy(d);if(f!==v)d.data=f}d.type=m.slice(0).sort().join(".");var s=e[n],x=this.special[n]||{};if(!s){s=e[n]={};if(!x.setup||x.setup.call(a,f,m,d)===false)if(a.addEventListener)a.addEventListener(n,i,false);else a.attachEvent&&a.attachEvent("on"+n,i)}if(x.add)if((m=x.add.call(a,
+d,f,m,s))&&c.isFunction(m)){m.guid=m.guid||d.guid;m.data=m.data||d.data;m.type=m.type||d.type;d=m}s[d.guid]=d;this.global[n]=true}a=null}}},global:{},remove:function(a,b,d){if(!(a.nodeType===3||a.nodeType===8)){var f=c.data(a,"events"),e,i,j;if(f){if(b===v||typeof b==="string"&&b.charAt(0)===".")for(i in f)this.remove(a,i+(b||""));else{if(b.type){d=b.handler;b=b.type}b=b.split(/\s+/);for(var n=0;i=b[n++];){var o=i.split(".");i=o.shift();var m=!o.length,s=c.map(o.slice(0).sort(),ab);s=new RegExp("(^|\\.)"+
+s.join("\\.(?:.*\\.)?")+"(\\.|$)");var x=this.special[i]||{};if(f[i]){if(d){j=f[i][d.guid];delete f[i][d.guid]}else for(var A in f[i])if(m||s.test(f[i][A].type))delete f[i][A];x.remove&&x.remove.call(a,o,j);for(e in f[i])break;if(!e){if(!x.teardown||x.teardown.call(a,o)===false)if(a.removeEventListener)a.removeEventListener(i,c.data(a,"handle"),false);else a.detachEvent&&a.detachEvent("on"+i,c.data(a,"handle"));e=null;delete f[i]}}}}for(e in f)break;if(!e){if(A=c.data(a,"handle"))A.elem=null;c.removeData(a,
+"events");c.removeData(a,"handle")}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();this.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return v;a.result=v;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,
+b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(i){}if(!a.isPropagationStopped()&&f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){d=a.target;var j;if(!(c.nodeName(d,"a")&&e==="click")&&!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()])){try{if(d[e]){if(j=d["on"+e])d["on"+e]=null;this.triggered=true;d[e]()}}catch(n){}if(j)d["on"+e]=j;this.triggered=false}}},handle:function(a){var b,
+d;a=arguments[0]=c.event.fix(a||z.event);a.currentTarget=this;d=a.type.split(".");a.type=d.shift();b=!d.length&&!a.exclusive;var f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)");d=(c.data(this,"events")||{})[a.type];for(var e in d){var i=d[e];if(b||f.test(i.type)){a.handler=i;a.data=i.data;i=i.apply(this,arguments);if(i!==v){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||r;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=r.documentElement;d=r.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||
+d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==v)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a,b){c.extend(a,b||{});a.guid+=b.selector+b.live;b.liveProxy=a;c.event.add(this,b.live,na,b)},remove:function(a){if(a.length){var b=
+0,d=new RegExp("(^|\\.)"+a[0]+"(\\.|$)");c.each(c.data(this,"events").live||{},function(){d.test(this.type)&&b++});b<1&&c.event.remove(this,a[0],na)}},special:{}},beforeunload:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true};
+c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y};var Aa=function(a){for(var b=
+a.relatedTarget;b&&b!==this;)try{b=b.parentNode}catch(d){break}if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}},Ba=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ba:Aa,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ba:Aa)}}});if(!c.support.submitBubbles)c.event.special.submit={setup:function(a,b,d){if(this.nodeName.toLowerCase()!==
+"form"){c.event.add(this,"click.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="submit"||i==="image")&&c(e).closest("form").length)return ma("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="text"||i==="password")&&c(e).closest("form").length&&f.keyCode===13)return ma("submit",this,arguments)})}else return false},remove:function(a,b){c.event.remove(this,"click.specialSubmit"+(b?"."+b.guid:""));c.event.remove(this,
+"keypress.specialSubmit"+(b?"."+b.guid:""))}};if(!c.support.changeBubbles){var da=/textarea|input|select/i;function Ca(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d}function ea(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Ca(d);if(a.type!=="focusout"||
+d.type!=="radio")c.data(d,"_change_data",e);if(!(f===v||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}}c.event.special.change={filters:{focusout:ea,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return ea.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return ea.call(this,a)},beforeactivate:function(a){a=
+a.target;a.nodeName.toLowerCase()==="input"&&a.type==="radio"&&c.data(a,"_change_data",Ca(a))}},setup:function(a,b,d){for(var f in T)c.event.add(this,f+".specialChange."+d.guid,T[f]);return da.test(this.nodeName)},remove:function(a,b){for(var d in T)c.event.remove(this,d+".specialChange"+(b?"."+b.guid:""),T[d]);return da.test(this.nodeName)}};var T=c.event.special.change.filters}r.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,
+f)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var i in d)this[b](i,f,d[i],e);return this}if(c.isFunction(f)){e=f;f=v}var j=b==="one"?c.proxy(e,function(n){c(this).unbind(n,j);return e.apply(this,arguments)}):e;return d==="unload"&&b!=="one"?this.one(d,f,e):this.each(function(){c.event.add(this,d,j,f)})}});c.fn.extend({unbind:function(a,
+b){if(typeof a==="object"&&!a.preventDefault){for(var d in a)this.unbind(d,a[d]);return this}return this.each(function(){c.event.remove(this,a,b)})},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+
+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});c.each(["live","die"],function(a,b){c.fn[b]=function(d,f,e){var i,j=0;if(c.isFunction(f)){e=f;f=v}for(d=(d||"").split(/\s+/);(i=d[j++])!=null;){i=i==="focus"?"focusin":i==="blur"?"focusout":i==="hover"?d.push("mouseleave")&&"mouseenter":i;b==="live"?c(this.context).bind(oa(i,this.selector),{data:f,selector:this.selector,
+live:i},e):c(this.context).unbind(oa(i,this.selector),e?{guid:e.guid+this.selector+i}:null)}return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){c.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});z.attachEvent&&!z.addEventListener&&z.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});
+(function(){function a(g){for(var h="",k,l=0;g[l];l++){k=g[l];if(k.nodeType===3||k.nodeType===4)h+=k.nodeValue;else if(k.nodeType!==8)h+=a(k.childNodes)}return h}function b(g,h,k,l,q,p){q=0;for(var u=l.length;q<u;q++){var t=l[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===k){y=l[t.sizset];break}if(t.nodeType===1&&!p){t.sizcache=k;t.sizset=q}if(t.nodeName.toLowerCase()===h){y=t;break}t=t[g]}l[q]=y}}}function d(g,h,k,l,q,p){q=0;for(var u=l.length;q<u;q++){var t=l[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===
+k){y=l[t.sizset];break}if(t.nodeType===1){if(!p){t.sizcache=k;t.sizset=q}if(typeof h!=="string"){if(t===h){y=true;break}}else if(o.filter(h,[t]).length>0){y=t;break}}t=t[g]}l[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,i=Object.prototype.toString,j=false,n=true;[0,0].sort(function(){n=false;return 0});var o=function(g,h,k,l){k=k||[];var q=h=h||r;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||
+typeof g!=="string")return k;for(var p=[],u,t,y,R,H=true,M=w(h),I=g;(f.exec(""),u=f.exec(I))!==null;){I=u[3];p.push(u[1]);if(u[2]){R=u[3];break}}if(p.length>1&&s.exec(g))if(p.length===2&&m.relative[p[0]])t=fa(p[0]+p[1],h);else for(t=m.relative[p[0]]?[h]:o(p.shift(),h);p.length;){g=p.shift();if(m.relative[g])g+=p.shift();t=fa(g,t)}else{if(!l&&p.length>1&&h.nodeType===9&&!M&&m.match.ID.test(p[0])&&!m.match.ID.test(p[p.length-1])){u=o.find(p.shift(),h,M);h=u.expr?o.filter(u.expr,u.set)[0]:u.set[0]}if(h){u=
+l?{expr:p.pop(),set:A(l)}:o.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=u.expr?o.filter(u.expr,u.set):u.set;if(p.length>0)y=A(t);else H=false;for(;p.length;){var D=p.pop();u=D;if(m.relative[D])u=p.pop();else D="";if(u==null)u=h;m.relative[D](y,u,M)}}else y=[]}y||(y=t);y||o.error(D||g);if(i.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))k.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&
+y[g].nodeType===1&&k.push(t[g]);else k.push.apply(k,y);else A(y,k);if(R){o(R,q,k,l);o.uniqueSort(k)}return k};o.uniqueSort=function(g){if(C){j=n;g.sort(C);if(j)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};o.matches=function(g,h){return o(g,null,null,h)};o.find=function(g,h,k){var l,q;if(!g)return[];for(var p=0,u=m.order.length;p<u;p++){var t=m.order[p];if(q=m.leftMatch[t].exec(g)){var y=q[1];q.splice(1,1);if(y.substr(y.length-1)!=="\\"){q[1]=(q[1]||"").replace(/\\/g,"");l=m.find[t](q,
+h,k);if(l!=null){g=g.replace(m.match[t],"");break}}}}l||(l=h.getElementsByTagName("*"));return{set:l,expr:g}};o.filter=function(g,h,k,l){for(var q=g,p=[],u=h,t,y,R=h&&h[0]&&w(h[0]);g&&h.length;){for(var H in m.filter)if((t=m.leftMatch[H].exec(g))!=null&&t[2]){var M=m.filter[H],I,D;D=t[1];y=false;t.splice(1,1);if(D.substr(D.length-1)!=="\\"){if(u===p)p=[];if(m.preFilter[H])if(t=m.preFilter[H](t,u,k,p,l,R)){if(t===true)continue}else y=I=true;if(t)for(var U=0;(D=u[U])!=null;U++)if(D){I=M(D,t,U,u);var Da=
+l^!!I;if(k&&I!=null)if(Da)y=true;else u[U]=false;else if(Da){p.push(D);y=true}}if(I!==v){k||(u=p);g=g.replace(m.match[H],"");if(!y)return[];break}}}if(g===q)if(y==null)o.error(g);else break;q=g}return u};o.error=function(g){throw"Syntax error, unrecognized expression: "+g;};var m=o.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
+TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}},relative:{"+":function(g,h){var k=typeof h==="string",l=k&&!/\W/.test(h);k=k&&!l;if(l)h=h.toLowerCase();l=0;for(var q=g.length,
+p;l<q;l++)if(p=g[l]){for(;(p=p.previousSibling)&&p.nodeType!==1;);g[l]=k||p&&p.nodeName.toLowerCase()===h?p||false:p===h}k&&o.filter(h,g,true)},">":function(g,h){var k=typeof h==="string";if(k&&!/\W/.test(h)){h=h.toLowerCase();for(var l=0,q=g.length;l<q;l++){var p=g[l];if(p){k=p.parentNode;g[l]=k.nodeName.toLowerCase()===h?k:false}}}else{l=0;for(q=g.length;l<q;l++)if(p=g[l])g[l]=k?p.parentNode:p.parentNode===h;k&&o.filter(h,g,true)}},"":function(g,h,k){var l=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=
+h=h.toLowerCase();q=b}q("parentNode",h,l,g,p,k)},"~":function(g,h,k){var l=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("previousSibling",h,l,g,p,k)}},find:{ID:function(g,h,k){if(typeof h.getElementById!=="undefined"&&!k)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var k=[];h=h.getElementsByName(g[1]);for(var l=0,q=h.length;l<q;l++)h[l].getAttribute("name")===g[1]&&k.push(h[l]);return k.length===0?null:k}},
+TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,k,l,q,p){g=" "+g[1].replace(/\\/g,"")+" ";if(p)return g;p=0;for(var u;(u=h[p])!=null;p++)if(u)if(q^(u.className&&(" "+u.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))k||l.push(u);else if(k)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&
+"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,k,l,q,p){h=g[1].replace(/\\/g,"");if(!p&&m.attrMap[h])g[1]=m.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,k,l,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=o(g[3],null,null,h);else{g=o.filter(g[3],h,k,true^q);k||l.push.apply(l,g);return false}else if(m.match.POS.test(g[0])||m.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);
+return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,k){return!!o(k[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===
+g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,h){return h===0},last:function(g,h,k,l){return h===l.length-1},even:function(g,h){return h%2===
+0},odd:function(g,h){return h%2===1},lt:function(g,h,k){return h<k[3]-0},gt:function(g,h,k){return h>k[3]-0},nth:function(g,h,k){return k[3]-0===h},eq:function(g,h,k){return k[3]-0===h}},filter:{PSEUDO:function(g,h,k,l){var q=h[1],p=m.filters[q];if(p)return p(g,k,h,l);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=h[3];k=0;for(l=h.length;k<l;k++)if(h[k]===g)return false;return true}else o.error("Syntax error, unrecognized expression: "+
+q)},CHILD:function(g,h){var k=h[1],l=g;switch(k){case "only":case "first":for(;l=l.previousSibling;)if(l.nodeType===1)return false;if(k==="first")return true;l=g;case "last":for(;l=l.nextSibling;)if(l.nodeType===1)return false;return true;case "nth":k=h[2];var q=h[3];if(k===1&&q===0)return true;h=h[0];var p=g.parentNode;if(p&&(p.sizcache!==h||!g.nodeIndex)){var u=0;for(l=p.firstChild;l;l=l.nextSibling)if(l.nodeType===1)l.nodeIndex=++u;p.sizcache=h}g=g.nodeIndex-q;return k===0?g===0:g%k===0&&g/k>=
+0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var k=h[1];g=m.attrHandle[k]?m.attrHandle[k](g):g[k]!=null?g[k]:g.getAttribute(k);k=g+"";var l=h[2];h=h[4];return g==null?l==="!=":l==="="?k===h:l==="*="?k.indexOf(h)>=0:l==="~="?(" "+k+" ").indexOf(h)>=0:!h?k&&g!==false:l==="!="?k!==h:l==="^="?
+k.indexOf(h)===0:l==="$="?k.substr(k.length-h.length)===h:l==="|="?k===h||k.substr(0,h.length+1)===h+"-":false},POS:function(g,h,k,l){var q=m.setFilters[h[2]];if(q)return q(g,k,h,l)}}},s=m.match.POS;for(var x in m.match){m.match[x]=new RegExp(m.match[x].source+/(?![^\[]*\])(?![^\(]*\))/.source);m.leftMatch[x]=new RegExp(/(^(?:.|\r|\n)*?)/.source+m.match[x].source.replace(/\\(\d+)/g,function(g,h){return"\\"+(h-0+1)}))}var A=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};
+try{Array.prototype.slice.call(r.documentElement.childNodes,0)}catch(B){A=function(g,h){h=h||[];if(i.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var k=0,l=g.length;k<l;k++)h.push(g[k]);else for(k=0;g[k];k++)h.push(g[k]);return h}}var C;if(r.documentElement.compareDocumentPosition)C=function(g,h){if(!g.compareDocumentPosition||!h.compareDocumentPosition){if(g==h)j=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===
+h?0:1;if(g===0)j=true;return g};else if("sourceIndex"in r.documentElement)C=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)j=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)j=true;return g};else if(r.createRange)C=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)j=true;return g.ownerDocument?-1:1}var k=g.ownerDocument.createRange(),l=h.ownerDocument.createRange();k.setStart(g,0);k.setEnd(g,0);l.setStart(h,0);l.setEnd(h,0);g=k.compareBoundaryPoints(Range.START_TO_END,
+l);if(g===0)j=true;return g};(function(){var g=r.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var k=r.documentElement;k.insertBefore(g,k.firstChild);if(r.getElementById(h)){m.find.ID=function(l,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(l[1]))?q.id===l[1]||typeof q.getAttributeNode!=="undefined"&&q.getAttributeNode("id").nodeValue===l[1]?[q]:v:[]};m.filter.ID=function(l,q){var p=typeof l.getAttributeNode!=="undefined"&&l.getAttributeNode("id");
+return l.nodeType===1&&p&&p.nodeValue===q}}k.removeChild(g);k=g=null})();(function(){var g=r.createElement("div");g.appendChild(r.createComment(""));if(g.getElementsByTagName("*").length>0)m.find.TAG=function(h,k){k=k.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var l=0;k[l];l++)k[l].nodeType===1&&h.push(k[l]);k=h}return k};g.innerHTML="<a href='#'></a>";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")m.attrHandle.href=function(h){return h.getAttribute("href",
+2)};g=null})();r.querySelectorAll&&function(){var g=o,h=r.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){o=function(l,q,p,u){q=q||r;if(!u&&q.nodeType===9&&!w(q))try{return A(q.querySelectorAll(l),p)}catch(t){}return g(l,q,p,u)};for(var k in g)o[k]=g[k];h=null}}();(function(){var g=r.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===
+0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){m.order.splice(1,0,"CLASS");m.find.CLASS=function(h,k,l){if(typeof k.getElementsByClassName!=="undefined"&&!l)return k.getElementsByClassName(h[1])};g=null}}})();var E=r.compareDocumentPosition?function(g,h){return g.compareDocumentPosition(h)&16}:function(g,h){return g!==h&&(g.contains?g.contains(h):true)},w=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},fa=function(g,h){var k=[],
+l="",q;for(h=h.nodeType?[h]:h;q=m.match.PSEUDO.exec(g);){l+=q[0];g=g.replace(m.match.PSEUDO,"")}g=m.relative[g]?g+"*":g;q=0;for(var p=h.length;q<p;q++)o(g,h[q],k);return o.filter(l,k)};c.find=o;c.expr=o.selectors;c.expr[":"]=c.expr.filters;c.unique=o.uniqueSort;c.getText=a;c.isXMLDoc=w;c.contains=E})();var bb=/Until$/,cb=/^(?:parents|prevUntil|prevAll)/,db=/,/;Q=Array.prototype.slice;var Ea=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,i){return!!b.call(e,i,e)===d});else if(b.nodeType)return c.grep(a,
+function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Qa.test(b))return c.filter(b,f,!d);else b=c.filter(b,f)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length;c.find(a,this[f],b);if(f>0)for(var i=d;i<b.length;i++)for(var j=0;j<d;j++)if(b[j]===b[i]){b.splice(i--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=
+0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Ea(this,a,false),"not",a)},filter:function(a){return this.pushStack(Ea(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,i={},j;if(f&&a.length){e=0;for(var n=a.length;e<n;e++){j=a[e];i[j]||(i[j]=c.expr.match.POS.test(j)?c(j,b||this.context):j)}for(;f&&f.ownerDocument&&f!==b;){for(j in i){e=i[j];if(e.jquery?e.index(f)>
+-1:c(f).is(e)){d.push({selector:j,elem:f});delete i[j]}}f=f.parentNode}}return d}var o=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(m,s){for(;s&&s.ownerDocument&&s!==b;){if(o?o.index(s)>-1:c(s).is(a))return s;s=s.parentNode}return null})},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),
+a);return this.pushStack(pa(a[0])||pa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},
+nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);bb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):
+e;if((this.length>1||db.test(f))&&cb.test(a))e=e.reverse();return this.pushStack(e,a,Q.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===v||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==
+b&&d.push(a);return d}});var Fa=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ga=/(<([\w:]+)[^>]*?)\/>/g,eb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,Ha=/<([\w:]+)/,fb=/<tbody/i,gb=/<|&\w+;/,sa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ia=function(a,b,d){return eb.test(d)?a:b+"></"+d+">"},F={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],
+col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==v)return this.empty().append((this[0]&&this[0].ownerDocument||r).createTextNode(a));return c.getText(this)},
+wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?
+d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,
+false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&
+!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Fa,"").replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){qa(this,b);qa(this.find("*"),b.find("*"))}return b},html:function(a){if(a===v)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Fa,""):null;else if(typeof a==="string"&&!/<script/i.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(Ha.exec(a)||
+["",""])[1].toLowerCase()]){a=a.replace(Ga,Ia);try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){c.cleanData(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}}else c.isFunction(a)?this.each(function(e){var i=c(this),j=i.html();i.empty().append(function(){return a.call(this,e,j)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(c.isFunction(a))return this.each(function(b){var d=c(this),f=d.html();d.replaceWith(a.call(this,
+b,f))});else a=c(a).detach();return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(s){return c.nodeName(s,"table")?s.getElementsByTagName("tbody")[0]||s.appendChild(s.ownerDocument.createElement("tbody")):s}var e,i,j=a[0],n=[];if(!c.support.checkClone&&arguments.length===3&&typeof j===
+"string"&&sa.test(j))return this.each(function(){c(this).domManip(a,b,d,true)});if(c.isFunction(j))return this.each(function(s){var x=c(this);a[0]=j.call(this,s,b?x.html():v);x.domManip(a,b,d)});if(this[0]){e=a[0]&&a[0].parentNode&&a[0].parentNode.nodeType===11?{fragment:a[0].parentNode}:ra(a,this,n);if(i=e.fragment.firstChild){b=b&&c.nodeName(i,"tr");for(var o=0,m=this.length;o<m;o++)d.call(b?f(this[o],i):this[o],e.cacheable||this.length>1||o>0?e.fragment.cloneNode(true):e.fragment)}n&&c.each(n,
+Ma)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);for(var e=0,i=d.length;e<i;e++){var j=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),j);f=f.concat(j)}return this.pushStack(f,a,d.selector)}});c.each({remove:function(a,b){if(!a||c.filter(a,[this]).length){if(!b&&this.nodeType===1){c.cleanData(this.getElementsByTagName("*"));c.cleanData([this])}this.parentNode&&
+this.parentNode.removeChild(this)}},empty:function(){for(this.nodeType===1&&c.cleanData(this.getElementsByTagName("*"));this.firstChild;)this.removeChild(this.firstChild)}},function(a,b){c.fn[a]=function(){return this.each(b,arguments)}});c.extend({clean:function(a,b,d,f){b=b||r;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||r;var e=[];c.each(a,function(i,j){if(typeof j==="number")j+="";if(j){if(typeof j==="string"&&!gb.test(j))j=b.createTextNode(j);else if(typeof j===
+"string"){j=j.replace(Ga,Ia);var n=(Ha.exec(j)||["",""])[1].toLowerCase(),o=F[n]||F._default,m=o[0];i=b.createElement("div");for(i.innerHTML=o[1]+j+o[2];m--;)i=i.lastChild;if(!c.support.tbody){m=fb.test(j);n=n==="table"&&!m?i.firstChild&&i.firstChild.childNodes:o[1]==="<table>"&&!m?i.childNodes:[];for(o=n.length-1;o>=0;--o)c.nodeName(n[o],"tbody")&&!n[o].childNodes.length&&n[o].parentNode.removeChild(n[o])}!c.support.leadingWhitespace&&V.test(j)&&i.insertBefore(b.createTextNode(V.exec(j)[0]),i.firstChild);
+j=c.makeArray(i.childNodes)}if(j.nodeType)e.push(j);else e=c.merge(e,j)}});if(d)for(a=0;e[a];a++)if(f&&c.nodeName(e[a],"script")&&(!e[a].type||e[a].type.toLowerCase()==="text/javascript"))f.push(e[a].parentNode?e[a].parentNode.removeChild(e[a]):e[a]);else{e[a].nodeType===1&&e.splice.apply(e,[a+1,0].concat(c.makeArray(e[a].getElementsByTagName("script"))));d.appendChild(e[a])}return e},cleanData:function(a){for(var b=0,d;(d=a[b])!=null;b++){c.event.remove(d);c.removeData(d)}}});var hb=/z-?index|font-?weight|opacity|zoom|line-?height/i,
+Ja=/alpha\([^)]*\)/,Ka=/opacity=([^)]*)/,ga=/float/i,ha=/-([a-z])/ig,ib=/([A-Z])/g,jb=/^-?\d+(?:px)?$/i,kb=/^-?\d/,lb={position:"absolute",visibility:"hidden",display:"block"},mb=["Left","Right"],nb=["Top","Bottom"],ob=r.defaultView&&r.defaultView.getComputedStyle,La=c.support.cssFloat?"cssFloat":"styleFloat",ia=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===v)return c.curCSS(d,f);if(typeof e==="number"&&!hb.test(f))e+="px";c.style(d,f,e)})};
+c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return v;if((b==="width"||b==="height")&&parseFloat(d)<0)d=v;var f=a.style||a,e=d!==v;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=Ja.test(a)?a.replace(Ja,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Ka.exec(f.filter)[1])/100+"":""}if(ga.test(b))b=La;b=b.replace(ha,ia);if(e)f[b]=d;return f[b]},css:function(a,
+b,d,f){if(b==="width"||b==="height"){var e,i=b==="width"?mb:nb;function j(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(i,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,"border"+this+"Width",true))||0})}a.offsetWidth!==0?j():c.swap(a,lb,j);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&
+a.currentStyle){f=Ka.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ga.test(b))b=La;if(!d&&e&&e[b])f=e[b];else if(ob){if(ga.test(b))b="float";b=b.replace(ib,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ha,ia);f=a.currentStyle[b]||a.currentStyle[d];if(!jb.test(f)&&kb.test(f)){b=e.left;var i=a.runtimeStyle.left;a.runtimeStyle.left=
+a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=i}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var pb=
+J(),qb=/<script(.|\s)*?\/script>/gi,rb=/select|textarea/i,sb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ja=/\?/,tb=/(\?|&)_=.*?(&|$)/,ub=/^(\w+:)?\/\/([^\/?#]+)/,vb=/%20/g;c.fn.extend({_load:c.fn.load,load:function(a,b,d){if(typeof a!=="string")return this._load(a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=
+c.param(b,c.ajaxSettings.traditional);f="POST"}var i=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(j,n){if(n==="success"||n==="notmodified")i.html(e?c("<div />").append(j.responseText.replace(qb,"")).find(e):j.responseText);d&&i.each(d,[j.responseText,n,j])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&
+(this.checked||rb.test(this.nodeName)||sb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,
+b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:z.XMLHttpRequest&&(z.location.protocol!=="file:"||!z.ActiveXObject)?function(){return new z.XMLHttpRequest}:
+function(){try{return new z.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&e.success.call(o,n,j,w);e.global&&f("ajaxSuccess",[w,e])}function d(){e.complete&&e.complete.call(o,w,j);e.global&&f("ajaxComplete",[w,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}
+function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),i,j,n,o=a&&a.context||e,m=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(m==="GET")N.test(e.url)||(e.url+=(ja.test(e.url)?"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||
+N.test(e.url))){i=e.jsonpCallback||"jsonp"+pb++;if(e.data)e.data=(e.data+"").replace(N,"="+i+"$1");e.url=e.url.replace(N,"="+i+"$1");e.dataType="script";z[i]=z[i]||function(q){n=q;b();d();z[i]=v;try{delete z[i]}catch(p){}A&&A.removeChild(B)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===false&&m==="GET"){var s=J(),x=e.url.replace(tb,"$1_="+s+"$2");e.url=x+(x===e.url?(ja.test(e.url)?"&":"?")+"_="+s:"")}if(e.data&&m==="GET")e.url+=(ja.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&
+c.event.trigger("ajaxStart");s=(s=ub.exec(e.url))&&(s[1]&&s[1]!==location.protocol||s[2]!==location.host);if(e.dataType==="script"&&m==="GET"&&s){var A=r.getElementsByTagName("head")[0]||r.documentElement,B=r.createElement("script");B.src=e.url;if(e.scriptCharset)B.charset=e.scriptCharset;if(!i){var C=false;B.onload=B.onreadystatechange=function(){if(!C&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){C=true;b();d();B.onload=B.onreadystatechange=null;A&&B.parentNode&&
+A.removeChild(B)}}}A.insertBefore(B,A.firstChild);return v}var E=false,w=e.xhr();if(w){e.username?w.open(m,e.url,e.async,e.username,e.password):w.open(m,e.url,e.async);try{if(e.data||a&&a.contentType)w.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[e.url]);c.etag[e.url]&&w.setRequestHeader("If-None-Match",c.etag[e.url])}s||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",
+e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(fa){}if(e.beforeSend&&e.beforeSend.call(o,w,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");w.abort();return false}e.global&&f("ajaxSend",[w,e]);var g=w.onreadystatechange=function(q){if(!w||w.readyState===0||q==="abort"){E||d();E=true;if(w)w.onreadystatechange=c.noop}else if(!E&&w&&(w.readyState===4||q==="timeout")){E=true;w.onreadystatechange=c.noop;j=q==="timeout"?"timeout":!c.httpSuccess(w)?
+"error":e.ifModified&&c.httpNotModified(w,e.url)?"notmodified":"success";var p;if(j==="success")try{n=c.httpData(w,e.dataType,e)}catch(u){j="parsererror";p=u}if(j==="success"||j==="notmodified")i||b();else c.handleError(e,w,j,p);d();q==="timeout"&&w.abort();if(e.async)w=null}};try{var h=w.abort;w.abort=function(){w&&h.call(w);g("abort")}}catch(k){}e.async&&e.timeout>0&&setTimeout(function(){w&&!E&&g("timeout")},e.timeout);try{w.send(m==="POST"||m==="PUT"||m==="DELETE"?e.data:null)}catch(l){c.handleError(e,
+w,null,l);d()}e.async||g();return w}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=
+f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(j,n){if(c.isArray(n))c.each(n,
+function(o,m){b?f(j,m):d(j+"["+(typeof m==="object"||c.isArray(m)?o:"")+"]",m)});else!b&&n!=null&&typeof n==="object"?c.each(n,function(o,m){d(j+"["+o+"]",m)}):f(j,n)}function f(j,n){n=c.isFunction(n)?n():n;e[e.length]=encodeURIComponent(j)+"="+encodeURIComponent(n)}var e=[];if(b===v)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var i in a)d(i,a[i]);return e.join("&").replace(vb,"+")}});var ka={},wb=/toggle|show|hide/,xb=/^([+-]=)?([\d+-.]+)(.*)$/,
+W,ta=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(ka[d])f=ka[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();
+ka[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a||a===0)return this.animate(K("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a],"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&&
+c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(K("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)},animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var i=c.extend({},e),j,n=this.nodeType===1&&c(this).is(":hidden"),
+o=this;for(j in a){var m=j.replace(ha,ia);if(j!==m){a[m]=a[j];delete a[j];j=m}if(a[j]==="hide"&&n||a[j]==="show"&&!n)return i.complete.call(this);if((j==="height"||j==="width")&&this.style){i.display=c.css(this,"display");i.overflow=this.style.overflow}if(c.isArray(a[j])){(i.specialEasing=i.specialEasing||{})[j]=a[j][1];a[j]=a[j][0]}}if(i.overflow!=null)this.style.overflow="hidden";i.curAnim=c.extend({},a);c.each(a,function(s,x){var A=new c.fx(o,i,s);if(wb.test(x))A[x==="toggle"?n?"show":"hide":x](a);
+else{var B=xb.exec(x),C=A.cur(true)||0;if(B){x=parseFloat(B[2]);var E=B[3]||"px";if(E!=="px"){o.style[s]=(x||1)+E;C=(x||1)/A.cur(true)*C;o.style[s]=C+E}if(B[1])x=(B[1]==="-="?-1:1)*x+C;A.custom(C,x,E)}else A.custom(C,x,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]);this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",
+1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration==="number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,
+b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==
+null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(i){return e.step(i)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop===
+"width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=
+this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=
+c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||c.fx.stop()},stop:function(){clearInterval(W);W=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!=
+null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in r.documentElement?function(a){var b=this[0];if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(),
+f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b=this[0];if(a)return this.each(function(s){c.offset.setOffset(this,a,s)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f=
+b,e=b.ownerDocument,i,j=e.documentElement,n=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var o=b.offsetTop,m=b.offsetLeft;(b=b.parentNode)&&b!==n&&b!==j;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;i=e?e.getComputedStyle(b,null):b.currentStyle;o-=b.scrollTop;m-=b.scrollLeft;if(b===d){o+=b.offsetTop;m+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){o+=parseFloat(i.borderTopWidth)||
+0;m+=parseFloat(i.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&i.overflow!=="visible"){o+=parseFloat(i.borderTopWidth)||0;m+=parseFloat(i.borderLeftWidth)||0}f=i}if(f.position==="relative"||f.position==="static"){o+=n.offsetTop;m+=n.offsetLeft}if(c.offset.supportsFixedPosition&&f.position==="fixed"){o+=Math.max(j.scrollTop,n.scrollTop);m+=Math.max(j.scrollLeft,n.scrollLeft)}return{top:o,left:m}};c.offset={initialize:function(){var a=r.body,b=r.createElement("div"),
+d,f,e,i=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";a.insertBefore(b,a.firstChild);
+d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i;a.removeChild(b);c.offset.initialize=c.noop},
+bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),i=parseInt(c.curCSS(a,"top",true),10)||0,j=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a,d,e);d={top:b.top-e.top+i,left:b.left-
+e.left+j};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top-f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=
+this.offsetParent||r.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],i;if(!e)return null;if(f!==v)return this.each(function(){if(i=ua(this))i.scrollTo(!a?f:c(i).scrollLeft(),a?f:c(i).scrollTop());else this[d]=f});else return(i=ua(e))?"pageXOffset"in i?i[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&i.document.documentElement[d]||i.document.body[d]:e[d]}});
+c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(i){var j=c(this);j[d](f.call(this,i,j[d]()))});return"scrollTo"in e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||
+e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===v?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});z.jQuery=z.$=c})(window);
+
diff --git a/js/util.js b/js/util.js
index b864867fd..3623337b9 100644
--- a/js/util.js
+++ b/js/util.js
@@ -143,87 +143,85 @@ var SN = { // StatusNet
SN.U.Counter(form);
},
- FormXHR: function(f) {
- if (jQuery.data(f[0], "ElementData") === undefined) {
- jQuery.data(f[0], "ElementData", {Bind:'submit'});
- f.bind('submit', function(e) {
- form_id = $(this)[0].id;
- $.ajax({
- type: 'POST',
- dataType: 'xml',
- url: $(this)[0].action,
- data: $(this).serialize() + '&ajax=1',
- beforeSend: function(xhr) {
- $('#'+form_id).addClass(SN.C.S.Processing);
- $('#'+form_id+' .submit').addClass(SN.C.S.Disabled);
- $('#'+form_id+' .submit').attr(SN.C.S.Disabled, SN.C.S.Disabled);
- },
- error: function (xhr, textStatus, errorThrown) {
- alert(errorThrown || textStatus);
- },
- success: function(data, textStatus) {
- if (typeof($('form', data)[0]) != 'undefined') {
- form_new = document._importNode($('form', data)[0], true);
- $('#'+form_id).replaceWith(form_new);
- $('#'+form_new.id).each(function() { SN.U.FormXHR($(this)); });
- }
- else {
- $('#'+form_id).replaceWith(document._importNode($('p', data)[0], true));
- }
- }
- });
- return false;
- });
- }
+ FormXHR: function(form) {
+ $.ajax({
+ type: 'POST',
+ dataType: 'xml',
+ url: form.attr('action'),
+ data: form.serialize() + '&ajax=1',
+ beforeSend: function(xhr) {
+ form
+ .addClass(SN.C.S.Processing)
+ .find('.submit')
+ .addClass(SN.C.S.Disabled)
+ .attr(SN.C.S.Disabled, SN.C.S.Disabled);
+ },
+ error: function (xhr, textStatus, errorThrown) {
+ alert(errorThrown || textStatus);
+ },
+ success: function(data, textStatus) {
+ if (typeof($('form', data)[0]) != 'undefined') {
+ form_new = document._importNode($('form', data)[0], true);
+ form.replaceWith(form_new);
+ }
+ else {
+ form.replaceWith(document._importNode($('p', data)[0], true));
+ }
+ }
+ });
},
FormNoticeXHR: function(form) {
- var NDG, NLat, NLon, NLNS, NLID;
+ SN.C.I.NoticeDataGeo = {};
form_id = form.attr('id');
form.append('<input type="hidden" name="ajax" value="1"/>');
form.ajaxForm({
dataType: 'xml',
timeout: '60000',
beforeSend: function(formData) {
- if ($('#'+form_id+' #'+SN.C.S.NoticeDataText)[0].value.length === 0) {
+ if (form.find('#'+SN.C.S.NoticeDataText)[0].value.length === 0) {
form.addClass(SN.C.S.Warning);
return false;
}
- form.addClass(SN.C.S.Processing);
- $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).addClass(SN.C.S.Disabled);
- $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).attr(SN.C.S.Disabled, SN.C.S.Disabled);
-
- NLat = $('#'+SN.C.S.NoticeLat).val();
- NLon = $('#'+SN.C.S.NoticeLon).val();
- NLNS = $('#'+SN.C.S.NoticeLocationNs).val();
- NLID = $('#'+SN.C.S.NoticeLocationId).val();
- NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked');
+ form
+ .addClass(SN.C.S.Processing)
+ .find('#'+SN.C.S.NoticeActionSubmit)
+ .addClass(SN.C.S.Disabled)
+ .attr(SN.C.S.Disabled, SN.C.S.Disabled);
+
+ SN.C.I.NoticeDataGeo.NLat = $('#'+SN.C.S.NoticeLat).val();
+ SN.C.I.NoticeDataGeo.NLon = $('#'+SN.C.S.NoticeLon).val();
+ SN.C.I.NoticeDataGeo.NLNS = $('#'+SN.C.S.NoticeLocationNs).val();
+ SN.C.I.NoticeDataGeo.NLID = $('#'+SN.C.S.NoticeLocationId).val();
+ SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked');
cookieValue = $.cookie(SN.C.S.NoticeDataGeoCookie);
if (cookieValue !== null && cookieValue != 'disabled') {
cookieValue = JSON.parse(cookieValue);
- NLat = $('#'+SN.C.S.NoticeLat).val(cookieValue.NLat).val();
- NLon = $('#'+SN.C.S.NoticeLon).val(cookieValue.NLon).val();
+ SN.C.I.NoticeDataGeo.NLat = $('#'+SN.C.S.NoticeLat).val(cookieValue.NLat).val();
+ SN.C.I.NoticeDataGeo.NLon = $('#'+SN.C.S.NoticeLon).val(cookieValue.NLon).val();
if ($('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS)) {
- NLNS = $('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS).val();
- NLID = $('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID).val();
+ SN.C.I.NoticeDataGeo.NLNS = $('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS).val();
+ SN.C.I.NoticeDataGeo.NLID = $('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID).val();
}
}
if (cookieValue == 'disabled') {
- NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', false).attr('checked');
+ SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', false).attr('checked');
}
else {
- NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', true).attr('checked');
+ SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', true).attr('checked');
}
return true;
},
error: function (xhr, textStatus, errorThrown) {
- form.removeClass(SN.C.S.Processing);
- $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).removeClass(SN.C.S.Disabled);
- $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).removeAttr(SN.C.S.Disabled, SN.C.S.Disabled);
- $('#'+form_id+' .form_response').remove();
+ form
+ .removeClass(SN.C.S.Processing)
+ .find('#'+SN.C.S.NoticeActionSubmit)
+ .removeClass(SN.C.S.Disabled)
+ .removeAttr(SN.C.S.Disabled, SN.C.S.Disabled);
+ form.find('.form_response').remove();
if (textStatus == 'timeout') {
form.append('<p class="form_response error">Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.</p>');
}
@@ -233,9 +231,10 @@ var SN = { // StatusNet
}
else {
if (parseInt(xhr.status) === 0 || jQuery.inArray(parseInt(xhr.status), SN.C.I.HTTP20x30x) >= 0) {
- $('#'+form_id).resetForm();
- $('#'+form_id+' #'+SN.C.S.NoticeDataAttachSelected).remove();
- SN.U.FormNoticeEnhancements($('#'+form_id));
+ form
+ .resetForm()
+ .find('#'+SN.C.S.NoticeDataAttachSelected).remove();
+ SN.U.FormNoticeEnhancements(form);
}
else {
form.append('<p class="form_response error">(Sorry! We had trouble sending your notice ('+xhr.status+' '+xhr.statusText+'). Please report the problem to the site administrator if this happens again.</p>');
@@ -244,7 +243,7 @@ var SN = { // StatusNet
}
},
success: function(data, textStatus) {
- $('#'+form_id+' .form_response').remove();
+ form.find('.form_response').remove();
var result;
if ($('#'+SN.C.S.Error, data).length > 0) {
result = document._importNode($('p', data)[0], true);
@@ -277,11 +276,11 @@ var SN = { // StatusNet
else {
notices.prepend(notice);
}
- $('#'+notice.id).css({display:'none'});
- $('#'+notice.id).fadeIn(2500);
+ $('#'+notice.id)
+ .css({display:'none'})
+ .fadeIn(2500);
SN.U.NoticeWithAttachment($('#'+notice.id));
SN.U.NoticeReplyTo($('#'+notice.id));
- SN.U.FormXHR($('#'+notice.id+' .form_favor'));
}
}
else {
@@ -290,24 +289,26 @@ var SN = { // StatusNet
form.append('<p class="form_response success">'+result_title+'</p>');
}
}
- $('#'+form_id).resetForm();
- $('#'+form_id+' #'+SN.C.S.NoticeInReplyTo).val('');
- $('#'+form_id+' #'+SN.C.S.NoticeDataAttachSelected).remove();
- SN.U.FormNoticeEnhancements($('#'+form_id));
+ form.resetForm();
+ form.find('#'+SN.C.S.NoticeInReplyTo).val('');
+ form.find('#'+SN.C.S.NoticeDataAttachSelected).remove();
+ SN.U.FormNoticeEnhancements(form);
}
},
complete: function(xhr, textStatus) {
- form.removeClass(SN.C.S.Processing);
- $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).removeAttr(SN.C.S.Disabled);
- $('#'+form_id+' #'+SN.C.S.NoticeActionSubmit).removeClass(SN.C.S.Disabled);
-
- $('#'+SN.C.S.NoticeLat).val(NLat);
- $('#'+SN.C.S.NoticeLon).val(NLon);
+ form
+ .removeClass(SN.C.S.Processing)
+ .find('#'+SN.C.S.NoticeActionSubmit)
+ .removeAttr(SN.C.S.Disabled)
+ .removeClass(SN.C.S.Disabled);
+
+ $('#'+SN.C.S.NoticeLat).val(SN.C.I.NoticeDataGeo.NLat);
+ $('#'+SN.C.S.NoticeLon).val(SN.C.I.NoticeDataGeo.NLon);
if ($('#'+SN.C.S.NoticeLocationNs)) {
- $('#'+SN.C.S.NoticeLocationNs).val(NLNS);
- $('#'+SN.C.S.NoticeLocationId).val(NLID);
+ $('#'+SN.C.S.NoticeLocationNs).val(SN.C.I.NoticeDataGeo.NLNS);
+ $('#'+SN.C.S.NoticeLocationId).val(SN.C.I.NoticeDataGeo.NLID);
}
- $('#'+SN.C.S.NoticeDataGeo).attr('checked', NDG);
+ $('#'+SN.C.S.NoticeDataGeo).attr('checked', SN.C.I.NoticeDataGeo.NDG);
}
});
},
@@ -350,46 +351,49 @@ var SN = { // StatusNet
},
NoticeFavor: function() {
- $('.form_favor').each(function() { SN.U.FormXHR($(this)); });
- $('.form_disfavor').each(function() { SN.U.FormXHR($(this)); });
+ $('.form_favor').live('click', function() { SN.U.FormXHR($(this)); return false; });
+ $('.form_disfavor').live('click', function() { SN.U.FormXHR($(this)); return false; });
},
NoticeRepeat: function() {
- $('.form_repeat').each(function() {
- SN.U.FormXHR($(this));
+ $('.form_repeat').live('click', function(e) {
+ e.preventDefault();
+
SN.U.NoticeRepeatConfirmation($(this));
+ return false;
});
},
NoticeRepeatConfirmation: function(form) {
- function NRC() {
- form.closest('.notice-options').addClass('opaque');
- form.addClass('dialogbox');
+ var submit_i = form.find('.submit');
- form.append('<button class="close">&#215;</button>');
- form.find('button.close').click(function(){
- $(this).remove();
+ var submit = submit_i.clone();
+ submit
+ .addClass('submit_dialogbox')
+ .removeClass('submit');
+ form.append(submit);
+ submit.bind('click', function() { SN.U.FormXHR(form); return false; });
- form.closest('.notice-options').removeClass('opaque');
- form.removeClass('dialogbox');
- form.find('.submit_dialogbox').remove();
- form.find('.submit').show();
+ submit_i.hide();
- return false;
- });
- };
+ form
+ .addClass('dialogbox')
+ .append('<button class="close">&#215;</button>')
+ .closest('.notice-options')
+ .addClass('opaque');
- form.find('.submit').bind('click', function(e) {
- e.preventDefault();
+ form.find('button.close').click(function(){
+ $(this).remove();
- var submit = form.find('.submit').clone();
- submit.addClass('submit_dialogbox');
- submit.removeClass('submit');
- form.append(submit);
+ form
+ .removeClass('dialogbox')
+ .closest('.notice-options')
+ .removeClass('opaque');
- $(this).hide();
+ form.find('.submit_dialogbox').remove();
+ form.find('.submit').show();
- NRC();
+ return false;
});
},
@@ -400,12 +404,10 @@ var SN = { // StatusNet
},
NoticeWithAttachment: function(notice) {
- if ($('.attachment', notice).length === 0) {
+ if (notice.find('.attachment').length === 0) {
return;
}
- var notice_id = notice.attr('id');
-
$.fn.jOverlay.options = {
method : 'GET',
data : '',
@@ -425,35 +427,37 @@ var SN = { // StatusNet
css : {'max-width':'542px', 'top':'5%', 'left':'32.5%'}
};
- $('#'+notice_id+' a.attachment').click(function() {
+ notice.find('a.attachment').click(function() {
$().jOverlay({url: $('address .url')[0].href+'attachment/' + ($(this).attr('id').substring('attachment'.length + 1)) + '/ajax'});
return false;
});
- var t;
- $("body:not(#shownotice) #"+notice_id+" a.thumbnail").hover(
- function() {
- var anchor = $(this);
- $("a.thumbnail").children('img').hide();
- anchor.closest(".entry-title").addClass('ov');
-
- if (anchor.children('img').length === 0) {
- t = setTimeout(function() {
- $.get($('address .url')[0].href+'attachment/' + (anchor.attr('id').substring('attachment'.length + 1)) + '/thumbnail', null, function(data) {
- anchor.append(data);
- });
- }, 500);
- }
- else {
- anchor.children('img').show();
+ if ($('#shownotice').length == 0) {
+ var t;
+ notice.find('a.thumbnail').hover(
+ function() {
+ var anchor = $(this);
+ $('a.thumbnail').children('img').hide();
+ anchor.closest(".entry-title").addClass('ov');
+
+ if (anchor.children('img').length === 0) {
+ t = setTimeout(function() {
+ $.get($('address .url')[0].href+'attachment/' + (anchor.attr('id').substring('attachment'.length + 1)) + '/thumbnail', null, function(data) {
+ anchor.append(data);
+ });
+ }, 500);
+ }
+ else {
+ anchor.children('img').show();
+ }
+ },
+ function() {
+ clearTimeout(t);
+ $('a.thumbnail').children('img').hide();
+ $(this).closest('.entry-title').removeClass('ov');
}
- },
- function() {
- clearTimeout(t);
- $("a.thumbnail").children('img').hide();
- $(this).closest(".entry-title").removeClass('ov');
- }
- );
+ );
+ }
},
NoticeDataAttach: function() {
@@ -639,7 +643,7 @@ var SN = { // StatusNet
NDM.bind('click', function() {
var NDMF = $('.entity_send-a-message form');
if (NDMF.length === 0) {
- $(this).addClass('processing');
+ $(this).addClass(SN.C.S.Processing);
$.get(NDM.attr('href'), null, function(data) {
$('.entity_send-a-message').append(document._importNode($('form', data)[0], true));
NDMF = $('.entity_send-a-message .form_notice');
@@ -650,7 +654,7 @@ var SN = { // StatusNet
NDMF.hide();
return false;
});
- NDM.removeClass('processing');
+ NDM.removeClass(SN.C.S.Processing);
});
}
else {
@@ -695,11 +699,11 @@ var SN = { // StatusNet
EntityActions: function() {
if ($('body.user_in').length > 0) {
- $('.form_user_subscribe').each(function() { SN.U.FormXHR($(this)); });
- $('.form_user_unsubscribe').each(function() { SN.U.FormXHR($(this)); });
- $('.form_group_join').each(function() { SN.U.FormXHR($(this)); });
- $('.form_group_leave').each(function() { SN.U.FormXHR($(this)); });
- $('.form_user_nudge').each(function() { SN.U.FormXHR($(this)); });
+ $('.form_user_subscribe').live('click', function() { SN.U.FormXHR($(this)); return false; });
+ $('.form_user_unsubscribe').live('click', function() { SN.U.FormXHR($(this)); return false; });
+ $('.form_group_join').live('click', function() { SN.U.FormXHR($(this)); return false; });
+ $('.form_group_leave').live('click', function() { SN.U.FormXHR($(this)); return false; });
+ $('.form_user_nudge').live('click', function() { SN.U.FormXHR($(this)); return false; });
SN.U.NewDirectMessage();
}
diff --git a/lib/action.php b/lib/action.php
index cc4f4aad0..fa9ddb911 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -249,7 +249,7 @@ class Action extends HTMLOutputter // lawsuit
$this->script('jquery.min.js');
$this->script('jquery.form.js');
$this->script('jquery.cookie.js');
- $this->script('json2.js');
+ $this->inlineScript('if (typeof window.JSON !== "object") { $.getScript("'.common_path('js/json2.js').'"); }');
$this->script('jquery.joverlay.min.js');
Event::handle('EndShowJQueryScripts', array($this));
}
@@ -259,8 +259,7 @@ class Action extends HTMLOutputter // lawsuit
$this->script('util.js');
$this->script('geometa.js');
// Frame-busting code to avoid clickjacking attacks.
- $this->element('script', array('type' => 'text/javascript'),
- 'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
+ $this->inlineScript('if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
Event::handle('EndShowStatusNetScripts', array($this));
Event::handle('EndShowLaconicaScripts', array($this));
}
@@ -405,6 +404,7 @@ class Action extends HTMLOutputter // lawsuit
'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'),
'alt' => common_config('site', 'name')));
}
+ $this->text(' ');
$this->element('span', array('class' => 'fn org'), common_config('site', 'name'));
$this->elementEnd('a');
Event::handle('EndAddressData', array($this));
@@ -822,12 +822,14 @@ class Action extends HTMLOutputter // lawsuit
'alt' => common_config('license', 'title'),
'width' => '80',
'height' => '15'));
+ $this->text(' ');
//TODO: This is dirty: i18n
$this->text(_('All '.common_config('site', 'name').' content and data are available under the '));
$this->element('a', array('class' => 'license',
'rel' => 'external license',
'href' => common_config('license', 'url')),
common_config('license', 'title'));
+ $this->text(' ');
$this->text(_('license.'));
$this->elementEnd('p');
break;
diff --git a/lib/api.php b/lib/api.php
index f81975216..0bcf4cc21 100644
--- a/lib/api.php
+++ b/lib/api.php
@@ -77,6 +77,7 @@ class ApiAction extends Action
function prepare($args)
{
+ StatusNet::setApi(true); // reduce exception reports to aid in debugging
parent::prepare($args);
$this->format = $this->arg('format');
@@ -357,7 +358,7 @@ class ApiAction extends Action
$entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
$entry['published'] = common_date_iso8601($notice->created);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$entry['id'] = "tag:$taguribase:$entry[link]";
$entry['updated'] = $entry['published'];
@@ -801,7 +802,7 @@ class ApiAction extends Action
$entry['link'] = common_local_url('showmessage', array('message' => $message->id));
$entry['published'] = common_date_iso8601($message->created);
- $taguribase = common_config('integration', 'taguri');
+ $taguribase = TagURI::base();
$entry['id'] = "tag:$taguribase:$entry[link]";
$entry['updated'] = $entry['published'];
@@ -1102,7 +1103,7 @@ class ApiAction extends Action
}
}
- function serverError($msg, $code = 500, $content_type = 'json')
+ function serverError($msg, $code = 500, $content_type = 'xml')
{
$action = $this->trimmed('action');
@@ -1153,7 +1154,6 @@ class ApiAction extends Action
$this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
'xml:lang' => 'en-US',
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
- Event::handle('StartApiAtom', array($this));
}
function endTwitterAtom()
@@ -1320,4 +1320,22 @@ class ApiAction extends Action
}
}
+ function getSelfUri($action, $aargs)
+ {
+ parse_str($_SERVER['QUERY_STRING'], $params);
+ $pstring = '';
+ if (!empty($params)) {
+ unset($params['p']);
+ $pstring = http_build_query($params);
+ }
+
+ $uri = common_local_url($action, $aargs);
+
+ if (!empty($pstring)) {
+ $uri .= '?' . $pstring;
+ }
+
+ return $uri;
+ }
+
}
diff --git a/lib/atom10entry.php b/lib/atom10entry.php
new file mode 100644
index 000000000..f8f16d594
--- /dev/null
+++ b/lib/atom10entry.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building / manipulating an Atom entry in memory
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class Atom10EntryException extends Exception
+{
+}
+
+/**
+ * Class for manipulating an Atom entry in memory. Get the entry as an XML
+ * string with Atom10Entry::getString().
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+class Atom10Entry extends XMLStringer
+{
+ private $namespaces;
+ private $categories;
+ private $content;
+ private $contributors;
+ private $id;
+ private $links;
+ private $published;
+ private $rights;
+ private $source;
+ private $summary;
+ private $title;
+
+ function __construct($indent = true) {
+ parent::__construct($indent);
+ $this->namespaces = array();
+ }
+
+ function addNamespace($namespace, $uri)
+ {
+ $ns = array($namespace => $uri);
+ $this->namespaces = array_merge($this->namespaces, $ns);
+ }
+
+ function initEntry()
+ {
+
+ }
+
+ function endEntry()
+ {
+
+ }
+
+ /**
+ * Check that all required elements have been set, etc.
+ * Throws an Atom10EntryException if something's missing.
+ *
+ * @return void
+ */
+ function validate()
+ {
+
+ }
+
+ function getString()
+ {
+ $this->validate();
+
+ $this->initEntry();
+ $this->renderEntries();
+ $this->endEntry();
+
+ return $this->xw->outputMemory();
+ }
+
+} \ No newline at end of file
diff --git a/lib/atom10feed.php b/lib/atom10feed.php
new file mode 100644
index 000000000..5e17b20d3
--- /dev/null
+++ b/lib/atom10feed.php
@@ -0,0 +1,305 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an Atom feed in memory
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+ exit(1);
+}
+
+class Atom10FeedException extends Exception
+{
+}
+
+/**
+ * Class for building an Atom feed in memory. Get the finished doc
+ * as a string with Atom10Feed::getString().
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+class Atom10Feed extends XMLStringer
+{
+ public $xw;
+ private $namespaces;
+ private $authors;
+ private $subject;
+ private $categories;
+ private $contributors;
+ private $generator;
+ private $icon;
+ private $links;
+ private $logo;
+ private $rights;
+ private $subtitle;
+ private $title;
+ private $published;
+ private $updated;
+ private $entries;
+
+ /**
+ * Constructor
+ *
+ * @param boolean $indent flag to turn indenting on or off
+ *
+ * @return void
+ */
+ function __construct($indent = true) {
+ parent::__construct($indent);
+ $this->namespaces = array();
+ $this->authors = array();
+ $this->links = array();
+ $this->entries = array();
+ $this->addNamespace('', 'http://www.w3.org/2005/Atom');
+ }
+
+ /**
+ * Add another namespace to the feed
+ *
+ * @param string $namespace the namespace
+ * @param string $uri namspace uri
+ *
+ * @return void
+ */
+ function addNamespace($namespace, $uri)
+ {
+ $ns = array($namespace => $uri);
+ $this->namespaces = array_merge($this->namespaces, $ns);
+ }
+
+ function addAuthor($name, $uri = null, $email = null)
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart('author');
+
+ if (!empty($name)) {
+ $xs->element('name', null, $name);
+ } else {
+ throw new Atom10FeedException(
+ 'author element must contain a name element.'
+ );
+ }
+
+ if (!is_null($uri)) {
+ $xs->element('uri', null, $uri);
+ }
+
+ if (!is_null(email)) {
+ $xs->element('email', null, $email);
+ }
+
+ $xs->elementEnd('author');
+
+ array_push($this->authors, $xs->getString());
+ }
+
+ /**
+ * Add an Author to the feed via raw XML string
+ *
+ * @param string $xmlAuthor An XML string representation author
+ *
+ * @return void
+ */
+ function addAuthorRaw($xmlAuthor)
+ {
+ array_push($this->authors, $xmlAuthor);
+ }
+
+ function renderAuthors()
+ {
+ foreach ($this->authors as $author) {
+ $this->raw($author);
+ }
+ }
+
+ /**
+ * Add a activity feed subject via raw XML string
+ *
+ * @param string $xmlSubject An XML string representation of the subject
+ *
+ * @return void
+ */
+ function setActivitySubject($xmlSubject)
+ {
+ $this->subject = $xmlSubject;
+ }
+
+ function getNamespaces()
+ {
+ return $this->namespaces;
+ }
+
+ function initFeed()
+ {
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $commonAttrs = array('xml:lang' => 'en-US');
+ foreach ($this->namespaces as $prefix => $uri) {
+ if ($prefix == '') {
+ $attr = 'xmlns';
+ } else {
+ $attr = 'xmlns:' . $prefix;
+ }
+ $commonAttrs[$attr] = $uri;
+ }
+ $this->elementStart('feed', $commonAttrs);
+
+ $this->element('id', null, $this->id);
+ $this->element('title', null, $this->title);
+ $this->element('subtitle', null, $this->subtitle);
+
+ if (!empty($this->logo)) {
+ $this->element('logo', null, $this->logo);
+ }
+
+ $this->element('updated', null, $this->updated);
+
+ $this->renderAuthors();
+
+ $this->renderLinks();
+ }
+
+ /**
+ * Check that all required elements have been set, etc.
+ * Throws an Atom10FeedException if something's missing.
+ *
+ * @return void
+ */
+ function validate()
+ {
+ }
+
+ function renderLinks()
+ {
+ foreach ($this->links as $attrs)
+ {
+ $this->element('link', $attrs, null);
+ }
+ }
+
+ function addEntryRaw($xmlEntry)
+ {
+ array_push($this->entries, $xmlEntry);
+ }
+
+ function addEntry($entry)
+ {
+ array_push($this->entries, $entry->getString());
+ }
+
+ function renderEntries()
+ {
+ foreach ($this->entries as $entry) {
+ $this->raw($entry);
+ }
+ }
+
+ function endFeed()
+ {
+ $this->elementEnd('feed');
+ $this->xw->endDocument();
+ }
+
+ function getString()
+ {
+ if (Event::handle('StartApiAtom', array($this))) {
+
+ $this->validate();
+ $this->initFeed();
+
+ if (!empty($this->subject)) {
+ $this->raw($this->subject);
+ }
+
+ $this->renderEntries();
+ $this->endFeed();
+
+ Event::handle('EndApiAtom', array($this));
+ }
+
+ return $this->xw->outputMemory();
+ }
+
+ function setId($id)
+ {
+ $this->id = $id;
+ }
+
+ function setTitle($title)
+ {
+ $this->title = $title;
+ }
+
+ function setSubtitle($subtitle)
+ {
+ $this->subtitle = $subtitle;
+ }
+
+ function setLogo($logo)
+ {
+ $this->logo = $logo;
+ }
+
+ function setUpdated($dt)
+ {
+ $this->updated = common_date_iso8601($dt);
+ }
+
+ function setPublished($dt)
+ {
+ $this->published = common_date_iso8601($dt);
+ }
+
+ /**
+ * Adds a link element into the Atom document
+ *
+ * Assumes you want rel="alternate" and type="text/html" unless
+ * you send in $otherAttrs.
+ *
+ * @param string $uri the uri the href needs to point to
+ * @param array $otherAttrs other attributes to stick in
+ *
+ * @return void
+ */
+ function addLink($uri, $otherAttrs = null) {
+ $attrs = array('href' => $uri);
+
+ if (is_null($otherAttrs)) {
+ $attrs['rel'] = 'alternate';
+ $attrs['type'] = 'text/html';
+ } else {
+ $attrs = array_merge($attrs, $otherAttrs);
+ }
+
+ array_push($this->links, $attrs);
+ }
+
+}
diff --git a/lib/atomgroupnoticefeed.php b/lib/atomgroupnoticefeed.php
new file mode 100644
index 000000000..52ee4c7d6
--- /dev/null
+++ b/lib/atomgroupnoticefeed.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an in-memory Atom feed for a particular group's
+ * timeline.
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+ exit(1);
+}
+
+/**
+ * Class for group notice feeds. May contains a reference to the group.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+class AtomGroupNoticeFeed extends AtomNoticeFeed
+{
+ private $group;
+
+ /**
+ * Constructor
+ *
+ * @param Group $group the group for the feed (optional)
+ * @param boolean $indent flag to turn indenting on or off
+ *
+ * @return void
+ */
+ function __construct($group = null, $indent = true) {
+ parent::__construct($indent);
+ $this->group = $group;
+ }
+
+ function getGroup()
+ {
+ return $this->group;
+ }
+
+}
diff --git a/lib/atomnoticefeed.php b/lib/atomnoticefeed.php
new file mode 100644
index 000000000..d2bf2a416
--- /dev/null
+++ b/lib/atomnoticefeed.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an Atom feed from a collection of notices
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+ exit(1);
+}
+
+/**
+ * Class for creating a feed that represents a collection of notices. Builds the
+ * feed in memory. Get the feed as a string with AtomNoticeFeed::getString().
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+class AtomNoticeFeed extends Atom10Feed
+{
+ function __construct($indent = true) {
+ parent::__construct($indent);
+
+ // Feeds containing notice info use these namespaces
+
+ $this->addNamespace(
+ 'thr',
+ 'http://purl.org/syndication/thread/1.0'
+ );
+
+ $this->addNamespace(
+ 'georss',
+ 'http://www.georss.org/georss'
+ );
+
+ $this->addNamespace(
+ 'activity',
+ 'http://activitystrea.ms/spec/1.0/'
+ );
+
+ $this->addNamespace(
+ 'poco',
+ 'http://portablecontacts.net/spec/1.0'
+ );
+
+ // XXX: What should the uri be?
+ $this->addNamespace(
+ 'ostatus',
+ 'http://ostatus.org/schema/1.0'
+ );
+ }
+
+ /**
+ * Add more than one Notice to the feed
+ *
+ * @param mixed $notices an array of Notice objects or handle
+ *
+ */
+ function addEntryFromNotices($notices)
+ {
+ if (is_array($notices)) {
+ foreach ($notices as $notice) {
+ $this->addEntryFromNotice($notice);
+ }
+ } else {
+ while ($notices->fetch()) {
+ $this->addEntryFromNotice($notices);
+ }
+ }
+ }
+
+ /**
+ * Add a single Notice to the feed
+ *
+ * @param Notice $notice a Notice to add
+ */
+ function addEntryFromNotice($notice)
+ {
+ $this->addEntryRaw($notice->asAtomEntry());
+ }
+
+}
+
+
diff --git a/lib/atomusernoticefeed.php b/lib/atomusernoticefeed.php
new file mode 100644
index 000000000..f71c721fe
--- /dev/null
+++ b/lib/atomusernoticefeed.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Class for building an in-memory Atom feed for a particular user's
+ * timeline.
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET'))
+{
+ exit(1);
+}
+
+/**
+ * Class for user notice feeds. May contain a reference to the user.
+ *
+ * @category Feed
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+class AtomUserNoticeFeed extends AtomNoticeFeed
+{
+ private $user;
+
+ /**
+ * Constructor
+ *
+ * @param User $user the user for the feed (optional)
+ * @param boolean $indent flag to turn indenting on or off
+ *
+ * @return void
+ */
+
+ function __construct($user = null, $indent = true) {
+ parent::__construct($indent);
+ $this->user = $user;
+ if (!empty($user)) {
+ $profile = $user->getProfile();
+ $this->addAuthor($profile->getBestName(),
+ $user->uri);
+ }
+ }
+
+ function getUser()
+ {
+ return $this->user;
+ }
+}
diff --git a/lib/cache.php b/lib/cache.php
index b3ec7534f..c09a1dd9f 100644
--- a/lib/cache.php
+++ b/lib/cache.php
@@ -47,6 +47,8 @@ class Cache
var $_items = array();
static $_inst = null;
+ const COMPRESSED = 1;
+
/**
* Singleton constructor
*
@@ -133,7 +135,7 @@ class Cache
*
* @param string $key The key to use for lookups
* @param string $value The value to store
- * @param integer $flag Flags to use, mostly ignored
+ * @param integer $flag Flags to use, may include Cache::COMPRESSED
* @param integer $expiry Expiry value, mostly ignored
*
* @return boolean success flag
diff --git a/lib/command.php b/lib/command.php
index 2a51fd687..ea7b60372 100644
--- a/lib/command.php
+++ b/lib/command.php
@@ -548,12 +548,19 @@ class SubCommand extends Command
return;
}
- $result = subs_subscribe_user($this->user, $this->other);
+ $otherUser = User::staticGet('nickname', $this->other);
- if ($result == 'true') {
+ if (empty($otherUser)) {
+ $channel->error($this->user, _('No such user'));
+ return;
+ }
+
+ try {
+ Subscription::start($this->user->getProfile(),
+ $otherUser->getProfile());
$channel->output($this->user, sprintf(_('Subscribed to %s'), $this->other));
- } else {
- $channel->error($this->user, $result);
+ } catch (Exception $e) {
+ $channel->error($this->user, $e->getMessage());
}
}
}
@@ -576,12 +583,18 @@ class UnsubCommand extends Command
return;
}
- $result=subs_unsubscribe_user($this->user, $this->other);
+ $otherUser = User::staticGet('nickname', $this->other);
- if ($result) {
+ if (empty($otherUser)) {
+ $channel->error($this->user, _('No such user'));
+ }
+
+ try {
+ Subscription::cancel($this->user->getProfile(),
+ $otherUser->getProfile());
$channel->output($this->user, sprintf(_('Unsubscribed from %s'), $this->other));
- } else {
- $channel->error($this->user, $result);
+ } catch (Exception $e) {
+ $channel->error($this->user, $e->getMessage());
}
}
}
diff --git a/lib/default.php b/lib/default.php
index c969c3b33..bb7708bfc 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -120,11 +120,13 @@ $default =
'avatar' =>
array('server' => null,
'dir' => INSTALLDIR . '/avatar/',
- 'path' => $_path . '/avatar/'),
+ 'path' => $_path . '/avatar/',
+ 'ssl' => null),
'background' =>
array('server' => null,
'dir' => INSTALLDIR . '/background/',
- 'path' => $_path . '/background/'),
+ 'path' => $_path . '/background/',
+ 'ssl' => null),
'public' =>
array('localonly' => true,
'blacklist' => array(),
@@ -132,10 +134,12 @@ $default =
'theme' =>
array('server' => null,
'dir' => null,
- 'path'=> null),
+ 'path'=> null,
+ 'ssl' => null),
'javascript' =>
array('server' => null,
- 'path'=> null),
+ 'path'=> null,
+ 'ssl' => null),
'throttle' =>
array('enabled' => false, // whether to throttle edits; false by default
'count' => 20, // number of allowed messages in timespan
@@ -171,7 +175,7 @@ $default =
array('enabled' => false),
'integration' =>
array('source' => 'StatusNet', # source attribute for Twitter
- 'taguri' => $_server.',2009'), # base for tag URIs
+ 'taguri' => null), # base for tag URIs
'twitter' =>
array('enabled' => true,
'consumer_key' => null,
@@ -193,6 +197,7 @@ $default =
array('server' => null,
'dir' => INSTALLDIR . '/file/',
'path' => $_path . '/file/',
+ 'ssl' => null,
'supported' => array('image/png',
'image/jpeg',
'image/gif',
diff --git a/lib/error.php b/lib/error.php
index 87a4d913b..a6a29119f 100644
--- a/lib/error.php
+++ b/lib/error.php
@@ -56,6 +56,7 @@ class ErrorAction extends Action
$this->code = $code;
$this->message = $message;
+ $this->minimal = StatusNet::isApi();
// XXX: hack alert: usually we aren't going to
// call this page directly, but because it's
@@ -102,7 +103,14 @@ class ErrorAction extends Action
function showPage()
{
- parent::showPage();
+ if ($this->minimal) {
+ // Even more minimal -- we're in a machine API
+ // and don't want to flood the output.
+ $this->extraHeaders();
+ $this->showContent();
+ } else {
+ parent::showPage();
+ }
// We don't want to have any more output after this
exit();
diff --git a/lib/grouplist.php b/lib/grouplist.php
index 99bff9cdc..854bc34e2 100644
--- a/lib/grouplist.php
+++ b/lib/grouplist.php
@@ -105,6 +105,7 @@ class GroupList extends Widget
'alt' =>
($this->group->fullname) ? $this->group->fullname :
$this->group->nickname));
+ $this->out->text(' ');
$hasFN = ($this->group->fullname) ? 'nickname' : 'fn org nickname';
$this->out->elementStart('span', $hasFN);
$this->out->raw($this->highlight($this->group->nickname));
@@ -112,16 +113,19 @@ class GroupList extends Widget
$this->out->elementEnd('a');
if ($this->group->fullname) {
+ $this->out->text(' ');
$this->out->elementStart('span', 'fn org');
$this->out->raw($this->highlight($this->group->fullname));
$this->out->elementEnd('span');
}
if ($this->group->location) {
+ $this->out->text(' ');
$this->out->elementStart('span', 'label');
$this->out->raw($this->highlight($this->group->location));
$this->out->elementEnd('span');
}
if ($this->group->homepage) {
+ $this->out->text(' ');
$this->out->elementStart('a', array('href' => $this->group->homepage,
'class' => 'url'));
$this->out->raw($this->highlight($this->group->homepage));
diff --git a/lib/groupsection.php b/lib/groupsection.php
index 7327f9e1a..3b0b3029d 100644
--- a/lib/groupsection.php
+++ b/lib/groupsection.php
@@ -85,9 +85,9 @@ class GroupSection extends Section
'href' => $group->homeUrl(),
'rel' => 'contact group',
'class' => 'url'));
+ $this->out->text(' ');
$logo = ($group->stream_logo) ?
$group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE);
-
$this->out->element('img', array('src' => $logo,
'width' => AVATAR_MINI_SIZE,
'height' => AVATAR_MINI_SIZE,
@@ -95,6 +95,7 @@ class GroupSection extends Section
'alt' => ($group->fullname) ?
$group->fullname :
$group->nickname));
+ $this->out->text(' ');
$this->out->element('span', 'fn org nickname', $group->nickname);
$this->out->elementEnd('a');
$this->out->elementEnd('span');
diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php
index 4a88337bc..7315fe2ad 100644
--- a/lib/htmloutputter.php
+++ b/lib/htmloutputter.php
@@ -376,9 +376,20 @@ class HTMLOutputter extends XMLOutputter
$server = common_config('site', 'server');
}
- // XXX: protocol
+ $ssl = common_config('javascript', 'ssl');
+
+ if (is_null($ssl)) { // null -> guess
+ if (common_config('site', 'ssl') == 'always' &&
+ !common_config('javascript', 'server')) {
+ $ssl = true;
+ } else {
+ $ssl = false;
+ }
+ }
+
+ $protocol = ($ssl) ? 'https' : 'http';
- $src = 'http://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
+ $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
}
$this->element('script', array('type' => $type,
diff --git a/lib/httpclient.php b/lib/httpclient.php
index 3f8262076..4c3af8d7d 100644
--- a/lib/httpclient.php
+++ b/lib/httpclient.php
@@ -81,12 +81,13 @@ class HTTPResponse extends HTTP_Request2_Response
}
/**
- * Check if the response is OK, generally a 200 status code.
+ * Check if the response is OK, generally a 200 or other 2xx status code.
* @return bool
*/
function isOk()
{
- return ($this->getStatus() == 200);
+ $status = $this->getStatus();
+ return ($status >= 200 && $status < 300);
}
}
diff --git a/lib/mysqlschema.php b/lib/mysqlschema.php
new file mode 100644
index 000000000..485096ac4
--- /dev/null
+++ b/lib/mysqlschema.php
@@ -0,0 +1,538 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Database schema utilities
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Database
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Class representing the database schema
+ *
+ * A class representing the database schema. Can be used to
+ * manipulate the schema -- especially for plugins and upgrade
+ * utilities.
+ *
+ * @category Database
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class MysqlSchema extends Schema
+{
+ static $_single = null;
+ protected $conn = null;
+
+ /**
+ * Constructor. Only run once for singleton object.
+ */
+
+ protected function __construct()
+ {
+ // XXX: there should be an easier way to do this.
+ $user = new User();
+
+ $this->conn = $user->getDatabaseConnection();
+
+ $user->free();
+
+ unset($user);
+ }
+
+ /**
+ * Main public entry point. Use this to get
+ * the singleton object.
+ *
+ * @return Schema the (single) Schema object
+ */
+
+ static function get()
+ {
+ if (empty(self::$_single)) {
+ self::$_single = new Schema();
+ }
+ return self::$_single;
+ }
+
+ /**
+ * Returns a TableDef object for the table
+ * in the schema with the given name.
+ *
+ * Throws an exception if the table is not found.
+ *
+ * @param string $name Name of the table to get
+ *
+ * @return TableDef tabledef for that table.
+ */
+
+ public function getTableDef($name)
+ {
+ $res = $this->conn->query('DESCRIBE ' . $name);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ $td = new TableDef();
+
+ $td->name = $name;
+ $td->columns = array();
+
+ $row = array();
+
+ while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
+
+ $cd = new ColumnDef();
+
+ $cd->name = $row['Field'];
+
+ $packed = $row['Type'];
+
+ if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
+ $cd->type = $match[1];
+ $cd->size = $match[2];
+ } else {
+ $cd->type = $packed;
+ }
+
+ $cd->nullable = ($row['Null'] == 'YES') ? true : false;
+ $cd->key = $row['Key'];
+ $cd->default = $row['Default'];
+ $cd->extra = $row['Extra'];
+
+ $td->columns[] = $cd;
+ }
+
+ return $td;
+ }
+
+ /**
+ * Gets a ColumnDef object for a single column.
+ *
+ * Throws an exception if the table is not found.
+ *
+ * @param string $table name of the table
+ * @param string $column name of the column
+ *
+ * @return ColumnDef definition of the column or null
+ * if not found.
+ */
+
+ public function getColumnDef($table, $column)
+ {
+ $td = $this->getTableDef($table);
+
+ foreach ($td->columns as $cd) {
+ if ($cd->name == $column) {
+ return $cd;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a table with the given names and columns.
+ *
+ * @param string $name Name of the table
+ * @param array $columns Array of ColumnDef objects
+ * for new table.
+ *
+ * @return boolean success flag
+ */
+
+ public function createTable($name, $columns)
+ {
+ $uniques = array();
+ $primary = array();
+ $indices = array();
+
+ $sql = "CREATE TABLE $name (\n";
+
+ for ($i = 0; $i < count($columns); $i++) {
+
+ $cd =& $columns[$i];
+
+ if ($i > 0) {
+ $sql .= ",\n";
+ }
+
+ $sql .= $this->_columnSql($cd);
+
+ switch ($cd->key) {
+ case 'UNI':
+ $uniques[] = $cd->name;
+ break;
+ case 'PRI':
+ $primary[] = $cd->name;
+ break;
+ case 'MUL':
+ $indices[] = $cd->name;
+ break;
+ }
+ }
+
+ if (count($primary) > 0) { // it really should be...
+ $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
+ }
+
+ foreach ($uniques as $u) {
+ $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
+ }
+
+ foreach ($indices as $i) {
+ $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+ }
+
+ $sql .= "); ";
+
+ common_log(LOG_INFO, $sql);
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Drops a table from the schema
+ *
+ * Throws an exception if the table is not found.
+ *
+ * @param string $name Name of the table to drop
+ *
+ * @return boolean success flag
+ */
+
+ public function dropTable($name)
+ {
+ $res = $this->conn->query("DROP TABLE $name");
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Adds an index to a table.
+ *
+ * If no name is provided, a name will be made up based
+ * on the table name and column names.
+ *
+ * Throws an exception on database error, esp. if the table
+ * does not exist.
+ *
+ * @param string $table Name of the table
+ * @param array $columnNames Name of columns to index
+ * @param string $name (Optional) name of the index
+ *
+ * @return boolean success flag
+ */
+
+ public function createIndex($table, $columnNames, $name=null)
+ {
+ if (!is_array($columnNames)) {
+ $columnNames = array($columnNames);
+ }
+
+ if (empty($name)) {
+ $name = "$table_".implode("_", $columnNames)."_idx";
+ }
+
+ $res = $this->conn->query("ALTER TABLE $table ".
+ "ADD INDEX $name (".
+ implode(",", $columnNames).")");
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Drops a named index from a table.
+ *
+ * @param string $table name of the table the index is on.
+ * @param string $name name of the index
+ *
+ * @return boolean success flag
+ */
+
+ public function dropIndex($table, $name)
+ {
+ $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Adds a column to a table
+ *
+ * @param string $table name of the table
+ * @param ColumnDef $columndef Definition of the new
+ * column.
+ *
+ * @return boolean success flag
+ */
+
+ public function addColumn($table, $columndef)
+ {
+ $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Modifies a column in the schema.
+ *
+ * The name must match an existing column and table.
+ *
+ * @param string $table name of the table
+ * @param ColumnDef $columndef new definition of the column.
+ *
+ * @return boolean success flag
+ */
+
+ public function modifyColumn($table, $columndef)
+ {
+ $sql = "ALTER TABLE $table MODIFY COLUMN " .
+ $this->_columnSql($columndef);
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Drops a column from a table
+ *
+ * The name must match an existing column.
+ *
+ * @param string $table name of the table
+ * @param string $columnName name of the column to drop
+ *
+ * @return boolean success flag
+ */
+
+ public function dropColumn($table, $columnName)
+ {
+ $sql = "ALTER TABLE $table DROP COLUMN $columnName";
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Ensures that a table exists with the given
+ * name and the given column definitions.
+ *
+ * If the table does not yet exist, it will
+ * create the table. If it does exist, it will
+ * alter the table to match the column definitions.
+ *
+ * @param string $tableName name of the table
+ * @param array $columns array of ColumnDef
+ * objects for the table
+ *
+ * @return boolean success flag
+ */
+
+ public function ensureTable($tableName, $columns)
+ {
+ // XXX: DB engine portability -> toilet
+
+ try {
+ $td = $this->getTableDef($tableName);
+ } catch (Exception $e) {
+ if (preg_match('/no such table/', $e->getMessage())) {
+ return $this->createTable($tableName, $columns);
+ } else {
+ throw $e;
+ }
+ }
+
+ $cur = $this->_names($td->columns);
+ $new = $this->_names($columns);
+
+ $toadd = array_diff($new, $cur);
+ $todrop = array_diff($cur, $new);
+ $same = array_intersect($new, $cur);
+ $tomod = array();
+
+ foreach ($same as $m) {
+ $curCol = $this->_byName($td->columns, $m);
+ $newCol = $this->_byName($columns, $m);
+
+ if (!$newCol->equals($curCol)) {
+ $tomod[] = $newCol->name;
+ }
+ }
+
+ if (count($toadd) + count($todrop) + count($tomod) == 0) {
+ // nothing to do
+ return true;
+ }
+
+ // For efficiency, we want this all in one
+ // query, instead of using our methods.
+
+ $phrase = array();
+
+ foreach ($toadd as $columnName) {
+ $cd = $this->_byName($columns, $columnName);
+
+ $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
+ }
+
+ foreach ($todrop as $columnName) {
+ $phrase[] = 'DROP COLUMN ' . $columnName;
+ }
+
+ foreach ($tomod as $columnName) {
+ $cd = $this->_byName($columns, $columnName);
+
+ $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
+ }
+
+ $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the array of names from an array of
+ * ColumnDef objects.
+ *
+ * @param array $cds array of ColumnDef objects
+ *
+ * @return array strings for name values
+ */
+
+ private function _names($cds)
+ {
+ $names = array();
+
+ foreach ($cds as $cd) {
+ $names[] = $cd->name;
+ }
+
+ return $names;
+ }
+
+ /**
+ * Get a ColumnDef from an array matching
+ * name.
+ *
+ * @param array $cds Array of ColumnDef objects
+ * @param string $name Name of the column
+ *
+ * @return ColumnDef matching item or null if no match.
+ */
+
+ private function _byName($cds, $name)
+ {
+ foreach ($cds as $cd) {
+ if ($cd->name == $name) {
+ return $cd;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the proper SQL for creating or
+ * altering a column.
+ *
+ * Appropriate for use in CREATE TABLE or
+ * ALTER TABLE statements.
+ *
+ * @param ColumnDef $cd column to create
+ *
+ * @return string correct SQL for that column
+ */
+
+ private function _columnSql($cd)
+ {
+ $sql = "{$cd->name} ";
+
+ if (!empty($cd->size)) {
+ $sql .= "{$cd->type}({$cd->size}) ";
+ } else {
+ $sql .= "{$cd->type} ";
+ }
+
+ if (!empty($cd->default)) {
+ $sql .= "default {$cd->default} ";
+ } else {
+ $sql .= ($cd->nullable) ? "null " : "not null ";
+ }
+
+ if (!empty($cd->auto_increment)) {
+ $sql .= " auto_increment ";
+ }
+
+ if (!empty($cd->extra)) {
+ $sql .= "{$cd->extra} ";
+ }
+
+ return $sql;
+ }
+}
diff --git a/lib/noticelist.php b/lib/noticelist.php
index a4a0f2651..837cb90fa 100644
--- a/lib/noticelist.php
+++ b/lib/noticelist.php
@@ -294,6 +294,7 @@ class NoticeListItem extends Widget
}
$this->out->elementStart('a', $attrs);
$this->showAvatar();
+ $this->out->text(' ');
$this->showNickname();
$this->out->elementEnd('a');
$this->out->elementEnd('span');
@@ -432,8 +433,10 @@ class NoticeListItem extends Widget
$url = $location->getUrl();
+ $this->out->text(' ');
$this->out->elementStart('span', array('class' => 'location'));
$this->out->text(_('at'));
+ $this->out->text(' ');
if (empty($url)) {
$this->out->element('span', array('class' => 'geo',
'title' => $latlon),
@@ -473,9 +476,11 @@ class NoticeListItem extends Widget
function showNoticeSource()
{
if ($this->notice->source) {
+ $this->out->text(' ');
$this->out->elementStart('span', 'source');
$this->out->text(_('from'));
$source_name = _($this->notice->source);
+ $this->out->text(' ');
switch ($this->notice->source) {
case 'web':
case 'xmpp':
@@ -487,30 +492,34 @@ class NoticeListItem extends Widget
break;
default:
- $name = null;
+ $name = $source_name;
$url = null;
- $ns = Notice_source::staticGet($this->notice->source);
-
- if ($ns) {
- $name = $ns->name;
- $url = $ns->url;
- } else {
- $app = Oauth_application::staticGet('name', $this->notice->source);
- if ($app) {
- $name = $app->name;
- $url = $app->source_url;
+ if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) {
+ $ns = Notice_source::staticGet($this->notice->source);
+
+ if ($ns) {
+ $name = $ns->name;
+ $url = $ns->url;
+ } else {
+ $app = Oauth_application::staticGet('name', $this->notice->source);
+ if ($app) {
+ $name = $app->name;
+ $url = $app->source_url;
+ }
}
}
+ Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title));
if (!empty($name) && !empty($url)) {
$this->out->elementStart('span', 'device');
$this->out->element('a', array('href' => $url,
- 'rel' => 'external'),
+ 'rel' => 'external',
+ 'title' => $title),
$name);
$this->out->elementEnd('span');
} else {
- $this->out->element('span', 'device', $source_name);
+ $this->out->element('span', 'device', $name);
}
break;
}
@@ -540,6 +549,7 @@ class NoticeListItem extends Widget
}
}
if ($hasConversation){
+ $this->out->text(' ');
$convurl = common_local_url('conversation',
array('id' => $this->notice->conversation));
$this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id,
@@ -591,12 +601,14 @@ class NoticeListItem extends Widget
function showReplyLink()
{
if (common_logged_in()) {
+ $this->out->text(' ');
$reply_url = common_local_url('newnotice',
array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id));
$this->out->elementStart('a', array('href' => $reply_url,
'class' => 'notice_reply',
'title' => _('Reply to this notice')));
$this->out->text(_('Reply'));
+ $this->out->text(' ');
$this->out->element('span', 'notice_id', $this->notice->id);
$this->out->elementEnd('a');
}
@@ -616,7 +628,7 @@ class NoticeListItem extends Widget
if (!empty($user) &&
($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) {
-
+ $this->out->text(' ');
$deleteurl = common_local_url('deletenotice',
array('notice' => $todel->id));
$this->out->element('a', array('href' => $deleteurl,
@@ -635,6 +647,7 @@ class NoticeListItem extends Widget
{
$user = common_current_user();
if ($user && $user->id != $this->notice->profile_id) {
+ $this->out->text(' ');
$profile = $user->getProfile();
if ($profile->hasRepeated($this->notice->id)) {
$this->out->element('span', array('class' => 'repeated',
diff --git a/lib/noticesection.php b/lib/noticesection.php
index 24465f8ba..7157feafc 100644
--- a/lib/noticesection.php
+++ b/lib/noticesection.php
@@ -90,6 +90,7 @@ class NoticeSection extends Section
'alt' => ($profile->fullname) ?
$profile->fullname :
$profile->nickname));
+ $this->out->text(' ');
$this->out->element('span', 'fn nickname', $profile->nickname);
$this->out->elementEnd('a');
$this->out->elementEnd('span');
diff --git a/lib/pgsqlschema.php b/lib/pgsqlschema.php
new file mode 100644
index 000000000..91bc09667
--- /dev/null
+++ b/lib/pgsqlschema.php
@@ -0,0 +1,503 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Database schema utilities
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Database
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Class representing the database schema
+ *
+ * A class representing the database schema. Can be used to
+ * manipulate the schema -- especially for plugins and upgrade
+ * utilities.
+ *
+ * @category Database
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class PgsqlSchema extends Schema
+{
+
+ /**
+ * Returns a TableDef object for the table
+ * in the schema with the given name.
+ *
+ * Throws an exception if the table is not found.
+ *
+ * @param string $name Name of the table to get
+ *
+ * @return TableDef tabledef for that table.
+ */
+
+ public function getTableDef($name)
+ {
+ $res = $this->conn->query("select *, column_default as default, is_nullable as Null, udt_name as Type, column_name AS Field from INFORMATION_SCHEMA.COLUMNS where table_name = '$name'");
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ $td = new TableDef();
+
+ $td->name = $name;
+ $td->columns = array();
+
+ $row = array();
+
+ while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
+// var_dump($row);
+ $cd = new ColumnDef();
+
+ $cd->name = $row['field'];
+
+ $packed = $row['type'];
+
+ if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
+ $cd->type = $match[1];
+ $cd->size = $match[2];
+ } else {
+ $cd->type = $packed;
+ }
+
+ $cd->nullable = ($row['null'] == 'YES') ? true : false;
+ $cd->key = $row['Key'];
+ $cd->default = $row['default'];
+ $cd->extra = $row['Extra'];
+
+ $td->columns[] = $cd;
+ }
+ return $td;
+ }
+
+ /**
+ * Gets a ColumnDef object for a single column.
+ *
+ * Throws an exception if the table is not found.
+ *
+ * @param string $table name of the table
+ * @param string $column name of the column
+ *
+ * @return ColumnDef definition of the column or null
+ * if not found.
+ */
+
+ public function getColumnDef($table, $column)
+ {
+ $td = $this->getTableDef($table);
+
+ foreach ($td->columns as $cd) {
+ if ($cd->name == $column) {
+ return $cd;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a table with the given names and columns.
+ *
+ * @param string $name Name of the table
+ * @param array $columns Array of ColumnDef objects
+ * for new table.
+ *
+ * @return boolean success flag
+ */
+
+ public function createTable($name, $columns)
+ {
+ $uniques = array();
+ $primary = array();
+ $indices = array();
+
+ $sql = "CREATE TABLE $name (\n";
+
+ for ($i = 0; $i < count($columns); $i++) {
+
+ $cd =& $columns[$i];
+
+ if ($i > 0) {
+ $sql .= ",\n";
+ }
+
+ $sql .= $this->_columnSql($cd);
+
+ switch ($cd->key) {
+ case 'UNI':
+ $uniques[] = $cd->name;
+ break;
+ case 'PRI':
+ $primary[] = $cd->name;
+ break;
+ case 'MUL':
+ $indices[] = $cd->name;
+ break;
+ }
+ }
+
+ if (count($primary) > 0) { // it really should be...
+ $sql .= ",\nconstraint primary key (" . implode(',', $primary) . ")";
+ }
+
+ foreach ($uniques as $u) {
+ $sql .= ",\nunique index {$name}_{$u}_idx ($u)";
+ }
+
+ foreach ($indices as $i) {
+ $sql .= ",\nindex {$name}_{$i}_idx ($i)";
+ }
+
+ $sql .= "); ";
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Drops a table from the schema
+ *
+ * Throws an exception if the table is not found.
+ *
+ * @param string $name Name of the table to drop
+ *
+ * @return boolean success flag
+ */
+
+ public function dropTable($name)
+ {
+ $res = $this->conn->query("DROP TABLE $name");
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Adds an index to a table.
+ *
+ * If no name is provided, a name will be made up based
+ * on the table name and column names.
+ *
+ * Throws an exception on database error, esp. if the table
+ * does not exist.
+ *
+ * @param string $table Name of the table
+ * @param array $columnNames Name of columns to index
+ * @param string $name (Optional) name of the index
+ *
+ * @return boolean success flag
+ */
+
+ public function createIndex($table, $columnNames, $name=null)
+ {
+ if (!is_array($columnNames)) {
+ $columnNames = array($columnNames);
+ }
+
+ if (empty($name)) {
+ $name = "$table_".implode("_", $columnNames)."_idx";
+ }
+
+ $res = $this->conn->query("ALTER TABLE $table ".
+ "ADD INDEX $name (".
+ implode(",", $columnNames).")");
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Drops a named index from a table.
+ *
+ * @param string $table name of the table the index is on.
+ * @param string $name name of the index
+ *
+ * @return boolean success flag
+ */
+
+ public function dropIndex($table, $name)
+ {
+ $res = $this->conn->query("ALTER TABLE $table DROP INDEX $name");
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Adds a column to a table
+ *
+ * @param string $table name of the table
+ * @param ColumnDef $columndef Definition of the new
+ * column.
+ *
+ * @return boolean success flag
+ */
+
+ public function addColumn($table, $columndef)
+ {
+ $sql = "ALTER TABLE $table ADD COLUMN " . $this->_columnSql($columndef);
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Modifies a column in the schema.
+ *
+ * The name must match an existing column and table.
+ *
+ * @param string $table name of the table
+ * @param ColumnDef $columndef new definition of the column.
+ *
+ * @return boolean success flag
+ */
+
+ public function modifyColumn($table, $columndef)
+ {
+ $sql = "ALTER TABLE $table MODIFY COLUMN " .
+ $this->_columnSql($columndef);
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Drops a column from a table
+ *
+ * The name must match an existing column.
+ *
+ * @param string $table name of the table
+ * @param string $columnName name of the column to drop
+ *
+ * @return boolean success flag
+ */
+
+ public function dropColumn($table, $columnName)
+ {
+ $sql = "ALTER TABLE $table DROP COLUMN $columnName";
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Ensures that a table exists with the given
+ * name and the given column definitions.
+ *
+ * If the table does not yet exist, it will
+ * create the table. If it does exist, it will
+ * alter the table to match the column definitions.
+ *
+ * @param string $tableName name of the table
+ * @param array $columns array of ColumnDef
+ * objects for the table
+ *
+ * @return boolean success flag
+ */
+
+ public function ensureTable($tableName, $columns)
+ {
+ // XXX: DB engine portability -> toilet
+
+ try {
+ $td = $this->getTableDef($tableName);
+ } catch (Exception $e) {
+ if (preg_match('/no such table/', $e->getMessage())) {
+ return $this->createTable($tableName, $columns);
+ } else {
+ throw $e;
+ }
+ }
+
+ $cur = $this->_names($td->columns);
+ $new = $this->_names($columns);
+
+ $toadd = array_diff($new, $cur);
+ $todrop = array_diff($cur, $new);
+ $same = array_intersect($new, $cur);
+ $tomod = array();
+
+ foreach ($same as $m) {
+ $curCol = $this->_byName($td->columns, $m);
+ $newCol = $this->_byName($columns, $m);
+
+ if (!$newCol->equals($curCol)) {
+ $tomod[] = $newCol->name;
+ }
+ }
+
+ if (count($toadd) + count($todrop) + count($tomod) == 0) {
+ // nothing to do
+ return true;
+ }
+
+ // For efficiency, we want this all in one
+ // query, instead of using our methods.
+
+ $phrase = array();
+
+ foreach ($toadd as $columnName) {
+ $cd = $this->_byName($columns, $columnName);
+
+ $phrase[] = 'ADD COLUMN ' . $this->_columnSql($cd);
+ }
+
+ foreach ($todrop as $columnName) {
+ $phrase[] = 'DROP COLUMN ' . $columnName;
+ }
+
+ foreach ($tomod as $columnName) {
+ $cd = $this->_byName($columns, $columnName);
+
+ $phrase[] = 'MODIFY COLUMN ' . $this->_columnSql($cd);
+ }
+
+ $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
+
+ $res = $this->conn->query($sql);
+
+ if (PEAR::isError($res)) {
+ throw new Exception($res->getMessage());
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the array of names from an array of
+ * ColumnDef objects.
+ *
+ * @param array $cds array of ColumnDef objects
+ *
+ * @return array strings for name values
+ */
+
+ private function _names($cds)
+ {
+ $names = array();
+
+ foreach ($cds as $cd) {
+ $names[] = $cd->name;
+ }
+
+ return $names;
+ }
+
+ /**
+ * Get a ColumnDef from an array matching
+ * name.
+ *
+ * @param array $cds Array of ColumnDef objects
+ * @param string $name Name of the column
+ *
+ * @return ColumnDef matching item or null if no match.
+ */
+
+ private function _byName($cds, $name)
+ {
+ foreach ($cds as $cd) {
+ if ($cd->name == $name) {
+ return $cd;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the proper SQL for creating or
+ * altering a column.
+ *
+ * Appropriate for use in CREATE TABLE or
+ * ALTER TABLE statements.
+ *
+ * @param ColumnDef $cd column to create
+ *
+ * @return string correct SQL for that column
+ */
+
+ private function _columnSql($cd)
+ {
+ $sql = "{$cd->name} ";
+
+ if (!empty($cd->size)) {
+ $sql .= "{$cd->type}({$cd->size}) ";
+ } else {
+ $sql .= "{$cd->type} ";
+ }
+
+ if (!empty($cd->default)) {
+ $sql .= "default {$cd->default} ";
+ } else {
+ $sql .= ($cd->nullable) ? "null " : "not null ";
+ }
+
+ if (!empty($cd->auto_increment)) {
+ $sql .= " auto_increment ";
+ }
+
+ if (!empty($cd->extra)) {
+ $sql .= "{$cd->extra} ";
+ }
+
+ return $sql;
+ }
+}
diff --git a/lib/profilelist.php b/lib/profilelist.php
index 3412d41d1..693cd6449 100644
--- a/lib/profilelist.php
+++ b/lib/profilelist.php
@@ -191,6 +191,7 @@ class ProfileListItem extends Widget
'alt' =>
($this->profile->fullname) ? $this->profile->fullname :
$this->profile->nickname));
+ $this->out->text(' ');
$hasFN = (!empty($this->profile->fullname)) ? 'nickname' : 'fn nickname';
$this->out->elementStart('span', $hasFN);
$this->out->raw($this->highlight($this->profile->nickname));
@@ -201,6 +202,7 @@ class ProfileListItem extends Widget
function showFullName()
{
if (!empty($this->profile->fullname)) {
+ $this->out->text(' ');
$this->out->elementStart('span', 'fn');
$this->out->raw($this->highlight($this->profile->fullname));
$this->out->elementEnd('span');
@@ -210,6 +212,7 @@ class ProfileListItem extends Widget
function showLocation()
{
if (!empty($this->profile->location)) {
+ $this->out->text(' ');
$this->out->elementStart('span', 'location');
$this->out->raw($this->highlight($this->profile->location));
$this->out->elementEnd('span');
@@ -219,6 +222,7 @@ class ProfileListItem extends Widget
function showHomepage()
{
if (!empty($this->profile->homepage)) {
+ $this->out->text(' ');
$this->out->elementStart('a', array('href' => $this->profile->homepage,
'class' => 'url'));
$this->out->raw($this->highlight($this->profile->homepage));
diff --git a/lib/profilesection.php b/lib/profilesection.php
index 504b1b7f7..a9482cd63 100644
--- a/lib/profilesection.php
+++ b/lib/profilesection.php
@@ -85,6 +85,7 @@ class ProfileSection extends Section
'href' => $profile->profileurl,
'rel' => 'contact member',
'class' => 'url'));
+ $this->out->text(' ');
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
$this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
'width' => AVATAR_MINI_SIZE,
@@ -93,6 +94,7 @@ class ProfileSection extends Section
'alt' => ($profile->fullname) ?
$profile->fullname :
$profile->nickname));
+ $this->out->text(' ');
$this->out->element('span', 'fn nickname', $profile->nickname);
$this->out->elementEnd('a');
$this->out->elementEnd('span');
diff --git a/lib/right.php b/lib/right.php
index 5e66eae0e..4e9c5a918 100644
--- a/lib/right.php
+++ b/lib/right.php
@@ -57,5 +57,6 @@ class Right
const EMAILONREPLY = 'emailonreply';
const EMAILONSUBSCRIBE = 'emailonsubscribe';
const EMAILONFAVE = 'emailonfave';
+ const MAKEGROUPADMIN = 'makegroupadmin';
}
diff --git a/lib/router.php b/lib/router.php
index 5981ef5d7..987d0152e 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -712,6 +712,10 @@ class Router
'nickname' => $nickname),
array('tag' => '[a-zA-Z0-9]+'));
+ $m->connect('rsd.xml',
+ array('action' => 'rsd',
+ 'nickname' => $nickname));
+
$m->connect('',
array('action' => 'showstream',
'nickname' => $nickname));
@@ -726,6 +730,7 @@ class Router
$m->connect('featured', array('action' => 'featured'));
$m->connect('favorited/', array('action' => 'favorited'));
$m->connect('favorited', array('action' => 'favorited'));
+ $m->connect('rsd.xml', array('action' => 'rsd'));
foreach (array('subscriptions', 'subscribers',
'nudge', 'all', 'foaf', 'xrds',
@@ -773,6 +778,10 @@ class Router
array('nickname' => '[a-zA-Z0-9]{1,64}'),
array('tag' => '[a-zA-Z0-9]+'));
+ $m->connect(':nickname/rsd.xml',
+ array('action' => 'rsd'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
$m->connect(':nickname',
array('action' => 'showstream'),
array('nickname' => '[a-zA-Z0-9]{1,64}'));
diff --git a/lib/schema.php b/lib/schema.php
index a7f64ebed..137b814e0 100644
--- a/lib/schema.php
+++ b/lib/schema.php
@@ -75,65 +75,15 @@ class Schema
static function get()
{
+ $type = common_config('db', 'type');
if (empty(self::$_single)) {
- self::$_single = new Schema();
+ $schemaClass = ucfirst($type).'Schema';
+ self::$_single = new $schemaClass();
}
return self::$_single;
}
/**
- * Returns a TableDef object for the table
- * in the schema with the given name.
- *
- * Throws an exception if the table is not found.
- *
- * @param string $name Name of the table to get
- *
- * @return TableDef tabledef for that table.
- */
-
- public function getTableDef($name)
- {
- $res = $this->conn->query('DESCRIBE ' . $name);
-
- if (PEAR::isError($res)) {
- throw new Exception($res->getMessage());
- }
-
- $td = new TableDef();
-
- $td->name = $name;
- $td->columns = array();
-
- $row = array();
-
- while ($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
-
- $cd = new ColumnDef();
-
- $cd->name = $row['Field'];
-
- $packed = $row['Type'];
-
- if (preg_match('/^(\w+)\((\d+)\)$/', $packed, $match)) {
- $cd->type = $match[1];
- $cd->size = $match[2];
- } else {
- $cd->type = $packed;
- }
-
- $cd->nullable = ($row['Null'] == 'YES') ? true : false;
- $cd->key = $row['Key'];
- $cd->default = $row['Default'];
- $cd->extra = $row['Extra'];
-
- $td->columns[] = $cd;
- }
-
- return $td;
- }
-
- /**
* Gets a ColumnDef object for a single column.
*
* Throws an exception if the table is not found.
@@ -523,7 +473,7 @@ class Schema
} else {
$sql .= ($cd->nullable) ? "null " : "not null ";
}
-
+
if (!empty($cd->auto_increment)) {
$sql .= " auto_increment ";
}
diff --git a/lib/statusnet.php b/lib/statusnet.php
index 257bd861d..7c4df84b4 100644
--- a/lib/statusnet.php
+++ b/lib/statusnet.php
@@ -30,6 +30,7 @@ global $config, $_server, $_path;
class StatusNet
{
protected static $have_config;
+ protected static $is_api;
/**
* Configure and instantiate a plugin into the current configuration.
@@ -201,6 +202,16 @@ class StatusNet
return self::$have_config;
}
+ public function isApi()
+ {
+ return self::$is_api;
+ }
+
+ public function setApi($mode)
+ {
+ self::$is_api = $mode;
+ }
+
/**
* Build default configuration array
* @return array
diff --git a/lib/subs.php b/lib/subs.php
index 5ac1a75a5..1c240c475 100644
--- a/lib/subs.php
+++ b/lib/subs.php
@@ -19,24 +19,6 @@
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-require_once('XMPPHP/XMPP.php');
-
-/* Subscribe $user to nickname $other_nickname
- Returns true or an error message.
-*/
-
-function subs_subscribe_user($user, $other_nickname)
-{
-
- $other = User::staticGet('nickname', $other_nickname);
-
- if (!$other) {
- return _('No such user.');
- }
-
- return subs_subscribe_to($user, $other);
-}
-
/* Subscribe user $user to other user $other.
* Note: $other must be a local user, not a remote profile.
* Because the other way is quite a bit more complicated.
@@ -44,136 +26,20 @@ function subs_subscribe_user($user, $other_nickname)
function subs_subscribe_to($user, $other)
{
- if (!$user->hasRight(Right::SUBSCRIBE)) {
- return _('You have been banned from subscribing.');
- }
-
- if ($user->isSubscribed($other)) {
- return _('Already subscribed!');
- }
-
- if ($other->hasBlocked($user)) {
- return _('User has blocked you.');
- }
-
try {
- if (Event::handle('StartSubscribe', array($user, $other))) {
-
- if (!$user->subscribeTo($other)) {
- return _('Could not subscribe.');
- return;
- }
-
- subs_notify($other, $user);
-
- $cache = common_memcache();
-
- if ($cache) {
- $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
- }
-
- $profile = $user->getProfile();
-
- $profile->blowSubscriptionsCount();
- $other->blowSubscribersCount();
-
- if ($other->autosubscribe && !$other->isSubscribed($user) && !$user->hasBlocked($other)) {
- if (!$other->subscribeTo($user)) {
- return _('Could not subscribe other to you.');
- }
- $cache = common_memcache();
-
- if ($cache) {
- $cache->delete(common_cache_key('user:notices_with_friends:' . $other->id));
- }
-
- subs_notify($user, $other);
- }
-
- Event::handle('EndSubscribe', array($user, $other));
- }
+ Subscription::start($user->getProfile(), $other);
+ return true;
} catch (Exception $e) {
return $e->getMessage();
}
-
- return true;
-}
-
-function subs_notify($listenee, $listener)
-{
- # XXX: add other notifications (Jabber, SMS) here
- # XXX: queue this and handle it offline
- # XXX: Whatever happens, do it in Twitter-like API, too
- subs_notify_email($listenee, $listener);
-}
-
-function subs_notify_email($listenee, $listener)
-{
- mail_subscribe_notify($listenee, $listener);
-}
-
-/* Unsubscribe $user from nickname $other_nickname
- Returns true or an error message.
-*/
-
-function subs_unsubscribe_user($user, $other_nickname)
-{
-
- $other = User::staticGet('nickname', $other_nickname);
-
- if (!$other) {
- return _('No such user.');
- }
-
- return subs_unsubscribe_to($user, $other->getProfile());
}
-/* Unsubscribe user $user from profile $other
- * NB: other can be a remote user. */
-
function subs_unsubscribe_to($user, $other)
{
- if (!$user->isSubscribed($other))
- return _('Not subscribed!');
-
- // Don't allow deleting self subs
-
- if ($user->id == $other->id) {
- return _('Couldn\'t delete self-subscription.');
- }
-
try {
- if (Event::handle('StartUnsubscribe', array($user, $other))) {
-
- $sub = DB_DataObject::factory('subscription');
-
- $sub->subscriber = $user->id;
- $sub->subscribed = $other->id;
-
- $sub->find(true);
-
- // note we checked for existence above
-
- if (!$sub->delete())
- return _('Couldn\'t delete subscription.');
-
- $cache = common_memcache();
-
- if ($cache) {
- $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
- }
-
- $profile = $user->getProfile();
-
- $profile->blowSubscriptionsCount();
- $other->blowSubscribersCount();
-
- Event::handle('EndUnsubscribe', array($user, $other));
- }
+ Subscription::cancel($user->getProfile(), $other);
+ return true;
} catch (Exception $e) {
return $e->getMessage();
}
-
- return true;
-}
-
+} \ No newline at end of file
diff --git a/lib/taguri.php b/lib/taguri.php
new file mode 100644
index 000000000..d8398eded
--- /dev/null
+++ b/lib/taguri.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Utility for creating new tag: URIs
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category URI
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Mint tag: URIs
+ *
+ * tag: URIs are unique identifiers according to http://tools.ietf.org/html/rfc4151.
+ *
+ * We use them for creating URIs for things that can't be HTTP retrieved.
+ *
+ * @category URI
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class TagURI
+{
+ /**
+ * Return the base part of a tag URI
+ *
+ * Note: use mint() instead.
+ *
+ * @return string Tag URI base to use
+ */
+
+ static function base()
+ {
+ $base = common_config('integration', 'taguri');
+
+ if (empty($base)) {
+
+ $base = common_config('site', 'server').','.date('Y-m-d');
+
+ $pathPart = trim(common_config('site', 'path'), '/');
+
+ if (!empty($pathPart)) {
+ $base .= ':'.str_replace('/', ':', $pathPart);
+ }
+ }
+
+ return $base;
+ }
+
+ /**
+ * Make a new tag URI
+ *
+ * Builds the proper base and creates all the parts
+ *
+ * @return string minted URI
+ */
+
+ static function mint()
+ {
+ $base = self::base();
+
+ $args = func_get_args();
+
+ $format = array_shift($args);
+
+ $extra = vsprintf($format, $args);
+
+ return 'tag:'.$base.':'.$extra;
+ }
+}
diff --git a/lib/theme.php b/lib/theme.php
index 020ce1ac4..0be8c3b9d 100644
--- a/lib/theme.php
+++ b/lib/theme.php
@@ -110,9 +110,20 @@ class Theme
$server = common_config('site', 'server');
}
- // XXX: protocol
+ $ssl = common_config('theme', 'ssl');
+
+ if (is_null($ssl)) { // null -> guess
+ if (common_config('site', 'ssl') == 'always' &&
+ !common_config('theme', 'server')) {
+ $ssl = true;
+ } else {
+ $ssl = false;
+ }
+ }
+
+ $protocol = ($ssl) ? 'https' : 'http';
- $this->path = 'http://'.$server.$path.$name;
+ $this->path = $protocol . '://'.$server.$path.$name;
}
}
diff --git a/lib/userprofile.php b/lib/userprofile.php
index 07e575085..43dfd05be 100644
--- a/lib/userprofile.php
+++ b/lib/userprofile.php
@@ -238,9 +238,12 @@ class UserProfile extends Widget
if (Event::handle('StartProfilePageActionsElements', array(&$this->out, $this->profile))) {
if (empty($cur)) { // not logged in
- $this->out->elementStart('li', 'entity_subscribe');
- $this->showRemoteSubscribeLink();
- $this->out->elementEnd('li');
+ if (Event::handle('StartProfileRemoteSubscribe', array(&$this->out, $this->profile))) {
+ $this->out->elementStart('li', 'entity_subscribe');
+ $this->showRemoteSubscribeLink();
+ $this->out->elementEnd('li');
+ Event::handle('EndProfileRemoteSubscribe', array(&$this->out, $this->profile));
+ }
} else {
if ($cur->id == $this->profile->id) { // your own page
$this->out->elementStart('li', 'entity_edit');
diff --git a/lib/util.php b/lib/util.php
index dd8189a58..7fb2c6c4b 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -367,7 +367,8 @@ function common_current_user()
if ($_cur === false) {
- if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
+ if (isset($_COOKIE[session_name()]) || isset($_GET[session_name()])
+ || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
common_ensure_session();
$id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false;
if ($id) {
@@ -425,13 +426,148 @@ function common_render_content($text, $notice)
{
$r = common_render_text($text);
$id = $notice->profile_id;
- $r = preg_replace('/(^|\s+)@(['.NICKNAME_FMT.']{1,64})/e', "'\\1@'.common_at_link($id, '\\2')", $r);
- $r = preg_replace('/^T ([A-Z0-9]{1,64}) /e', "'T '.common_at_link($id, '\\1').' '", $r);
- $r = preg_replace('/(^|[\s\.\,\:\;]+)@#([A-Za-z0-9]{1,64})/e', "'\\1@#'.common_at_hash_link($id, '\\2')", $r);
+ $r = common_linkify_mentions($id, $r);
$r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r);
return $r;
}
+function common_linkify_mentions($profile_id, $text)
+{
+ $mentions = common_find_mentions($profile_id, $text);
+
+ // We need to go through in reverse order by position,
+ // so our positions stay valid despite our fudging with the
+ // string!
+
+ $points = array();
+
+ foreach ($mentions as $mention)
+ {
+ $points[$mention['position']] = $mention;
+ }
+
+ krsort($points);
+
+ foreach ($points as $position => $mention) {
+
+ $linkText = common_linkify_mention($mention);
+
+ $text = substr_replace($text, $linkText, $position, mb_strlen($mention['text']));
+ }
+
+ return $text;
+}
+
+function common_linkify_mention($mention)
+{
+ $output = null;
+
+ if (Event::handle('StartLinkifyMention', array($mention, &$output))) {
+
+ $xs = new XMLStringer(false);
+
+ $attrs = array('href' => $mention['url'],
+ 'class' => 'url');
+
+ if (!empty($mention['title'])) {
+ $attrs['title'] = $mention['title'];
+ }
+
+ $xs->elementStart('span', 'vcard');
+ $xs->elementStart('a', $attrs);
+ $xs->element('span', 'fn nickname', $mention['text']);
+ $xs->elementEnd('a');
+ $xs->elementEnd('span');
+
+ $output = $xs->getString();
+
+ Event::handle('EndLinkifyMention', array($mention, &$output));
+ }
+
+ return $output;
+}
+
+function common_find_mentions($profile_id, $text)
+{
+ $mentions = array();
+
+ $sender = Profile::staticGet('id', $profile_id);
+
+ if (empty($sender)) {
+ return $mentions;
+ }
+
+ if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) {
+
+ preg_match_all('/^T ([A-Z0-9]{1,64}) /',
+ $text,
+ $tmatches,
+ PREG_OFFSET_CAPTURE);
+
+ preg_match_all('/(?:^|\s+)@(['.NICKNAME_FMT.']{1,64})/',
+ $text,
+ $atmatches,
+ PREG_OFFSET_CAPTURE);
+
+ $matches = array_merge($tmatches[1], $atmatches[1]);
+
+ foreach ($matches as $match) {
+
+ $nickname = common_canonical_nickname($match[0]);
+ $mentioned = common_relative_profile($sender, $nickname);
+
+ if (!empty($mentioned)) {
+
+ $user = User::staticGet('id', $mentioned->id);
+
+ if ($user) {
+ $url = common_local_url('userbyid', array('id' => $user->id));
+ } else {
+ $url = $mentioned->profileurl;
+ }
+
+ $mention = array('mentioned' => array($mentioned),
+ 'text' => $match[0],
+ 'position' => $match[1],
+ 'url' => $url);
+
+ if (!empty($mentioned->fullname)) {
+ $mention['title'] = $mentioned->fullname;
+ }
+
+ $mentions[] = $mention;
+ }
+ }
+
+ // @#tag => mention of all subscriptions tagged 'tag'
+
+ preg_match_all('/(?:^|[\s\.\,\:\;]+)@#([\pL\pN_\-\.]{1,64})/',
+ $text,
+ $hmatches,
+ PREG_OFFSET_CAPTURE);
+
+ foreach ($hmatches[1] as $hmatch) {
+
+ $tag = common_canonical_tag($hmatch[0]);
+
+ $tagged = Profile_tag::getTagged($sender->id, $tag);
+
+ $url = common_local_url('subscriptions',
+ array('nickname' => $sender->nickname,
+ 'tag' => $tag));
+
+ $mentions[] = array('mentioned' => $tagged,
+ 'text' => $hmatch[0],
+ 'position' => $hmatch[1],
+ 'url' => $url);
+ }
+
+ Event::handle('EndFindMentions', array($sender, $text, &$mentions));
+ }
+
+ return $mentions;
+}
+
function common_render_text($text)
{
$r = htmlspecialchars($text);
@@ -662,39 +798,11 @@ function common_valid_profile_tag($str)
return preg_match('/^[A-Za-z0-9_\-\.]{1,64}$/', $str);
}
-function common_at_link($sender_id, $nickname)
-{
- $sender = Profile::staticGet($sender_id);
- $recipient = common_relative_profile($sender, common_canonical_nickname($nickname));
- if ($recipient) {
- $user = User::staticGet('id', $recipient->id);
- if ($user) {
- $url = common_local_url('userbyid', array('id' => $user->id));
- } else {
- $url = $recipient->profileurl;
- }
- $xs = new XMLStringer(false);
- $attrs = array('href' => $url,
- 'class' => 'url');
- if (!empty($recipient->fullname)) {
- $attrs['title'] = $recipient->fullname . ' (' . $recipient->nickname . ')';
- }
- $xs->elementStart('span', 'vcard');
- $xs->elementStart('a', $attrs);
- $xs->element('span', 'fn nickname', $nickname);
- $xs->elementEnd('a');
- $xs->elementEnd('span');
- return $xs->getString();
- } else {
- return $nickname;
- }
-}
-
function common_group_link($sender_id, $nickname)
{
$sender = Profile::staticGet($sender_id);
$group = User_group::getForNickname($nickname);
- if ($group && $sender->isMember($group)) {
+ if ($sender && $group && $sender->isMember($group)) {
$attrs = array('href' => $group->permalink(),
'class' => 'url');
if (!empty($group->fullname)) {
@@ -712,29 +820,6 @@ function common_group_link($sender_id, $nickname)
}
}
-function common_at_hash_link($sender_id, $tag)
-{
- $user = User::staticGet($sender_id);
- if (!$user) {
- return $tag;
- }
- $tagged = Profile_tag::getTagged($user->id, common_canonical_tag($tag));
- if ($tagged) {
- $url = common_local_url('subscriptions',
- array('nickname' => $user->nickname,
- 'tag' => $tag));
- $xs = new XMLStringer();
- $xs->elementStart('span', 'tag');
- $xs->element('a', array('href' => $url,
- 'rel' => $tag),
- $tag);
- $xs->elementEnd('span');
- return $xs->getString();
- } else {
- return $tag;
- }
-}
-
function common_relative_profile($sender, $nickname, $dt=null)
{
// Try to find profiles this profile is subscribed to that have this nickname
@@ -996,9 +1081,13 @@ function common_enqueue_notice($notice)
static $localTransports = array('omb',
'ping');
- static $allTransports = array('sms', 'plugin');
-
- $transports = $allTransports;
+ $transports = array();
+ if (common_config('sms', 'enabled')) {
+ $transports[] = 'sms';
+ }
+ if (Event::hasHandler('HandleQueuedNotice')) {
+ $transports[] = 'plugin';
+ }
$xmpp = common_config('xmpp', 'enabled');
@@ -1006,6 +1095,7 @@ function common_enqueue_notice($notice)
$transports[] = 'jabber';
}
+ // @fixme move these checks into QueueManager and/or individual handlers
if ($notice->is_local == Notice::LOCAL_PUBLIC ||
$notice->is_local == Notice::LOCAL_NONPUBLIC) {
$transports = array_merge($transports, $localTransports);
@@ -1564,3 +1654,56 @@ function common_client_ip()
return array($proxy, $ip);
}
+
+function common_url_to_nickname($url)
+{
+ static $bad = array('query', 'user', 'password', 'port', 'fragment');
+
+ $parts = parse_url($url);
+
+ # If any of these parts exist, this won't work
+
+ foreach ($bad as $badpart) {
+ if (array_key_exists($badpart, $parts)) {
+ return null;
+ }
+ }
+
+ # We just have host and/or path
+
+ # If it's just a host...
+ if (array_key_exists('host', $parts) &&
+ (!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0))
+ {
+ $hostparts = explode('.', $parts['host']);
+
+ # Try to catch common idiom of nickname.service.tld
+
+ if ((count($hostparts) > 2) &&
+ (strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au
+ (strcmp($hostparts[0], 'www') != 0))
+ {
+ return common_nicknamize($hostparts[0]);
+ } else {
+ # Do the whole hostname
+ return common_nicknamize($parts['host']);
+ }
+ } else {
+ if (array_key_exists('path', $parts)) {
+ # Strip starting, ending slashes
+ $path = preg_replace('@/$@', '', $parts['path']);
+ $path = preg_replace('@^/@', '', $path);
+ if (strpos($path, '/') === false) {
+ return common_nicknamize($path);
+ }
+ }
+ }
+
+ return null;
+}
+
+function common_nicknamize($str)
+{
+ $str = preg_replace('/\W/', '', $str);
+ return strtolower($str);
+}
diff --git a/plugins/FeedSub/FeedSubPlugin.php b/plugins/FeedSub/FeedSubPlugin.php
deleted file mode 100644
index e49e2a648..000000000
--- a/plugins/FeedSub/FeedSubPlugin.php
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php
-/*
-StatusNet Plugin: 0.9
-Plugin Name: FeedSub
-Plugin URI: http://status.net/wiki/Feed_subscription
-Description: FeedSub allows subscribing to real-time updates from external feeds supporting PubHubSubbub protocol.
-Version: 0.1
-Author: Brion Vibber <brion@status.net>
-Author URI: http://status.net/
-*/
-
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-define('FEEDSUB_SERVICE', 100); // fixme -- avoid hardcoding these?
-
-// We bundle the XML_Parse_Feed library...
-set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib');
-
-class FeedSubException extends Exception
-{
-}
-
-class FeedSubPlugin extends Plugin
-{
- /**
- * Hook for RouterInitialized event.
- *
- * @param Net_URL_Mapper $m path-to-action mapper
- * @return boolean hook return
- */
- function onRouterInitialized($m)
- {
- $m->connect('feedsub/callback/:feed',
- array('action' => 'feedsubcallback'),
- array('feed' => '[0-9]+'));
- $m->connect('settings/feedsub',
- array('action' => 'feedsubsettings'));
- return true;
- }
-
- /**
- * Add the feed settings page to the Connect Settings menu
- *
- * @param Action &$action The calling page
- *
- * @return boolean hook return
- */
- function onEndConnectSettingsNav(&$action)
- {
- $action_name = $action->trimmed('action');
-
- $action->menuItem(common_local_url('feedsubsettings'),
- _m('Feeds'),
- _m('Feed subscription options'),
- $action_name === 'feedsubsettings');
-
- return true;
- }
-
- /**
- * Automatically load the actions and libraries used by the plugin
- *
- * @param Class $cls the class
- *
- * @return boolean hook return
- *
- */
- function onAutoload($cls)
- {
- $base = dirname(__FILE__);
- $lower = strtolower($cls);
- $files = array("$base/$lower.php");
- if (substr($lower, -6) == 'action') {
- $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php";
- }
- foreach ($files as $file) {
- if (file_exists($file)) {
- include_once $file;
- return false;
- }
- }
- return true;
- }
-
- function onCheckSchema() {
- // warning: the autoincrement doesn't seem to set.
- // alter table feedinfo change column id id int(11) not null auto_increment;
- $schema = Schema::get();
- $schema->ensureTable('feedinfo', Feedinfo::schemaDef());
- return true;
- }
-}
diff --git a/plugins/FeedSub/actions/feedsubcallback.php b/plugins/FeedSub/actions/feedsubcallback.php
deleted file mode 100644
index 0c4280c1f..000000000
--- a/plugins/FeedSub/actions/feedsubcallback.php
+++ /dev/null
@@ -1,100 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-
-class FeedSubCallbackAction extends Action
-{
- function handle()
- {
- parent::handle();
- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $this->handlePost();
- } else {
- $this->handleGet();
- }
- }
-
- /**
- * Handler for POST content updates from the hub
- */
- function handlePost()
- {
- $feedid = $this->arg('feed');
- common_log(LOG_INFO, "POST for feed id $feedid");
- if (!$feedid) {
- throw new ServerException('Empty or invalid feed id', 400);
- }
-
- $feedinfo = Feedinfo::staticGet('id', $feedid);
- if (!$feedinfo) {
- throw new ServerException('Unknown feed id ' . $feedid, 400);
- }
-
- $post = file_get_contents('php://input');
- $feedinfo->postUpdates($post);
- }
-
- /**
- * Handler for GET verification requests from the hub
- */
- function handleGet()
- {
- $mode = $this->arg('hub_mode');
- $topic = $this->arg('hub_topic');
- $challenge = $this->arg('hub_challenge');
- $lease_seconds = $this->arg('hub_lease_seconds');
- $verify_token = $this->arg('hub_verify_token');
-
- if ($mode != 'subscribe' && $mode != 'unsubscribe') {
- common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\"");
- throw new ServerException("Bogus hub callback: bad mode", 404);
- }
-
- $feedinfo = Feedinfo::staticGet('feeduri', $topic);
- if (!$feedinfo) {
- common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic");
- throw new ServerException("Bogus hub callback: unknown feed", 404);
- }
-
- # Can't currently set the token in our sub api
- #if ($feedinfo->verify_token !== $verify_token) {
- # common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
- # throw new ServerError("Bogus hub callback: bad token", 404);
- #}
-
- // OK!
- common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
- $feedinfo->sub_start = common_sql_date(time());
- if ($lease_seconds > 0) {
- $feedinfo->sub_end = common_sql_date(time() + $lease_seconds);
- } else {
- $feedinfo->sub_end = null;
- }
- $feedinfo->update();
-
- print $challenge;
- }
-}
diff --git a/plugins/FeedSub/extlib/README b/plugins/FeedSub/extlib/README
deleted file mode 100644
index 799b40c47..000000000
--- a/plugins/FeedSub/extlib/README
+++ /dev/null
@@ -1,9 +0,0 @@
-XML_Feed_Parser 1.0.3 is not currently actively maintained, and has
-a nasty bug which breaks getting the feed target link from WordPress
-feeds and possibly others that are RSS2-formatted but include an
-<atom:link> self-link element as well.
-
-Patch from this bug report is included:
-http://pear.php.net/bugs/bug.php?id=16416
-
-If upgrading, be sure that fix is included with the future upgrade!
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser.php b/plugins/FeedSub/extlib/XML/Feed/Parser.php
deleted file mode 100755
index ffe8220a5..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser.php
+++ /dev/null
@@ -1,351 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Key gateway class for XML_Feed_Parser package
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL
- * @version CVS: $Id: Parser.php,v 1.24 2006/08/15 13:04:00 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * XML_Feed_Parser_Type is an abstract class required by all of our
- * feed types. It makes sense to load it here to keep the other files
- * clean.
- */
-require_once 'XML/Feed/Parser/Type.php';
-
-/**
- * We will throw exceptions when errors occur.
- */
-require_once 'XML/Feed/Parser/Exception.php';
-
-/**
- * This is the core of the XML_Feed_Parser package. It identifies feed types
- * and abstracts access to them. It is an iterator, allowing for easy access
- * to the entire feed.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser implements Iterator
-{
- /**
- * This is where we hold the feed object
- * @var Object
- */
- private $feed;
-
- /**
- * To allow for extensions, we make a public reference to the feed model
- * @var DOMDocument
- */
- public $model;
-
- /**
- * A map between entry ID and offset
- * @var array
- */
- protected $idMappings = array();
-
- /**
- * A storage space for Namespace URIs.
- * @var array
- */
- private $feedNamespaces = array(
- 'rss2' => array(
- 'http://backend.userland.com/rss',
- 'http://backend.userland.com/rss2',
- 'http://blogs.law.harvard.edu/tech/rss'));
- /**
- * Detects feed types and instantiate appropriate objects.
- *
- * Our constructor takes care of detecting feed types and instantiating
- * appropriate classes. For now we're going to treat Atom 0.3 as Atom 1.0
- * but raise a warning. I do not intend to introduce full support for
- * Atom 0.3 as it has been deprecated, but others are welcome to.
- *
- * @param string $feed XML serialization of the feed
- * @param bool $strict Whether or not to validate the feed
- * @param bool $suppressWarnings Trigger errors for deprecated feed types?
- * @param bool $tidy Whether or not to try and use the tidy library on input
- */
- function __construct($feed, $strict = false, $suppressWarnings = false, $tidy = false)
- {
- $this->model = new DOMDocument;
- if (! $this->model->loadXML($feed)) {
- if (extension_loaded('tidy') && $tidy) {
- $tidy = new tidy;
- $tidy->parseString($feed,
- array('input-xml' => true, 'output-xml' => true));
- $tidy->cleanRepair();
- if (! $this->model->loadXML((string) $tidy)) {
- throw new XML_Feed_Parser_Exception('Invalid input: this is not ' .
- 'valid XML');
- }
- } else {
- throw new XML_Feed_Parser_Exception('Invalid input: this is not valid XML');
- }
-
- }
-
- /* detect feed type */
- $doc_element = $this->model->documentElement;
- $error = false;
-
- switch (true) {
- case ($doc_element->namespaceURI == 'http://www.w3.org/2005/Atom'):
- require_once 'XML/Feed/Parser/Atom.php';
- require_once 'XML/Feed/Parser/AtomElement.php';
- $class = 'XML_Feed_Parser_Atom';
- break;
- case ($doc_element->namespaceURI == 'http://purl.org/atom/ns#'):
- require_once 'XML/Feed/Parser/Atom.php';
- require_once 'XML/Feed/Parser/AtomElement.php';
- $class = 'XML_Feed_Parser_Atom';
- $error = 'Atom 0.3 deprecated, using 1.0 parser which won\'t provide ' .
- 'all options';
- break;
- case ($doc_element->namespaceURI == 'http://purl.org/rss/1.0/' ||
- ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1
- && $doc_element->childNodes->item(1)->namespaceURI ==
- 'http://purl.org/rss/1.0/')):
- require_once 'XML/Feed/Parser/RSS1.php';
- require_once 'XML/Feed/Parser/RSS1Element.php';
- $class = 'XML_Feed_Parser_RSS1';
- break;
- case ($doc_element->namespaceURI == 'http://purl.org/rss/1.1/' ||
- ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1
- && $doc_element->childNodes->item(1)->namespaceURI ==
- 'http://purl.org/rss/1.1/')):
- require_once 'XML/Feed/Parser/RSS11.php';
- require_once 'XML/Feed/Parser/RSS11Element.php';
- $class = 'XML_Feed_Parser_RSS11';
- break;
- case (($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1
- && $doc_element->childNodes->item(1)->namespaceURI ==
- 'http://my.netscape.com/rdf/simple/0.9/') ||
- $doc_element->namespaceURI == 'http://my.netscape.com/rdf/simple/0.9/'):
- require_once 'XML/Feed/Parser/RSS09.php';
- require_once 'XML/Feed/Parser/RSS09Element.php';
- $class = 'XML_Feed_Parser_RSS09';
- break;
- case ($doc_element->tagName == 'rss' and
- $doc_element->hasAttribute('version') &&
- $doc_element->getAttribute('version') == 0.91):
- $error = 'RSS 0.91 has been superceded by RSS2.0. Using RSS2.0 parser.';
- require_once 'XML/Feed/Parser/RSS2.php';
- require_once 'XML/Feed/Parser/RSS2Element.php';
- $class = 'XML_Feed_Parser_RSS2';
- break;
- case ($doc_element->tagName == 'rss' and
- $doc_element->hasAttribute('version') &&
- $doc_element->getAttribute('version') == 0.92):
- $error = 'RSS 0.92 has been superceded by RSS2.0. Using RSS2.0 parser.';
- require_once 'XML/Feed/Parser/RSS2.php';
- require_once 'XML/Feed/Parser/RSS2Element.php';
- $class = 'XML_Feed_Parser_RSS2';
- break;
- case (in_array($doc_element->namespaceURI, $this->feedNamespaces['rss2'])
- || $doc_element->tagName == 'rss'):
- if (! $doc_element->hasAttribute('version') ||
- $doc_element->getAttribute('version') != 2) {
- $error = 'RSS version not specified. Parsing as RSS2.0';
- }
- require_once 'XML/Feed/Parser/RSS2.php';
- require_once 'XML/Feed/Parser/RSS2Element.php';
- $class = 'XML_Feed_Parser_RSS2';
- break;
- default:
- throw new XML_Feed_Parser_Exception('Feed type unknown');
- break;
- }
-
- if (! $suppressWarnings && ! empty($error)) {
- trigger_error($error, E_USER_WARNING);
- }
-
- /* Instantiate feed object */
- $this->feed = new $class($this->model, $strict);
- }
-
- /**
- * Proxy to allow feed element names to be used as method names
- *
- * For top-level feed elements we will provide access using methods or
- * attributes. This function simply passes on a request to the appropriate
- * feed type object.
- *
- * @param string $call - the method being called
- * @param array $attributes
- */
- function __call($call, $attributes)
- {
- $attributes = array_pad($attributes, 5, false);
- list($a, $b, $c, $d, $e) = $attributes;
- return $this->feed->$call($a, $b, $c, $d, $e);
- }
-
- /**
- * Proxy to allow feed element names to be used as attribute names
- *
- * To allow variable-like access to feed-level data we use this
- * method. It simply passes along to __call() which in turn passes
- * along to the relevant object.
- *
- * @param string $val - the name of the variable required
- */
- function __get($val)
- {
- return $this->feed->$val;
- }
-
- /**
- * Provides iteration functionality.
- *
- * Of course we must be able to iterate... This function simply increases
- * our internal counter.
- */
- function next()
- {
- if (isset($this->current_item) &&
- $this->current_item <= $this->feed->numberEntries - 1) {
- ++$this->current_item;
- } else if (! isset($this->current_item)) {
- $this->current_item = 0;
- } else {
- return false;
- }
- }
-
- /**
- * Return XML_Feed_Type object for current element
- *
- * @return XML_Feed_Parser_Type Object
- */
- function current()
- {
- return $this->getEntryByOffset($this->current_item);
- }
-
- /**
- * For iteration -- returns the key for the current stage in the array.
- *
- * @return int
- */
- function key()
- {
- return $this->current_item;
- }
-
- /**
- * For iteration -- tells whether we have reached the
- * end.
- *
- * @return bool
- */
- function valid()
- {
- return $this->current_item < $this->feed->numberEntries;
- }
-
- /**
- * For iteration -- resets the internal counter to the beginning.
- */
- function rewind()
- {
- $this->current_item = 0;
- }
-
- /**
- * Provides access to entries by ID if one is specified in the source feed.
- *
- * As well as allowing the items to be iterated over we want to allow
- * users to be able to access a specific entry. This is one of two ways of
- * doing that, the other being by offset. This method can be quite slow
- * if dealing with a large feed that hasn't yet been processed as it
- * instantiates objects for every entry until it finds the one needed.
- *
- * @param string $id Valid ID for the given feed format
- * @return XML_Feed_Parser_Type|false
- */
- function getEntryById($id)
- {
- if (isset($this->idMappings[$id])) {
- return $this->getEntryByOffset($this->idMappings[$id]);
- }
-
- /*
- * Since we have not yet encountered that ID, let's go through all the
- * remaining entries in order till we find it.
- * This is a fairly slow implementation, but it should work.
- */
- return $this->feed->getEntryById($id);
- }
-
- /**
- * Retrieve entry by numeric offset, starting from zero.
- *
- * As well as allowing the items to be iterated over we want to allow
- * users to be able to access a specific entry. This is one of two ways of
- * doing that, the other being by ID.
- *
- * @param int $offset The position of the entry within the feed, starting from 0
- * @return XML_Feed_Parser_Type|false
- */
- function getEntryByOffset($offset)
- {
- if ($offset < $this->feed->numberEntries) {
- if (isset($this->feed->entries[$offset])) {
- return $this->feed->entries[$offset];
- } else {
- try {
- $this->feed->getEntryByOffset($offset);
- } catch (Exception $e) {
- return false;
- }
- $id = $this->feed->entries[$offset]->getID();
- $this->idMappings[$id] = $offset;
- return $this->feed->entries[$offset];
- }
- } else {
- return false;
- }
- }
-
- /**
- * Retrieve version details from feed type class.
- *
- * @return void
- * @author James Stewart
- */
- function version()
- {
- return $this->feed->version;
- }
-
- /**
- * Returns a string representation of the feed.
- *
- * @return String
- **/
- function __toString()
- {
- return $this->feed->__toString();
- }
-}
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php
deleted file mode 100644
index c7e218a1e..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php
+++ /dev/null
@@ -1,365 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Atom feed class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: Atom.php,v 1.29 2008/03/30 22:00:36 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
-*/
-
-/**
- * This is the class that determines how we manage Atom 1.0 feeds
- *
- * How we deal with constructs:
- * date - return as unix datetime for use with the 'date' function unless specified otherwise
- * text - return as is. optional parameter will give access to attributes
- * person - defaults to name, but parameter based access
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_Atom extends XML_Feed_Parser_Type
-{
- /**
- * The URI of the RelaxNG schema used to (optionally) validate the feed
- * @var string
- */
- private $relax = 'atom.rnc';
-
- /**
- * We're likely to use XPath, so let's keep it global
- * @var DOMXPath
- */
- public $xpath;
-
- /**
- * When performing XPath queries we will use this prefix
- * @var string
- */
- private $xpathPrefix = '//';
-
- /**
- * The feed type we are parsing
- * @var string
- */
- public $version = 'Atom 1.0';
-
- /**
- * The class used to represent individual items
- * @var string
- */
- protected $itemClass = 'XML_Feed_Parser_AtomElement';
-
- /**
- * The element containing entries
- * @var string
- */
- protected $itemElement = 'entry';
-
- /**
- * Here we map those elements we're not going to handle individually
- * to the constructs they are. The optional second parameter in the array
- * tells the parser whether to 'fall back' (not apt. at the feed level) or
- * fail if the element is missing. If the parameter is not set, the function
- * will simply return false and leave it to the client to decide what to do.
- * @var array
- */
- protected $map = array(
- 'author' => array('Person'),
- 'contributor' => array('Person'),
- 'icon' => array('Text'),
- 'logo' => array('Text'),
- 'id' => array('Text', 'fail'),
- 'rights' => array('Text'),
- 'subtitle' => array('Text'),
- 'title' => array('Text', 'fail'),
- 'updated' => array('Date', 'fail'),
- 'link' => array('Link'),
- 'generator' => array('Text'),
- 'category' => array('Category'));
-
- /**
- * Here we provide a few mappings for those very special circumstances in
- * which it makes sense to map back to the RSS2 spec. Key is RSS2 version
- * value is an array consisting of the equivalent in atom and any attributes
- * needed to make the mapping.
- * @var array
- */
- protected $compatMap = array(
- 'guid' => array('id'),
- 'links' => array('link'),
- 'tags' => array('category'),
- 'contributors' => array('contributor'));
-
- /**
- * Our constructor does nothing more than its parent.
- *
- * @param DOMDocument $xml A DOM object representing the feed
- * @param bool (optional) $string Whether or not to validate this feed
- */
- function __construct(DOMDocument $model, $strict = false)
- {
- $this->model = $model;
-
- if ($strict) {
- if (! $this->model->relaxNGValidateSource($this->relax)) {
- throw new XML_Feed_Parser_Exception('Failed required validation');
- }
- }
-
- $this->xpath = new DOMXPath($this->model);
- $this->xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
- $this->numberEntries = $this->count('entry');
- }
-
- /**
- * Implement retrieval of an entry based on its ID for atom feeds.
- *
- * This function uses XPath to get the entry based on its ID. If DOMXPath::evaluate
- * is available, we also use that to store a reference to the entry in the array
- * used by getEntryByOffset so that method does not have to seek out the entry
- * if it's requested that way.
- *
- * @param string $id any valid Atom ID.
- * @return XML_Feed_Parser_AtomElement
- */
- function getEntryById($id)
- {
- if (isset($this->idMappings[$id])) {
- return $this->entries[$this->idMappings[$id]];
- }
-
- $entries = $this->xpath->query("//atom:entry[atom:id='$id']");
-
- if ($entries->length > 0) {
- $xmlBase = $entries->item(0)->baseURI;
- $entry = new $this->itemClass($entries->item(0), $this, $xmlBase);
-
- if (in_array('evaluate', get_class_methods($this->xpath))) {
- $offset = $this->xpath->evaluate("count(preceding-sibling::atom:entry)", $entries->item(0));
- $this->entries[$offset] = $entry;
- }
-
- $this->idMappings[$id] = $entry;
-
- return $entry;
- }
-
- }
-
- /**
- * Retrieves data from a person construct.
- *
- * Get a person construct. We default to the 'name' element but allow
- * access to any of the elements.
- *
- * @param string $method The name of the person construct we want
- * @param array $arguments An array which we hope gives a 'param'
- * @return string|false
- */
- protected function getPerson($method, $arguments)
- {
- $offset = empty($arguments[0]) ? 0 : $arguments[0];
- $parameter = empty($arguments[1]['param']) ? 'name' : $arguments[1]['param'];
- $section = $this->model->getElementsByTagName($method);
-
- if ($parameter == 'url') {
- $parameter = 'uri';
- }
-
- if ($section->length <= $offset) {
- return false;
- }
-
- $param = $section->item($offset)->getElementsByTagName($parameter);
- if ($param->length == 0) {
- return false;
- }
- return $param->item(0)->nodeValue;
- }
-
- /**
- * Retrieves an element's content where that content is a text construct.
- *
- * Get a text construct. When calling this method, the two arguments
- * allowed are 'offset' and 'attribute', so $parser->subtitle() would
- * return the content of the element, while $parser->subtitle(false, 'type')
- * would return the value of the type attribute.
- *
- * @todo Clarify overlap with getContent()
- * @param string $method The name of the text construct we want
- * @param array $arguments An array which we hope gives a 'param'
- * @return string
- */
- protected function getText($method, $arguments)
- {
- $offset = empty($arguments[0]) ? 0: $arguments[0];
- $attribute = empty($arguments[1]) ? false : $arguments[1];
- $tags = $this->model->getElementsByTagName($method);
-
- if ($tags->length <= $offset) {
- return false;
- }
-
- $content = $tags->item($offset);
-
- if (! $content->hasAttribute('type')) {
- $content->setAttribute('type', 'text');
- }
- $type = $content->getAttribute('type');
-
- if (! empty($attribute) and
- ! ($method == 'generator' and $attribute == 'name')) {
- if ($content->hasAttribute($attribute)) {
- return $content->getAttribute($attribute);
- } else if ($attribute == 'href' and $content->hasAttribute('uri')) {
- return $content->getAttribute('uri');
- }
- return false;
- }
-
- return $this->parseTextConstruct($content);
- }
-
- /**
- * Extract content appropriately from atom text constructs
- *
- * Because of different rules applied to the content element and other text
- * constructs, they are deployed as separate functions, but they share quite
- * a bit of processing. This method performs the core common process, which is
- * to apply the rules for different mime types in order to extract the content.
- *
- * @param DOMNode $content the text construct node to be parsed
- * @return String
- * @author James Stewart
- **/
- protected function parseTextConstruct(DOMNode $content)
- {
- if ($content->hasAttribute('type')) {
- $type = $content->getAttribute('type');
- } else {
- $type = 'text';
- }
-
- if (strpos($type, 'text/') === 0) {
- $type = 'text';
- }
-
- switch ($type) {
- case 'text':
- case 'html':
- return $content->textContent;
- break;
- case 'xhtml':
- $container = $content->getElementsByTagName('div');
- if ($container->length == 0) {
- return false;
- }
- $contents = $container->item(0);
- if ($contents->hasChildNodes()) {
- /* Iterate through, applying xml:base and store the result */
- $result = '';
- foreach ($contents->childNodes as $node) {
- $result .= $this->traverseNode($node);
- }
- return $result;
- }
- break;
- case preg_match('@^[a-zA-Z]+/[a-zA-Z+]*xml@i', $type) > 0:
- return $content;
- break;
- case 'application/octet-stream':
- default:
- return base64_decode(trim($content->nodeValue));
- break;
- }
- return false;
- }
- /**
- * Get a category from the entry.
- *
- * A feed or entry can have any number of categories. A category can have the
- * attributes term, scheme and label.
- *
- * @param string $method The name of the text construct we want
- * @param array $arguments An array which we hope gives a 'param'
- * @return string
- */
- function getCategory($method, $arguments)
- {
- $offset = empty($arguments[0]) ? 0: $arguments[0];
- $attribute = empty($arguments[1]) ? 'term' : $arguments[1];
- $categories = $this->model->getElementsByTagName('category');
- if ($categories->length <= $offset) {
- $category = $categories->item($offset);
- if ($category->hasAttribute($attribute)) {
- return $category->getAttribute($attribute);
- }
- }
- return false;
- }
-
- /**
- * This element must be present at least once with rel="feed". This element may be
- * present any number of further times so long as there is no clash. If no 'rel' is
- * present and we're asked for one, we follow the example of the Universal Feed
- * Parser and presume 'alternate'.
- *
- * @param int $offset the position of the link within the container
- * @param string $attribute the attribute name required
- * @param array an array of attributes to search by
- * @return string the value of the attribute
- */
- function getLink($offset = 0, $attribute = 'href', $params = false)
- {
- if (is_array($params) and !empty($params)) {
- $terms = array();
- $alt_predicate = '';
- $other_predicate = '';
-
- foreach ($params as $key => $value) {
- if ($key == 'rel' && $value == 'alternate') {
- $alt_predicate = '[not(@rel) or @rel="alternate"]';
- } else {
- $terms[] = "@$key='$value'";
- }
- }
- if (!empty($terms)) {
- $other_predicate = '[' . join(' and ', $terms) . ']';
- }
- $query = $this->xpathPrefix . 'atom:link' . $alt_predicate . $other_predicate;
- $links = $this->xpath->query($query);
- } else {
- $links = $this->model->getElementsByTagName('link');
- }
- if ($links->length > $offset) {
- if ($links->item($offset)->hasAttribute($attribute)) {
- $value = $links->item($offset)->getAttribute($attribute);
- if ($attribute == 'href') {
- $value = $this->addBase($value, $links->item($offset));
- }
- return $value;
- } else if ($attribute == 'rel') {
- return 'alternate';
- }
- }
- return false;
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php b/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php
deleted file mode 100755
index 063ecb617..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/AtomElement.php
+++ /dev/null
@@ -1,261 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * AtomElement class for XML_Feed_Parser package
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: AtomElement.php,v 1.19 2007/03/26 12:43:11 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class provides support for atom entries. It will usually be called by
- * XML_Feed_Parser_Atom with which it shares many methods.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_AtomElement extends XML_Feed_Parser_Atom
-{
- /**
- * This will be a reference to the parent object for when we want
- * to use a 'fallback' rule
- * @var XML_Feed_Parser_Atom
- */
- protected $parent;
-
- /**
- * When performing XPath queries we will use this prefix
- * @var string
- */
- private $xpathPrefix = '';
-
- /**
- * xml:base values inherited by the element
- * @var string
- */
- protected $xmlBase;
-
- /**
- * Here we provide a few mappings for those very special circumstances in
- * which it makes sense to map back to the RSS2 spec or to manage other
- * compatibilities (eg. with the Univeral Feed Parser). Key is the other version's
- * name for the command, value is an array consisting of the equivalent in our atom
- * api and any attributes needed to make the mapping.
- * @var array
- */
- protected $compatMap = array(
- 'guid' => array('id'),
- 'links' => array('link'),
- 'tags' => array('category'),
- 'contributors' => array('contributor'));
-
- /**
- * Our specific element map
- * @var array
- */
- protected $map = array(
- 'author' => array('Person', 'fallback'),
- 'contributor' => array('Person'),
- 'id' => array('Text', 'fail'),
- 'published' => array('Date'),
- 'updated' => array('Date', 'fail'),
- 'title' => array('Text', 'fail'),
- 'rights' => array('Text', 'fallback'),
- 'summary' => array('Text'),
- 'content' => array('Content'),
- 'link' => array('Link'),
- 'enclosure' => array('Enclosure'),
- 'category' => array('Category'));
-
- /**
- * Store useful information for later.
- *
- * @param DOMElement $element - this item as a DOM element
- * @param XML_Feed_Parser_Atom $parent - the feed of which this is a member
- */
- function __construct(DOMElement $element, $parent, $xmlBase = '')
- {
- $this->model = $element;
- $this->parent = $parent;
- $this->xmlBase = $xmlBase;
- $this->xpathPrefix = "//atom:entry[atom:id='" . $this->id . "']/";
- $this->xpath = $this->parent->xpath;
- }
-
- /**
- * Provides access to specific aspects of the author data for an atom entry
- *
- * Author data at the entry level is more complex than at the feed level.
- * If atom:author is not present for the entry we need to look for it in
- * an atom:source child of the atom:entry. If it's not there either, then
- * we look to the parent for data.
- *
- * @param array
- * @return string
- */
- function getAuthor($arguments)
- {
- /* Find out which part of the author data we're looking for */
- if (isset($arguments['param'])) {
- $parameter = $arguments['param'];
- } else {
- $parameter = 'name';
- }
-
- $test = $this->model->getElementsByTagName('author');
- if ($test->length > 0) {
- $item = $test->item(0);
- return $item->getElementsByTagName($parameter)->item(0)->nodeValue;
- }
-
- $source = $this->model->getElementsByTagName('source');
- if ($source->length > 0) {
- $test = $this->model->getElementsByTagName('author');
- if ($test->length > 0) {
- $item = $test->item(0);
- return $item->getElementsByTagName($parameter)->item(0)->nodeValue;
- }
- }
- return $this->parent->getAuthor($arguments);
- }
-
- /**
- * Returns the content of the content element or info on a specific attribute
- *
- * This element may or may not be present. It cannot be present more than
- * once. It may have a 'src' attribute, in which case there's no content
- * If not present, then the entry must have link with rel="alternate".
- * If there is content we return it, if not and there's a 'src' attribute
- * we return the value of that instead. The method can take an 'attribute'
- * argument, in which case we return the value of that attribute if present.
- * eg. $item->content("type") will return the type of the content. It is
- * recommended that all users check the type before getting the content to
- * ensure that their script is capable of handling the type of returned data.
- * (data carried in the content element can be either 'text', 'html', 'xhtml',
- * or any standard MIME type).
- *
- * @return string|false
- */
- protected function getContent($method, $arguments = array())
- {
- $attribute = empty($arguments[0]) ? false : $arguments[0];
- $tags = $this->model->getElementsByTagName('content');
-
- if ($tags->length == 0) {
- return false;
- }
-
- $content = $tags->item(0);
-
- if (! $content->hasAttribute('type')) {
- $content->setAttribute('type', 'text');
- }
- if (! empty($attribute)) {
- return $content->getAttribute($attribute);
- }
-
- $type = $content->getAttribute('type');
-
- if (! empty($attribute)) {
- if ($content->hasAttribute($attribute))
- {
- return $content->getAttribute($attribute);
- }
- return false;
- }
-
- if ($content->hasAttribute('src')) {
- return $content->getAttribute('src');
- }
-
- return $this->parseTextConstruct($content);
- }
-
- /**
- * For compatibility, this method provides a mapping to access enclosures.
- *
- * The Atom spec doesn't provide for an enclosure element, but it is
- * generally supported using the link element with rel='enclosure'.
- *
- * @param string $method - for compatibility with our __call usage
- * @param array $arguments - for compatibility with our __call usage
- * @return array|false
- */
- function getEnclosure($method, $arguments = array())
- {
- $offset = isset($arguments[0]) ? $arguments[0] : 0;
- $query = "//atom:entry[atom:id='" . $this->getText('id', false) .
- "']/atom:link[@rel='enclosure']";
-
- $encs = $this->parent->xpath->query($query);
- if ($encs->length > $offset) {
- try {
- if (! $encs->item($offset)->hasAttribute('href')) {
- return false;
- }
- $attrs = $encs->item($offset)->attributes;
- $length = $encs->item($offset)->hasAttribute('length') ?
- $encs->item($offset)->getAttribute('length') : false;
- return array(
- 'url' => $attrs->getNamedItem('href')->value,
- 'type' => $attrs->getNamedItem('type')->value,
- 'length' => $length);
- } catch (Exception $e) {
- return false;
- }
- }
- return false;
- }
-
- /**
- * Get details of this entry's source, if available/relevant
- *
- * Where an atom:entry is taken from another feed then the aggregator
- * is supposed to include an atom:source element which replicates at least
- * the atom:id, atom:title, and atom:updated metadata from the original
- * feed. Atom:source therefore has a very similar structure to atom:feed
- * and if we find it we will return it as an XML_Feed_Parser_Atom object.
- *
- * @return XML_Feed_Parser_Atom|false
- */
- function getSource()
- {
- $test = $this->model->getElementsByTagName('source');
- if ($test->length == 0) {
- return false;
- }
- $source = new XML_Feed_Parser_Atom($test->item(0));
- }
-
- /**
- * Get the entry as an XML string
- *
- * Return an XML serialization of the feed, should it be required. Most
- * users however, will already have a serialization that they used when
- * instantiating the object.
- *
- * @return string XML serialization of element
- */
- function __toString()
- {
- $simple = simplexml_import_dom($this->model);
- return $simple->asXML();
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php
deleted file mode 100755
index 1e76e3f85..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/Exception.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Keeps the exception class for XML_Feed_Parser.
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL
- * @version CVS: $Id: Exception.php,v 1.3 2005/11/07 01:52:35 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * We are extending PEAR_Exception
- */
-require_once 'PEAR/Exception.php';
-
-/**
- * XML_Feed_Parser_Exception is a simple extension of PEAR_Exception, existing
- * to help with identification of the source of exceptions.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_Exception extends PEAR_Exception
-{
-
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php
deleted file mode 100755
index 07f38f911..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09.php
+++ /dev/null
@@ -1,214 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS0.9 class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: RSS09.php,v 1.5 2006/07/26 21:18:46 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class handles RSS0.9 feeds.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- * @todo Find a Relax NG URI we can use
- */
-class XML_Feed_Parser_RSS09 extends XML_Feed_Parser_Type
-{
- /**
- * The URI of the RelaxNG schema used to (optionally) validate the feed
- * @var string
- */
- private $relax = '';
-
- /**
- * We're likely to use XPath, so let's keep it global
- * @var DOMXPath
- */
- protected $xpath;
-
- /**
- * The feed type we are parsing
- * @var string
- */
- public $version = 'RSS 0.9';
-
- /**
- * The class used to represent individual items
- * @var string
- */
- protected $itemClass = 'XML_Feed_Parser_RSS09Element';
-
- /**
- * The element containing entries
- * @var string
- */
- protected $itemElement = 'item';
-
- /**
- * Here we map those elements we're not going to handle individually
- * to the constructs they are. The optional second parameter in the array
- * tells the parser whether to 'fall back' (not apt. at the feed level) or
- * fail if the element is missing. If the parameter is not set, the function
- * will simply return false and leave it to the client to decide what to do.
- * @var array
- */
- protected $map = array(
- 'title' => array('Text'),
- 'link' => array('Text'),
- 'description' => array('Text'),
- 'image' => array('Image'),
- 'textinput' => array('TextInput'));
-
- /**
- * Here we map some elements to their atom equivalents. This is going to be
- * quite tricky to pull off effectively (and some users' methods may vary)
- * but is worth trying. The key is the atom version, the value is RSS2.
- * @var array
- */
- protected $compatMap = array(
- 'title' => array('title'),
- 'link' => array('link'),
- 'subtitle' => array('description'));
-
- /**
- * We will be working with multiple namespaces and it is useful to
- * keep them together
- * @var array
- */
- protected $namespaces = array(
- 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
-
- /**
- * Our constructor does nothing more than its parent.
- *
- * @todo RelaxNG validation
- * @param DOMDocument $xml A DOM object representing the feed
- * @param bool (optional) $string Whether or not to validate this feed
- */
- function __construct(DOMDocument $model, $strict = false)
- {
- $this->model = $model;
-
- $this->xpath = new DOMXPath($model);
- foreach ($this->namespaces as $key => $value) {
- $this->xpath->registerNamespace($key, $value);
- }
- $this->numberEntries = $this->count('item');
- }
-
- /**
- * Included for compatibility -- will not work with RSS 0.9
- *
- * This is not something that will work with RSS0.9 as it does not have
- * clear restrictions on the global uniqueness of IDs.
- *
- * @param string $id any valid ID.
- * @return false
- */
- function getEntryById($id)
- {
- return false;
- }
-
- /**
- * Get details of the image associated with the feed.
- *
- * @return array|false an array simply containing the child elements
- */
- protected function getImage()
- {
- $images = $this->model->getElementsByTagName('image');
- if ($images->length > 0) {
- $image = $images->item(0);
- $details = array();
- if ($image->hasChildNodes()) {
- $details = array(
- 'title' => $image->getElementsByTagName('title')->item(0)->value,
- 'link' => $image->getElementsByTagName('link')->item(0)->value,
- 'url' => $image->getElementsByTagName('url')->item(0)->value);
- } else {
- $details = array('title' => false,
- 'link' => false,
- 'url' => $image->attributes->getNamedItem('resource')->nodeValue);
- }
- $details = array_merge($details,
- array('description' => false, 'height' => false, 'width' => false));
- if (! empty($details)) {
- return $details;
- }
- }
- return false;
- }
-
- /**
- * The textinput element is little used, but in the interests of
- * completeness we will support it.
- *
- * @return array|false
- */
- protected function getTextInput()
- {
- $inputs = $this->model->getElementsByTagName('textinput');
- if ($inputs->length > 0) {
- $input = $inputs->item(0);
- $results = array();
- $results['title'] = isset(
- $input->getElementsByTagName('title')->item(0)->value) ?
- $input->getElementsByTagName('title')->item(0)->value : null;
- $results['description'] = isset(
- $input->getElementsByTagName('description')->item(0)->value) ?
- $input->getElementsByTagName('description')->item(0)->value : null;
- $results['name'] = isset(
- $input->getElementsByTagName('name')->item(0)->value) ?
- $input->getElementsByTagName('name')->item(0)->value : null;
- $results['link'] = isset(
- $input->getElementsByTagName('link')->item(0)->value) ?
- $input->getElementsByTagName('link')->item(0)->value : null;
- if (empty($results['link']) &&
- $input->attributes->getNamedItem('resource')) {
- $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue;
- }
- if (! empty($results)) {
- return $results;
- }
- }
- return false;
- }
-
- /**
- * Get details of a link from the feed.
- *
- * In RSS1 a link is a text element but in order to ensure that we resolve
- * URLs properly we have a special function for them.
- *
- * @return string
- */
- function getLink($offset = 0, $attribute = 'href', $params = false)
- {
- $links = $this->model->getElementsByTagName('link');
- if ($links->length <= $offset) {
- return false;
- }
- $link = $links->item($offset);
- return $this->addBase($link->nodeValue, $link);
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php
deleted file mode 100755
index d41f36e8d..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS09Element.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS0.9 Element class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: RSS09Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/*
- * This class provides support for RSS 0.9 entries. It will usually be called by
- * XML_Feed_Parser_RSS09 with which it shares many methods.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS09Element extends XML_Feed_Parser_RSS09
-{
- /**
- * This will be a reference to the parent object for when we want
- * to use a 'fallback' rule
- * @var XML_Feed_Parser_RSS09
- */
- protected $parent;
-
- /**
- * Our specific element map
- * @var array
- */
- protected $map = array(
- 'title' => array('Text'),
- 'link' => array('Link'));
-
- /**
- * Store useful information for later.
- *
- * @param DOMElement $element - this item as a DOM element
- * @param XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
- */
- function __construct(DOMElement $element, $parent, $xmlBase = '')
- {
- $this->model = $element;
- $this->parent = $parent;
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php
deleted file mode 100755
index 60c9938ba..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1.php
+++ /dev/null
@@ -1,277 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS1 class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: RSS1.php,v 1.10 2006/07/27 13:52:05 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class handles RSS1.0 feeds.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- * @todo Find a Relax NG URI we can use
- */
-class XML_Feed_Parser_RSS1 extends XML_Feed_Parser_Type
-{
- /**
- * The URI of the RelaxNG schema used to (optionally) validate the feed
- * @var string
- */
- private $relax = 'rss10.rnc';
-
- /**
- * We're likely to use XPath, so let's keep it global
- * @var DOMXPath
- */
- protected $xpath;
-
- /**
- * The feed type we are parsing
- * @var string
- */
- public $version = 'RSS 1.0';
-
- /**
- * The class used to represent individual items
- * @var string
- */
- protected $itemClass = 'XML_Feed_Parser_RSS1Element';
-
- /**
- * The element containing entries
- * @var string
- */
- protected $itemElement = 'item';
-
- /**
- * Here we map those elements we're not going to handle individually
- * to the constructs they are. The optional second parameter in the array
- * tells the parser whether to 'fall back' (not apt. at the feed level) or
- * fail if the element is missing. If the parameter is not set, the function
- * will simply return false and leave it to the client to decide what to do.
- * @var array
- */
- protected $map = array(
- 'title' => array('Text'),
- 'link' => array('Text'),
- 'description' => array('Text'),
- 'image' => array('Image'),
- 'textinput' => array('TextInput'),
- 'updatePeriod' => array('Text'),
- 'updateFrequency' => array('Text'),
- 'updateBase' => array('Date'),
- 'rights' => array('Text'), # dc:rights
- 'description' => array('Text'), # dc:description
- 'creator' => array('Text'), # dc:creator
- 'publisher' => array('Text'), # dc:publisher
- 'contributor' => array('Text'), # dc:contributor
- 'date' => array('Date') # dc:contributor
- );
-
- /**
- * Here we map some elements to their atom equivalents. This is going to be
- * quite tricky to pull off effectively (and some users' methods may vary)
- * but is worth trying. The key is the atom version, the value is RSS2.
- * @var array
- */
- protected $compatMap = array(
- 'title' => array('title'),
- 'link' => array('link'),
- 'subtitle' => array('description'),
- 'author' => array('creator'),
- 'updated' => array('date'));
-
- /**
- * We will be working with multiple namespaces and it is useful to
- * keep them together
- * @var array
- */
- protected $namespaces = array(
- 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
- 'rss' => 'http://purl.org/rss/1.0/',
- 'dc' => 'http://purl.org/rss/1.0/modules/dc/',
- 'content' => 'http://purl.org/rss/1.0/modules/content/',
- 'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/');
-
- /**
- * Our constructor does nothing more than its parent.
- *
- * @param DOMDocument $xml A DOM object representing the feed
- * @param bool (optional) $string Whether or not to validate this feed
- */
- function __construct(DOMDocument $model, $strict = false)
- {
- $this->model = $model;
- if ($strict) {
- $validate = $this->model->relaxNGValidate(self::getSchemaDir .
- DIRECTORY_SEPARATOR . $this->relax);
- if (! $validate) {
- throw new XML_Feed_Parser_Exception('Failed required validation');
- }
- }
-
- $this->xpath = new DOMXPath($model);
- foreach ($this->namespaces as $key => $value) {
- $this->xpath->registerNamespace($key, $value);
- }
- $this->numberEntries = $this->count('item');
- }
-
- /**
- * Allows retrieval of an entry by ID where the rdf:about attribute is used
- *
- * This is not really something that will work with RSS1 as it does not have
- * clear restrictions on the global uniqueness of IDs. We will employ the
- * _very_ hit and miss method of selecting entries based on the rdf:about
- * attribute. If DOMXPath::evaluate is available, we also use that to store
- * a reference to the entry in the array used by getEntryByOffset so that
- * method does not have to seek out the entry if it's requested that way.
- *
- * @param string $id any valid ID.
- * @return XML_Feed_Parser_RSS1Element
- */
- function getEntryById($id)
- {
- if (isset($this->idMappings[$id])) {
- return $this->entries[$this->idMappings[$id]];
- }
-
- $entries = $this->xpath->query("//rss:item[@rdf:about='$id']");
- if ($entries->length > 0) {
- $classname = $this->itemClass;
- $entry = new $classname($entries->item(0), $this);
- if (in_array('evaluate', get_class_methods($this->xpath))) {
- $offset = $this->xpath->evaluate("count(preceding-sibling::rss:item)", $entries->item(0));
- $this->entries[$offset] = $entry;
- }
- $this->idMappings[$id] = $entry;
- return $entry;
- }
- return false;
- }
-
- /**
- * Get details of the image associated with the feed.
- *
- * @return array|false an array simply containing the child elements
- */
- protected function getImage()
- {
- $images = $this->model->getElementsByTagName('image');
- if ($images->length > 0) {
- $image = $images->item(0);
- $details = array();
- if ($image->hasChildNodes()) {
- $details = array(
- 'title' => $image->getElementsByTagName('title')->item(0)->value,
- 'link' => $image->getElementsByTagName('link')->item(0)->value,
- 'url' => $image->getElementsByTagName('url')->item(0)->value);
- } else {
- $details = array('title' => false,
- 'link' => false,
- 'url' => $image->attributes->getNamedItem('resource')->nodeValue);
- }
- $details = array_merge($details, array('description' => false, 'height' => false, 'width' => false));
- if (! empty($details)) {
- return $details;
- }
- }
- return false;
- }
-
- /**
- * The textinput element is little used, but in the interests of
- * completeness we will support it.
- *
- * @return array|false
- */
- protected function getTextInput()
- {
- $inputs = $this->model->getElementsByTagName('textinput');
- if ($inputs->length > 0) {
- $input = $inputs->item(0);
- $results = array();
- $results['title'] = isset(
- $input->getElementsByTagName('title')->item(0)->value) ?
- $input->getElementsByTagName('title')->item(0)->value : null;
- $results['description'] = isset(
- $input->getElementsByTagName('description')->item(0)->value) ?
- $input->getElementsByTagName('description')->item(0)->value : null;
- $results['name'] = isset(
- $input->getElementsByTagName('name')->item(0)->value) ?
- $input->getElementsByTagName('name')->item(0)->value : null;
- $results['link'] = isset(
- $input->getElementsByTagName('link')->item(0)->value) ?
- $input->getElementsByTagName('link')->item(0)->value : null;
- if (empty($results['link']) and
- $input->attributes->getNamedItem('resource')) {
- $results['link'] =
- $input->attributes->getNamedItem('resource')->nodeValue;
- }
- if (! empty($results)) {
- return $results;
- }
- }
- return false;
- }
-
- /**
- * Employs various techniques to identify the author
- *
- * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher
- * elements for defining authorship in RSS1. We will try each of those in
- * turn in order to simulate the atom author element and will return it
- * as text.
- *
- * @return array|false
- */
- function getAuthor()
- {
- $options = array('creator', 'contributor', 'publisher');
- foreach ($options as $element) {
- $test = $this->model->getElementsByTagName($element);
- if ($test->length > 0) {
- return $test->item(0)->value;
- }
- }
- return false;
- }
-
- /**
- * Retrieve a link
- *
- * In RSS1 a link is a text element but in order to ensure that we resolve
- * URLs properly we have a special function for them.
- *
- * @return string
- */
- function getLink($offset = 0, $attribute = 'href', $params = false)
- {
- $links = $this->model->getElementsByTagName('link');
- if ($links->length <= $offset) {
- return false;
- }
- $link = $links->item($offset);
- return $this->addBase($link->nodeValue, $link);
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php
deleted file mode 100755
index 3cd1ef15d..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11.php
+++ /dev/null
@@ -1,276 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS1.1 class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: RSS11.php,v 1.6 2006/07/27 13:52:05 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class handles RSS1.1 feeds. RSS1.1 is documented at:
- * http://inamidst.com/rss1.1/
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- * @todo Support for RDF:List
- * @todo Ensure xml:lang is accessible to users
- */
-class XML_Feed_Parser_RSS11 extends XML_Feed_Parser_Type
-{
- /**
- * The URI of the RelaxNG schema used to (optionally) validate the feed
- * @var string
- */
- private $relax = 'rss11.rnc';
-
- /**
- * We're likely to use XPath, so let's keep it global
- * @var DOMXPath
- */
- protected $xpath;
-
- /**
- * The feed type we are parsing
- * @var string
- */
- public $version = 'RSS 1.0';
-
- /**
- * The class used to represent individual items
- * @var string
- */
- protected $itemClass = 'XML_Feed_Parser_RSS1Element';
-
- /**
- * The element containing entries
- * @var string
- */
- protected $itemElement = 'item';
-
- /**
- * Here we map those elements we're not going to handle individually
- * to the constructs they are. The optional second parameter in the array
- * tells the parser whether to 'fall back' (not apt. at the feed level) or
- * fail if the element is missing. If the parameter is not set, the function
- * will simply return false and leave it to the client to decide what to do.
- * @var array
- */
- protected $map = array(
- 'title' => array('Text'),
- 'link' => array('Text'),
- 'description' => array('Text'),
- 'image' => array('Image'),
- 'updatePeriod' => array('Text'),
- 'updateFrequency' => array('Text'),
- 'updateBase' => array('Date'),
- 'rights' => array('Text'), # dc:rights
- 'description' => array('Text'), # dc:description
- 'creator' => array('Text'), # dc:creator
- 'publisher' => array('Text'), # dc:publisher
- 'contributor' => array('Text'), # dc:contributor
- 'date' => array('Date') # dc:contributor
- );
-
- /**
- * Here we map some elements to their atom equivalents. This is going to be
- * quite tricky to pull off effectively (and some users' methods may vary)
- * but is worth trying. The key is the atom version, the value is RSS2.
- * @var array
- */
- protected $compatMap = array(
- 'title' => array('title'),
- 'link' => array('link'),
- 'subtitle' => array('description'),
- 'author' => array('creator'),
- 'updated' => array('date'));
-
- /**
- * We will be working with multiple namespaces and it is useful to
- * keep them together. We will retain support for some common RSS1.0 modules
- * @var array
- */
- protected $namespaces = array(
- 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
- 'rss' => 'http://purl.org/net/rss1.1#',
- 'dc' => 'http://purl.org/rss/1.0/modules/dc/',
- 'content' => 'http://purl.org/rss/1.0/modules/content/',
- 'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/');
-
- /**
- * Our constructor does nothing more than its parent.
- *
- * @param DOMDocument $xml A DOM object representing the feed
- * @param bool (optional) $string Whether or not to validate this feed
- */
- function __construct(DOMDocument $model, $strict = false)
- {
- $this->model = $model;
-
- if ($strict) {
- $validate = $this->model->relaxNGValidate(self::getSchemaDir .
- DIRECTORY_SEPARATOR . $this->relax);
- if (! $validate) {
- throw new XML_Feed_Parser_Exception('Failed required validation');
- }
- }
-
- $this->xpath = new DOMXPath($model);
- foreach ($this->namespaces as $key => $value) {
- $this->xpath->registerNamespace($key, $value);
- }
- $this->numberEntries = $this->count('item');
- }
-
- /**
- * Attempts to identify an element by ID given by the rdf:about attribute
- *
- * This is not really something that will work with RSS1.1 as it does not have
- * clear restrictions on the global uniqueness of IDs. We will employ the
- * _very_ hit and miss method of selecting entries based on the rdf:about
- * attribute. Please note that this is even more hit and miss with RSS1.1 than
- * with RSS1.0 since RSS1.1 does not require the rdf:about attribute for items.
- *
- * @param string $id any valid ID.
- * @return XML_Feed_Parser_RSS1Element
- */
- function getEntryById($id)
- {
- if (isset($this->idMappings[$id])) {
- return $this->entries[$this->idMappings[$id]];
- }
-
- $entries = $this->xpath->query("//rss:item[@rdf:about='$id']");
- if ($entries->length > 0) {
- $classname = $this->itemClass;
- $entry = new $classname($entries->item(0), $this);
- return $entry;
- }
- return false;
- }
-
- /**
- * Get details of the image associated with the feed.
- *
- * @return array|false an array simply containing the child elements
- */
- protected function getImage()
- {
- $images = $this->model->getElementsByTagName('image');
- if ($images->length > 0) {
- $image = $images->item(0);
- $details = array();
- if ($image->hasChildNodes()) {
- $details = array(
- 'title' => $image->getElementsByTagName('title')->item(0)->value,
- 'url' => $image->getElementsByTagName('url')->item(0)->value);
- if ($image->getElementsByTagName('link')->length > 0) {
- $details['link'] =
- $image->getElementsByTagName('link')->item(0)->value;
- }
- } else {
- $details = array('title' => false,
- 'link' => false,
- 'url' => $image->attributes->getNamedItem('resource')->nodeValue);
- }
- $details = array_merge($details,
- array('description' => false, 'height' => false, 'width' => false));
- if (! empty($details)) {
- return $details;
- }
- }
- return false;
- }
-
- /**
- * The textinput element is little used, but in the interests of
- * completeness we will support it.
- *
- * @return array|false
- */
- protected function getTextInput()
- {
- $inputs = $this->model->getElementsByTagName('textinput');
- if ($inputs->length > 0) {
- $input = $inputs->item(0);
- $results = array();
- $results['title'] = isset(
- $input->getElementsByTagName('title')->item(0)->value) ?
- $input->getElementsByTagName('title')->item(0)->value : null;
- $results['description'] = isset(
- $input->getElementsByTagName('description')->item(0)->value) ?
- $input->getElementsByTagName('description')->item(0)->value : null;
- $results['name'] = isset(
- $input->getElementsByTagName('name')->item(0)->value) ?
- $input->getElementsByTagName('name')->item(0)->value : null;
- $results['link'] = isset(
- $input->getElementsByTagName('link')->item(0)->value) ?
- $input->getElementsByTagName('link')->item(0)->value : null;
- if (empty($results['link']) and
- $input->attributes->getNamedItem('resource')) {
- $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue;
- }
- if (! empty($results)) {
- return $results;
- }
- }
- return false;
- }
-
- /**
- * Attempts to discern authorship
- *
- * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher
- * elements for defining authorship in RSS1. We will try each of those in
- * turn in order to simulate the atom author element and will return it
- * as text.
- *
- * @return array|false
- */
- function getAuthor()
- {
- $options = array('creator', 'contributor', 'publisher');
- foreach ($options as $element) {
- $test = $this->model->getElementsByTagName($element);
- if ($test->length > 0) {
- return $test->item(0)->value;
- }
- }
- return false;
- }
-
- /**
- * Retrieve a link
- *
- * In RSS1 a link is a text element but in order to ensure that we resolve
- * URLs properly we have a special function for them.
- *
- * @return string
- */
- function getLink($offset = 0, $attribute = 'href', $params = false)
- {
- $links = $this->model->getElementsByTagName('link');
- if ($links->length <= $offset) {
- return false;
- }
- $link = $links->item($offset);
- return $this->addBase($link->nodeValue, $link);
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php
deleted file mode 100755
index 75918beda..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS11Element.php
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS1 Element class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: RSS11Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/*
- * This class provides support for RSS 1.1 entries. It will usually be called by
- * XML_Feed_Parser_RSS11 with which it shares many methods.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS11Element extends XML_Feed_Parser_RSS11
-{
- /**
- * This will be a reference to the parent object for when we want
- * to use a 'fallback' rule
- * @var XML_Feed_Parser_RSS1
- */
- protected $parent;
-
- /**
- * Our specific element map
- * @var array
- */
- protected $map = array(
- 'id' => array('Id'),
- 'title' => array('Text'),
- 'link' => array('Link'),
- 'description' => array('Text'), # or dc:description
- 'category' => array('Category'),
- 'rights' => array('Text'), # dc:rights
- 'creator' => array('Text'), # dc:creator
- 'publisher' => array('Text'), # dc:publisher
- 'contributor' => array('Text'), # dc:contributor
- 'date' => array('Date'), # dc:date
- 'content' => array('Content')
- );
-
- /**
- * Here we map some elements to their atom equivalents. This is going to be
- * quite tricky to pull off effectively (and some users' methods may vary)
- * but is worth trying. The key is the atom version, the value is RSS1.
- * @var array
- */
- protected $compatMap = array(
- 'content' => array('content'),
- 'updated' => array('lastBuildDate'),
- 'published' => array('pubdate'),
- 'subtitle' => array('description'),
- 'updated' => array('date'),
- 'author' => array('creator'),
- 'contributor' => array('contributor')
- );
-
- /**
- * Store useful information for later.
- *
- * @param DOMElement $element - this item as a DOM element
- * @param XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
- */
- function __construct(DOMElement $element, $parent, $xmlBase = '')
- {
- $this->model = $element;
- $this->parent = $parent;
- }
-
- /**
- * If an rdf:about attribute is specified, return that as an ID
- *
- * There is no established way of showing an ID for an RSS1 entry. We will
- * simulate it using the rdf:about attribute of the entry element. This cannot
- * be relied upon for unique IDs but may prove useful.
- *
- * @return string|false
- */
- function getId()
- {
- if ($this->model->attributes->getNamedItem('about')) {
- return $this->model->attributes->getNamedItem('about')->nodeValue;
- }
- return false;
- }
-
- /**
- * Return the entry's content
- *
- * The official way to include full content in an RSS1 entry is to use
- * the content module's element 'encoded'. Often, however, the 'description'
- * element is used instead. We will offer that as a fallback.
- *
- * @return string|false
- */
- function getContent()
- {
- $options = array('encoded', 'description');
- foreach ($options as $element) {
- $test = $this->model->getElementsByTagName($element);
- if ($test->length == 0) {
- continue;
- }
- if ($test->item(0)->hasChildNodes()) {
- $value = '';
- foreach ($test->item(0)->childNodes as $child) {
- if ($child instanceof DOMText) {
- $value .= $child->nodeValue;
- } else {
- $simple = simplexml_import_dom($child);
- $value .= $simple->asXML();
- }
- }
- return $value;
- } else if ($test->length > 0) {
- return $test->item(0)->nodeValue;
- }
- }
- return false;
- }
-
- /**
- * How RSS1.1 should support for enclosures is not clear. For now we will return
- * false.
- *
- * @return false
- */
- function getEnclosure()
- {
- return false;
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php
deleted file mode 100755
index 8e36d5a9b..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS1Element.php
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * RSS1 Element class for XML_Feed_Parser
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: RSS1Element.php,v 1.6 2006/06/30 17:41:56 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/*
- * This class provides support for RSS 1.0 entries. It will usually be called by
- * XML_Feed_Parser_RSS1 with which it shares many methods.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS1Element extends XML_Feed_Parser_RSS1
-{
- /**
- * This will be a reference to the parent object for when we want
- * to use a 'fallback' rule
- * @var XML_Feed_Parser_RSS1
- */
- protected $parent;
-
- /**
- * Our specific element map
- * @var array
- */
- protected $map = array(
- 'id' => array('Id'),
- 'title' => array('Text'),
- 'link' => array('Link'),
- 'description' => array('Text'), # or dc:description
- 'category' => array('Category'),
- 'rights' => array('Text'), # dc:rights
- 'creator' => array('Text'), # dc:creator
- 'publisher' => array('Text'), # dc:publisher
- 'contributor' => array('Text'), # dc:contributor
- 'date' => array('Date'), # dc:date
- 'content' => array('Content')
- );
-
- /**
- * Here we map some elements to their atom equivalents. This is going to be
- * quite tricky to pull off effectively (and some users' methods may vary)
- * but is worth trying. The key is the atom version, the value is RSS1.
- * @var array
- */
- protected $compatMap = array(
- 'content' => array('content'),
- 'updated' => array('lastBuildDate'),
- 'published' => array('pubdate'),
- 'subtitle' => array('description'),
- 'updated' => array('date'),
- 'author' => array('creator'),
- 'contributor' => array('contributor')
- );
-
- /**
- * Store useful information for later.
- *
- * @param DOMElement $element - this item as a DOM element
- * @param XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
- */
- function __construct(DOMElement $element, $parent, $xmlBase = '')
- {
- $this->model = $element;
- $this->parent = $parent;
- }
-
- /**
- * If an rdf:about attribute is specified, return it as an ID
- *
- * There is no established way of showing an ID for an RSS1 entry. We will
- * simulate it using the rdf:about attribute of the entry element. This cannot
- * be relied upon for unique IDs but may prove useful.
- *
- * @return string|false
- */
- function getId()
- {
- if ($this->model->attributes->getNamedItem('about')) {
- return $this->model->attributes->getNamedItem('about')->nodeValue;
- }
- return false;
- }
-
- /**
- * How RSS1 should support for enclosures is not clear. For now we will return
- * false.
- *
- * @return false
- */
- function getEnclosure()
- {
- return false;
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php
deleted file mode 100644
index 0936bd2f5..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2.php
+++ /dev/null
@@ -1,335 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Class representing feed-level data for an RSS2 feed
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: RSS2.php,v 1.12 2008/03/08 18:16:45 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class handles RSS2 feeds.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type
-{
- /**
- * The URI of the RelaxNG schema used to (optionally) validate the feed
- * @var string
- */
- private $relax = 'rss20.rnc';
-
- /**
- * We're likely to use XPath, so let's keep it global
- * @var DOMXPath
- */
- protected $xpath;
-
- /**
- * The feed type we are parsing
- * @var string
- */
- public $version = 'RSS 2.0';
-
- /**
- * The class used to represent individual items
- * @var string
- */
- protected $itemClass = 'XML_Feed_Parser_RSS2Element';
-
- /**
- * The element containing entries
- * @var string
- */
- protected $itemElement = 'item';
-
- /**
- * Here we map those elements we're not going to handle individually
- * to the constructs they are. The optional second parameter in the array
- * tells the parser whether to 'fall back' (not apt. at the feed level) or
- * fail if the element is missing. If the parameter is not set, the function
- * will simply return false and leave it to the client to decide what to do.
- * @var array
- */
- protected $map = array(
- 'ttl' => array('Text'),
- 'pubDate' => array('Date'),
- 'lastBuildDate' => array('Date'),
- 'title' => array('Text'),
- 'link' => array('Link'),
- 'description' => array('Text'),
- 'language' => array('Text'),
- 'copyright' => array('Text'),
- 'managingEditor' => array('Text'),
- 'webMaster' => array('Text'),
- 'category' => array('Text'),
- 'generator' => array('Text'),
- 'docs' => array('Text'),
- 'ttl' => array('Text'),
- 'image' => array('Image'),
- 'skipDays' => array('skipDays'),
- 'skipHours' => array('skipHours'));
-
- /**
- * Here we map some elements to their atom equivalents. This is going to be
- * quite tricky to pull off effectively (and some users' methods may vary)
- * but is worth trying. The key is the atom version, the value is RSS2.
- * @var array
- */
- protected $compatMap = array(
- 'title' => array('title'),
- 'rights' => array('copyright'),
- 'updated' => array('lastBuildDate'),
- 'subtitle' => array('description'),
- 'date' => array('pubDate'),
- 'author' => array('managingEditor'));
-
- protected $namespaces = array(
- 'dc' => 'http://purl.org/rss/1.0/modules/dc/',
- 'content' => 'http://purl.org/rss/1.0/modules/content/');
-
- /**
- * Our constructor does nothing more than its parent.
- *
- * @param DOMDocument $xml A DOM object representing the feed
- * @param bool (optional) $string Whether or not to validate this feed
- */
- function __construct(DOMDocument $model, $strict = false)
- {
- $this->model = $model;
-
- if ($strict) {
- if (! $this->model->relaxNGValidate($this->relax)) {
- throw new XML_Feed_Parser_Exception('Failed required validation');
- }
- }
-
- $this->xpath = new DOMXPath($this->model);
- foreach ($this->namespaces as $key => $value) {
- $this->xpath->registerNamespace($key, $value);
- }
- $this->numberEntries = $this->count('item');
- }
-
- /**
- * Retrieves an entry by ID, if the ID is specified with the guid element
- *
- * This is not really something that will work with RSS2 as it does not have
- * clear restrictions on the global uniqueness of IDs. But we can emulate
- * it by allowing access based on the 'guid' element. If DOMXPath::evaluate
- * is available, we also use that to store a reference to the entry in the array
- * used by getEntryByOffset so that method does not have to seek out the entry
- * if it's requested that way.
- *
- * @param string $id any valid ID.
- * @return XML_Feed_Parser_RSS2Element
- */
- function getEntryById($id)
- {
- if (isset($this->idMappings[$id])) {
- return $this->entries[$this->idMappings[$id]];
- }
-
- $entries = $this->xpath->query("//item[guid='$id']");
- if ($entries->length > 0) {
- $entry = new $this->itemElement($entries->item(0), $this);
- if (in_array('evaluate', get_class_methods($this->xpath))) {
- $offset = $this->xpath->evaluate("count(preceding-sibling::item)", $entries->item(0));
- $this->entries[$offset] = $entry;
- }
- $this->idMappings[$id] = $entry;
- return $entry;
- }
- }
-
- /**
- * Get a category from the element
- *
- * The category element is a simple text construct which can occur any number
- * of times. We allow access by offset or access to an array of results.
- *
- * @param string $call for compatibility with our overloading
- * @param array $arguments - arg 0 is the offset, arg 1 is whether to return as array
- * @return string|array|false
- */
- function getCategory($call, $arguments = array())
- {
- $categories = $this->model->getElementsByTagName('category');
- $offset = empty($arguments[0]) ? 0 : $arguments[0];
- $array = empty($arguments[1]) ? false : true;
- if ($categories->length <= $offset) {
- return false;
- }
- if ($array) {
- $list = array();
- foreach ($categories as $category) {
- array_push($list, $category->nodeValue);
- }
- return $list;
- }
- return $categories->item($offset)->nodeValue;
- }
-
- /**
- * Get details of the image associated with the feed.
- *
- * @return array|false an array simply containing the child elements
- */
- protected function getImage()
- {
- $images = $this->xpath->query("//image");
- if ($images->length > 0) {
- $image = $images->item(0);
- $desc = $image->getElementsByTagName('description');
- $description = $desc->length ? $desc->item(0)->nodeValue : false;
- $heigh = $image->getElementsByTagName('height');
- $height = $heigh->length ? $heigh->item(0)->nodeValue : false;
- $widt = $image->getElementsByTagName('width');
- $width = $widt->length ? $widt->item(0)->nodeValue : false;
- return array(
- 'title' => $image->getElementsByTagName('title')->item(0)->nodeValue,
- 'link' => $image->getElementsByTagName('link')->item(0)->nodeValue,
- 'url' => $image->getElementsByTagName('url')->item(0)->nodeValue,
- 'description' => $description,
- 'height' => $height,
- 'width' => $width);
- }
- return false;
- }
-
- /**
- * The textinput element is little used, but in the interests of
- * completeness...
- *
- * @return array|false
- */
- function getTextInput()
- {
- $inputs = $this->model->getElementsByTagName('input');
- if ($inputs->length > 0) {
- $input = $inputs->item(0);
- return array(
- 'title' => $input->getElementsByTagName('title')->item(0)->value,
- 'description' =>
- $input->getElementsByTagName('description')->item(0)->value,
- 'name' => $input->getElementsByTagName('name')->item(0)->value,
- 'link' => $input->getElementsByTagName('link')->item(0)->value);
- }
- return false;
- }
-
- /**
- * Utility function for getSkipDays and getSkipHours
- *
- * This is a general function used by both getSkipDays and getSkipHours. It simply
- * returns an array of the values of the children of the appropriate tag.
- *
- * @param string $tagName The tag name (getSkipDays or getSkipHours)
- * @return array|false
- */
- protected function getSkips($tagName)
- {
- $hours = $this->model->getElementsByTagName($tagName);
- if ($hours->length == 0) {
- return false;
- }
- $skipHours = array();
- foreach($hours->item(0)->childNodes as $hour) {
- if ($hour instanceof DOMElement) {
- array_push($skipHours, $hour->nodeValue);
- }
- }
- return $skipHours;
- }
-
- /**
- * Retrieve skipHours data
- *
- * The skiphours element provides a list of hours on which this feed should
- * not be checked. We return an array of those hours (integers, 24 hour clock)
- *
- * @return array
- */
- function getSkipHours()
- {
- return $this->getSkips('skipHours');
- }
-
- /**
- * Retrieve skipDays data
- *
- * The skipdays element provides a list of days on which this feed should
- * not be checked. We return an array of those days.
- *
- * @return array
- */
- function getSkipDays()
- {
- return $this->getSkips('skipDays');
- }
-
- /**
- * Return content of the little-used 'cloud' element
- *
- * The cloud element is rarely used. It is designed to provide some details
- * of a location to update the feed.
- *
- * @return array an array of the attributes of the element
- */
- function getCloud()
- {
- $cloud = $this->model->getElementsByTagName('cloud');
- if ($cloud->length == 0) {
- return false;
- }
- $cloudData = array();
- foreach ($cloud->item(0)->attributes as $attribute) {
- $cloudData[$attribute->name] = $attribute->value;
- }
- return $cloudData;
- }
-
- /**
- * Get link URL
- *
- * In RSS2 a link is a text element but in order to ensure that we resolve
- * URLs properly we have a special function for them. We maintain the
- * parameter used by the atom getLink method, though we only use the offset
- * parameter.
- *
- * @param int $offset The position of the link within the feed. Starts from 0
- * @param string $attribute The attribute of the link element required
- * @param array $params An array of other parameters. Not used.
- * @return string
- */
- function getLink($offset, $attribute = 'href', $params = array())
- {
- $xPath = new DOMXPath($this->model);
- $links = $xPath->query('//link');
-
- if ($links->length <= $offset) {
- return false;
- }
- $link = $links->item($offset);
- return $this->addBase($link->nodeValue, $link);
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php b/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php
deleted file mode 100755
index 6edf910dc..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/RSS2Element.php
+++ /dev/null
@@ -1,171 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Class representing entries in an RSS2 feed.
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: RSS2Element.php,v 1.11 2006/07/26 21:18:47 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This class provides support for RSS 2.0 entries. It will usually be
- * called by XML_Feed_Parser_RSS2 with which it shares many methods.
- *
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- * @package XML_Feed_Parser
- */
-class XML_Feed_Parser_RSS2Element extends XML_Feed_Parser_RSS2
-{
- /**
- * This will be a reference to the parent object for when we want
- * to use a 'fallback' rule
- * @var XML_Feed_Parser_RSS2
- */
- protected $parent;
-
- /**
- * Our specific element map
- * @var array
- */
- protected $map = array(
- 'title' => array('Text'),
- 'guid' => array('Guid'),
- 'description' => array('Text'),
- 'author' => array('Text'),
- 'comments' => array('Text'),
- 'enclosure' => array('Enclosure'),
- 'pubDate' => array('Date'),
- 'source' => array('Source'),
- 'link' => array('Text'),
- 'content' => array('Content'));
-
- /**
- * Here we map some elements to their atom equivalents. This is going to be
- * quite tricky to pull off effectively (and some users' methods may vary)
- * but is worth trying. The key is the atom version, the value is RSS2.
- * @var array
- */
- protected $compatMap = array(
- 'id' => array('guid'),
- 'updated' => array('lastBuildDate'),
- 'published' => array('pubdate'),
- 'guidislink' => array('guid', 'ispermalink'),
- 'summary' => array('description'));
-
- /**
- * Store useful information for later.
- *
- * @param DOMElement $element - this item as a DOM element
- * @param XML_Feed_Parser_RSS2 $parent - the feed of which this is a member
- */
- function __construct(DOMElement $element, $parent, $xmlBase = '')
- {
- $this->model = $element;
- $this->parent = $parent;
- }
-
- /**
- * Get the value of the guid element, if specified
- *
- * guid is the closest RSS2 has to atom's ID. It is usually but not always a
- * URI. The one attribute that RSS2 can posess is 'ispermalink' which specifies
- * whether the guid is itself dereferencable. Use of guid is not obligatory,
- * but is advisable. To get the guid you would call $item->id() (for atom
- * compatibility) or $item->guid(). To check if this guid is a permalink call
- * $item->guid("ispermalink").
- *
- * @param string $method - the method name being called
- * @param array $params - parameters required
- * @return string the guid or value of ispermalink
- */
- protected function getGuid($method, $params)
- {
- $attribute = (isset($params[0]) and $params[0] == 'ispermalink') ?
- true : false;
- $tag = $this->model->getElementsByTagName('guid');
- if ($tag->length > 0) {
- if ($attribute) {
- if ($tag->hasAttribute("ispermalink")) {
- return $tag->getAttribute("ispermalink");
- }
- }
- return $tag->item(0)->nodeValue;
- }
- return false;
- }
-
- /**
- * Access details of file enclosures
- *
- * The RSS2 spec is ambiguous as to whether an enclosure element must be
- * unique in a given entry. For now we will assume it needn't, and allow
- * for an offset.
- *
- * @param string $method - the method being called
- * @param array $parameters - we expect the first of these to be our offset
- * @return array|false
- */
- protected function getEnclosure($method, $parameters)
- {
- $encs = $this->model->getElementsByTagName('enclosure');
- $offset = isset($parameters[0]) ? $parameters[0] : 0;
- if ($encs->length > $offset) {
- try {
- if (! $encs->item($offset)->hasAttribute('url')) {
- return false;
- }
- $attrs = $encs->item($offset)->attributes;
- return array(
- 'url' => $attrs->getNamedItem('url')->value,
- 'length' => $attrs->getNamedItem('length')->value,
- 'type' => $attrs->getNamedItem('type')->value);
- } catch (Exception $e) {
- return false;
- }
- }
- return false;
- }
-
- /**
- * Get the entry source if specified
- *
- * source is an optional sub-element of item. Like atom:source it tells
- * us about where the entry came from (eg. if it's been copied from another
- * feed). It is not a rich source of metadata in the same way as atom:source
- * and while it would be good to maintain compatibility by returning an
- * XML_Feed_Parser_RSS2 element, it makes a lot more sense to return an array.
- *
- * @return array|false
- */
- protected function getSource()
- {
- $get = $this->model->getElementsByTagName('source');
- if ($get->length) {
- $source = $get->item(0);
- $array = array(
- 'content' => $source->nodeValue);
- foreach ($source->attributes as $attribute) {
- $array[$attribute->name] = $attribute->value;
- }
- return $array;
- }
- return false;
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php b/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php
deleted file mode 100644
index 75052619b..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/Parser/Type.php
+++ /dev/null
@@ -1,467 +0,0 @@
-<?php
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * Abstract class providing common methods for XML_Feed_Parser feeds.
- *
- * PHP versions 5
- *
- * LICENSE: This source file is subject to version 3.0 of the PHP license
- * that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
- * the PHP License and are unable to obtain it through the web, please
- * send a note to license@php.net so we can mail you a copy immediately.
- *
- * @category XML
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @copyright 2005 James Stewart <james@jystewart.net>
- * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
- * @version CVS: $Id: Type.php,v 1.25 2008/03/08 18:39:09 jystewart Exp $
- * @link http://pear.php.net/package/XML_Feed_Parser/
- */
-
-/**
- * This abstract class provides some general methods that are likely to be
- * implemented exactly the same way for all feed types.
- *
- * @package XML_Feed_Parser
- * @author James Stewart <james@jystewart.net>
- * @version Release: 1.0.3
- */
-abstract class XML_Feed_Parser_Type
-{
- /**
- * Where we store our DOM object for this feed
- * @var DOMDocument
- */
- public $model;
-
- /**
- * For iteration we'll want a count of the number of entries
- * @var int
- */
- public $numberEntries;
-
- /**
- * Where we store our entry objects once instantiated
- * @var array
- */
- public $entries = array();
-
- /**
- * Store mappings between entry IDs and their position in the feed
- */
- public $idMappings = array();
-
- /**
- * Proxy to allow use of element names as method names
- *
- * We are not going to provide methods for every entry type so this
- * function will allow for a lot of mapping. We rely pretty heavily
- * on this to handle our mappings between other feed types and atom.
- *
- * @param string $call - the method attempted
- * @param array $arguments - arguments to that method
- * @return mixed
- */
- function __call($call, $arguments = array())
- {
- if (! is_array($arguments)) {
- $arguments = array();
- }
-
- if (isset($this->compatMap[$call])) {
- $tempMap = $this->compatMap;
- $tempcall = array_pop($tempMap[$call]);
- if (! empty($tempMap)) {
- $arguments = array_merge($arguments, $tempMap[$call]);
- }
- $call = $tempcall;
- }
-
- /* To be helpful, we allow a case-insensitive search for this method */
- if (! isset($this->map[$call])) {
- foreach (array_keys($this->map) as $key) {
- if (strtoupper($key) == strtoupper($call)) {
- $call = $key;
- break;
- }
- }
- }
-
- if (empty($this->map[$call])) {
- return false;
- }
-
- $method = 'get' . $this->map[$call][0];
- if ($method == 'getLink') {
- $offset = empty($arguments[0]) ? 0 : $arguments[0];
- $attribute = empty($arguments[1]) ? 'href' : $arguments[1];
- $params = isset($arguments[2]) ? $arguments[2] : array();
- return $this->getLink($offset, $attribute, $params);
- }
- if (method_exists($this, $method)) {
- return $this->$method($call, $arguments);
- }
-
- return false;
- }
-
- /**
- * Proxy to allow use of element names as attribute names
- *
- * For many elements variable-style access will be desirable. This function
- * provides for that.
- *
- * @param string $value - the variable required
- * @return mixed
- */
- function __get($value)
- {
- return $this->__call($value, array());
- }
-
- /**
- * Utility function to help us resolve xml:base values
- *
- * We have other methods which will traverse the DOM and work out the different
- * xml:base declarations we need to be aware of. We then need to combine them.
- * If a declaration starts with a protocol then we restart the string. If it
- * starts with a / then we add on to the domain name. Otherwise we simply tag
- * it on to the end.
- *
- * @param string $base - the base to add the link to
- * @param string $link
- */
- function combineBases($base, $link)
- {
- if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
- return $link;
- } else if (preg_match('/^\//', $link)) {
- /* Extract domain and suffix link to that */
- preg_match('/^([A-Za-z]+:\/\/.*)?\/*/', $base, $results);
- $firstLayer = $results[0];
- return $firstLayer . "/" . $link;
- } else if (preg_match('/^\.\.\//', $base)) {
- /* Step up link to find place to be */
- preg_match('/^((\.\.\/)+)(.*)$/', $link, $bases);
- $suffix = $bases[3];
- $count = preg_match_all('/\.\.\//', $bases[1], $steps);
- $url = explode("/", $base);
- for ($i = 0; $i <= $count; $i++) {
- array_pop($url);
- }
- return implode("/", $url) . "/" . $suffix;
- } else if (preg_match('/^(?!\/$)/', $base)) {
- $base = preg_replace('/(.*\/).*$/', '$1', $base) ;
- return $base . $link;
- } else {
- /* Just stick it on the end */
- return $base . $link;
- }
- }
-
- /**
- * Determine whether we need to apply our xml:base rules
- *
- * Gets us the xml:base data and then processes that with regard
- * to our current link.
- *
- * @param string
- * @param DOMElement
- * @return string
- */
- function addBase($link, $element)
- {
- if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
- return $link;
- }
-
- return $this->combineBases($element->baseURI, $link);
- }
-
- /**
- * Get an entry by its position in the feed, starting from zero
- *
- * As well as allowing the items to be iterated over we want to allow
- * users to be able to access a specific entry. This is one of two ways of
- * doing that, the other being by ID.
- *
- * @param int $offset
- * @return XML_Feed_Parser_RSS1Element
- */
- function getEntryByOffset($offset)
- {
- if (! isset($this->entries[$offset])) {
- $entries = $this->model->getElementsByTagName($this->itemElement);
- if ($entries->length > $offset) {
- $xmlBase = $entries->item($offset)->baseURI;
- $this->entries[$offset] = new $this->itemClass(
- $entries->item($offset), $this, $xmlBase);
- if ($id = $this->entries[$offset]->id) {
- $this->idMappings[$id] = $this->entries[$offset];
- }
- } else {
- throw new XML_Feed_Parser_Exception('No entries found');
- }
- }
-
- return $this->entries[$offset];
- }
-
- /**
- * Return a date in seconds since epoch.
- *
- * Get a date construct. We use PHP's strtotime to return it as a unix datetime, which
- * is the number of seconds since 1970-01-01 00:00:00.
- *
- * @link http://php.net/strtotime
- * @param string $method The name of the date construct we want
- * @param array $arguments Included for compatibility with our __call usage
- * @return int|false datetime
- */
- protected function getDate($method, $arguments)
- {
- $time = $this->model->getElementsByTagName($method);
- if ($time->length == 0 || empty($time->item(0)->nodeValue)) {
- return false;
- }
- return strtotime($time->item(0)->nodeValue);
- }
-
- /**
- * Get a text construct.
- *
- * @param string $method The name of the text construct we want
- * @param array $arguments Included for compatibility with our __call usage
- * @return string
- */
- protected function getText($method, $arguments = array())
- {
- $tags = $this->model->getElementsByTagName($method);
- if ($tags->length > 0) {
- $value = $tags->item(0)->nodeValue;
- return $value;
- }
- return false;
- }
-
- /**
- * Apply various rules to retrieve category data.
- *
- * There is no single way of declaring a category in RSS1/1.1 as there is in RSS2
- * and Atom. Instead the usual approach is to use the dublin core namespace to
- * declare categories. For example delicious use both:
- * <dc:subject>PEAR</dc:subject> and: <taxo:topics><rdf:Bag>
- * <rdf:li resource="http://del.icio.us/tag/PEAR" /></rdf:Bag></taxo:topics>
- * to declare a categorisation of 'PEAR'.
- *
- * We need to be sensitive to this where possible.
- *
- * @param string $call for compatibility with our overloading
- * @param array $arguments - arg 0 is the offset, arg 1 is whether to return as array
- * @return string|array|false
- */
- protected function getCategory($call, $arguments)
- {
- $categories = $this->model->getElementsByTagName('subject');
- $offset = empty($arguments[0]) ? 0 : $arguments[0];
- $array = empty($arguments[1]) ? false : true;
- if ($categories->length <= $offset) {
- return false;
- }
- if ($array) {
- $list = array();
- foreach ($categories as $category) {
- array_push($list, $category->nodeValue);
- }
- return $list;
- }
- return $categories->item($offset)->nodeValue;
- }
-
- /**
- * Count occurrences of an element
- *
- * This function will tell us how many times the element $type
- * appears at this level of the feed.
- *
- * @param string $type the element we want to get a count of
- * @return int
- */
- protected function count($type)
- {
- if ($tags = $this->model->getElementsByTagName($type)) {
- return $tags->length;
- }
- return 0;
- }
-
- /**
- * Part of our xml:base processing code
- *
- * We need a couple of methods to access XHTML content stored in feeds.
- * This is because we dereference all xml:base references before returning
- * the element. This method handles the attributes.
- *
- * @param DOMElement $node The DOM node we are iterating over
- * @return string
- */
- function processXHTMLAttributes($node) {
- $return = '';
- foreach ($node->attributes as $attribute) {
- if ($attribute->name == 'src' or $attribute->name == 'href') {
- $attribute->value = $this->addBase(htmlentities($attribute->value, NULL, 'utf-8'), $attribute);
- }
- if ($attribute->name == 'base') {
- continue;
- }
- $return .= $attribute->name . '="' . htmlentities($attribute->value, NULL, 'utf-8') .'" ';
- }
- if (! empty($return)) {
- return ' ' . trim($return);
- }
- return '';
- }
-
- /**
- * Convert HTML entities based on the current character set.
- *
- * @param String
- * @return String
- */
- function processEntitiesForNodeValue($node)
- {
- if (function_exists('iconv')) {
- $current_encoding = $node->ownerDocument->encoding;
- $value = iconv($current_encoding, 'UTF-8', $node->nodeValue);
- } else if ($current_encoding == 'iso-8859-1') {
- $value = utf8_encode($node->nodeValue);
- } else {
- $value = $node->nodeValue;
- }
-
- $decoded = html_entity_decode($value, NULL, 'UTF-8');
- return htmlentities($decoded, NULL, 'UTF-8');
- }
-
- /**
- * Part of our xml:base processing code
- *
- * We need a couple of methods to access XHTML content stored in feeds.
- * This is because we dereference all xml:base references before returning
- * the element. This method recurs through the tree descending from the node
- * and builds our string.
- *
- * @param DOMElement $node The DOM node we are processing
- * @return string
- */
- function traverseNode($node)
- {
- $content = '';
-
- /* Add the opening of this node to the content */
- if ($node instanceof DOMElement) {
- $content .= '<' . $node->tagName .
- $this->processXHTMLAttributes($node) . '>';
- }
-
- /* Process children */
- if ($node->hasChildNodes()) {
- foreach ($node->childNodes as $child) {
- $content .= $this->traverseNode($child);
- }
- }
-
- if ($node instanceof DOMText) {
- $content .= $this->processEntitiesForNodeValue($node);
- }
-
- /* Add the closing of this node to the content */
- if ($node instanceof DOMElement) {
- $content .= '</' . $node->tagName . '>';
- }
-
- return $content;
- }
-
- /**
- * Get content from RSS feeds (atom has its own implementation)
- *
- * The official way to include full content in an RSS1 entry is to use
- * the content module's element 'encoded', and RSS2 feeds often duplicate that.
- * Often, however, the 'description' element is used instead. We will offer that
- * as a fallback. Atom uses its own approach and overrides this method.
- *
- * @return string|false
- */
- protected function getContent()
- {
- $options = array('encoded', 'description');
- foreach ($options as $element) {
- $test = $this->model->getElementsByTagName($element);
- if ($test->length == 0) {
- continue;
- }
- if ($test->item(0)->hasChildNodes()) {
- $value = '';
- foreach ($test->item(0)->childNodes as $child) {
- if ($child instanceof DOMText) {
- $value .= $child->nodeValue;
- } else {
- $simple = simplexml_import_dom($child);
- $value .= $simple->asXML();
- }
- }
- return $value;
- } else if ($test->length > 0) {
- return $test->item(0)->nodeValue;
- }
- }
- return false;
- }
-
- /**
- * Checks if this element has a particular child element.
- *
- * @param String
- * @param Integer
- * @return bool
- **/
- function hasKey($name, $offset = 0)
- {
- $search = $this->model->getElementsByTagName($name);
- return $search->length > $offset;
- }
-
- /**
- * Return an XML serialization of the feed, should it be required. Most
- * users however, will already have a serialization that they used when
- * instantiating the object.
- *
- * @return string XML serialization of element
- */
- function __toString()
- {
- $simple = simplexml_import_dom($this->model);
- return $simple->asXML();
- }
-
- /**
- * Get directory holding RNG schemas. Method is based on that
- * found in Contact_AddressBook.
- *
- * @return string PEAR data directory.
- * @access public
- * @static
- */
- static function getSchemaDir()
- {
- require_once 'PEAR/Config.php';
- $config = new PEAR_Config;
- return $config->get('data_dir') . '/XML_Feed_Parser/schemas';
- }
-}
-
-?> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml
deleted file mode 100755
index 02e1c5800..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-entryonly.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<entry xmlns="http://www.w3.org/2005/Atom">
- <title>Atom draft-07 snapshot</title>
- <link rel="alternate" type="text/html"
- href="http://example.org/2005/04/02/atom"/>
- <link rel='enclosure' type="audio/mpeg" length="1337"
- href="http://example.org/audio/ph34r_my_podcast.mp3"/>
- <id>tag:example.org,2003:3.2397</id>
- <updated>2005-07-10T12:29:29Z</updated>
- <published>2003-12-13T08:29:29-04:00</published>
- <author>
- <name>Mark Pilgrim</name>
- <uri>http://example.org/</uri>
- <email>f8dy@example.com</email>
- </author>
- <contributor>
- <name>Sam Ruby</name>
- </contributor>
- <contributor>
- <name>Joe Gregorio</name>
- </contributor>
- <content type="xhtml" xml:lang="en"
- xml:base="http://diveintomark.org/">
- <div xmlns="http://www.w3.org/1999/xhtml">
- <p><i>[Update: The Atom draft is finished.]</i></p>
- </div>
- </content>
- </entry> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml
deleted file mode 100755
index d181d2b6f..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example1.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<feed xmlns="http://www.w3.org/2005/Atom">
-
- <title>Example Feed</title>
- <link href="http://example.org/"/>
- <updated>2003-12-13T18:30:02Z</updated>
- <author>
- <name>John Doe</name>
- </author>
- <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
-
- <entry>
- <title>Atom-Powered Robots Run Amok</title>
- <link href="http://example.org/2003/12/13/atom03"/>
- <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
- <updated>2003-12-13T18:30:02Z</updated>
- <summary>Some text.</summary>
- </entry>
-
-</feed> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml b/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml
deleted file mode 100755
index 98abf9d54..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/atom10-example2.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
- <feed xmlns="http://www.w3.org/2005/Atom">
- <title type="text">dive into mark</title>
- <subtitle type="html">
- A &lt;em&gt;lot&lt;/em&gt; of effort
- went into making this effortless
- </subtitle>
- <updated>2005-07-31T12:29:29Z</updated>
- <id>tag:example.org,2003:3</id>
- <link rel="alternate" type="text/html"
- hreflang="en" href="http://example.org/"/>
- <link rel="self" type="application/atom+xml"
- href="http://example.org/feed.atom"/>
- <rights>Copyright (c) 2003, Mark Pilgrim</rights>
- <generator uri="http://www.example.com/" version="1.0">
- Example Toolkit
- </generator>
- <entry>
- <title>Atom draft-07 snapshot</title>
- <link rel="alternate" type="text/html"
- href="http://example.org/2005/04/02/atom"/>
- <link rel='enclosure' type="audio/mpeg" length="1337"
- href="http://example.org/audio/ph34r_my_podcast.mp3"/>
- <id>tag:example.org,2003:3.2397</id>
- <updated>2005-07-31T12:29:29Z</updated>
- <published>2003-12-13T08:29:29-04:00</published>
- <author>
- <name>Mark Pilgrim</name>
- <uri>http://example.org/</uri>
- <email>f8dy@example.com</email>
- </author>
- <contributor>
- <name>Sam Ruby</name>
- </contributor>
- <contributor>
- <name>Joe Gregorio</name>
- </contributor>
- <content type="xhtml" xml:lang="en"
- xml:base="http://diveintomark.org/">
- <div xmlns="http://www.w3.org/1999/xhtml">
- <p><i>[Update: The Atom draft is finished.]</i></p>
- </div>
- </content>
- </entry>
- </feed> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed b/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed
deleted file mode 100755
index 32f9fa493..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/delicious.feed
+++ /dev/null
@@ -1,177 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<rdf:RDF
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://purl.org/rss/1.0/"
- xmlns:cc="http://web.resource.org/cc/"
- xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
- xmlns:admin="http://webns.net/mvcb/"
->
-<channel rdf:about="http://del.icio.us/tag/greenbelt">
-<title>del.icio.us/tag/greenbelt</title>
-<link>http://del.icio.us/tag/greenbelt</link>
-<description>Text</description>
-<items>
- <rdf:Seq>
- <rdf:li rdf:resource="http://www.greenbelt.org.uk/" />
- <rdf:li rdf:resource="http://www.greenbelt.org.uk/" />
- <rdf:li rdf:resource="http://www.natuerlichwien.at/rundumadum/dergruenguertel/" />
- <rdf:li rdf:resource="http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars" />
- <rdf:li rdf:resource="http://www.greenbelt.ca/home.htm" />
- <rdf:li rdf:resource="http://pipwilsonbhp.blogspot.com/" />
- <rdf:li rdf:resource="http://maggidawn.typepad.com/maggidawn/" />
- <rdf:li rdf:resource="http://www.johndavies.org/" />
- <rdf:li rdf:resource="http://jonnybaker.blogs.com/" />
- </rdf:Seq>
-</items>
-</channel>
-
-<item rdf:about="http://www.greenbelt.org.uk/">
-<dc:title>Greenbelt - Homepage Section</dc:title>
-<link>http://www.greenbelt.org.uk/</link>
-<dc:creator>jonnybaker</dc:creator>
-<dc:date>2005-05-16T16:30:38Z</dc:date>
-<dc:subject>greenbelt</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/greenbelt" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.greenbelt.org.uk/">
-<title>Greenbelt festival (uk)</title>
-<link>http://www.greenbelt.org.uk/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2005-05-14T18:19:40Z</dc:date>
-<dc:subject>audiology festival gigs greenbelt</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/gigs" />
- <rdf:li resource="http://del.icio.us/tag/audiology" />
- <rdf:li resource="http://del.icio.us/tag/festival" />
- <rdf:li resource="http://del.icio.us/tag/greenbelt" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.natuerlichwien.at/rundumadum/dergruenguertel/">
-<title>Natuerlichwien.at - Rundumadum</title>
-<link>http://www.natuerlichwien.at/rundumadum/dergruenguertel/</link>
-<dc:creator>egmilman47</dc:creator>
-<dc:date>2005-05-06T21:33:41Z</dc:date>
-<dc:subject>Austria Vienna Wien greenbelt nature walking</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/Vienna" />
- <rdf:li resource="http://del.icio.us/tag/Wien" />
- <rdf:li resource="http://del.icio.us/tag/Austria" />
- <rdf:li resource="http://del.icio.us/tag/walking" />
- <rdf:li resource="http://del.icio.us/tag/nature" />
- <rdf:li resource="http://del.icio.us/tag/greenbelt" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars">
-<title>Tank - GBMediaWiki</title>
-<link>http://www.flickerweb.co.uk/wiki/index.php/Tank#Seminars</link>
-<dc:creator>jystewart</dc:creator>
-<dc:date>2005-03-21T22:44:11Z</dc:date>
-<dc:subject>greenbelt</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/greenbelt" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.greenbelt.ca/home.htm">
-<title>Greenbelt homepage</title>
-<link>http://www.greenbelt.ca/home.htm</link>
-<dc:creator>Gooberoo</dc:creator>
-<dc:date>2005-03-01T22:43:17Z</dc:date>
-<dc:subject>greenbelt ontario</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/ontario" />
- <rdf:li resource="http://del.icio.us/tag/greenbelt" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://pipwilsonbhp.blogspot.com/">
-<title>Pip Wilson bhp ...... blog</title>
-<link>http://pipwilsonbhp.blogspot.com/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2004-12-27T11:20:51Z</dc:date>
-<dc:subject>Greenbelt friend ideas links thinking weblog</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
- <rdf:li resource="http://del.icio.us/tag/thinking" />
- <rdf:li resource="http://del.icio.us/tag/ideas" />
- <rdf:li resource="http://del.icio.us/tag/links" />
- <rdf:li resource="http://del.icio.us/tag/friend" />
- <rdf:li resource="http://del.icio.us/tag/weblog" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://maggidawn.typepad.com/maggidawn/">
-<title>maggi dawn</title>
-<link>http://maggidawn.typepad.com/maggidawn/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2004-12-27T11:20:11Z</dc:date>
-<dc:subject>Greenbelt ideas links thinking weblog</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
- <rdf:li resource="http://del.icio.us/tag/thinking" />
- <rdf:li resource="http://del.icio.us/tag/ideas" />
- <rdf:li resource="http://del.icio.us/tag/links" />
- <rdf:li resource="http://del.icio.us/tag/weblog" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://www.johndavies.org/">
-<title>John Davies</title>
-<link>http://www.johndavies.org/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2004-12-27T11:18:37Z</dc:date>
-<dc:subject>Greenbelt ideas links thinking weblog</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
- <rdf:li resource="http://del.icio.us/tag/thinking" />
- <rdf:li resource="http://del.icio.us/tag/ideas" />
- <rdf:li resource="http://del.icio.us/tag/links" />
- <rdf:li resource="http://del.icio.us/tag/weblog" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-<item rdf:about="http://jonnybaker.blogs.com/">
-<title>jonnybaker</title>
-<link>http://jonnybaker.blogs.com/</link>
-<dc:creator>sssshhhh</dc:creator>
-<dc:date>2004-12-27T11:18:17Z</dc:date>
-<dc:subject>Greenbelt event ideas links resources thinking weblog youth</dc:subject>
-<taxo:topics>
- <rdf:Bag>
- <rdf:li resource="http://del.icio.us/tag/Greenbelt" />
- <rdf:li resource="http://del.icio.us/tag/thinking" />
- <rdf:li resource="http://del.icio.us/tag/ideas" />
- <rdf:li resource="http://del.icio.us/tag/links" />
- <rdf:li resource="http://del.icio.us/tag/weblog" />
- <rdf:li resource="http://del.icio.us/tag/youth" />
- <rdf:li resource="http://del.icio.us/tag/event" />
- <rdf:li resource="http://del.icio.us/tag/resources" />
- </rdf:Bag>
-</taxo:topics>
-</item>
-
-</rdf:RDF>
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed b/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed
deleted file mode 100755
index 57e83af57..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/flickr.feed
+++ /dev/null
@@ -1,184 +0,0 @@
-<?xml version="1.0" encoding="utf-8" standalone="yes"?>
-<feed version="0.3" xmlns="http://purl.org/atom/ns#"
- xmlns:dc="http://purl.org/dc/elements/1.1/">
-
- <title>jamesstewart - Everyone's Tagged Photos</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/tags/jamesstewart/"/>
- <link rel="icon" type="image/jpeg" href="http://www.flickr.com/images/buddyicon.jpg"/>
- <info type="text/html" mode="escaped">A feed of jamesstewart - Everyone's Tagged Photos</info>
- <modified>2005-08-01T18:50:26Z</modified>
- <generator url="http://www.flickr.com/">Flickr</generator>
-
- <entry>
- <title>Oma and James</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/30484029@N00/30367516/"/>
- <link rel='enclosure' type="application/xml" href="http://james.anthropiccollective.org" />
- <id>tag:flickr.com,2004:/photo/30367516</id>
- <issued>2005-08-01T18:50:26Z</issued>
- <modified>2005-08-01T18:50:26Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/30484029@N00/&quot;&gt;kstewart&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/30484029@N00/30367516/&quot; title=&quot;Oma and James&quot;&gt;&lt;img src=&quot;http://photos23.flickr.com/30367516_1f685a16e8_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Oma and James&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-&lt;p&gt;I have a beautiful Oma and a gorgeous husband.&lt;/p&gt;</content>
- <author>
- <name>kstewart</name>
- <url>http://www.flickr.com/people/30484029@N00/</url>
- </author>
- <dc:subject>jamesstewart oma stoelfamily</dc:subject>
- </entry>
- <entry>
- <title></title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21376174/"/>
- <id>tag:flickr.com,2004:/photo/21376174</id>
- <issued>2005-06-25T02:00:35Z</issued>
- <modified>2005-06-25T02:00:35Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21376174/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos17.flickr.com/21376174_4314fd8d5c_m.jpg&quot; width=&quot;240&quot; height=&quot;160&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-&lt;p&gt;AMA Motocross Championship 2005, Budds Creek, Maryland&lt;/p&gt;</content>
- <author>
- <name>Lan Rover</name>
- <url>http://www.flickr.com/people/buddscreek/</url>
- </author>
- <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational rickycarmichael 259 jamesstewart 4</dc:subject>
- </entry>
- <entry>
- <title></title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21375650/"/>
- <id>tag:flickr.com,2004:/photo/21375650</id>
- <issued>2005-06-25T01:56:24Z</issued>
- <modified>2005-06-25T01:56:24Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21375650/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos16.flickr.com/21375650_5c60e0dab1_m.jpg&quot; width=&quot;240&quot; height=&quot;160&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-</content>
- <author>
- <name>Lan Rover</name>
- <url>http://www.flickr.com/people/buddscreek/</url>
- </author>
- <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart</dc:subject>
- </entry>
- <entry>
- <title></title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/buddscreek/21375345/"/>
- <id>tag:flickr.com,2004:/photo/21375345</id>
- <issued>2005-06-25T01:54:11Z</issued>
- <modified>2005-06-25T01:54:11Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/buddscreek/&quot;&gt;Lan Rover&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/buddscreek/21375345/&quot; title=&quot;&quot;&gt;&lt;img src=&quot;http://photos15.flickr.com/21375345_4205fdd22b_m.jpg&quot; width=&quot;160&quot; height=&quot;240&quot; alt=&quot;&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-</content>
- <author>
- <name>Lan Rover</name>
- <url>http://www.flickr.com/people/buddscreek/</url>
- </author>
- <dc:subject>amamotocrosschampionship buddscreek maryland 2005 fathersday motocrossnational 259 jamesstewart</dc:subject>
- </entry>
- <entry>
- <title>Lunch with Kari &amp; James, café in the crypt of St Martin in the fields</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/16516618/"/>
- <id>tag:flickr.com,2004:/photo/16516618</id>
- <issued>2005-05-30T21:56:39Z</issued>
- <modified>2005-05-30T21:56:39Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/16516618/&quot; title=&quot;Lunch with Kari &amp;amp; James, café in the crypt of St Martin in the fields&quot;&gt;&lt;img src=&quot;http://photos14.flickr.com/16516618_afaa4a395e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Lunch with Kari &amp;amp; James, café in the crypt of St Martin in the fields&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-</content>
- <author>
- <name>fidothe</name>
- <url>http://www.flickr.com/people/fidothe/</url>
- </author>
- <dc:subject>nokia7610 london stmartininthefields clarepatterson jamesstewart parvinstewart jimstewart susanstewart</dc:subject>
- </entry>
- <entry>
- <title>Stewart keeping it low over the obstacle.</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/pqbon/10224728/"/>
- <id>tag:flickr.com,2004:/photo/10224728</id>
- <issued>2005-04-21T07:30:29Z</issued>
- <modified>2005-04-21T07:30:29Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/pqbon/&quot;&gt;pqbon&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/pqbon/10224728/&quot; title=&quot;Stewart keeping it low over the obstacle.&quot;&gt;&lt;img src=&quot;http://photos7.flickr.com/10224728_b756341957_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;Stewart keeping it low over the obstacle.&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-</content>
- <author>
- <name>pqbon</name>
- <url>http://www.flickr.com/people/pqbon/</url>
- </author>
- <dc:subject>ama hangtown motocross jamesstewart bubba</dc:subject>
- </entry>
- <entry>
- <title>king james stewart</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/jjlook/7152910/"/>
- <id>tag:flickr.com,2004:/photo/7152910</id>
- <issued>2005-03-22T21:53:37Z</issued>
- <modified>2005-03-22T21:53:37Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/jjlook/&quot;&gt;jj look&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/jjlook/7152910/&quot; title=&quot;king james stewart&quot;&gt;&lt;img src=&quot;http://photos7.flickr.com/7152910_a02ab5a750_m.jpg&quot; width=&quot;180&quot; height=&quot;240&quot; alt=&quot;king james stewart&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-&lt;p&gt;11th&lt;/p&gt;</content>
- <author>
- <name>jj look</name>
- <url>http://www.flickr.com/people/jjlook/</url>
- </author>
- <dc:subject>dilomar05 eastside austin texas 78702 kingjames stewart jamesstewart borrowed</dc:subject>
- </entry>
- <entry>
- <title>It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586562/"/>
- <id>tag:flickr.com,2004:/photo/1586562</id>
- <issued>2004-11-20T09:34:28Z</issued>
- <modified>2004-11-20T09:34:28Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586562/&quot; title=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586562_0bc5313a3e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-</content>
- <author>
- <name>fidothe</name>
- <url>http://www.flickr.com/people/fidothe/</url>
- </author>
- <dc:subject>holiday grandrapids jamesstewart</dc:subject>
- </entry>
- <entry>
- <title>It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586539/"/>
- <id>tag:flickr.com,2004:/photo/1586539</id>
- <issued>2004-11-20T09:28:16Z</issued>
- <modified>2004-11-20T09:28:16Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586539/&quot; title=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586539_c51e5f2e7a_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, downtown Grand Rapids (James, Susan, Jim, Harv, Lawson)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-</content>
- <author>
- <name>fidothe</name>
- <url>http://www.flickr.com/people/fidothe/</url>
- </author>
- <dc:subject>holiday grandrapids jamesstewart</dc:subject>
- </entry>
- <entry>
- <title>It's a Grind, James and Jim can't decide)</title>
- <link rel="alternate" type="text/html" href="http://www.flickr.com/photos/fidothe/1586514/"/>
- <id>tag:flickr.com,2004:/photo/1586514</id>
- <issued>2004-11-20T09:25:05Z</issued>
- <modified>2004-11-20T09:25:05Z</modified>
- <content type="text/html" mode="escaped">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/fidothe/&quot;&gt;fidothe&lt;/a&gt; posted a photo:&lt;/p&gt;
-
-&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/fidothe/1586514/&quot; title=&quot;It's a Grind, James and Jim can't decide)&quot;&gt;&lt;img src=&quot;http://photos2.flickr.com/1586514_733c2dfa3e_m.jpg&quot; width=&quot;240&quot; height=&quot;180&quot; alt=&quot;It's a Grind, James and Jim can't decide)&quot; style=&quot;border: 1px solid #000000;&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
-
-</content>
- <author>
- <name>fidothe</name>
- <url>http://www.flickr.com/people/fidothe/</url>
- </author>
- <dc:subject>holiday grandrapids jamesstewart johnkentish</dc:subject>
- </entry>
-
-</feed> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml b/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml
deleted file mode 100755
index c351d3c16..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/grwifi-atom.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="iso-8859-1"?> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en"> <title>Updates to Grand Rapids WiFi hotspot details</title> <link rel="alternate" type="text/html" href="http://grwifi.net/"/> <link rel="self" type="application/atom+xml" href="http://grwifi.net/atom/locations"/> <updated>2005-09-01T15:43:01-05:00</updated> <subtitle>WiFi Hotspots in Grand Rapids, MI</subtitle> <id>http://grwifi.net/atom/locations</id> <rights>Creative Commons Attribution-NonCommercial-ShareAlike 2.0 http://creativecommons.org/licenses/by-nc-sa/2.0/ </rights> <entry> <title>Hotspot Details Updated: Sweetwaters</title> <link rel="alternate" type="text/html" href="http://grwifi.net/location/sweetwaters"/> <id>http://grwifi.net/location/sweetwaters</id> <updated>2005-09-01T15:43:01-05:00</updated> <summary type="html"> The details of the WiFi hotspot at: Sweetwaters have been updated. Find out more at:
-http://grwifi.net/location/sweetwaters </summary> <author> <name>James</name> <uri>http://jystewart.net</uri> <email>james@jystewart.net</email> </author> <dc:subject>wifi hotspot</dc:subject> </entry> <entry> <title>Hotspot Details Updated: Common Ground Coffee Shop</title> <link rel="alternate" type="text/html" href="http://grwifi.net/location/common-ground"/> <id>http://grwifi.net/location/common-ground</id> <updated>2005-09-01T15:42:39-05:00</updated> <summary type="html"> The details of the WiFi hotspot at: Common Ground Coffee Shop have been updated. Find out more at:
-http://grwifi.net/location/common-ground </summary> <author> <name>James</name> <uri>http://jystewart.net</uri> <email>james@jystewart.net</email> </author> <dc:subject>wifi hotspot</dc:subject> </entry> <entry> <title>Hotspot Details Updated: Grand Rapids Public Library, Main Branch</title> <link rel="alternate" type="text/html" href="http://grwifi.net/location/grpl-main-branch"/> <id>http://grwifi.net/location/grpl-main-branch</id> <updated>2005-09-01T15:42:20-05:00</updated> <summary type="html"> The details of the WiFi hotspot at: Grand Rapids Public Library, Main Branch have been updated. Find out more at:
-http://grwifi.net/location/grpl-main-branch </summary> <author> <name>James</name> <uri>http://jystewart.net</uri> <email>james@jystewart.net</email> </author> <dc:subject>wifi hotspot</dc:subject> </entry> <entry> <title>Hotspot Details Updated: Four Friends Coffee House</title> <link rel="alternate" type="text/html" href="http://grwifi.net/location/four-friends"/> <id>http://grwifi.net/location/four-friends</id> <updated>2005-09-01T15:41:35-05:00</updated> <summary type="html"> The details of the WiFi hotspot at: Four Friends Coffee House have been updated. Find out more at:
-http://grwifi.net/location/four-friends </summary> <author> <name>James</name> <uri>http://jystewart.net</uri> <email>james@jystewart.net</email> </author> <dc:subject>wifi hotspot</dc:subject> </entry> <entry> <title>Hotspot Details Updated: Barnes and Noble, Rivertown Crossings</title> <link rel="alternate" type="text/html" href="http://grwifi.net/location/barnes-noble-rivertown"/> <id>http://grwifi.net/location/barnes-noble-rivertown</id> <updated>2005-09-01T15:40:41-05:00</updated> <summary type="html"> The details of the WiFi hotspot at: Barnes and Noble, Rivertown Crossings have been updated. Find out more at:
-http://grwifi.net/location/barnes-noble-rivertown </summary> <author> <name>James</name> <uri>http://jystewart.net</uri> <email>james@jystewart.net</email> </author> <dc:subject>wifi hotspot</dc:subject> </entry> <entry> <title>Hotspot Details Updated: The Boss Sports Bar &amp; Grille</title> <link rel="alternate" type="text/html" href="http://grwifi.net/location/boss-sports-bar"/> <id>http://grwifi.net/location/boss-sports-bar</id> <updated>2005-09-01T15:40:19-05:00</updated> <summary type="html"> The details of the WiFi hotspot at: The Boss Sports Bar &amp; Grille have been updated. Find out more at:
-http://grwifi.net/location/boss-sports-bar </summary> <author> <name>James</name> <uri>http://jystewart.net</uri> <email>james@jystewart.net</email> </author> <dc:subject>wifi hotspot</dc:subject> </entry> </feed> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml b/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml
deleted file mode 100755
index 099463570..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/hoder.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<rss version="2.0"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
- xmlns:admin="http://webns.net/mvcb/"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
-
-<channel>
-<title>Editor: Myself (Persian)</title>
-<link>http://editormyself.info</link>
-<description>This is a Persian (Farsi) weblog, written by Hossein Derakhshan (aka, Hoder), an Iranian Multimedia designer and a journalist who lives in Toronto since Dec 2000. He also keeps an English weblog with the same name.</description>
-<dc:language>en-us</dc:language>
-<dc:creator>hoder@hotmail.com</dc:creator>
-<dc:date>2005-10-12T19:45:32-05:00</dc:date>
-<admin:generatorAgent rdf:resource="http://www.movabletype.org/?v=3.15" />
-<sy:updatePeriod>hourly</sy:updatePeriod>
-<sy:updateFrequency>1</sy:updateFrequency>
-<sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
-
-
-<item>
-<title>لينکدونی‌ | جلسه‌ی امریکن انترپرایز برای تقسیم قومی ایران</title>
-<link>http://www.aei.org/events/type.upcoming,eventID.1166,filter.all/event_detail.asp</link>
-<description>چطور بعضی‌ها فکر می‌کنند دست راستی‌های آمریکا از خامنه‌ای ملی‌گراترند</description>
-<guid isPermaLink="false">14645@http://i.hoder.com/</guid>
-<dc:subject>iran</dc:subject>
-<dc:date>2005-10-12T19:45:32-05:00</dc:date>
-</item>
-
-<item>
-<title>لينکدونی‌ | به صبحانه آگهی بدهید</title>
-<link>http://www.adbrite.com/mb/commerce/purchase_form.php?opid=24346&amp;afsid=1</link>
-<description>خیلی ارزان و راحت است</description>
-<guid isPermaLink="false">14644@http://i.hoder.com/</guid>
-<dc:subject>media/journalism</dc:subject>
-<dc:date>2005-10-12T17:23:15-05:00</dc:date>
-</item>
-
-<item>
-<title>لينکدونی‌ | نیروی انتظامی چگونه تابوهای هم‌جنس‌گرایانه را می‌شکند؛ فرنگوپولیس</title>
-<link>http://farangeopolis.blogspot.com/2005/10/blog-post_08.html</link>
-<description>از پس و پیش و حاشیه‌ی این ماجرا می‌توان یک مستند بی‌نظیر ساخت</description>
-<guid isPermaLink="false">14643@http://i.hoder.com/</guid>
-<dc:subject>soc_popculture</dc:subject>
-<dc:date>2005-10-12T17:06:40-05:00</dc:date>
-</item>
-
-<item>
-<title>لينکدونی‌ | بازتاب توقیف شد</title>
-<link>http://www.baztab.com/news/30201.php</link>
-<description>اگر گفتید یک وب‌سایت را چطور توقیف می‌کنند؟ لابد ماوس‌شان را قایم می‌کنند.</description>
-<guid isPermaLink="false">14642@http://i.hoder.com/</guid>
-<dc:subject>media/journalism</dc:subject>
-<dc:date>2005-10-12T14:41:57-05:00</dc:date>
-</item>
-
-<item>
-<title>لينکدونی‌ | رشد وب در سال 2005 از همیشه بیشتر بوده است&quot; بی.بی.سی</title>
-<link>http://news.bbc.co.uk/2/hi/technology/4325918.stm</link>
-<description></description>
-<guid isPermaLink="false">14640@http://i.hoder.com/</guid>
-<dc:subject>tech</dc:subject>
-<dc:date>2005-10-12T13:04:46-05:00</dc:date>
-</item>
-
-
-
-<item>
-<title>==قرعه کشی گرین کارد به زودی شروع می‌شود==</title>
-<link>http://nice.newsxphotos.biz/05/09/2007_dv_lottery_registration_to_begin_oct_5_14589.php</link>
-<description></description>
-<guid isPermaLink="false">14613@http://vagrantly.com</guid>
-<dc:subject>ads03</dc:subject>
-<dc:date>2005-09-27T04:49:22-05:00</dc:date>
-</item>
-
-
-
-
-
-
-<item>
-<title>پروژه‌ی هاروارد، قدم دوم</title>
-<link>http://editormyself.info/archives/2005/10/051012_014641.shtml</link>
-<description><![CDATA[<p>اگر یادتان باشد <a href="/archives/2005/09/050906_014504.shtml">چند وقت پیش نوشتم</a> که دانشگاه هاروارد پروژه‌ای دارد با نام آواهای جهانی که در آن به وبلاگ‌های غیر انگلیسی‌زبان می‌پردازد. خواشتم که اگر کسی علاقه دارد ایمیل بزند. تعداد زیادی جواب دادند و ابراز علاقه کردند. حالا وقت قدم دوم است.</p>
-
-<p>قدم دوم این است که برای اینکه مسوولین پروژه بتوانند تصمیم بگیرند که با چه کسی کار کنند، می‌خواهند نمونه‌ی کارهای علاقمندان مشارکت در این پرزو‌ه را ببینند.</p>
-
-<p>برای همین از همه‌ی علاقماندان، حتی کسانی که قبلا اعلام آمادگی نکرده بودند، می‌‌خواهم که یک موضوع رایج این روزهای وبلاگستان فارسی را انتخاب کنند و در هفتصد کلمه، به انگلیسی، بنویسند که وبلاگ‌دارهای درباره‌اش چه می‌گویند. لینک به پنج، شش وبلاگ و بازنویسی آنچه آنها از جنبه‌های گوناگون درباره‌ی آن موضوع نوشته‌اند با نقل قول مستقیم از آنها (البته ترجمه شده از فارسی) کافی است. دو سه جمله هم اول کار توضیح دهید که چرا این موضوع مهم است.</p>
-
-<p>متن نمونه را به آدرس ایمیل من hoder@hoder.com و نیز برای افراد زیر تا روز دوشنبه بفرستید:<br />
-ربکا : rmackinnon@cyber.law.harvard.edu<br />
-هیثم: haitham.sabbah@gmail.com</p>]]></description>
-<guid isPermaLink="false">14641@http://editormyself.info</guid>
-<dc:subject>weblog</dc:subject>
-<dc:date>2005-10-12T14:04:23-05:00</dc:date>
-</item>
-
-
-
-</channel>
-</rss> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml b/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml
deleted file mode 100755
index 612186897..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/illformed_atom10.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<!--
-Description: entry author name
-Expect: bozo and entries[0]['author_detail']['name'] == u'Example author'
--->
-<feed xmlns="http://www.w3.org/2005/Atom">
-<entry>
- <author>
- <name>Example author</name>
- <email>me@example.com</email>
- <uri>http://example.com/</uri>
- </author>
-</entry>
-</feed \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml
deleted file mode 100755
index b0a1fee2d..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-complete.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
-<rss version="0.91">
-<channel>
-<copyright>Copyright 1997-1999 UserLand Software, Inc.</copyright>
-<pubDate>Thu, 08 Jul 1999 07:00:00 GMT</pubDate>
-<lastBuildDate>Thu, 08 Jul 1999 16:20:26 GMT</lastBuildDate>
-<docs>http://my.userland.com/stories/storyReader$11</docs>
-<description>News and commentary from the cross-platform scripting community.</description>
-<link>http://www.scripting.com/</link>
-<title>Scripting News</title>
-<image>
-<link>http://www.scripting.com/</link>
-<title>Scripting News</title>
-<url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
-<height>40</height>
-<width>78</width>
-<description>What is this used for?</description>
-</image>
-<managingEditor>dave@userland.com (Dave Winer)</managingEditor>
-<webMaster>dave@userland.com (Dave Winer)</webMaster>
-<language>en-us</language>
-<skipHours>
-<hour>6</hour>
-<hour>7</hour>
-<hour>8</hour>
-<hour>9</hour>
-<hour>10</hour>
-<hour>11</hour>
-</skipHours>
-<skipDays>
-<day>Sunday</day>
-</skipDays>
-<rating>(PICS-1.1 "http://www.rsac.org/ratingsv01.html" l gen true comment "RSACi North America Server" for "http://www.rsac.org" on "1996.04.16T08:15-0500" r (n 0 s 0 v 0 l 0))</rating>
-<item>
-<title>stuff</title>
-<link>http://bar</link>
-<description>This is an article about some stuff</description>
-</item>
-<textinput>
-<title>Search Now!</title>
-<description>Enter your search &lt;terms&gt;</description>
-<name>find</name>
-<link>http://my.site.com/search.cgi</link>
-</textinput>
-</channel>
-</rss> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml
deleted file mode 100755
index cfe91691f..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-international.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="EuC-JP"?>
-<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
-<rss version="0.91">
-<channel>
-<title>膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ</title>
-<link>http://www.mozilla.org</link>
-<description>膮ŸÛë´é´Ì´×´è´ŒÁ¹´Õ</description>
-<language>ja</language> <!-- tagged as Japanese content -->
-<item>
-<title>NYÒ™Á¢¸»ÌêÛì15285.25´ƒ´‘Á£´Û´—´ÀÁ¹´ê´Ì´éÒ™Ûì¡êçÒÕ‰ÌêÁ£</title>
-<link>http://www.mozilla.org/status/</link>
-<description>This is an item description...</description>
-</item>
-<item>
-<title>‚§±Çç¡ËßÛÂҏéøÓ¸Á£Ë²®Ÿè†Ûè危ÇÌ’¡Íæ—éøë‡Á£</title>
-<link>http://www.mozilla.org/status/</link>
-<description>This is an item description...</description>
-</item>
-<item>
-<title>ËÜË”ïÌëÈšÁ¢È†Ë§æàÀ豎ˉۂÁ¢Ë‚åܼšÛ˜íËüËÁ£</title>
-<link>http://www.mozilla.org/status/</link>
-<description>This is an item description...</description>
-</item>
-<item>
-<title>2000‚øíŠåÁ¢«‘¦éÛ빏ېçéÛ§ÛÂè†ÒæÓ¸Á£Ì¾«…æ—ÕÝéøƒ¸Á£</title>
-<link>http://www.mozilla.org/status/</link>
-<description>This is an item description...</description>
-</item>
-</channel>
-</rss> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml
deleted file mode 100755
index f0964a227..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/rss091-simple.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE rss SYSTEM "http://my.netscape.com/publish/formats/rss-0.91.dtd">
-<rss version="0.91">
-<channel>
-<language>en</language>
-<description>News and commentary from the cross-platform scripting community.</description>
-<link>http://www.scripting.com/</link>
-<title>Scripting News</title>
-<image>
-<link>http://www.scripting.com/</link>
-<title>Scripting News</title>
-<url>http://www.scripting.com/gifs/tinyScriptingNews.gif</url>
-</image>
-</channel>
-</rss> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml
deleted file mode 100755
index 5d75c352b..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/rss092-sample.xml
+++ /dev/null
@@ -1,103 +0,0 @@
-<?xml version="1.0"?>
-<!-- RSS generation done by 'Radio UserLand' on Fri, 13 Apr 2001 19:23:02 GMT -->
-<rss version="0.92">
- <channel>
- <title>Dave Winer: Grateful Dead</title>
- <link>http://www.scripting.com/blog/categories/gratefulDead.html</link>
- <description>A high-fidelity Grateful Dead song every day. This is where we&apos;re experimenting with enclosures on RSS news items that download when you&apos;re not using your computer. If it works (it will) it will be the end of the Click-And-Wait multimedia experience on the Internet. </description>
- <lastBuildDate>Fri, 13 Apr 2001 19:23:02 GMT</lastBuildDate>
- <docs>http://backend.userland.com/rss092</docs>
- <managingEditor>dave@userland.com (Dave Winer)</managingEditor>
- <webMaster>dave@userland.com (Dave Winer)</webMaster>
- <cloud domain="data.ourfavoritesongs.com" port="80" path="/RPC2" registerProcedure="ourFavoriteSongs.rssPleaseNotify" protocol="xml-rpc"/>
- <item>
- <description>It&apos;s been a few days since I added a song to the Grateful Dead channel. Now that there are all these new Radio users, many of whom are tuned into this channel (it&apos;s #16 on the hotlist of upstreaming Radio users, there&apos;s no way of knowing how many non-upstreaming users are subscribing, have to do something about this..). Anyway, tonight&apos;s song is a live version of Weather Report Suite from Dick&apos;s Picks Volume 7. It&apos;s wistful music. Of course a beautiful song, oft-quoted here on Scripting News. &lt;i&gt;A little change, the wind and rain.&lt;/i&gt;
-</description>
- <enclosure url="http://www.scripting.com/mp3s/weatherReportDicksPicsVol7.mp3" length="6182912" type="audio/mpeg"/>
- </item>
- <item>
- <description>Kevin Drennan started a &lt;a href=&quot;http://deadend.editthispage.com/&quot;&gt;Grateful Dead Weblog&lt;/a&gt;. Hey it&apos;s cool, he even has a &lt;a href=&quot;http://deadend.editthispage.com/directory/61&quot;&gt;directory&lt;/a&gt;. &lt;i&gt;A Frontier 7 feature.&lt;/i&gt;</description>
- <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source>
- </item>
- <item>
- <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/other1.html&quot;&gt;The Other One&lt;/a&gt;, live instrumental, One From The Vault. Very rhythmic very spacy, you can listen to it many times, and enjoy something new every time.</description>
- <enclosure url="http://www.scripting.com/mp3s/theOtherOne.mp3" length="6666097" type="audio/mpeg"/>
- </item>
- <item>
- <description>This is a test of a change I just made. Still diggin..</description>
- </item>
- <item>
- <description>The HTML rendering almost &lt;a href=&quot;http://validator.w3.org/check/referer&quot;&gt;validates&lt;/a&gt;. Close. Hey I wonder if anyone has ever published a style guide for ALT attributes on images? What are you supposed to say in the ALT attribute? I sure don&apos;t know. If you&apos;re blind send me an email if u cn rd ths. </description>
- </item>
- <item>
- <description>&lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Franklin&apos;s_Tower.txt&quot;&gt;Franklin&apos;s Tower&lt;/a&gt;, a live version from One From The Vault.</description>
- <enclosure url="http://www.scripting.com/mp3s/franklinsTower.mp3" length="6701402" type="audio/mpeg"/>
- </item>
- <item>
- <description>Moshe Weitzman says Shakedown Street is what I&apos;m lookin for for tonight. I&apos;m listening right now. It&apos;s one of my favorites. &quot;Don&apos;t tell me this town ain&apos;t got no heart.&quot; Too bright. I like the jazziness of Weather Report Suite. Dreamy and soft. How about The Other One? &quot;Spanish lady come to me..&quot;</description>
- <source url="http://scriptingnews.userland.com/xml/scriptingNews2.xml">Scripting News</source>
- </item>
- <item>
- <description>&lt;a href=&quot;http://www.scripting.com/mp3s/youWinAgain.mp3&quot;&gt;The news is out&lt;/a&gt;, all over town..&lt;p&gt;
-You&apos;ve been seen, out runnin round. &lt;p&gt;
-The lyrics are &lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/You_Win_Again.txt&quot;&gt;here&lt;/a&gt;, short and sweet. &lt;p&gt;
-&lt;i&gt;You win again!&lt;/i&gt;
-</description>
- <enclosure url="http://www.scripting.com/mp3s/youWinAgain.mp3" length="3874816" type="audio/mpeg"/>
- </item>
- <item>
- <description>&lt;a href=&quot;http://www.getlyrics.com/lyrics/grateful-dead/wake-of-the-flood/07.htm&quot;&gt;Weather Report Suite&lt;/a&gt;: &quot;Winter rain, now tell me why, summers fade, and roses die? The answer came. The wind and rain. Golden hills, now veiled in grey, summer leaves have blown away. Now what remains? The wind and rain.&quot;</description>
- <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" length="12216320" type="audio/mpeg"/>
- </item>
- <item>
- <description>&lt;a href=&quot;http://arts.ucsc.edu/gdead/agdl/darkstar.html&quot;&gt;Dark Star&lt;/a&gt; crashes, pouring its light into ashes.</description>
- <enclosure url="http://www.scripting.com/mp3s/darkStar.mp3" length="10889216" type="audio/mpeg"/>
- </item>
- <item>
- <description>DaveNet: &lt;a href=&quot;http://davenet.userland.com/2001/01/21/theUsBlues&quot;&gt;The U.S. Blues&lt;/a&gt;.</description>
- </item>
- <item>
- <description>Still listening to the US Blues. &lt;i&gt;&quot;Wave that flag, wave it wide and high..&quot;&lt;/i&gt; Mistake made in the 60s. We gave our country to the assholes. Ah ah. Let&apos;s take it back. Hey I&apos;m still a hippie. &lt;i&gt;&quot;You could call this song The United States Blues.&quot;&lt;/i&gt;</description>
- </item>
- <item>
- <description>&lt;a href=&quot;http://www.sixties.com/html/garcia_stack_0.html&quot;&gt;&lt;img src=&quot;http://www.scripting.com/images/captainTripsSmall.gif&quot; height=&quot;51&quot; width=&quot;42&quot; border=&quot;0&quot; hspace=&quot;10&quot; vspace=&quot;10&quot; align=&quot;right&quot;&gt;&lt;/a&gt;In celebration of today&apos;s inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the &lt;a href=&quot;http://searchlyrics2.homestead.com/gd_usblues.html&quot;&gt;lyrics&lt;/a&gt;. Click on the audio icon to the left to give it a listen. &quot;Red and white, blue suede shoes, I&apos;m Uncle Sam, how do you do?&quot; It&apos;s a different kind of patriotic music, but man I love my country and I love Jerry and the band. &lt;i&gt;I truly do!&lt;/i&gt;</description>
- <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/>
- </item>
- <item>
- <description>Grateful Dead: &quot;Tennessee, Tennessee, ain&apos;t no place I&apos;d rather be.&quot;</description>
- <enclosure url="http://www.scripting.com/mp3s/tennesseeJed.mp3" length="3442648" type="audio/mpeg"/>
- </item>
- <item>
- <description>Ed Cone: &quot;Had a nice Deadhead experience with my wife, who never was one but gets the vibe and knows and likes a lot of the music. Somehow she made it to the age of 40 without ever hearing Wharf Rat. We drove to Jersey and back over Christmas with the live album commonly known as Skull and Roses in the CD player much of the way, and it was cool to see her discover one the band&apos;s finest moments. That song is unique and underappreciated. Fun to hear that disc again after a few years off -- you get Jerry as blues-guitar hero on Big Railroad Blues and a nice version of Bertha.&quot;</description>
- <enclosure url="http://www.scripting.com/mp3s/darkStarWharfRat.mp3" length="27503386" type="audio/mpeg"/>
- </item>
- <item>
- <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/fotd.html&quot;&gt;Tonight&apos;s Song&lt;/a&gt;: &quot;If I get home before daylight I just might get some sleep tonight.&quot; </description>
- <enclosure url="http://www.scripting.com/mp3s/friendOfTheDevil.mp3" length="3219742" type="audio/mpeg"/>
- </item>
- <item>
- <description>&lt;a href=&quot;http://arts.ucsc.edu/GDead/AGDL/uncle.html&quot;&gt;Tonight&apos;s song&lt;/a&gt;: &quot;Come hear Uncle John&apos;s Band by the river side. Got some things to talk about here beside the rising tide.&quot;</description>
- <enclosure url="http://www.scripting.com/mp3s/uncleJohnsBand.mp3" length="4587102" type="audio/mpeg"/>
- </item>
- <item>
- <description>&lt;a href=&quot;http://www.cs.cmu.edu/~mleone/gdead/dead-lyrics/Me_and_My_Uncle.txt&quot;&gt;Me and My Uncle&lt;/a&gt;: &quot;I loved my uncle, God rest his soul, taught me good, Lord, taught me all I know. Taught me so well, I grabbed that gold and I left his dead ass there by the side of the road.&quot;
-</description>
- <enclosure url="http://www.scripting.com/mp3s/meAndMyUncle.mp3" length="2949248" type="audio/mpeg"/>
- </item>
- <item>
- <description>Truckin, like the doo-dah man, once told me gotta play your hand. Sometimes the cards ain&apos;t worth a dime, if you don&apos;t lay em down.</description>
- <enclosure url="http://www.scripting.com/mp3s/truckin.mp3" length="4847908" type="audio/mpeg"/>
- </item>
- <item>
- <description>Two-Way-Web: &lt;a href=&quot;http://www.thetwowayweb.com/payloadsForRss&quot;&gt;Payloads for RSS&lt;/a&gt;. &quot;When I started talking with Adam late last year, he wanted me to think about high quality video on the Internet, and I totally didn&apos;t want to hear about it.&quot;</description>
- </item>
- <item>
- <description>A touch of gray, kinda suits you anyway..</description>
- <enclosure url="http://www.scripting.com/mp3s/touchOfGrey.mp3" length="5588242" type="audio/mpeg"/>
- </item>
- <item>
- <description>&lt;a href=&quot;http://www.sixties.com/html/garcia_stack_0.html&quot;&gt;&lt;img src=&quot;http://www.scripting.com/images/captainTripsSmall.gif&quot; height=&quot;51&quot; width=&quot;42&quot; border=&quot;0&quot; hspace=&quot;10&quot; vspace=&quot;10&quot; align=&quot;right&quot;&gt;&lt;/a&gt;In celebration of today&apos;s inauguration, after hearing all those great patriotic songs, America the Beautiful, even The Star Spangled Banner made my eyes mist up. It made my choice of Grateful Dead song of the night realllly easy. Here are the &lt;a href=&quot;http://searchlyrics2.homestead.com/gd_usblues.html&quot;&gt;lyrics&lt;/a&gt;. Click on the audio icon to the left to give it a listen. &quot;Red and white, blue suede shoes, I&apos;m Uncle Sam, how do you do?&quot; It&apos;s a different kind of patriotic music, but man I love my country and I love Jerry and the band. &lt;i&gt;I truly do!&lt;/i&gt;</description>
- <enclosure url="http://www.scripting.com/mp3s/usBlues.mp3" length="5272510" type="audio/mpeg"/>
- </item>
- </channel>
- </rss> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml
deleted file mode 100755
index 0edecf58e..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example1.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0"?>
-
-<rdf:RDF
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://purl.org/rss/1.0/"
->
-
- <channel rdf:about="http://www.xml.com/xml/news.rss">
- <title>XML.com</title>
- <link>http://xml.com/pub</link>
- <description>
- XML.com features a rich mix of information and services
- for the XML community.
- </description>
-
- <image rdf:resource="http://xml.com/universal/images/xml_tiny.gif" />
-
- <items>
- <rdf:Seq>
- <rdf:li resource="http://xml.com/pub/2000/08/09/xslt/xslt.html" />
- <rdf:li resource="http://xml.com/pub/2000/08/09/rdfdb/index.html" />
- </rdf:Seq>
- </items>
-
- <textinput rdf:resource="http://search.xml.com" />
-
- </channel>
-
- <image rdf:about="http://xml.com/universal/images/xml_tiny.gif">
- <title>XML.com</title>
- <link>http://www.xml.com</link>
- <url>http://xml.com/universal/images/xml_tiny.gif</url>
- </image>
-
- <item rdf:about="http://xml.com/pub/2000/08/09/xslt/xslt.html">
- <title>Processing Inclusions with XSLT</title>
- <link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>
- <description>
- Processing document inclusions with general XML tools can be
- problematic. This article proposes a way of preserving inclusion
- information through SAX-based processing.
- </description>
- </item>
-
- <item rdf:about="http://xml.com/pub/2000/08/09/rdfdb/index.html">
- <title>Putting RDF to Work</title>
- <link>http://xml.com/pub/2000/08/09/rdfdb/index.html</link>
- <description>
- Tool and API support for the Resource Description Framework
- is slowly coming of age. Edd Dumbill takes a look at RDFDB,
- one of the most exciting new RDF toolkits.
- </description>
- </item>
-
- <textinput rdf:about="http://search.xml.com">
- <title>Search XML.com</title>
- <description>Search XML.com's XML collection</description>
- <name>s</name>
- <link>http://search.xml.com</link>
- </textinput>
-
-</rdf:RDF> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml
deleted file mode 100755
index 26235f78f..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/rss10-example2.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<rdf:RDF
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
- xmlns:co="http://purl.org/rss/1.0/modules/company/"
- xmlns:ti="http://purl.org/rss/1.0/modules/textinput/"
- xmlns="http://purl.org/rss/1.0/"
->
-
- <channel rdf:about="http://meerkat.oreillynet.com/?_fl=rss1.0">
- <title>Meerkat</title>
- <link>http://meerkat.oreillynet.com</link>
- <description>Meerkat: An Open Wire Service</description>
- <dc:publisher>The O'Reilly Network</dc:publisher>
- <dc:creator>Rael Dornfest (mailto:rael@oreilly.com)</dc:creator>
- <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
- <dc:date>2000-01-01T12:00+00:00</dc:date>
- <sy:updatePeriod>hourly</sy:updatePeriod>
- <sy:updateFrequency>2</sy:updateFrequency>
- <sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
-
- <image rdf:resource="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg" />
-
- <items>
- <rdf:Seq>
- <rdf:li resource="http://c.moreover.com/click/here.pl?r123" />
- </rdf:Seq>
- </items>
-
- <textinput rdf:resource="http://meerkat.oreillynet.com" />
-
- </channel>
-
- <image rdf:about="http://meerkat.oreillynet.com/icons/meerkat-powered.jpg">
- <title>Meerkat Powered!</title>
- <url>http://meerkat.oreillynet.com/icons/meerkat-powered.jpg</url>
- <link>http://meerkat.oreillynet.com</link>
- </image>
-
- <item rdf:about="http://c.moreover.com/click/here.pl?r123">
- <title>XML: A Disruptive Technology</title>
- <link>http://c.moreover.com/click/here.pl?r123</link>
- <dc:description>
- XML is placing increasingly heavy loads on the existing technical
- infrastructure of the Internet.
- </dc:description>
- <dc:publisher>The O'Reilly Network</dc:publisher>
- <dc:creator>Simon St.Laurent (mailto:simonstl@simonstl.com)</dc:creator>
- <dc:rights>Copyright &#169; 2000 O'Reilly &amp; Associates, Inc.</dc:rights>
- <dc:subject>XML</dc:subject>
- <co:name>XML.com</co:name>
- <co:market>NASDAQ</co:market>
- <co:symbol>XML</co:symbol>
- </item>
-
- <textinput rdf:about="http://meerkat.oreillynet.com">
- <title>Search Meerkat</title>
- <description>Search Meerkat's RSS Database...</description>
- <name>s</name>
- <link>http://meerkat.oreillynet.com/</link>
- <ti:function>search</ti:function>
- <ti:inputType>regex</ti:inputType>
- </textinput>
-
-</rdf:RDF> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml b/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml
deleted file mode 100755
index 53483cc51..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/rss2sample.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0"?>
-<rss version="2.0" xmlns:content="http://purl.org/rss/1.0.modules/content/">
- <channel>
- <title>Liftoff News</title>
- <link>http://liftoff.msfc.nasa.gov/</link>
- <description>Liftoff to Space Exploration.</description>
- <language>en-us</language>
- <pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>
- <lastBuildDate>Tue, 10 Jun 2003 09:41:01 GMT</lastBuildDate>
- <docs>http://blogs.law.harvard.edu/tech/rss</docs>
- <generator>Weblog Editor 2.0</generator>
- <managingEditor>editor@example.com</managingEditor>
- <webMaster>webmaster@example.com</webMaster>
- <item>
- <title>Star City</title>
- <link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
- <description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's &lt;a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm"&gt;Star City&lt;/a&gt;.</description>
- <pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate>
- <guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>
- </item>
- <item>
- <description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.</description>
- <pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>
- <guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid>
- </item>
- <item>
- <title>The Engine That Does More</title>
- <link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>
- <description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.</description>
- <pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>
- <guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>
- <content:encoded><![CDATA[<p>Test content</p>]]></content:encoded>
- </item>
- <item>
- <title>Astronauts' Dirty Laundry</title>
- <link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>
- <description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.</description>
- <pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>
- <guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>
- </item>
- </channel>
-</rss> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml b/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml
deleted file mode 100755
index f8a04bba5..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/sixapart-jp.xml
+++ /dev/null
@@ -1,226 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<rss version="2.0">
-<channel>
-<title>Six Apart - News</title>
-<link>http://www.sixapart.jp/</link>
-<description></description>
-<language>ja</language>
-<copyright>Copyright 2005</copyright>
-<lastBuildDate>Fri, 07 Oct 2005 19:09:34 +0900</lastBuildDate>
-<generator>http://www.movabletype.org/?v=3.2-ja</generator>
-<docs>http://blogs.law.harvard.edu/tech/rss</docs>
-
-<item>
-<title>ファイブ・ディーが、Movable Typeでブログプロモーションをスタート</title>
-<description><![CDATA[<p><img alt="MIYAZAWAblog_banner.jpg" src="http://www.sixapart.jp/MIYAZAWAblog_banner.jpg" width="200" height="88" align="right" /><br />
-ファイブ・ディーは、Movable Typeで構築したプロモーション ブログ『宮沢和史 中南米ツアーblog Latin America 2005』を開設しました。</p>
-
-<p>9月21日に開設されたこのブログは、ブラジル、ホンジュラス、ニカラグア、メキシコ、キューバの5か国を巡る「Latin America 2005」ツアーに合わせ、そのツアーの模様を同行マネージャーがレポートしていきます。<br />
-さらに今月2日からは宮沢和史自身が日々録音した声をPodcastingするという点でも、ブログを使ったユニークなプロモーションとなっています。</p>
-
-<p><a href="http://www.five-d.co.jp/miyazawa/jp/blog/la2005/">「宮沢和史 中南米ツアーblog Latin America 2005」</a></p>
-
-<p>※シックス・アパートではこうしたブログを使ったプロモーションに最適な製品をご用意しております。<br />
-<ul><li><a href="/movabletype/">Movable Type</a><br />
-<li><a href="/typepad/typepad_promotion.html">TypePad Promotion</a><br />
-</ul></p>]]></description>
-<link>http://www.sixapart.jp/news/2005/10/07-1909.html</link>
-<guid>http://www.sixapart.jp/news/2005/10/07-1909.html</guid>
-<category>news</category>
-<pubDate>Fri, 07 Oct 2005 19:09:34 +0900</pubDate>
-</item>
-<item>
-<title>Movable Type 3.2日本語版の提供を開始</title>
-<description><![CDATA[<p><img alt="Movable Type Logo" src="/images/mt3-logo-small.gif" width="151" height="37"/></p>
-<p>シックス・アパートは、Movable Type 3.2日本語版の提供を開始いたしました。<br />
-ベータテストにご協力いただいた多くの皆様に、スタッフ一同、心から感謝いたします。</p>
-<p>製品概要など、詳しくは<a href="http://www.sixapart.jp/press_releases/2005/09/29-1529.html" title="Six Apart - News: シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始">プレスリリース</a>をご参照下さい。</p>
-<p>ご購入のご検討は、<a href="http://www.sixapart.jp/movabletype/purchase-mt.html">Movable Typeのご購入</a>からどうぞ。</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/29-1530.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/29-1530.html</guid>
-<category>news</category>
-<pubDate>Thu, 29 Sep 2005 15:30:00 +0900</pubDate>
-</item>
-<item>
-<title>シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始</title>
-<description><![CDATA[<p><プレスリリース資料></p>
-<ul>
- <li><a href="http://www.sixapart.jp/sixapart20050929.pdf">印刷用(PDF版)</a></li>
-</ul>
-<p><strong>シックス・アパートが、スパム対策強化の「Movable Type 3.2 日本語版」を提供開始 ~ スパムの自動判別機能や新ユーザー・インターフェースで、運用管理の機能を強化 ~</strong></p>
-<p>2005年9月29日<br />
-シックス・アパート株式会社</p>
-<p>ブログ・ソフトウェア大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、「Movable Type(ムーバブル・タイプ) 3.2 日本語版」(URL:<a href="http://www.sixapart.jp/movabletype/">http://www.sixapart.jp/movabletype/</a>)を9月29日より提供開始いたします。</p>]]></description>
-<link>http://www.sixapart.jp/press_releases/2005/09/29-1529.html</link>
-<guid>http://www.sixapart.jp/press_releases/2005/09/29-1529.html</guid>
-<category>Press Releases</category>
-<pubDate>Thu, 29 Sep 2005 15:29:00 +0900</pubDate>
-</item>
-<item>
-<title>スタッフを募集しています</title>
-<description><![CDATA[<p>シックス・アパートはMovable TypeやTypePadの開発エンジニアなど、スタッフを広く募集しています。具体的な募集職種は次の通りです。</p>
-
-<ul>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0007.html">Movable Type開発エンジニア</a></li>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0004.html">TypePad開発エンジニア</a></li>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0003.html">カスタマーサポート・ディレクター</a></li>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0002.html">マーケティング・広報アシスタント</a></li>
-<li><a href="http://www.sixapart.jp/jobs/2005/09/13-0001.html">開発アシスタント</a></li>
-</ul>
-
-<p>拡大を続ける、日本のブログ市場を積極的にリードする人材を、シックス・アパートは募集しています。上記以外の職種につきましても、お気軽にお問い合わせください。詳しい募集要項や応募方法については、<a href="/jobs/">求人情報のページ</a>をご覧ください。<br />
-</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/27-0906.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/27-0906.html</guid>
-<category>news</category>
-<pubDate>Tue, 27 Sep 2005 09:06:10 +0900</pubDate>
-</item>
-<item>
-<title>サイト接続不具合に関するお詫びと復旧のお知らせ</title>
-<description><![CDATA[<p>9月24日(土)の14:45ごろから、同日18:30ごろまで、シックス・アパート社のウェブサイトが不安定になっており、断続的に接続できない不具合が発生しておりました。このため、この期間中にウェブサイトの閲覧や製品のダウンロードができませんでした。</p>
-
-<p>なお現在は不具合は解消しております。みなさまにご迷惑をおかけしたことをお詫びいたします。</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/26-1000.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/26-1000.html</guid>
-<category>news</category>
-<pubDate>Mon, 26 Sep 2005 10:00:56 +0900</pubDate>
-</item>
-<item>
-<title>企業ブログ向けパッケージ「TypePad Promotion」を新発売</title>
-<description><![CDATA[<p>シックス・アパートは、ウェブログ・サービスTypePadの企業ブログ向けパッケージ「TypePad Promotion」(タイプパッド・プロモーションの発売を10月下旬から開始いたします。</p>
-
-<p>詳しくは、<a href="http://www.sixapart.jp/press_releases/2005/09/20-1500.html" title="プレスリリース: 「TypePad Promotion」新発売">プレスリリース</a>をご参照下さい。</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/20-1500.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/20-1500.html</guid>
-<category>news</category>
-<pubDate>Tue, 20 Sep 2005 15:00:01 +0900</pubDate>
-</item>
-<item>
-<title>シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売</title>
-<description><![CDATA[<p><プレスリリース資料><br />
-<a href="http://www.sixapart.jp/sixapart20050920.pdf">印刷用(PDF版)</a></p>
-
-<p><br />
-<strong>シックス・アパートが、法人向けブログパッケージ「TypePad Promotion」を発売<br />
-~PR/IRサイトやキャンペーンサイトなど企業のプロモーションニーズに特化~<br />
-</strong><br />
-2005年9月20日<br />
-シックス・アパート株式会社</p>
-
-<p>ブログ・サービス大手のシックス・アパート株式会社(本社:東京都港区、代表取締役:関 信浩)は、法人向けプロモーションブログ・パッケージ「TypePad Promotion(タイプパッド・プロモーション)」(URL:<a href="http://www.sixapart.jp/typepad/typepad_promotion.html">http://www.sixapart.jp/typepad/typepad_promotion.html</a>)を10月下旬より販売開始いたします。</p>]]></description>
-<link>http://www.sixapart.jp/press_releases/2005/09/20-1500.html</link>
-<guid>http://www.sixapart.jp/press_releases/2005/09/20-1500.html</guid>
-<category>Press Releases</category>
-<pubDate>Tue, 20 Sep 2005 15:00:00 +0900</pubDate>
-</item>
-<item>
-<title>Six [days] Apart Week</title>
-<description><![CDATA[<p>本日、9月16日はSix Apartの創業者ミナ・トロットの誕生日です。<br />
-私たちの会社は、創業者のトロット夫妻(ベンとミナ)の誕生日が、6日離れていることからSix [days] Apart →Six Apartという風に名付けられています。本日から22日までの6日間を社名の由来となる Six [days] Apart Weekとして、私たちのプロダクトをご紹介させていただきます。</p>
-
-<p>今日は、ブログ・サービスのTypePad(タイプパッド)をご紹介します。<br />
-<img alt="tp-logo.gif" src="http://www.sixapart.jp/tp-logo.gif" width="227" height="52" /></p>
-
-<p>TypePadは、米国PC MAGAZINE誌の2003年EDITOR'S CHOICE とBEST OF THE YEARに選ばれております。<br />
-<img alt="pcmag-ad.gif" src="http://www.sixapart.jp/pcmag-ad.gif" width="297" height="100" /><br />
-</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/16-1941.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/16-1941.html</guid>
-<category>news</category>
-<pubDate>Fri, 16 Sep 2005 19:41:47 +0900</pubDate>
-</item>
-<item>
-<title>ハイパーワークスが商用フォントを利用できるMovable Typeホスティングサービスを開始</title>
-<description><![CDATA[<p>ソフト開発会社の<a href="http://www.hyperwrx.co.jp/">有限会社ハイパーワークス</a>は、商用フォントなど多彩なフォントをブログ上で利用できるブログ・サービス「<a href="http://glyph-on.jp/">Glyph-On!(グリフォン) Movable Type ホスティング サービス</a>」の提供を開始しました。<br />
-</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/14-1700.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/14-1700.html</guid>
-<category>news</category>
-<pubDate>Wed, 14 Sep 2005 17:00:00 +0900</pubDate>
-</item>
-<item>
-<title>Movable Type開発エンジニアの募集</title>
-<description><![CDATA[<p>
-勤務形態: フルタイム<br />
-勤務地: 東京 (赤坂)<br />
-職種: ソフトウェア・エンジニア<br />
-職務内容: Movable Typeの開発業務全般<br />
-募集人数: 若干名
-</p>]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0007.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0007.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:07:00 +0900</pubDate>
-</item>
-<item>
-<title>TypePad開発エンジニアの募集</title>
-<description><![CDATA[<p>
-勤務形態: フルタイム<br />
-勤務地: 東京 (赤坂)<br />
-職種: アプリケーション・エンジニア<br />
-職務内容: TypePadのカスタマイズ、周辺開発<br />
-募集人数: 若干名
-</p>]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0004.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0004.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:04:00 +0900</pubDate>
-</item>
-<item>
-<title>カスタマーサポート・ディレクターの募集</title>
-<description><![CDATA[<p>勤務形態: フルタイム<br />
-勤務地: 東京(赤坂)<br />
-職種: カスタマーサポート・ディレクター<br />
-職務内容: TypePadやMovable Typeのカスタマーサポート業務の統括<br />
-募集人数: 若干名
-</p>
-]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0003.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0003.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:03:30 +0900</pubDate>
-</item>
-<item>
-<title>アルバイト(マーケティング・広報アシスタント)の募集</title>
-<description><![CDATA[<p>勤務形態: アルバイト<br />
-勤務地: 東京(港区)<br />
-職種:マーケティング・PRのアシスタント業務<br />
-募集人数: 若干名<br />
-時給:1000円~(但し、試用期間終了後に応相談)。交通費支給<br />
-時間:平日10時30分~18時30分まで。週3日以上(応相談)<br />
-</p>]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0002.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0002.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:02:00 +0900</pubDate>
-</item>
-<item>
-<title>アルバイト(開発アシスタント)の募集</title>
-<description><![CDATA[<p>勤務形態: アルバイト<br />
-勤務地: 東京(港区)<br />
-職種: アプリケーション開発のアシスタント業務<br />
-募集人数: 若干名<br />
-時給:1000円~(但し、試用期間終了後に応相談)。交通費支給<br />
-時間:平日10時30分~18時30分まで。週3日以上(応相談)
-</p>]]></description>
-<link>http://www.sixapart.jp/jobs/2005/09/13-0001.html</link>
-<guid>http://www.sixapart.jp/jobs/2005/09/13-0001.html</guid>
-<category>Jobs</category>
-<pubDate>Tue, 13 Sep 2005 00:01:00 +0900</pubDate>
-</item>
-<item>
-<title>TypePad Japan がバージョンアップしました。</title>
-<description><![CDATA[<p><a href="http://www.sixapart.jp/typepad/">「TypePad Japan(タイプパッドジャパン)」</a>において、本日、「TypePad 1.6 日本語版」へのバージョンアップを行いました。最新版となる「TypePad 1.6 日本語版」では、ブログデザインの機能強化、ポッドキャスティング対応、モブログ対応に加え、今回新たに大幅な容量アップが行われております。皆様、新しくなった<a href="http://www.sixapart.jp/typepad/">TypePad Japan</a>にどうぞご期待ください。</p>
-
-<p>なお、TypePadの携帯対応強化に関しましては、本日よりTypePad Japanのお客様を対象にオープン・ベータを開始しております。</p>
-
-<p>2005年9月5日発表のTypePad日本語版 1.6プレスリリースは<a href="http://www.sixapart.jp/press_releases/2005/09/05-1420.html">こちら</a>をご覧下さい。</p>]]></description>
-<link>http://www.sixapart.jp/news/2005/09/12-1953.html</link>
-<guid>http://www.sixapart.jp/news/2005/09/12-1953.html</guid>
-<category>news</category>
-<pubDate>Mon, 12 Sep 2005 19:53:07 +0900</pubDate>
-</item>
-
-
-</channel>
-</rss> \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed b/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed
deleted file mode 100755
index 6274a32cd..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/samples/technorati.feed
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<rss version="2.0"
- xmlns:tapi="http://api.technorati.com/dtd/tapi-002.xml">
- <channel>
- <title>[Technorati] Tag results for greenbelt</title>
- <link>http://www.technorati.com/tag/greenbelt</link>
- <description>Posts tagged with "greenbelt" on Technorati.</description>
- <pubDate>Mon, 08 Aug 2005 15:15:08 GMT</pubDate>
- <category domain="http://www.technorati.com/tag">greenbelt</category>
- <tapi:inboundblogs>2</tapi:inboundblogs>
- <tapi:inboundlinks>2</tapi:inboundlinks>
- <cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="myCloud.rssPleaseNotify" protocol="xml-rpc" />
- <generator>Technorati v1.0</generator>
- <image>
- <url>http://static.technorati.com/pix/logos/logo_reverse_sm.gif</url>
- <title>Technorati logo</title>
- <link>http://www.technorati.com</link>
- </image>
- <skipHours>
- <hour>1</hour>
- <hour>7</hour>
- <hour>9</hour>
- </skipHours>
- <webMaster>support@technorati.com (Technorati Support)</webMaster>
- <docs>http://blogs.law.harvad.edu/tech/rss</docs>
- <ttl>60</ttl>
- <item>
- <title>Greenbelt</title>
- <link>http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html</link>
- <description>So if the plan goes according to plan (!)... I'll be speaking at Greenbelt at these times: Slot 1...</description>
- <guid isPermaLink="true">http://maggidawn.typepad.com/maggidawn/2005/07/greenbelt.html</guid>
- <pubDate>Mon, 18 Jul 2005 02:11:42 GMT</pubDate>
- <category>James</category>
- <tapi:linkcreated>2005-07-11 02:08:12</tapi:linkcreated>
- <comments>http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fmaggidawn.typepad.com%2Fmaggidawn%2F2005%2F07%2Fgreenbelt.html</comments>
- <tapi:inboundblogs>190</tapi:inboundblogs>
- <tapi:inboundlinks>237</tapi:inboundlinks>
- <source url="http://maggidawn.typepad.com/maggidawn/index.rdf">maggi dawn</source>
- </item>
-
- <item>
- <title>Walking along the Greenbelt</title>
- <link>http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html</link>
- <description>[IMG] Photo of homeless man walking near the greenbelt in Boise, Idaho Tags: photo homeless greenbelt Boise Idaho picture</description>
- <guid isPermaLink="true">http://pictureshomeless.blogspot.com/2005/06/walking-along-greenbelt.html</guid>
- <pubDate>Tue, 28 Jun 2005 01:41:24 GMT</pubDate>
- <tapi:linkcreated>2005-06-26 17:24:03</tapi:linkcreated>
- <comments>http://www.technorati.com/cosmos/search.html?url=http%3A%2F%2Fpictureshomeless.blogspot.com%2F2005%2F06%2Fwalking-along-greenbelt.html</comments>
- <tapi:inboundblogs>2</tapi:inboundblogs>
- <tapi:inboundlinks>2</tapi:inboundlinks>
- </item>
-
- </channel>
-</rss>
diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc
deleted file mode 100755
index e662d2626..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/schemas/atom.rnc
+++ /dev/null
@@ -1,338 +0,0 @@
-# -*- rnc -*-
-# RELAX NG Compact Syntax Grammar for the
-# Atom Format Specification Version 11
-
-namespace atom = "http://www.w3.org/2005/Atom"
-namespace xhtml = "http://www.w3.org/1999/xhtml"
-namespace s = "http://www.ascc.net/xml/schematron"
-namespace local = ""
-
-start = atomFeed | atomEntry
-
-# Common attributes
-
-atomCommonAttributes =
- attribute xml:base { atomUri }?,
- attribute xml:lang { atomLanguageTag }?,
- undefinedAttribute*
-
-# Text Constructs
-
-atomPlainTextConstruct =
- atomCommonAttributes,
- attribute type { "text" | "html" }?,
- text
-
-atomXHTMLTextConstruct =
- atomCommonAttributes,
- attribute type { "xhtml" },
- xhtmlDiv
-
-atomTextConstruct = atomPlainTextConstruct | atomXHTMLTextConstruct
-
-# Person Construct
-
-atomPersonConstruct =
- atomCommonAttributes,
- (element atom:name { text }
- & element atom:uri { atomUri }?
- & element atom:email { atomEmailAddress }?
- & extensionElement*)
-
-# Date Construct
-
-atomDateConstruct =
- atomCommonAttributes,
- xsd:dateTime
-
-# atom:feed
-
-atomFeed =
- [
- s:rule [
- context = "atom:feed"
- s:assert [
- test = "atom:author or not(atom:entry[not(atom:author)])"
- "An atom:feed must have an atom:author unless all "
- ~ "of its atom:entry children have an atom:author."
- ]
- ]
- ]
- element atom:feed {
- atomCommonAttributes,
- (atomAuthor*
- & atomCategory*
- & atomContributor*
- & atomGenerator?
- & atomIcon?
- & atomId
- & atomLink*
- & atomLogo?
- & atomRights?
- & atomSubtitle?
- & atomTitle
- & atomUpdated
- & extensionElement*),
- atomEntry*
- }
-
-# atom:entry
-
-atomEntry =
- [
- s:rule [
- context = "atom:entry"
- s:assert [
- test = "atom:link[@rel='alternate'] "
- ~ "or atom:link[not(@rel)] "
- ~ "or atom:content"
- "An atom:entry must have at least one atom:link element "
- ~ "with a rel attribute of 'alternate' "
- ~ "or an atom:content."
- ]
- ]
- s:rule [
- context = "atom:entry"
- s:assert [
- test = "atom:author or "
- ~ "../atom:author or atom:source/atom:author"
- "An atom:entry must have an atom:author "
- ~ "if its feed does not."
- ]
- ]
- ]
- element atom:entry {
- atomCommonAttributes,
- (atomAuthor*
- & atomCategory*
- & atomContent?
- & atomContributor*
- & atomId
- & atomLink*
- & atomPublished?
- & atomRights?
- & atomSource?
- & atomSummary?
- & atomTitle
- & atomUpdated
- & extensionElement*)
- }
-
-# atom:content
-
-atomInlineTextContent =
- element atom:content {
- atomCommonAttributes,
- attribute type { "text" | "html" }?,
- (text)*
- }
-
-atomInlineXHTMLContent =
- element atom:content {
- atomCommonAttributes,
- attribute type { "xhtml" },
- xhtmlDiv
- }
-
-atomInlineOtherContent =
- element atom:content {
- atomCommonAttributes,
- attribute type { atomMediaType }?,
- (text|anyElement)*
- }
-
-atomOutOfLineContent =
- element atom:content {
- atomCommonAttributes,
- attribute type { atomMediaType }?,
- attribute src { atomUri },
- empty
- }
-
-atomContent = atomInlineTextContent
- | atomInlineXHTMLContent
- | atomInlineOtherContent
- | atomOutOfLineContent
-
-# atom:author
-
-atomAuthor = element atom:author { atomPersonConstruct }
-
-# atom:category
-
-atomCategory =
- element atom:category {
- atomCommonAttributes,
- attribute term { text },
- attribute scheme { atomUri }?,
- attribute label { text }?,
- undefinedContent
- }
-
-# atom:contributor
-
-atomContributor = element atom:contributor { atomPersonConstruct }
-
-# atom:generator
-
-atomGenerator = element atom:generator {
- atomCommonAttributes,
- attribute uri { atomUri }?,
- attribute version { text }?,
- text
-}
-
-# atom:icon
-
-atomIcon = element atom:icon {
- atomCommonAttributes,
- (atomUri)
-}
-
-# atom:id
-
-atomId = element atom:id {
- atomCommonAttributes,
- (atomUri)
-}
-
-# atom:logo
-
-atomLogo = element atom:logo {
- atomCommonAttributes,
- (atomUri)
-}
-
-# atom:link
-
-atomLink =
- element atom:link {
- atomCommonAttributes,
- attribute href { atomUri },
- attribute rel { atomNCName | atomUri }?,
- attribute type { atomMediaType }?,
- attribute hreflang { atomLanguageTag }?,
- attribute title { text }?,
- attribute length { text }?,
- undefinedContent
- }
-
-# atom:published
-
-atomPublished = element atom:published { atomDateConstruct }
-
-# atom:rights
-
-atomRights = element atom:rights { atomTextConstruct }
-
-# atom:source
-
-atomSource =
- element atom:source {
- atomCommonAttributes,
- (atomAuthor*
- & atomCategory*
- & atomContributor*
- & atomGenerator?
- & atomIcon?
- & atomId?
- & atomLink*
- & atomLogo?
- & atomRights?
- & atomSubtitle?
- & atomTitle?
- & atomUpdated?
- & extensionElement*)
- }
-
-# atom:subtitle
-
-atomSubtitle = element atom:subtitle { atomTextConstruct }
-
-# atom:summary
-
-atomSummary = element atom:summary { atomTextConstruct }
-
-# atom:title
-
-atomTitle = element atom:title { atomTextConstruct }
-
-# atom:updated
-
-atomUpdated = element atom:updated { atomDateConstruct }
-
-# Low-level simple types
-
-atomNCName = xsd:string { minLength = "1" pattern = "[^:]*" }
-
-# Whatever a media type is, it contains at least one slash
-atomMediaType = xsd:string { pattern = ".+/.+" }
-
-# As defined in RFC 3066
-atomLanguageTag = xsd:string {
- pattern = "[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*"
-}
-
-# Unconstrained; it's not entirely clear how IRI fit into
-# xsd:anyURI so let's not try to constrain it here
-atomUri = text
-
-# Whatever an email address is, it contains at least one @
-atomEmailAddress = xsd:string { pattern = ".+@.+" }
-
-# Simple Extension
-
-simpleExtensionElement =
- element * - atom:* {
- text
- }
-
-# Structured Extension
-
-structuredExtensionElement =
- element * - atom:* {
- (attribute * { text }+,
- (text|anyElement)*)
- | (attribute * { text }*,
- (text?, anyElement+, (text|anyElement)*))
- }
-
-# Other Extensibility
-
-extensionElement =
- simpleExtensionElement | structuredExtensionElement
-
-undefinedAttribute =
- attribute * - (xml:base | xml:lang | local:*) { text }
-
-undefinedContent = (text|anyForeignElement)*
-
-anyElement =
- element * {
- (attribute * { text }
- | text
- | anyElement)*
- }
-
-anyForeignElement =
- element * - atom:* {
- (attribute * { text }
- | text
- | anyElement)*
- }
-
-# XHTML
-
-anyXHTML = element xhtml:* {
- (attribute * { text }
- | text
- | anyXHTML)*
-}
-
-xhtmlDiv = element xhtml:div {
- (attribute * { text }
- | text
- | anyXHTML)*
-}
-
-# EOF \ No newline at end of file
diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc
deleted file mode 100755
index 725094788..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/schemas/rss10.rnc
+++ /dev/null
@@ -1,113 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<!-- http://www.xml.com/lpt/a/2002/01/23/relaxng.html -->
-<!-- http://www.oasis-open.org/committees/relax-ng/tutorial-20011203.html -->
-<!-- http://www.zvon.org/xxl/XMLSchemaTutorial/Output/ser_wildcards_st8.html -->
-
-<grammar xmlns='http://relaxng.org/ns/structure/1.0'
- xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
- xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
- ns='http://purl.org/rss/1.0/'
- datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes'>
-
- <start>
- <element name='RDF' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
- <ref name='RDFContent'/>
- </element>
- </start>
-
- <define name='RDFContent' ns='http://purl.org/rss/1.0/'>
- <interleave>
- <element name='channel'>
- <ref name='channelContent'/>
- </element>
- <optional>
- <element name='image'><ref name='imageContent'/></element>
- </optional>
- <oneOrMore>
- <element name='item'><ref name='itemContent'/></element>
- </oneOrMore>
- </interleave>
- </define>
-
- <define name='channelContent' combine="interleave">
- <interleave>
- <element name='title'><data type='string'/></element>
- <element name='link'><data type='anyURI'/></element>
- <element name='description'><data type='string'/></element>
- <element name='image'>
- <attribute name='resource' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
- <data type='anyURI'/>
- </attribute>
- </element>
- <element name='items'>
- <ref name='itemsContent'/>
- </element>
- <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
- <data type='anyURI'/>
- </attribute>
- </interleave>
- </define>
-
- <define name="itemsContent">
- <element name="Seq" ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
- <oneOrMore>
- <element name="li" ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
- <choice>
- <attribute name='resource'> <!-- Why doesn't RDF/RSS1.0 ns qualify this attribute? -->
- <data type='anyURI'/>
- </attribute>
- <attribute name='resource' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
- <data type='anyURI'/>
- </attribute>
- </choice>
- </element>
- </oneOrMore>
- </element>
- </define>
-
- <define name='imageContent'>
- <interleave>
- <element name='title'><data type='string'/></element>
- <element name='link'><data type='anyURI'/></element>
- <element name='url'><data type='anyURI'/></element>
- <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
- <data type='anyURI'/>
- </attribute>
- </interleave>
- </define>
-
- <define name='itemContent'>
- <interleave>
- <element name='title'><data type='string'/></element>
- <element name='link'><data type='anyURI'/></element>
- <optional><element name='description'><data type='string'/></element></optional>
- <ref name="anyThing"/>
- <attribute name='about' ns='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
- <data type='anyURI'/>
- </attribute>
- </interleave>
- </define>
-
-
- <define name='anyThing'>
- <zeroOrMore>
- <choice>
- <text/>
- <element>
- <anyName>
- <except>
- <nsName/>
- </except>
- </anyName>
- <ref name='anyThing'/>
- <zeroOrMore>
- <attribute>
- <anyName/>
- </attribute>
- </zeroOrMore>
- </element>
- </choice>
- </zeroOrMore>
- </define>
-
-</grammar>
diff --git a/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc b/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc
deleted file mode 100755
index c8633766f..000000000
--- a/plugins/FeedSub/extlib/XML/Feed/schemas/rss11.rnc
+++ /dev/null
@@ -1,218 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- RELAX NG Compact Schema for RSS 1.1
- Sean B. Palmer, inamidst.com
- Christopher Schmidt, crschmidt.net
- License: This schema is in the public domain
--->
-<grammar xmlns:rss="http://purl.org/net/rss1.1#" xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" ns="http://purl.org/net/rss1.1#" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
- <start>
- <ref name="Channel"/>
- </start>
- <define name="Channel">
- <a:documentation>http://purl.org/net/rss1.1#Channel</a:documentation>
- <element name="Channel">
- <ref name="Channel.content"/>
-
- </element>
- </define>
- <define name="Channel.content">
- <optional>
- <ref name="AttrXMLLang"/>
- </optional>
- <optional>
- <ref name="AttrXMLBase"/>
- </optional>
-
- <ref name="AttrRDFAbout"/>
- <interleave>
- <ref name="title"/>
- <ref name="link"/>
- <ref name="description"/>
- <optional>
- <ref name="image"/>
- </optional>
- <zeroOrMore>
-
- <ref name="Any"/>
- </zeroOrMore>
- <ref name="items"/>
- </interleave>
- </define>
- <define name="title">
- <a:documentation>http://purl.org/net/rss1.1#title</a:documentation>
- <element name="title">
-
- <ref name="title.content"/>
- </element>
- </define>
- <define name="title.content">
- <optional>
- <ref name="AttrXMLLang"/>
- </optional>
- <text/>
- </define>
-
- <define name="link">
- <a:documentation>http://purl.org/net/rss1.1#link</a:documentation>
- <element name="link">
- <ref name="link.content"/>
- </element>
- </define>
- <define name="link.content">
- <data type="anyURI"/>
-
- </define>
- <define name="description">
- <a:documentation>http://purl.org/net/rss1.1#description</a:documentation>
- <element name="description">
- <ref name="description.content"/>
- </element>
- </define>
- <define name="description.content">
-
- <optional>
- <ref name="AttrXMLLang"/>
- </optional>
- <text/>
- </define>
- <define name="image">
- <a:documentation>http://purl.org/net/rss1.1#image</a:documentation>
- <element name="image">
-
- <ref name="image.content"/>
- </element>
- </define>
- <define name="image.content">
- <optional>
- <ref name="AttrXMLLang"/>
- </optional>
- <ref name="AttrRDFResource"/>
- <interleave>
-
- <ref name="title"/>
- <optional>
- <ref name="link"/>
- </optional>
- <ref name="url"/>
- <zeroOrMore>
- <ref name="Any"/>
- </zeroOrMore>
- </interleave>
-
- </define>
- <define name="url">
- <a:documentation>http://purl.org/net/rss1.1#url</a:documentation>
- <element name="url">
- <ref name="url.content"/>
- </element>
- </define>
- <define name="url.content">
-
- <data type="anyURI"/>
- </define>
- <define name="items">
- <a:documentation>http://purl.org/net/rss1.1#items</a:documentation>
- <element name="items">
- <ref name="items.content"/>
- </element>
- </define>
-
- <define name="items.content">
- <optional>
- <ref name="AttrXMLLang"/>
- </optional>
- <ref name="AttrRDFCollection"/>
- <zeroOrMore>
- <ref name="item"/>
- </zeroOrMore>
- </define>
-
- <define name="item">
- <a:documentation>http://purl.org/net/rss1.1#item</a:documentation>
- <element name="item">
- <ref name="item.content"/>
- </element>
- </define>
- <define name="item.content">
- <optional>
-
- <ref name="AttrXMLLang"/>
- </optional>
- <ref name="AttrRDFAbout"/>
- <interleave>
- <ref name="title"/>
- <ref name="link"/>
- <optional>
- <ref name="description"/>
- </optional>
-
- <optional>
- <ref name="image"/>
- </optional>
- <zeroOrMore>
- <ref name="Any"/>
- </zeroOrMore>
- </interleave>
- </define>
- <define name="Any">
-
- <a:documentation>http://purl.org/net/rss1.1#Any</a:documentation>
- <element>
- <anyName>
- <except>
- <nsName/>
- </except>
- </anyName>
- <ref name="Any.content"/>
-
- </element>
- </define>
- <define name="Any.content">
- <zeroOrMore>
- <attribute>
- <anyName>
- <except>
- <nsName/>
- <nsName ns=""/>
-
- </except>
- </anyName>
- </attribute>
- </zeroOrMore>
- <mixed>
- <zeroOrMore>
- <ref name="Any"/>
- </zeroOrMore>
- </mixed>
-
- </define>
- <define name="AttrXMLLang">
- <attribute name="xml:lang">
- <data type="language"/>
- </attribute>
- </define>
- <define name="AttrXMLBase">
- <attribute name="xml:base">
- <data type="anyURI"/>
-
- </attribute>
- </define>
- <define name="AttrRDFAbout">
- <attribute name="rdf:about">
- <data type="anyURI"/>
- </attribute>
- </define>
- <define name="AttrRDFResource">
- <attribute name="rdf:parseType">
-
- <value>Resource</value>
- </attribute>
- </define>
- <define name="AttrRDFCollection">
- <attribute name="rdf:parseType">
- <value>Collection</value>
- </attribute>
- </define>
-
-</grammar>
diff --git a/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch b/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch
deleted file mode 100644
index c53bd9737..000000000
--- a/plugins/FeedSub/extlib/xml-feed-parser-bug-16416.patch
+++ /dev/null
@@ -1,14 +0,0 @@
-diff --git a/htdocs/lib/pear/XML/Feed/Parser/RSS2.php b/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
-index c5d79d1..308a4ab 100644
---- a/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
-+++ b/htdocs/lib/pear/XML/Feed/Parser/RSS2.php
-@@ -321,7 +321,8 @@ class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type
- */
- function getLink($offset, $attribute = 'href', $params = array())
- {
-- $links = $this->model->getElementsByTagName('link');
-+ $xPath = new DOMXPath($this->model);
-+ $links = $xPath->query('//link');
-
- if ($links->length <= $offset) {
- return false;
diff --git a/plugins/FeedSub/feedinfo.php b/plugins/FeedSub/feedinfo.php
deleted file mode 100644
index b166bd6e1..000000000
--- a/plugins/FeedSub/feedinfo.php
+++ /dev/null
@@ -1,268 +0,0 @@
-<?php
-
-/*
-
-Subscription flow:
-
- $feedinfo->subscribe()
- generate random verification token
- save to verify_token
- sends a sub request to the hub...
-
- feedsub/callback
- hub sends confirmation back to us via GET
- We verify the request, then echo back the challenge.
- On our end, we save the time we subscribed and the lease expiration
-
- feedsub/callback
- hub sends us updates via POST
- ?
-
-*/
-
-class FeedDBException extends FeedSubException
-{
- public $obj;
-
- function __construct($obj)
- {
- parent::__construct('Database insert failure');
- $this->obj = $obj;
- }
-}
-
-class Feedinfo extends Memcached_DataObject
-{
- public $__table = 'feedinfo';
-
- public $id;
- public $profile_id;
-
- public $feeduri;
- public $homeuri;
- public $huburi;
-
- // PuSH subscription data
- public $verify_token;
- public $sub_start;
- public $sub_end;
-
- public $created;
- public $lastupdate;
-
-
- public /*static*/ function staticGet($k, $v=null)
- {
- return parent::staticGet(__CLASS__, $k, $v);
- }
-
- /**
- * return table definition for DB_DataObject
- *
- * DB_DataObject needs to know something about the table to manipulate
- * instances. This method provides all the DB_DataObject needs to know.
- *
- * @return array array of column definitions
- */
-
- function table()
- {
- return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
- 'profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
- 'feeduri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'homeuri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'huburi' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
- 'verify_token' => DB_DATAOBJECT_STR,
- 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
- 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
- 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
- 'lastupdate' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
- }
-
- static function schemaDef()
- {
- return array(new ColumnDef('id', 'integer',
- /*size*/ null,
- /*nullable*/ false,
- /*key*/ 'PRI',
- /*default*/ '0',
- /*extra*/ null,
- /*auto_increment*/ true),
- new ColumnDef('profile_id', 'integer',
- null, false),
- new ColumnDef('feeduri', 'varchar',
- 255, false, 'UNI'),
- new ColumnDef('homeuri', 'varchar',
- 255, false),
- new ColumnDef('huburi', 'varchar',
- 255, false),
- new ColumnDef('verify_token', 'varchar',
- 32, true),
- new ColumnDef('sub_start', 'datetime',
- null, true),
- new ColumnDef('sub_end', 'datetime',
- null, true),
- new ColumnDef('created', 'datetime',
- null, false),
- new ColumnDef('lastupdate', 'datetime',
- null, false));
- }
-
- /**
- * return key definitions for DB_DataObject
- *
- * DB_DataObject needs to know about keys that the table has; this function
- * defines them.
- *
- * @return array key definitions
- */
-
- function keys()
- {
- return array('id' => 'P'); //?
- }
-
- /**
- * return key definitions for Memcached_DataObject
- *
- * Our caching system uses the same key definitions, but uses a different
- * method to get them.
- *
- * @return array key definitions
- */
-
- function keyTypes()
- {
- return $this->keys();
- }
-
- /**
- * Fetch the StatusNet-side profile for this feed
- * @return Profile
- */
- public function getProfile()
- {
- return Profile::staticGet('id', $this->profile_id);
- }
-
- /**
- * @param FeedMunger $munger
- * @return Feedinfo
- */
- public static function ensureProfile($munger)
- {
- $feedinfo = $munger->feedinfo();
-
- $current = self::staticGet('feeduri', $feedinfo->feeduri);
- if ($current) {
- // @fixme we should probably update info as necessary
- return $current;
- }
-
- $feedinfo->query('BEGIN');
-
- try {
- $profile = $munger->profile();
- $result = $profile->insert();
- if (empty($result)) {
- throw new FeedDBException($profile);
- }
-
- $feedinfo->profile_id = $profile->id;
- $result = $feedinfo->insert();
- if (empty($result)) {
- throw new FeedDBException($feedinfo);
- }
-
- $feedinfo->query('COMMIT');
- } catch (FeedDBException $e) {
- common_log_db_error($e->obj, 'INSERT', __FILE__);
- $feedinfo->query('ROLLBACK');
- return false;
- }
- return $feedinfo;
- }
-
- /**
- * Send a subscription request to the hub for this feed.
- * The hub will later send us a confirmation POST to /feedsub/callback.
- *
- * @return bool true on success, false on failure
- */
- public function subscribe()
- {
- // @fixme use the verification token
- #$token = md5(mt_rand() . ':' . $this->feeduri);
- #$this->verify_token = $token;
- #$this->update(); // @fixme
-
- try {
- $callback = common_local_url('feedsubcallback', array('feed' => $this->id));
- $headers = array('Content-Type: application/x-www-form-urlencoded');
- $post = array('hub.mode' => 'subscribe',
- 'hub.callback' => $callback,
- 'hub.verify' => 'async',
- //'hub.verify_token' => $token,
- //'hub.lease_seconds' => 0,
- 'hub.topic' => $this->feeduri);
- $client = new HTTPClient();
- $response = $client->post($this->huburi, $headers, $post);
- if ($response->getStatus() >= 200 && $response->getStatus() < 300) {
- common_log(LOG_INFO, __METHOD__ . ': sub req ok');
- return true;
- } else {
- common_log(LOG_INFO, __METHOD__ . ': sub req failed');
- return false;
- }
- } catch (Exception $e) {
- // wtf!
- common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->feeduri");
- return false;
- }
- }
-
- /**
- * Read and post notices for updates from the feed.
- * Currently assumes that all items in the feed are new,
- * coming from a PuSH hub.
- *
- * @param string $xml source of Atom or RSS feed
- */
- public function postUpdates($xml)
- {
- common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->feeduri\"! $xml");
- require_once "XML/Feed/Parser.php";
- $feed = new XML_Feed_Parser($xml, false, false, true);
- $munger = new FeedMunger($feed);
-
- $hits = 0;
- foreach ($feed as $index => $entry) {
- // @fixme this might sort in wrong order if we get multiple updates
-
- $notice = $munger->notice($index);
- $notice->profile_id = $this->profile_id;
-
- // Double-check for oldies
- // @fixme this could explode horribly for multiple feeds on a blog. sigh
- $dupe = new Notice();
- $dupe->uri = $notice->uri;
- $dupe->find();
- if ($dupe->fetch()) {
- common_log(LOG_WARNING, __METHOD__ . ": tried to save dupe notice for entry {$notice->uri} of feed {$this->feeduri}");
- continue;
- }
-
- if (Event::handle('StartNoticeSave', array(&$notice))) {
- $id = $notice->insert();
- Event::handle('EndNoticeSave', array($notice));
- }
- $notice->addToInboxes();
-
- common_log(LOG_INFO, __METHOD__ . ": saved notice {$notice->id} for entry $index of update to \"{$this->feeduri}\"");
- $hits++;
- }
- if ($hits == 0) {
- common_log(LOG_INFO, __METHOD__ . ": no updates in packet for \"$this->feeduri\"! $xml");
- }
- }
-}
diff --git a/plugins/FeedSub/feedinfo.sql b/plugins/FeedSub/feedinfo.sql
deleted file mode 100644
index e9b53d26e..000000000
--- a/plugins/FeedSub/feedinfo.sql
+++ /dev/null
@@ -1,14 +0,0 @@
-CREATE TABLE `feedinfo` (
- `id` int(11) NOT NULL auto_increment,
- `profile_id` int(11) NOT NULL,
- `feeduri` varchar(255) NOT NULL,
- `homeuri` varchar(255) NOT NULL,
- `huburi` varchar(255) NOT NULL,
- `verify_token` varchar(32) default NULL,
- `sub_start` datetime default NULL,
- `sub_end` datetime default NULL,
- `created` datetime NOT NULL,
- `lastupdate` datetime NOT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `feedinfo_feeduri_idx` (`feeduri`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
diff --git a/plugins/FeedSub/feedmunger.php b/plugins/FeedSub/feedmunger.php
deleted file mode 100644
index f3618b8eb..000000000
--- a/plugins/FeedSub/feedmunger.php
+++ /dev/null
@@ -1,238 +0,0 @@
-<?php
-/*
- * StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2009, StatusNet, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-/**
- * @package FeedSubPlugin
- * @maintainer Brion Vibber <brion@status.net>
- */
-
-if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
-
-class FeedSubPreviewNotice extends Notice
-{
- protected $fetched = true;
-
- function __construct($profile)
- {
- //parent::__construct(); // uhhh?
- $this->profile = $profile;
- }
-
- function getProfile()
- {
- return $this->profile;
- }
-
- function find()
- {
- return true;
- }
-
- function fetch()
- {
- $got = $this->fetched;
- $this->fetched = false;
- return $got;
- }
-}
-
-class FeedSubPreviewProfile extends Profile
-{
- function getAvatar($width, $height=null)
- {
- return new FeedSubPreviewAvatar($width, $height);
- }
-}
-
-class FeedSubPreviewAvatar extends Avatar
-{
- function displayUrl() {
- return common_path('plugins/FeedSub/images/48px-Feed-icon.svg.png');
- }
-}
-
-class FeedMunger
-{
- /**
- * @param XML_Feed_Parser $feed
- */
- function __construct($feed, $url=null)
- {
- $this->feed = $feed;
- $this->url = $url;
- }
-
- function feedinfo()
- {
- $feedinfo = new Feedinfo();
- $feedinfo->feeduri = $this->url;
- $feedinfo->homeuri = $this->feed->link;
- $feedinfo->huburi = $this->getHubLink();
- return $feedinfo;
- }
-
- function getAtomLink($item, $attribs=array())
- {
- // XML_Feed_Parser gets confused by multiple <link> elements.
- $dom = $item->model;
-
- // Note that RSS feeds would embed an <atom:link> so this should work for both.
- /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds
- // <link rel='hub' href='http://pubsubhubbub.appspot.com/'/>
- $links = $dom->getElementsByTagNameNS('http://www.w3.org/2005/Atom', 'link');
- for ($i = 0; $i < $links->length; $i++) {
- $node = $links->item($i);
- if ($node->hasAttributes()) {
- $href = $node->attributes->getNamedItem('href');
- if ($href) {
- $matches = 0;
- foreach ($attribs as $name => $val) {
- $attrib = $node->attributes->getNamedItem($name);
- if ($attrib && $attrib->value == $val) {
- $matches++;
- }
- }
- if ($matches == count($attribs)) {
- return $href->value;
- }
- }
- }
- }
- return false;
- }
-
- function getRssLink($item)
- {
- // XML_Feed_Parser gets confused by multiple <link> elements.
- $dom = $item->model;
-
- // Note that RSS feeds would embed an <atom:link> so this should work for both.
- /// http://code.google.com/p/pubsubhubbub/wiki/RssFeeds
- // <link rel='hub' href='http://pubsubhubbub.appspot.com/'/>
- $links = $dom->getElementsByTagName('link');
- for ($i = 0; $i < $links->length; $i++) {
- $node = $links->item($i);
- if (!$node->hasAttributes()) {
- return $node->textContent;
- }
- }
- return false;
- }
-
- function getAltLink($item)
- {
- // Check for an atom link...
- $link = $this->getAtomLink($item, array('rel' => 'alternate', 'type' => 'text/html'));
- if (!$link) {
- $link = $this->getRssLink($item);
- }
- return $link;
- }
-
- function getHubLink()
- {
- return $this->getAtomLink($this->feed, array('rel' => 'hub'));
- }
-
- function profile($preview=false)
- {
- if ($preview) {
- $profile = new FeedSubPreviewProfile();
- } else {
- $profile = new Profile();
- }
-
- // @todo validate/normalize nick?
- $profile->nickname = $this->feed->title;
- $profile->fullname = $this->feed->title;
- $profile->homepage = $this->getAltLink($this->feed);
- $profile->bio = $this->feed->description;
- $profile->profileurl = $this->getAltLink($this->feed);
-
- // @todo tags from categories
- // @todo lat/lon/location?
-
- return $profile;
- }
-
- function notice($index=1, $preview=false)
- {
- $entry = $this->feed->getEntryByOffset($index);
- if (!$entry) {
- return null;
- }
-
- if ($preview) {
- $notice = new FeedSubPreviewNotice($this->profile(true));
- $notice->id = -1;
- } else {
- $notice = new Notice();
- }
-
- $link = $this->getAltLink($entry);
- $notice->uri = $link;
- $notice->url = $link;
- $notice->content = $this->noticeFromEntry($entry);
- $notice->rendered = common_render_content($notice->content, $notice);
- $notice->created = common_sql_date($entry->updated); // @fixme
- $notice->is_local = Notice::GATEWAY;
- $notice->source = 'feed';
-
- return $notice;
- }
-
- /**
- * @param XML_Feed_Type $entry
- * @return string notice text, within post size limit
- */
- function noticeFromEntry($entry)
- {
- $title = $entry->title;
- $link = $entry->link;
-
- // @todo We can get <category> entries like this:
- // $cats = $entry->getCategory('category', array(0, true));
- // but it feels like an awful hack. If it's accessible cleanly,
- // try adding #hashtags from the categories/tags on a post.
-
- // @todo Should we force a language here?
- $format = _m('New post: "%1$s" %2$s');
- $title = $entry->title;
- $link = $this->getAltLink($entry);
- $out = sprintf($format, $title, $link);
-
- // Trim link if needed...
- $max = Notice::maxContent();
- if (mb_strlen($out) > $max) {
- $link = common_shorten_url($link);
- $out = sprintf($format, $title, $link);
- }
-
- // Trim title if needed...
- if (mb_strlen($out) > $max) {
- $ellipsis = "\xe2\x80\xa6"; // U+2026 HORIZONTAL ELLIPSIS
- $used = mb_strlen($out) - mb_strlen($title);
- $available = $max - $used - mb_strlen($ellipsis);
- $title = mb_substr($title, 0, $available) . $ellipsis;
- $out = sprintf($format, $title, $link);
- }
-
- return $out;
- }
-}
diff --git a/plugins/FeedSub/tests/FeedMungerTest.php b/plugins/FeedSub/tests/FeedMungerTest.php
deleted file mode 100644
index 0ce24c9fb..000000000
--- a/plugins/FeedSub/tests/FeedMungerTest.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-
-if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
- print "This script must be run from the command line\n";
- exit();
-}
-
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
-define('STATUSNET', true);
-define('LACONICA', true);
-
-require_once INSTALLDIR . '/lib/common.php';
-require_once INSTALLDIR . '/plugins/FeedSub/feedsub.php';
-
-require_once 'XML/Feed/Parser.php';
-
-class FeedMungerTest extends PHPUnit_Framework_TestCase
-{
- /**
- * @dataProvider profileProvider
- *
- */
- public function testProfiles($xml, $expected)
- {
- $feed = new XML_Feed_Parser($xml, false, false, true);
-
- $munger = new FeedMunger($feed);
- $profile = $munger->profile();
-
- foreach ($expected as $field => $val) {
- $this->assertEquals($expected[$field], $profile->$field, "profile->$field");
- }
- }
-
- static public function profileProvider()
- {
- return array(
- array(self::samplefeed(),
- array('nickname' => 'leŭksman', // @todo does this need to be asciified?
- 'fullname' => 'leŭksman',
- 'bio' => 'reticula, electronica, & oddities',
- 'homepage' => 'http://leuksman.com/log')));
- }
-
- /**
- * @dataProvider noticeProvider
- *
- */
- public function testNotices($xml, $entryIndex, $expected)
- {
- $feed = new XML_Feed_Parser($xml, false, false, true);
- $entry = $feed->getEntryByOffset($entryIndex);
-
- $munger = new FeedMunger($feed);
- $notice = $munger->noticeFromEntry($entry);
-
- $this->assertTrue(mb_strlen($notice) <= Notice::maxContent());
- $this->assertEquals($expected, $notice);
- }
-
- static public function noticeProvider()
- {
- return array(
- array('<rss version="2.0"><channel><item><title>A fairly short title</title><link>http://example.com/short/link</link></item></channel></rss>', 0,
- 'New post: "A fairly short title" http://example.com/short/link'),
- // Requires URL shortening ...
- array('<rss version="2.0"><channel><item><title>A fairly short title</title><link>http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh</link></item></channel></rss>', 0,
- 'New post: "A fairly short title" http://ur1.ca/g2o1'),
- array('<rss version="2.0"><channel><item><title>A fairly long title in this case, which will have to get cut down at some point alongside its very long link. Really who even makes titles this long? It\'s just ridiculous imo...</title><link>http://example.com/but/a/very/long/link/indeed/this/is/far/too/long/for/mere/humans/to/comprehend/oh/my/gosh</link></item></channel></rss>', 0,
- 'New post: "A fairly long title in this case, which will have to get cut down at some point alongside its very long li…" http://ur1.ca/g2o1'),
- // Some real sample feeds
- array(self::samplefeed(), 0,
- 'New post: "Compiling PHP on Snow Leopard" http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/'),
- array(self::samplefeedBlogspot(), 0,
- 'New post: "I love posting" http://briontest.blogspot.com/2009/11/i-love-posting.html'),
- array(self::samplefeedBlogspot(), 1,
- 'New post: "Hey dude" http://briontest.blogspot.com/2009/11/hey-dude.html'),
- );
- }
-
- static protected function samplefeed()
- {
- $xml = '<' . '?xml version="1.0" encoding="UTF-8"?' . ">\n";
- $samplefeed = $xml . <<<END
-<rss version="2.0"
- xmlns:content="http://purl.org/rss/1.0/modules/content/"
- xmlns:wfw="http://wellformedweb.org/CommentAPI/"
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:atom="http://www.w3.org/2005/Atom"
- xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
- xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
- >
-
-<channel>
- <title>leŭksman</title>
- <atom:link href="http://leuksman.com/log/feed/" rel="self" type="application/rss+xml" />
- <link>http://leuksman.com/log</link>
- <description>reticula, electronica, &#38; oddities</description>
-
- <lastBuildDate>Thu, 12 Nov 2009 17:44:42 +0000</lastBuildDate>
- <generator>http://wordpress.org/?v=2.8.6</generator>
- <language>en</language>
- <sy:updatePeriod>hourly</sy:updatePeriod>
- <sy:updateFrequency>1</sy:updateFrequency>
- <item>
-
- <title>Compiling PHP on Snow Leopard</title>
- <link>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/</link>
- <comments>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/#comments</comments>
- <pubDate>Thu, 12 Nov 2009 17:44:42 +0000</pubDate>
- <dc:creator>brion</dc:creator>
- <category><![CDATA[apple]]></category>
-
- <category><![CDATA[devel]]></category>
-
- <guid isPermaLink="false">http://leuksman.com/log/?p=649</guid>
- <description><![CDATA[If you&#8217;ve been having trouble compiling your own PHP installations on Mac OS X 10.6, here&#8217;s the secret to making it not suck! After running the configure script, edit the generated Makefile and make these fixes:
-
-Find the EXTRA_LIBS definition and add -lresolv to the end
-Find the EXE_EXT definition and remove .dSYM
-
-Standard make and make install [...]]]></description>
- <content:encoded><![CDATA[<p>If you&#8217;ve been having trouble compiling your own PHP installations on Mac OS X 10.6, here&#8217;s the secret to making it not suck! After running the configure script, edit the generated Makefile and make these fixes:</p>
-<ul>
-<li>Find the <strong>EXTRA_LIBS</strong> definition and add <strong>-lresolv</strong> to the end</li>
-<li>Find the <strong>EXE_EXT</strong> definition and remove <strong>.dSYM</strong></li>
-</ul>
-<p>Standard make and make install should work from here&#8230;</p>
-<p>For reference, here&#8217;s the whole configure line I currently use; MySQL is installed from the downloadable installer; other deps from MacPorts:</p>
-<p>&#8216;./configure&#8217; &#8216;&#8211;prefix=/opt/php52&#8242; &#8216;&#8211;with-mysql=/usr/local/mysql&#8217; &#8216;&#8211;with-zlib&#8217; &#8216;&#8211;with-bz2&#8242; &#8216;&#8211;enable-mbstring&#8217; &#8216;&#8211;enable-exif&#8217; &#8216;&#8211;enable-fastcgi&#8217; &#8216;&#8211;with-xmlrpc&#8217; &#8216;&#8211;with-xsl&#8217; &#8216;&#8211;with-readline=/opt/local&#8217; &#8211;without-iconv &#8211;with-gd &#8211;with-png-dir=/opt/local &#8211;with-jpeg-dir=/opt/local &#8211;with-curl &#8211;with-gettext=/opt/local &#8211;with-mysqli=/usr/local/mysql/bin/mysql_config &#8211;with-tidy=/opt/local &#8211;enable-pcntl &#8211;with-openssl</p>
-]]></content:encoded>
- <wfw:commentRss>http://leuksman.com/log/2009/11/12/compiling-php-on-snow-leopard/feed/</wfw:commentRss>
- <slash:comments>0</slash:comments>
- </item>
-</channel>
-</rss>
-END;
- return $samplefeed;
- }
-
- static protected function samplefeedBlogspot()
- {
- return <<<END
-<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss'><id>tag:blogger.com,1999:blog-7780083508531697167</id><updated>2009-11-19T12:56:11.233-08:00</updated><title type='text'>Brion's Cool Test Blog</title><subtitle type='html'></subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>2</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-7780083508531697167.post-8456671879000290677</id><published>2009-11-19T12:55:00.000-08:00</published><updated>2009-11-19T12:56:11.241-08:00</updated><title type='text'>I love posting</title><content type='html'>It's pretty awesome, if you like that sort of thing.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8456671879000290677?l=briontest.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/8456671879000290677/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://briontest.blogspot.com/2009/11/i-love-posting.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8456671879000290677'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8456671879000290677'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/2009/11/i-love-posting.html' title='I love posting'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='05912464053145602436'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-7780083508531697167.post-8202296917897346633</id><published>2009-11-18T13:52:00.001-08:00</published><updated>2009-11-18T13:52:48.444-08:00</updated><title type='text'>Hey dude</title><content type='html'>testingggggggggg&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/7780083508531697167-8202296917897346633?l=briontest.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://briontest.blogspot.com/feeds/8202296917897346633/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://briontest.blogspot.com/2009/11/hey-dude.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8202296917897346633'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/7780083508531697167/posts/default/8202296917897346633'/><link rel='alternate' type='text/html' href='http://briontest.blogspot.com/2009/11/hey-dude.html' title='Hey dude'/><author><name>brion</name><uri>http://www.blogger.com/profile/12932299467049762017</uri><email>noreply@blogger.com</email><gd:extendedProperty xmlns:gd='http://schemas.google.com/g/2005' name='OpenSocialUserId' value='05912464053145602436'/></author><thr:total xmlns:thr='http://purl.org/syndication/thread/1.0'>0</thr:total></entry></feed>
-END;
- }
-}
diff --git a/plugins/MemcachePlugin.php b/plugins/MemcachePlugin.php
index 93a0583fe..c3ca5c135 100644
--- a/plugins/MemcachePlugin.php
+++ b/plugins/MemcachePlugin.php
@@ -115,7 +115,7 @@ class MemcachePlugin extends Plugin
*
* @param string &$key in; Key to use for lookups
* @param mixed &$value in; Value to associate
- * @param integer &$flag in; Flag (passed through to Memcache)
+ * @param integer &$flag in; Flag empty or Cache::COMPRESSED
* @param integer &$expiry in; Expiry (passed through to Memcache)
* @param boolean &$success out; Whether the set was successful
*
@@ -128,7 +128,7 @@ class MemcachePlugin extends Plugin
if ($expiry === null) {
$expiry = $this->defaultExpiry;
}
- $success = $this->_conn->set($key, $value, $flag, $expiry);
+ $success = $this->_conn->set($key, $value, $this->flag(intval($flag)), $expiry);
Event::handle('EndCacheSet', array($key, $value, $flag,
$expiry));
return false;
@@ -228,6 +228,20 @@ class MemcachePlugin extends Plugin
}
}
+ /**
+ * Translate general flags to Memcached-specific flags
+ * @param int $flag
+ * @return int
+ */
+ protected function flag($flag)
+ {
+ $out = 0;
+ if ($flag & Cache::COMPRESSED == Cache::COMPRESSED) {
+ $out |= MEMCACHE_COMPRESSED;
+ }
+ return $out;
+ }
+
function onPluginVersion(&$versions)
{
$versions[] = array('name' => 'Memcache',
diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php
index 5c913836d..cd2531fa7 100644
--- a/plugins/MobileProfile/MobileProfilePlugin.php
+++ b/plugins/MobileProfile/MobileProfilePlugin.php
@@ -240,6 +240,8 @@ class MobileProfilePlugin extends WAP20Plugin
return true;
}
+ $action->cssLink('css/display.css');
+
if (file_exists(Theme::file('css/mp-screen.css'))) {
$action->cssLink('css/mp-screen.css', null, 'screen');
} else {
@@ -256,6 +258,14 @@ class MobileProfilePlugin extends WAP20Plugin
}
+ function onStartShowUAStyles($action) {
+ if (!$this->serveMobile) {
+ return true;
+ }
+
+ return false;
+ }
+
function onStartShowHeader($action)
{
if (!$this->serveMobile) {
diff --git a/plugins/MobileProfile/mp-screen.css b/plugins/MobileProfile/mp-screen.css
index 04fa5fb00..0fc801612 100644
--- a/plugins/MobileProfile/mp-screen.css
+++ b/plugins/MobileProfile/mp-screen.css
@@ -1,15 +1,12 @@
/** theme: mobile profile screen
*
* @package StatusNet
- * @author Sarven Capadisli <csarven@status.net>
+ * @author Sarven Capadisli <csarven@status.net>
* @copyright 2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
-@import url(../../theme/base/css/display.css);
-@import url(../../theme/identica/css/display.css);
-
#wrap {
min-width:0;
max-width:100%;
diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php
new file mode 100644
index 000000000..60bb144a8
--- /dev/null
+++ b/plugins/OStatus/OStatusPlugin.php
@@ -0,0 +1,489 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class FeedSubException extends Exception
+{
+}
+
+class OStatusPlugin extends Plugin
+{
+ /**
+ * Hook for RouterInitialized event.
+ *
+ * @param Net_URL_Mapper $m path-to-action mapper
+ * @return boolean hook return
+ */
+ function onRouterInitialized($m)
+ {
+ // Discovery actions
+ $m->connect('.well-known/host-meta',
+ array('action' => 'hostmeta'));
+ $m->connect('main/webfinger',
+ array('action' => 'webfinger'));
+ $m->connect('main/ostatus',
+ array('action' => 'ostatusinit'));
+ $m->connect('main/ostatus?nickname=:nickname',
+ array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
+ $m->connect('main/ostatussub',
+ array('action' => 'ostatussub'));
+ $m->connect('main/ostatussub',
+ array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
+
+ // PuSH actions
+ $m->connect('main/push/hub', array('action' => 'pushhub'));
+
+ $m->connect('main/push/callback/:feed',
+ array('action' => 'pushcallback'),
+ array('feed' => '[0-9]+'));
+ $m->connect('settings/feedsub',
+ array('action' => 'feedsubsettings'));
+
+ // Salmon endpoint
+ $m->connect('main/salmon/user/:id',
+ array('action' => 'usersalmon'),
+ array('id' => '[0-9]+'));
+ $m->connect('main/salmon/group/:id',
+ array('action' => 'groupsalmon'),
+ array('id' => '[0-9]+'));
+ return true;
+ }
+
+ /**
+ * Set up queue handlers for outgoing hub pushes
+ * @param QueueManager $qm
+ * @return boolean hook return
+ */
+ function onEndInitializeQueueManager(QueueManager $qm)
+ {
+ // Outgoing from our internal PuSH hub
+ $qm->connect('hubverify', 'HubVerifyQueueHandler');
+ $qm->connect('hubdistrib', 'HubDistribQueueHandler');
+ $qm->connect('hubout', 'HubOutQueueHandler');
+
+ // Incoming from a foreign PuSH hub
+ $qm->connect('pushinput', 'PushInputQueueHandler');
+ return true;
+ }
+
+ /**
+ * Put saved notices into the queue for pubsub distribution.
+ */
+ function onStartEnqueueNotice($notice, &$transports)
+ {
+ $transports[] = 'hubdistrib';
+ return true;
+ }
+
+ /**
+ * Set up a PuSH hub link to our internal link for canonical timeline
+ * Atom feeds for users and groups.
+ */
+ function onStartApiAtom($feed)
+ {
+ $id = null;
+
+ if ($feed instanceof AtomUserNoticeFeed) {
+ $salmonAction = 'usersalmon';
+ $user = $feed->getUser();
+ $id = $user->id;
+ $profile = $user->getProfile();
+ $feed->setActivitySubject($profile->asActivityNoun('subject'));
+ } else if ($feed instanceof AtomGroupNoticeFeed) {
+ $salmonAction = 'groupsalmon';
+ $group = $feed->getGroup();
+ $id = $group->id;
+ $feed->setActivitySubject($group->asActivitySubject());
+ } else {
+ return true;
+ }
+
+ if (!empty($id)) {
+ $hub = common_config('ostatus', 'hub');
+ if (empty($hub)) {
+ // Updates will be handled through our internal PuSH hub.
+ $hub = common_local_url('pushhub');
+ }
+ $feed->addLink($hub, array('rel' => 'hub'));
+
+ // Also, we'll add in the salmon link
+ $salmon = common_local_url($salmonAction, array('id' => $id));
+ $feed->addLink($salmon, array('rel' => 'salmon'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Automatically load the actions and libraries used by the plugin
+ *
+ * @param Class $cls the class
+ *
+ * @return boolean hook return
+ *
+ */
+ function onAutoload($cls)
+ {
+ $base = dirname(__FILE__);
+ $lower = strtolower($cls);
+ $map = array('activityverb' => 'activity',
+ 'activityobject' => 'activity',
+ 'activityutils' => 'activity');
+ if (isset($map[$lower])) {
+ $lower = $map[$lower];
+ }
+ $files = array("$base/classes/$cls.php",
+ "$base/lib/$lower.php");
+ if (substr($lower, -6) == 'action') {
+ $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php";
+ }
+ foreach ($files as $file) {
+ if (file_exists($file)) {
+ include_once $file;
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Add in an OStatus subscribe button
+ */
+ function onStartProfileRemoteSubscribe($output, $profile)
+ {
+ $cur = common_current_user();
+
+ if (empty($cur)) {
+ // Add an OStatus subscribe
+ $output->elementStart('li', 'entity_subscribe');
+ $url = common_local_url('ostatusinit',
+ array('nickname' => $profile->nickname));
+ $output->element('a', array('href' => $url,
+ 'class' => 'entity_remote_subscribe'),
+ _m('Subscribe'));
+
+ $output->elementEnd('li');
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if we've got remote replies to send via Salmon.
+ *
+ * @fixme push webfinger lookup & sending to a background queue
+ * @fixme also detect short-form name for remote subscribees where not ambiguous
+ */
+
+ function onEndNoticeSave($notice)
+ {
+ $mentioned = $notice->getReplies();
+
+ foreach ($mentioned as $profile) {
+
+ $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id);
+
+ if (!empty($oprofile) && !empty($oprofile->salmonuri)) {
+
+ // FIXME: this needs to go out in a queue handler
+
+ $xml = '<?xml version="1.0" encoding="UTF-8" ?>';
+ $xml .= $notice->asAtomEntry();
+
+ $salmon = new Salmon();
+ $salmon->post($oprofile->salmonuri, $xml);
+ }
+ }
+ }
+
+ /**
+ *
+ */
+
+ function onEndFindMentions($sender, $text, &$mentions)
+ {
+ preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/',
+ $text,
+ $wmatches,
+ PREG_OFFSET_CAPTURE);
+
+ foreach ($wmatches[1] as $wmatch) {
+
+ $webfinger = $wmatch[0];
+
+ $oprofile = Ostatus_profile::ensureWebfinger($webfinger);
+
+ if (!empty($oprofile)) {
+
+ $profile = $oprofile->localProfile();
+
+ $mentions[] = array('mentioned' => array($profile),
+ 'text' => $wmatch[0],
+ 'position' => $wmatch[1],
+ 'url' => $profile->profileurl);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Notify remote server and garbage collect unused feeds on unsubscribe.
+ * @fixme send these operations to background queues
+ *
+ * @param User $user
+ * @param Profile $other
+ * @return hook return value
+ */
+ function onEndUnsubscribe($profile, $other)
+ {
+ $user = User::staticGet('id', $profile->id);
+
+ if (empty($user)) {
+ return true;
+ }
+
+ $oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
+
+ if (empty($oprofile)) {
+ return true;
+ }
+
+ // Drop the PuSH subscription if there are no other subscribers.
+
+ if ($other->subscriberCount() == 0) {
+ common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri");
+ $oprofile->unsubscribe();
+ }
+
+ $act = new Activity();
+
+ $act->verb = ActivityVerb::UNFOLLOW;
+
+ $act->id = TagURI::mint('unfollow:%d:%d:%s',
+ $profile->id,
+ $other->id,
+ common_date_iso8601(time()));
+
+ $act->time = time();
+ $act->title = _("Unfollow");
+ $act->content = sprintf(_("%s stopped following %s."),
+ $profile->getBestName(),
+ $other->getBestName());
+
+ $act->actor = ActivityObject::fromProfile($profile);
+ $act->object = ActivityObject::fromProfile($other);
+
+ $oprofile->notifyActivity($act);
+
+ return true;
+ }
+
+ /**
+ * Make sure necessary tables are filled out.
+ */
+ function onCheckSchema() {
+ $schema = Schema::get();
+ $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
+ $schema->ensureTable('feedsub', FeedSub::schemaDef());
+ $schema->ensureTable('hubsub', HubSub::schemaDef());
+ return true;
+ }
+
+ function onEndShowStatusNetStyles($action) {
+ $action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css'));
+ return true;
+ }
+
+ function onEndShowStatusNetScripts($action) {
+ $action->script(common_path('plugins/OStatus/js/ostatus.js'));
+ return true;
+ }
+
+ /**
+ * Override the "from ostatus" bit in notice lists to link to the
+ * original post and show the domain it came from.
+ *
+ * @param Notice in $notice
+ * @param string out &$name
+ * @param string out &$url
+ * @param string out &$title
+ * @return mixed hook return code
+ */
+ function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
+ {
+ if ($notice->source == 'ostatus') {
+ $bits = parse_url($notice->uri);
+ $domain = $bits['host'];
+
+ $name = $domain;
+ $url = $notice->uri;
+ $title = sprintf(_m("Sent from %s via OStatus"), $domain);
+ return false;
+ }
+ }
+
+ /**
+ * Send incoming PuSH feeds for OStatus endpoints in for processing.
+ *
+ * @param FeedSub $feedsub
+ * @param DOMDocument $feed
+ * @return mixed hook return code
+ */
+ function onStartFeedSubReceive($feedsub, $feed)
+ {
+ $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
+ if ($oprofile) {
+ $oprofile->processFeed($feed);
+ } else {
+ common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
+ }
+ }
+
+ function onEndSubscribe($subscriber, $other)
+ {
+ $user = User::staticGet('id', $subscriber->id);
+
+ if (empty($user)) {
+ return true;
+ }
+
+ $oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
+
+ if (empty($oprofile)) {
+ return true;
+ }
+
+ $act = new Activity();
+
+ $act->verb = ActivityVerb::FOLLOW;
+
+ $act->id = TagURI::mint('follow:%d:%d:%s',
+ $subscriber->id,
+ $other->id,
+ common_date_iso8601(time()));
+
+ $act->time = time();
+ $act->title = _("Follow");
+ $act->content = sprintf(_("%s is now following %s."),
+ $subscriber->getBestName(),
+ $other->getBestName());
+
+ $act->actor = ActivityObject::fromProfile($subscriber);
+ $act->object = ActivityObject::fromProfile($other);
+
+ $oprofile->notifyActivity($act);
+
+ return true;
+ }
+
+ /**
+ * Notify remote users when their notices get favorited.
+ *
+ * @param Profile or User $profile of local user doing the faving
+ * @param Notice $notice being favored
+ * @return hook return value
+ */
+
+ function onEndFavorNotice(Profile $profile, Notice $notice)
+ {
+ $user = User::staticGet('id', $profile->id);
+
+ if (empty($user)) {
+ return true;
+ }
+
+ $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id);
+
+ if (empty($oprofile)) {
+ return true;
+ }
+
+ $act = new Activity();
+
+ $act->verb = ActivityVerb::FAVORITE;
+ $act->id = TagURI::mint('favor:%d:%d:%s',
+ $profile->id,
+ $notice->id,
+ common_date_iso8601(time()));
+
+ $act->time = time();
+ $act->title = _("Favor");
+ $act->content = sprintf(_("%s marked notice %s as a favorite."),
+ $profile->getBestName(),
+ $notice->uri);
+
+ $act->actor = ActivityObject::fromProfile($profile);
+ $act->object = ActivityObject::fromNotice($notice);
+
+ $oprofile->notifyActivity($act);
+
+ return true;
+ }
+
+ /**
+ * Notify remote users when their notices get de-favorited.
+ *
+ * @param Profile $profile Profile person doing the de-faving
+ * @param Notice $notice Notice being favored
+ *
+ * @return hook return value
+ */
+
+ function onEndDisfavorNotice(Profile $profile, Notice $notice)
+ {
+ $user = User::staticGet('id', $profile->id);
+
+ if (empty($user)) {
+ return true;
+ }
+
+ $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id);
+
+ if (empty($oprofile)) {
+ return true;
+ }
+
+ $act = new Activity();
+
+ $act->verb = ActivityVerb::UNFAVORITE;
+ $act->id = TagURI::mint('disfavor:%d:%d:%s',
+ $profile->id,
+ $notice->id,
+ common_date_iso8601(time()));
+ $act->time = time();
+ $act->title = _("Disfavor");
+ $act->content = sprintf(_("%s marked notice %s as no longer a favorite."),
+ $profile->getBestName(),
+ $notice->uri);
+
+ $act->actor = ActivityObject::fromProfile($profile);
+ $act->object = ActivityObject::fromNotice($notice);
+
+ $oprofile->notifyActivity($act);
+
+ return true;
+ }
+}
diff --git a/plugins/FeedSub/README b/plugins/OStatus/README
index cbf3adbb9..cbf3adbb9 100644
--- a/plugins/FeedSub/README
+++ b/plugins/OStatus/README
diff --git a/plugins/FeedSub/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php
index 0fba20a39..aee4cee9a 100644
--- a/plugins/FeedSub/actions/feedsubsettings.php
+++ b/plugins/OStatus/actions/feedsubsettings.php
@@ -26,7 +26,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
class FeedSubSettingsAction extends ConnectSettingsAction
{
- protected $feedurl;
+ protected $profile_uri;
protected $preview;
protected $munger;
@@ -68,14 +68,6 @@ class FeedSubSettingsAction extends ConnectSettingsAction
$profile = $user->getProfile();
- $fuser = null;
-
- $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE);
-
- if (!empty($flink)) {
- $fuser = $flink->getForeignUser();
- }
-
$this->elementStart('form', array('method' => 'post',
'id' => 'form_settings_feedsub',
'class' => 'form_settings',
@@ -88,7 +80,10 @@ class FeedSubSettingsAction extends ConnectSettingsAction
$this->elementStart('ul', 'form_data');
$this->elementStart('li', array('id' => 'settings_twitter_login_button'));
- $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed'));
+ $this->input('profile_uri',
+ _m('Feed URL'),
+ $this->profile_uri,
+ _m('Enter the profile URL of a PubSubHubbub-enabled feed'));
$this->elementEnd('li');
$this->elementEnd('ul');
@@ -145,81 +140,71 @@ class FeedSubSettingsAction extends ConnectSettingsAction
*/
function validateFeed()
{
- $feedurl = trim($this->arg('feedurl'));
+ $profile_uri = trim($this->arg('profile_uri'));
- if ($feedurl == '') {
- $this->showForm(_m('Empty feed URL!'));
+ if ($profile_uri == '') {
+ $this->showForm(_m('Empty remote profile URL!'));
return;
}
- $this->feedurl = $feedurl;
+ $this->profile_uri = $profile_uri;
- // Get the canonical feed URI and check it
+ // @fixme validate, normalize bla bla
try {
- $discover = new FeedDiscovery();
- $uri = $discover->discoverFromURL($feedurl);
+ $oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
+ $this->oprofile = $oprofile;
+ return true;
} catch (FeedSubBadURLException $e) {
- $this->showForm(_m('Invalid URL or could not reach server.'));
- return false;
+ $err = _m('Invalid URL or could not reach server.');
} catch (FeedSubBadResponseException $e) {
- $this->showForm(_m('Cannot read feed; server returned error.'));
- return false;
+ $err = _m('Cannot read feed; server returned error.');
} catch (FeedSubEmptyException $e) {
- $this->showForm(_m('Cannot read feed; server returned an empty page.'));
- return false;
+ $err = _m('Cannot read feed; server returned an empty page.');
} catch (FeedSubBadHTMLException $e) {
- $this->showForm(_m('Bad HTML, could not find feed link.'));
- return false;
+ $err = _m('Bad HTML, could not find feed link.');
} catch (FeedSubNoFeedException $e) {
- $this->showForm(_m('Could not find a feed linked from this URL.'));
- return false;
+ $err = _m('Could not find a feed linked from this URL.');
} catch (FeedSubUnrecognizedTypeException $e) {
- $this->showForm(_m('Not a recognized feed type.'));
- return false;
+ $err = _m('Not a recognized feed type.');
} catch (FeedSubException $e) {
// Any new ones we forgot about
- $this->showForm(_m('Bad feed URL.'));
- return false;
+ $err = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage());
}
-
- $this->munger = $discover->feedMunger();
- $this->feedinfo = $this->munger->feedInfo();
- if ($this->feedinfo->huburi == '') {
- $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.'));
- return false;
- }
-
- return true;
+ $this->showForm($err);
+ return false;
}
function saveFeed()
{
if ($this->validateFeed()) {
$this->preview = true;
- $this->feedinfo = Feedinfo::ensureProfile($this->munger);
- // If not already in use, subscribe to updates via the hub
- if ($this->feedinfo->sub_start) {
- common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->feedinfo->feeduri} last subbed {$this->feedinfo->sub_start}");
- } else {
- $ok = $this->feedinfo->subscribe();
- common_log(LOG_INFO, __METHOD__ . ": sub was $ok");
- if (!$ok) {
- $this->showForm(_m('Feed subscription failed! Bad response from hub.'));
- return;
- }
- }
-
// And subscribe the current user to the local profile
$user = common_current_user();
- $profile = $this->feedinfo->getProfile();
-
- if ($user->isSubscribed($profile)) {
- $this->showForm(_m('Already subscribed!'));
- } elseif ($user->subscribeTo($profile)) {
- $this->showForm(_m('Feed subscribed!'));
+
+ if (!$this->oprofile->subscribe()) {
+ $this->showForm(_m("Failed to set up server-to-server subscription."));
+ return;
+ }
+
+ if ($this->oprofile->isGroup()) {
+ $group = $this->oprofile->localGroup();
+ if ($user->isMember($group)) {
+ $this->showForm(_m('Already a member!'));
+ } elseif (Group_member::join($this->profile->group_id, $user->id)) {
+ $this->showForm(_m('Joined remote group!'));
+ } else {
+ $this->showForm(_m('Remote group join failed!'));
+ }
} else {
- $this->showForm(_m('Feed subscription failed!'));
+ $local = $this->oprofile->localProfile();
+ if ($user->isSubscribed($local)) {
+ $this->showForm(_m('Already subscribed!'));
+ } elseif ($this->oprofile->subscribeLocalToRemote($user)) {
+ $this->showForm(_m('Remote user subscribed!'));
+ } else {
+ $this->showForm(_m('Remote subscription failed!'));
+ }
}
}
}
@@ -234,17 +219,7 @@ class FeedSubSettingsAction extends ConnectSettingsAction
function previewFeed()
{
- $feedinfo = $this->munger->feedinfo();
- $notice = $this->munger->notice(0, true); // preview
-
- if ($notice) {
- $this->element('b', null, 'Preview of latest post from this feed:');
-
- $item = new NoticeList($notice, $this);
- $item->show();
- } else {
- $this->element('b', null, 'No posts in this feed yet.');
- }
+ $this->text('Profile preview should go here');
}
function showScripts()
diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php
new file mode 100644
index 000000000..64ae9f3cc
--- /dev/null
+++ b/plugins/OStatus/actions/groupsalmon.php
@@ -0,0 +1,108 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @author James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class GroupsalmonAction extends SalmonAction
+{
+ var $group = null;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $id = $this->trimmed('id');
+
+ if (!$id) {
+ $this->clientError(_('No ID.'));
+ }
+
+ $this->group = User_group::staticGet('id', $id);
+
+ if (empty($this->group)) {
+ $this->clientError(_('No such group.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * We've gotten a post event on the Salmon backchannel, probably a reply.
+ */
+
+ function handlePost()
+ {
+ switch ($this->act->object->type) {
+ case ActivityObject::ARTICLE:
+ case ActivityObject::BLOGENTRY:
+ case ActivityObject::NOTE:
+ case ActivityObject::STATUS:
+ case ActivityObject::COMMENT:
+ break;
+ default:
+ throw new ClientException("Can't handle that kind of post.");
+ }
+
+ // Notice must be to the attention of this group
+
+ $context = $this->act->context;
+
+ if (empty($context->attention)) {
+ throw new ClientException("Not to the attention of anyone.");
+ } else {
+ $uri = common_local_url('groupbyid', array('id' => $this->group->id));
+ if (!in_array($context->attention, $uri)) {
+ throw new ClientException("Not to the attention of this group.");
+ }
+ }
+
+ $profile = $this->ensureProfile();
+ // @fixme save the post
+ }
+
+ /**
+ * We've gotten a follow/subscribe notification from a remote user.
+ * Save a subscription relationship for them.
+ */
+
+ function handleFollow()
+ {
+ $this->handleJoin(); // ???
+ }
+
+ function handleUnfollow()
+ {
+ }
+
+ /**
+ * A remote user joined our group.
+ */
+
+ function handleJoin()
+ {
+ }
+
+}
diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php
new file mode 100644
index 000000000..850b8a0fe
--- /dev/null
+++ b/plugins/OStatus/actions/hostmeta.php
@@ -0,0 +1,42 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class HostMetaAction extends Action
+{
+
+ function handle()
+ {
+ parent::handle();
+
+ $w = new Webfinger();
+
+
+ $domain = common_config('site', 'server');
+ $url = common_local_url('webfinger');
+ $url.= '?uri={uri}';
+ print $w->getHostMeta($domain, $url);
+ }
+}
diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php
new file mode 100644
index 000000000..4afde2c36
--- /dev/null
+++ b/plugins/OStatus/actions/ostatusinit.php
@@ -0,0 +1,168 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+
+class OStatusInitAction extends Action
+{
+
+ var $nickname;
+ var $acct;
+ var $err;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ if (common_logged_in()) {
+ $this->clientError(_m('You can use the local subscription!'));
+ return false;
+ }
+
+ $this->nickname = $this->trimmed('nickname');
+ $this->acct = $this->trimmed('acct');
+
+ return true;
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ /* Use a session token for CSRF protection. */
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_m('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+ $this->ostatusConnect();
+ } else {
+ $this->showForm();
+ }
+ }
+
+ function showForm($err = null)
+ {
+ $this->err = $err;
+ if ($this->boolean('ajax')) {
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ $this->element('title', null, _m('Subscribe to user'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->showContent();
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ } else {
+ $this->showPage();
+ }
+ }
+
+ function showContent()
+ {
+ $this->elementStart('form', array('id' => 'form_ostatus_connect',
+ 'method' => 'post',
+ 'class' => 'form_settings',
+ 'action' => common_local_url('ostatusinit')));
+ $this->elementStart('fieldset');
+ $this->element('legend', null, sprintf(_m('Subscribe to %s'), $this->nickname));
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li', array('id' => 'ostatus_nickname'));
+ $this->input('nickname', _m('User nickname'), $this->nickname,
+ _m('Nickname of the user you want to follow'));
+ $this->elementEnd('li');
+ $this->elementStart('li', array('id' => 'ostatus_profile'));
+ $this->input('acct', _m('Profile Account'), $this->acct,
+ _m('Your account id (i.e. user@identi.ca)'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('submit', _m('Subscribe'));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ function ostatusConnect()
+ {
+ $opts = array('allowed_schemes' => array('http', 'https', 'acct'));
+ if (Validate::uri($this->acct, $opts)) {
+ $bits = parse_url($this->acct);
+ if ($bits['scheme'] == 'acct') {
+ $this->connectWebfinger($bits['path']);
+ } else {
+ $this->connectProfile($this->acct);
+ }
+ } elseif (strpos('@', $this->acct) !== false) {
+ $this->connectWebfinger($this->acct);
+ }
+ }
+
+ function connectWebfinger($acct)
+ {
+ $w = new Webfinger;
+
+ $result = $w->lookup($acct);
+ if (!$result) {
+ $this->clientError(_m("Couldn't look up OStatus account profile."));
+ }
+ foreach ($result->links as $link) {
+ if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') {
+ // We found a URL - let's redirect!
+
+ $user = User::staticGet('nickname', $this->nickname);
+ $target_profile = common_local_url('userbyid', array('id' => $user->id));
+
+ $url = $w->applyTemplate($link['template'], $feed_url);
+
+ common_redirect($url, 303);
+ }
+
+ }
+
+ }
+
+ function connectProfile($subscriber_profile)
+ {
+ $user = User::staticGet('nickname', $this->nickname);
+ $target_profile = common_local_url('userbyid', array('id' => $user->id));
+
+ // @fixme hack hack! We should look up the remote sub URL from XRDS
+ $suburl = preg_replace('!^(.*)/(.*?)$!', '$1/main/ostatussub', $subscriber_profile);
+ $suburl .= '?profile=' . urlencode($target_profile);
+
+ common_redirect($suburl, 303);
+ }
+
+ function title()
+ {
+ return _m('OStatus Connect');
+ }
+
+}
diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php
new file mode 100644
index 000000000..bbbd1b7e6
--- /dev/null
+++ b/plugins/OStatus/actions/ostatussub.php
@@ -0,0 +1,272 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class OStatusSubAction extends Action
+{
+ protected $profile_uri;
+ protected $preview;
+ protected $munger;
+
+ /**
+ * Title of the page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
+ return _m('Authorize subscription');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:');
+ }
+
+ function showForm($error=null)
+ {
+ $this->error = $error;
+ $this->showPage();
+ }
+
+ /**
+ * Content area of the page
+ *
+ * Shows a form for associating a remote OStatus account with this
+ * StatusNet account.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ // @fixme is this right place?
+ if ($this->error) {
+ $this->text($this->error);
+ }
+
+ $user = common_current_user();
+
+ $profile = $user->getProfile();
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'ostatus_sub',
+ 'class' => 'form_settings',
+ 'action' =>
+ common_local_url('ostatussub')));
+
+ $this->hidden('token', common_session_token());
+
+ $this->elementStart('fieldset', array('id' => 'settings_feeds'));
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('profile',
+ _m('Address or profile URL'),
+ $this->profile_uri,
+ _m('Enter the profile URL of a PubSubHubbub-enabled feed'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ if ($this->preview) {
+ $this->submit('subscribe', _m('Subscribe'));
+ } else {
+ $this->submit('validate', _m('Continue'));
+ }
+
+ $this->elementEnd('fieldset');
+
+ $this->elementEnd('form');
+
+ if ($this->preview) {
+ $this->previewFeed();
+ }
+ }
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ if (!common_logged_in()) {
+ // XXX: selfURL() didn't work. :<
+ common_set_returnto($_SERVER['REQUEST_URI']);
+ if (Event::handle('RedirectToLogin', array($this, null))) {
+ common_redirect(common_local_url('login'), 303);
+ }
+ return false;
+ }
+
+ $this->profile_uri = $this->arg('profile');
+
+ return true;
+ }
+
+ function handle($args)
+ {
+ parent::handle($args);
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->handlePost();
+ } else {
+ if ($this->profile_uri) {
+ $this->validateAndPreview();
+ } else {
+ $this->showPage();
+ }
+ }
+ }
+
+ /**
+ * Handle posts to this form
+ *
+ * Based on the button that was pressed, muxes out to other functions
+ * to do the actual task requested.
+ *
+ * All sub-functions reload the form with a message -- success or failure.
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('validate')) {
+ $this->validateAndPreview();
+ } else if ($this->arg('subscribe')) {
+ $this->saveFeed();
+ } else {
+ $this->showForm(_('Unexpected form submission.'));
+ }
+ }
+
+ /**
+ * Set up and add a feed
+ *
+ * @return boolean true if feed successfully read
+ * Sends you back to input form if not.
+ */
+ function validateFeed()
+ {
+ $profile_uri = trim($this->arg('profile'));
+
+ if ($profile_uri == '') {
+ $this->showForm(_m('Empty remote profile URL!'));
+ return;
+ }
+ $this->profile_uri = $profile_uri;
+
+ // @fixme validate, normalize bla bla
+ try {
+ $oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
+ $this->oprofile = $oprofile;
+ return true;
+ } catch (FeedSubBadURLException $e) {
+ $err = _m('Invalid URL or could not reach server.');
+ } catch (FeedSubBadResponseException $e) {
+ $err = _m('Cannot read feed; server returned error.');
+ } catch (FeedSubEmptyException $e) {
+ $err = _m('Cannot read feed; server returned an empty page.');
+ } catch (FeedSubBadHTMLException $e) {
+ $err = _m('Bad HTML, could not find feed link.');
+ } catch (FeedSubNoFeedException $e) {
+ $err = _m('Could not find a feed linked from this URL.');
+ } catch (FeedSubUnrecognizedTypeException $e) {
+ $err = _m('Not a recognized feed type.');
+ } catch (FeedSubException $e) {
+ // Any new ones we forgot about
+ $err = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage());
+ }
+
+ $this->showForm($err);
+ return false;
+ }
+
+ function saveFeed()
+ {
+ if ($this->validateFeed()) {
+ $this->preview = true;
+
+ // And subscribe the current user to the local profile
+ $user = common_current_user();
+
+ if (!$this->oprofile->subscribe()) {
+ $this->showForm(_m("Failed to set up server-to-server subscription."));
+ return;
+ }
+
+ if ($this->oprofile->isGroup()) {
+ $group = $this->oprofile->localGroup();
+ if ($user->isMember($group)) {
+ $this->showForm(_m('Already a member!'));
+ } elseif (Group_member::join($this->profile->group_id, $user->id)) {
+ $this->showForm(_m('Joined remote group!'));
+ } else {
+ $this->showForm(_m('Remote group join failed!'));
+ }
+ } else {
+ $local = $this->oprofile->localProfile();
+ if ($user->isSubscribed($local)) {
+ $this->showForm(_m('Already subscribed!'));
+ } elseif ($this->oprofile->subscribeLocalToRemote($user)) {
+ $this->showForm(_m('Remote user subscribed!'));
+ } else {
+ $this->showForm(_m('Remote subscription failed!'));
+ }
+ }
+ }
+ }
+
+ function validateAndPreview()
+ {
+ if ($this->validateFeed()) {
+ $this->preview = true;
+ $this->showForm(_m('Previewing feed:'));
+ }
+ }
+
+ function previewFeed()
+ {
+ $this->text('Profile preview should go here');
+ }
+
+ function showScripts()
+ {
+ parent::showScripts();
+ $this->autofocus('feedurl');
+ }
+}
diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php
new file mode 100644
index 000000000..4184f0e0c
--- /dev/null
+++ b/plugins/OStatus/actions/pushcallback.php
@@ -0,0 +1,122 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package FeedSubPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+
+class PushCallbackAction extends Action
+{
+ function handle()
+ {
+ StatusNet::setApi(true); // Minimize error messages to aid in debugging
+ parent::handle();
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->handlePost();
+ } else {
+ $this->handleGet();
+ }
+ }
+
+ /**
+ * Handler for POST content updates from the hub
+ */
+ function handlePost()
+ {
+ $feedid = $this->arg('feed');
+ common_log(LOG_INFO, "POST for feed id $feedid");
+ if (!$feedid) {
+ throw new ServerException('Empty or invalid feed id', 400);
+ }
+
+ $feedsub = FeedSub::staticGet('id', $feedid);
+ if (!$feedsub) {
+ throw new ServerException('Unknown PuSH feed id ' . $feedid, 400);
+ }
+
+ $hmac = '';
+ if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) {
+ $hmac = $_SERVER['HTTP_X_HUB_SIGNATURE'];
+ }
+
+ $post = file_get_contents('php://input');
+
+ // Queue this to a background process; we should return
+ // as quickly as possible from a distribution POST.
+ // If queues are disabled this'll process immediately.
+ $data = array('feedsub_id' => $feedsub->id,
+ 'post' => $post,
+ 'hmac' => $hmac);
+ $qm = QueueManager::get();
+ $qm->enqueue($data, 'pushinput');
+ }
+
+ /**
+ * Handler for GET verification requests from the hub.
+ */
+ function handleGet()
+ {
+ $mode = $this->arg('hub_mode');
+ $topic = $this->arg('hub_topic');
+ $challenge = $this->arg('hub_challenge');
+ $lease_seconds = $this->arg('hub_lease_seconds');
+ $verify_token = $this->arg('hub_verify_token');
+
+ if ($mode != 'subscribe' && $mode != 'unsubscribe') {
+ throw new ClientException("Bad hub.mode $mode", 404);
+ }
+
+ $feedsub = FeedSub::staticGet('uri', $topic);
+ if (!$feedsub) {
+ throw new ClientException("Bad hub.topic feed $topic", 404);
+ }
+
+ if ($feedsub->verify_token !== $verify_token) {
+ throw new ClientException("Bad hub.verify_token $token for $topic", 404);
+ }
+
+ if ($mode == 'subscribe') {
+ // We may get re-sub requests legitimately.
+ if ($feedsub->sub_state != 'subscribe' && $feedsub->sub_state != 'active') {
+ throw new ClientException("Unexpected subscribe request for $topic.", 404);
+ }
+ } else {
+ if ($feedsub->sub_state != 'unsubscribe') {
+ throw new ClientException("Unexpected unsubscribe request for $topic.", 404);
+ }
+ }
+
+ if ($mode == 'subscribe') {
+ if ($feedsub->sub_state == 'active') {
+ common_log(LOG_INFO, __METHOD__ . ': sub update confirmed');
+ } else {
+ common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
+ }
+ $feedsub->confirmSubscribe($lease_seconds);
+ } else {
+ common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
+ $feedsub->confirmUnsubscribe();
+ }
+ print $challenge;
+ }
+}
diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php
new file mode 100644
index 000000000..f33690bc4
--- /dev/null
+++ b/plugins/OStatus/actions/pushhub.php
@@ -0,0 +1,202 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Integrated PuSH hub; lets us only ping them what need it.
+ * @package Hub
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+/**
+
+
+Things to consider...
+* should we purge incomplete subscriptions that never get a verification pingback?
+* when can we send subscription renewal checks?
+ - at next send time probably ok
+* when can we handle trimming of subscriptions?
+ - at next send time probably ok
+* should we keep a fail count?
+
+*/
+
+
+class PushHubAction extends Action
+{
+ function arg($arg, $def=null)
+ {
+ // PHP converts '.'s in incoming var names to '_'s.
+ // It also merges multiple values, which'll break hub.verify and hub.topic for publishing
+ // @fixme handle multiple args
+ $arg = str_replace('hub.', 'hub_', $arg);
+ return parent::arg($arg, $def);
+ }
+
+ function prepare($args)
+ {
+ StatusNet::setApi(true); // reduce exception reports to aid in debugging
+ return parent::prepare($args);
+ }
+
+ function handle()
+ {
+ $mode = $this->trimmed('hub.mode');
+ switch ($mode) {
+ case "subscribe":
+ case "unsubscribe":
+ $this->subunsub($mode);
+ break;
+ case "publish":
+ throw new ClientException("Publishing outside feeds not supported.", 400);
+ default:
+ throw new ClientException("Unrecognized mode '$mode'.", 400);
+ }
+ }
+
+ /**
+ * Process a request for a new or modified PuSH feed subscription.
+ * If asynchronous verification is requested, updates won't be saved immediately.
+ *
+ * HTTP return codes:
+ * 202 Accepted - request saved and awaiting verification
+ * 204 No Content - already subscribed
+ * 400 Bad Request - rejecting this (not specifically spec'd)
+ */
+ function subunsub($mode)
+ {
+ $callback = $this->argUrl('hub.callback');
+
+ $topic = $this->argUrl('hub.topic');
+ if (!$this->recognizedFeed($topic)) {
+ throw new ClientException("Unsupported hub.topic $topic; this hub only serves local user and group Atom feeds.");
+ }
+
+ $verify = $this->arg('hub.verify'); // @fixme may be multiple
+ if ($verify != 'sync' && $verify != 'async') {
+ throw new ClientException("Invalid hub.verify $verify; must be sync or async.");
+ }
+
+ $lease = $this->arg('hub.lease_seconds', null);
+ if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) {
+ throw new ClientException("Invalid hub.lease $lease; must be empty or positive integer.");
+ }
+
+ $token = $this->arg('hub.verify_token', null);
+
+ $secret = $this->arg('hub.secret', null);
+ if ($secret != '' && strlen($secret) >= 200) {
+ throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes.");
+ }
+
+ $sub = HubSub::staticGet($sub->topic, $sub->callback);
+ if (!$sub) {
+ // Creating a new one!
+ $sub = new HubSub();
+ $sub->topic = $topic;
+ $sub->callback = $callback;
+ }
+ if ($mode == 'subscribe') {
+ if ($secret) {
+ $sub->secret = $secret;
+ }
+ if ($lease) {
+ $sub->setLease(intval($lease));
+ }
+ }
+
+ if (!common_config('queue', 'enabled')) {
+ // Won't be able to background it.
+ $verify = 'sync';
+ }
+ if ($verify == 'async') {
+ $sub->scheduleVerify($mode, $token);
+ header('HTTP/1.1 202 Accepted');
+ } else {
+ $sub->verify($mode, $token);
+ header('HTTP/1.1 204 No Content');
+ }
+ }
+
+ /**
+ * Check whether the given URL represents one of our canonical
+ * user or group Atom feeds.
+ *
+ * @param string $feed URL
+ * @return boolean true if it matches
+ */
+ function recognizedFeed($feed)
+ {
+ $matches = array();
+ if (preg_match('!/(\d+)\.atom$!', $feed, $matches)) {
+ $id = $matches[1];
+ $params = array('id' => $id, 'format' => 'atom');
+ $userFeed = common_local_url('ApiTimelineUser', $params);
+ $groupFeed = common_local_url('ApiTimelineGroup', $params);
+
+ if ($feed == $userFeed) {
+ $user = User::staticGet('id', $id);
+ if (!$user) {
+ throw new ClientException("Invalid hub.topic $feed; user doesn't exist.");
+ } else {
+ return true;
+ }
+ }
+ if ($feed == $groupFeed) {
+ $user = User_group::staticGet('id', $id);
+ if (!$user) {
+ throw new ClientException("Invalid hub.topic $feed; group doesn't exist.");
+ } else {
+ return true;
+ }
+ }
+ common_log(LOG_DEBUG, "Not a user or group feed? $feed $userFeed $groupFeed");
+ }
+ common_log(LOG_DEBUG, "LOST $feed");
+ return false;
+ }
+
+ /**
+ * Grab and validate a URL from POST parameters.
+ * @throws ClientException for malformed or non-http/https URLs
+ */
+ protected function argUrl($arg)
+ {
+ $url = $this->arg($arg);
+ $params = array('domain_check' => false, // otherwise breaks my local tests :P
+ 'allowed_schemes' => array('http', 'https'));
+ if (Validate::uri($url, $params)) {
+ return $url;
+ } else {
+ throw new ClientException("Invalid URL passed for $arg: '$url'");
+ }
+ }
+
+ /**
+ * Get HubSub subscription record for a given feed & subscriber.
+ *
+ * @param string $feed
+ * @param string $callback
+ * @return mixed HubSub or false
+ */
+ protected function getSub($feed, $callback)
+ {
+ return HubSub::staticGet($feed, $callback);
+ }
+}
+
diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php
new file mode 100644
index 000000000..12c74798f
--- /dev/null
+++ b/plugins/OStatus/actions/usersalmon.php
@@ -0,0 +1,202 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @author James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class UsersalmonAction extends SalmonAction
+{
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $id = $this->trimmed('id');
+
+ if (!$id) {
+ $this->clientError(_('No ID.'));
+ }
+
+ $this->user = User::staticGet('id', $id);
+
+ if (empty($this->user)) {
+ $this->clientError(_('No such user.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * We've gotten a post event on the Salmon backchannel, probably a reply.
+ *
+ * @todo validate if we need to handle this post, then call into
+ * ostatus_profile's general incoming-post handling.
+ */
+ function handlePost()
+ {
+ switch ($this->act->object->type) {
+ case ActivityObject::ARTICLE:
+ case ActivityObject::BLOGENTRY:
+ case ActivityObject::NOTE:
+ case ActivityObject::STATUS:
+ case ActivityObject::COMMENT:
+ break;
+ default:
+ throw new ClientException("Can't handle that kind of post.");
+ }
+
+ // Notice must either be a) in reply to a notice by this user
+ // or b) to the attention of this user
+
+ $context = $this->act->context;
+
+ if (!empty($context->replyToID)) {
+ $notice = Notice::staticGet('uri', $context->replyToID);
+ if (empty($notice)) {
+ throw new ClientException("In reply to unknown notice");
+ }
+ if ($notice->profile_id != $this->user->id) {
+ throw new ClientException("In reply to a notice not by this user");
+ }
+ } else if (!empty($context->attention)) {
+ if (!in_array($context->attention, $this->user->uri)) {
+ throw new ClientException("To the attention of user(s) not including this one!");
+ }
+ } else {
+ throw new ClientException("Not to anyone in reply to anything!");
+ }
+
+ $this->saveNotice();
+ }
+
+ /**
+ * We've gotten a follow/subscribe notification from a remote user.
+ * Save a subscription relationship for them.
+ */
+
+ function handleFollow()
+ {
+ $oprofile = $this->ensureProfile();
+ if ($oprofile) {
+ common_log(LOG_INFO, "Setting up subscription from remote {$oprofile->uri} to local {$this->user->nickname}");
+ Subscription::start($oprofile->localProfile(),
+ $this->user->getProfile());
+ } else {
+ common_log(LOG_INFO, "Can't set up subscription from remote; missing profile.");
+ }
+ }
+
+ /**
+ * We've gotten an unfollow/unsubscribe notification from a remote user.
+ * Check if we have a subscription relationship for them and kill it.
+ *
+ * @fixme probably catch exceptions on fail?
+ */
+ function handleUnfollow()
+ {
+ $oprofile = $this->ensureProfile();
+ if ($oprofile) {
+ common_log(LOG_INFO, "Canceling subscription from remote {$oprofile->uri} to local {$this->user->nickname}");
+ Subscription::cancel($oprofile->localProfile(), $this->user->getProfile());
+ } else {
+ common_log(LOG_ERR, "Can't cancel subscription from remote, didn't find the profile");
+ }
+ }
+
+ /**
+ * Remote user likes one of our posts.
+ * Confirm the post is ours, and save a local favorite event.
+ */
+
+ function handleFavorite()
+ {
+ $notice = $this->getNotice($this->act->object);
+ $profile = $this->ensureProfile()->localProfile();
+
+ $old = Fave::pkeyGet(array('user_id' => $profile->id,
+ 'notice_id' => $notice->id));
+
+ if (!empty($old)) {
+ throw new ClientException("We already know that's a fave!");
+ }
+
+ if (!Fave::addNew($profile, $notice)) {
+ throw new ClientException("Could not save new favorite.");
+ }
+ }
+
+ /**
+ * Remote user doesn't like one of our posts after all!
+ * Confirm the post is ours, and save a local favorite event.
+ */
+ function handleUnfavorite()
+ {
+ $notice = $this->getNotice($this->act->object);
+ $profile = $this->ensureProfile()->localProfile();
+
+ $fave = Fave::pkeyGet(array('user_id' => $profile->id,
+ 'notice_id' => $notice->id));
+ if (empty($fave)) {
+ throw new ClientException("Notice wasn't favorited!");
+ }
+
+ $fave->delete();
+ }
+
+ /**
+ * @param ActivityObject $object
+ * @return Notice
+ * @throws ClientException on invalid input
+ */
+ function getNotice($object)
+ {
+ if (!$object) {
+ throw new ClientException("Can't favorite/unfavorite without an object.");
+ }
+
+ switch ($object->type) {
+ case ActivityObject::ARTICLE:
+ case ActivityObject::BLOGENTRY:
+ case ActivityObject::NOTE:
+ case ActivityObject::STATUS:
+ case ActivityObject::COMMENT:
+ break;
+ default:
+ throw new ClientException("Can't handle that kind of object for liking/faving.");
+ }
+
+ $notice = Notice::staticGet('uri', $object->id);
+
+ if (empty($notice)) {
+ throw new ClientException("Notice with ID $object->id unknown.");
+ }
+
+ if ($notice->profile_id != $this->user->id) {
+ throw new ClientException("Notice with ID $object->id not posted by $this->user->id.");
+ }
+
+ return $notice;
+ }
+
+}
diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/webfinger.php
new file mode 100644
index 000000000..cf60b8069
--- /dev/null
+++ b/plugins/OStatus/actions/webfinger.php
@@ -0,0 +1,83 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+class WebfingerAction extends Action
+{
+
+ public $uri;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $this->uri = $this->trimmed('uri');
+
+ return true;
+ }
+
+ function handle()
+ {
+ $acct = Webfinger::normalize($this->uri);
+
+ $xrd = new XRD();
+
+ list($nick, $domain) = explode('@', urldecode($acct));
+ $nick = common_canonical_nickname($nick);
+
+ $this->user = User::staticGet('nickname', $nick);
+ if (!$this->user) {
+ $this->clientError(_('No such user.'), 404);
+ return false;
+ }
+
+ $xrd->subject = $this->uri;
+ $xrd->alias[] = common_profile_url($nick);
+ $xrd->links[] = array('rel' => Webfinger::PROFILEPAGE,
+ 'type' => 'text/html',
+ 'href' => common_profile_url($nick));
+
+ $xrd->links[] = array('rel' => Webfinger::UPDATESFROM,
+ 'href' => common_local_url('ApiTimelineUser',
+ array('id' => $this->user->id,
+ 'format' => 'atom')),
+ 'type' => 'application/atom+xml');
+
+ $salmon_url = common_local_url('salmon',
+ array('id' => $this->user->id));
+
+ $xrd->links[] = array('rel' => 'salmon',
+ 'href' => $salmon_url);
+
+ // TODO - finalize where the redirect should go on the publisher
+ $url = common_local_url('ostatussub') . '?profile={uri}';
+ $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
+ 'template' => $url );
+
+ header('Content-type: text/xml');
+ print $xrd->toXML();
+ }
+
+}
diff --git a/plugins/OStatus/classes/FeedSub.php b/plugins/OStatus/classes/FeedSub.php
new file mode 100644
index 000000000..b848b6b1d
--- /dev/null
+++ b/plugins/OStatus/classes/FeedSub.php
@@ -0,0 +1,452 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+/*
+PuSH subscription flow:
+
+ $profile->subscribe()
+ generate random verification token
+ save to verify_token
+ sends a sub request to the hub...
+
+ main/push/callback
+ hub sends confirmation back to us via GET
+ We verify the request, then echo back the challenge.
+ On our end, we save the time we subscribed and the lease expiration
+
+ main/push/callback
+ hub sends us updates via POST
+
+*/
+
+class FeedDBException extends FeedSubException
+{
+ public $obj;
+
+ function __construct($obj)
+ {
+ parent::__construct('Database insert failure');
+ $this->obj = $obj;
+ }
+}
+
+/**
+ * FeedSub handles low-level PubHubSubbub (PuSH) subscriptions.
+ * Higher-level behavior building OStatus stuff on top is handled
+ * under Ostatus_profile.
+ */
+class FeedSub extends Memcached_DataObject
+{
+ public $__table = 'feedsub';
+
+ public $id;
+ public $feeduri;
+
+ // PuSH subscription data
+ public $huburi;
+ public $secret;
+ public $verify_token;
+ public $sub_state; // subscribe, active, unsubscribe, inactive
+ public $sub_start;
+ public $sub_end;
+ public $last_update;
+
+ public $created;
+ public $modified;
+
+ public /*static*/ function staticGet($k, $v=null)
+ {
+ return parent::staticGet(__CLASS__, $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+
+ function table()
+ {
+ return array('id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+ 'uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'huburi' => DB_DATAOBJECT_STR,
+ 'secret' => DB_DATAOBJECT_STR,
+ 'verify_token' => DB_DATAOBJECT_STR,
+ 'sub_state' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+ 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+ 'last_update' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+ 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+ 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+ }
+
+ static function schemaDef()
+ {
+ return array(new ColumnDef('id', 'integer',
+ /*size*/ null,
+ /*nullable*/ false,
+ /*key*/ 'PRI',
+ /*default*/ '0',
+ /*extra*/ null,
+ /*auto_increment*/ true),
+ new ColumnDef('uri', 'varchar',
+ 255, false, 'UNI'),
+ new ColumnDef('huburi', 'text',
+ null, true),
+ new ColumnDef('verify_token', 'text',
+ null, true),
+ new ColumnDef('secret', 'text',
+ null, true),
+ new ColumnDef('sub_state', "enum('subscribe','active','unsubscribe','inactive')",
+ null, false),
+ new ColumnDef('sub_start', 'datetime',
+ null, true),
+ new ColumnDef('sub_end', 'datetime',
+ null, true),
+ new ColumnDef('last_update', 'datetime',
+ null, false),
+ new ColumnDef('created', 'datetime',
+ null, false),
+ new ColumnDef('modified', 'datetime',
+ null, false));
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has; this function
+ * defines them.
+ *
+ * @return array key definitions
+ */
+
+ function keys()
+ {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * Our caching system uses the same key definitions, but uses a different
+ * method to get them.
+ *
+ * @return array key definitions
+ */
+
+ function keyTypes()
+ {
+ return array('id' => 'K', 'uri' => 'U');
+ }
+
+ function sequenceKey()
+ {
+ return array('id', true, false);
+ }
+
+ /**
+ * Fetch the StatusNet-side profile for this feed
+ * @return Profile
+ */
+ public function localProfile()
+ {
+ if ($this->profile_id) {
+ return Profile::staticGet('id', $this->profile_id);
+ }
+ return null;
+ }
+
+ /**
+ * Fetch the StatusNet-side profile for this feed
+ * @return Profile
+ */
+ public function localGroup()
+ {
+ if ($this->group_id) {
+ return User_group::staticGet('id', $this->group_id);
+ }
+ return null;
+ }
+
+ /**
+ * @param string $feeduri
+ * @return FeedSub
+ * @throws FeedSubException if feed is invalid or lacks PuSH setup
+ */
+ public static function ensureFeed($feeduri)
+ {
+ $current = self::staticGet('uri', $feeduri);
+ if ($current) {
+ return $current;
+ }
+
+ $discover = new FeedDiscovery();
+ $discover->discoverFromFeedURL($feeduri);
+
+ $huburi = $discover->getAtomLink('hub');
+ if (!$huburi) {
+ throw new FeedSubNoHubException();
+ }
+
+ $feedsub = new FeedSub();
+ $feedsub->uri = $feeduri;
+ $feedsub->huburi = $huburi;
+ $feedsub->sub_state = 'inactive';
+
+ $feedsub->created = common_sql_now();
+ $feedsub->modified = common_sql_now();
+
+ $result = $feedsub->insert();
+ if (empty($result)) {
+ throw new FeedDBException($feedsub);
+ }
+
+ return $feedsub;
+ }
+
+ /**
+ * Send a subscription request to the hub for this feed.
+ * The hub will later send us a confirmation POST to /main/push/callback.
+ *
+ * @return bool true on success, false on failure
+ * @throws ServerException if feed state is not valid
+ */
+ public function subscribe($mode='subscribe')
+ {
+ if ($this->sub_state && $this->sub_state != 'inactive') {
+ throw new ServerException("Attempting to start PuSH subscription to feed in state $this->sub_state");
+ }
+ if (empty($this->huburi)) {
+ if (common_config('feedsub', 'nohub')) {
+ // Fake it! We're just testing remote feeds w/o hubs.
+ return true;
+ } else {
+ throw new ServerException("Attempting to start PuSH subscription for feed with no hub");
+ }
+ }
+
+ return $this->doSubscribe('subscribe');
+ }
+
+ /**
+ * Send a PuSH unsubscription request to the hub for this feed.
+ * The hub will later send us a confirmation POST to /main/push/callback.
+ *
+ * @return bool true on success, false on failure
+ * @throws ServerException if feed state is not valid
+ */
+ public function unsubscribe() {
+ if ($this->sub_state != 'active') {
+ throw new ServerException("Attempting to end PuSH subscription to feed in state $this->sub_state");
+ }
+ if (empty($this->huburi)) {
+ if (common_config('feedsub', 'nohub')) {
+ // Fake it! We're just testing remote feeds w/o hubs.
+ return true;
+ } else {
+ throw new ServerException("Attempting to end PuSH subscription for feed with no hub");
+ }
+ }
+
+ return $this->doSubscribe('unsubscribe');
+ }
+
+ protected function doSubscribe($mode)
+ {
+ $orig = clone($this);
+ $this->verify_token = common_good_rand(16);
+ if ($mode == 'subscribe') {
+ $this->secret = common_good_rand(32);
+ }
+ $this->sub_state = $mode;
+ $this->update($orig);
+ unset($orig);
+
+ try {
+ $callback = common_local_url('pushcallback', array('feed' => $this->id));
+ $headers = array('Content-Type: application/x-www-form-urlencoded');
+ $post = array('hub.mode' => $mode,
+ 'hub.callback' => $callback,
+ 'hub.verify' => 'sync',
+ 'hub.verify_token' => $this->verify_token,
+ 'hub.secret' => $this->secret,
+ 'hub.topic' => $this->uri);
+ $client = new HTTPClient();
+ $response = $client->post($this->huburi, $headers, $post);
+ $status = $response->getStatus();
+ if ($status == 202) {
+ common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
+ return true;
+ } else if ($status == 204) {
+ common_log(LOG_INFO, __METHOD__ . ': sub req ok and verified');
+ return true;
+ } else if ($status >= 200 && $status < 300) {
+ common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
+ return false;
+ } else {
+ common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
+ return false;
+ }
+ } catch (Exception $e) {
+ // wtf!
+ common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->uri");
+
+ $orig = clone($this);
+ $this->verify_token = '';
+ $this->sub_state = 'inactive';
+ $this->update($orig);
+ unset($orig);
+
+ return false;
+ }
+ }
+
+ /**
+ * Save PuSH subscription confirmation.
+ * Sets approximate lease start and end times and finalizes state.
+ *
+ * @param int $lease_seconds provided hub.lease_seconds parameter, if given
+ */
+ public function confirmSubscribe($lease_seconds=0)
+ {
+ $original = clone($this);
+
+ $this->sub_state = 'active';
+ $this->sub_start = common_sql_date(time());
+ if ($lease_seconds > 0) {
+ $this->sub_end = common_sql_date(time() + $lease_seconds);
+ } else {
+ $this->sub_end = null;
+ }
+ $this->modified = common_sql_now();
+
+ return $this->update($original);
+ }
+
+ /**
+ * Save PuSH unsubscription confirmation.
+ * Wipes active PuSH sub info and resets state.
+ */
+ public function confirmUnsubscribe()
+ {
+ $original = clone($this);
+
+ // @fixme these should all be null, but DB_DataObject doesn't save null values...?????
+ $this->verify_token = '';
+ $this->secret = '';
+ $this->sub_state = '';
+ $this->sub_start = '';
+ $this->sub_end = '';
+ $this->modified = common_sql_now();
+
+ return $this->update($original);
+ }
+
+ /**
+ * Accept updates from a PuSH feed. If validated, this object and the
+ * feed (as a DOMDocument) will be passed to the StartFeedSubHandleFeed
+ * and EndFeedSubHandleFeed events for processing.
+ *
+ * Not guaranteed to be running in an immediate POST context; may be run
+ * from a queue handler.
+ *
+ * Side effects: the feedsub record's lastupdate field will be updated
+ * to the current time (not published time) if we got a legit update.
+ *
+ * @param string $post source of Atom or RSS feed
+ * @param string $hmac X-Hub-Signature header, if present
+ */
+ public function receive($post, $hmac)
+ {
+ common_log(LOG_INFO, __METHOD__ . ": packet for \"$this->uri\"! $hmac $post");
+
+ if ($this->sub_state != 'active') {
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed $this->uri (in state '$this->sub_state')");
+ return;
+ }
+
+ if ($post === '') {
+ common_log(LOG_ERR, __METHOD__ . ": ignoring empty post");
+ return;
+ }
+
+ if (!$this->validatePushSig($post, $hmac)) {
+ // Per spec we silently drop input with a bad sig,
+ // while reporting receipt to the server.
+ return;
+ }
+
+ $feed = new DOMDocument();
+ if (!$feed->loadXML($post)) {
+ // @fixme might help to include the err message
+ common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML");
+ return;
+ }
+
+ $orig = clone($this);
+ $this->last_update = common_sql_now();
+ $this->update($orig);
+
+ Event::handle('StartFeedSubReceive', array($this, $feed));
+ Event::handle('EndFeedSubReceive', array($this, $feed));
+ }
+
+ /**
+ * Validate the given Atom chunk and HMAC signature against our
+ * shared secret that was set up at subscription time.
+ *
+ * If we don't have a shared secret, there should be no signature.
+ * If we we do, our the calculated HMAC should match theirs.
+ *
+ * @param string $post raw XML source as POSTed to us
+ * @param string $hmac X-Hub-Signature HTTP header value, or empty
+ * @return boolean true for a match
+ */
+ protected function validatePushSig($post, $hmac)
+ {
+ if ($this->secret) {
+ if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
+ $their_hmac = strtolower($matches[1]);
+ $our_hmac = hash_hmac('sha1', $post, $this->secret);
+ if ($their_hmac === $our_hmac) {
+ return true;
+ }
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac");
+ } else {
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
+ }
+ } else {
+ if (empty($hmac)) {
+ return true;
+ } else {
+ common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php
new file mode 100644
index 000000000..eae2928c3
--- /dev/null
+++ b/plugins/OStatus/classes/HubSub.php
@@ -0,0 +1,305 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PuSH feed subscription record
+ * @package Hub
+ * @author Brion Vibber <brion@status.net>
+ */
+class HubSub extends Memcached_DataObject
+{
+ public $__table = 'hubsub';
+
+ public $hashkey; // sha1(topic . '|' . $callback); (topic, callback) key is too long for myisam in utf8
+ public $topic;
+ public $callback;
+ public $secret;
+ public $lease;
+ public $sub_start;
+ public $sub_end;
+ public $created;
+ public $modified;
+
+ public /*static*/ function staticGet($topic, $callback)
+ {
+ return parent::staticGet(__CLASS__, 'hashkey', self::hashkey($topic, $callback));
+ }
+
+ protected static function hashkey($topic, $callback)
+ {
+ return sha1($topic . '|' . $callback);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+
+ function table()
+ {
+ return array('hashkey' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'secret' => DB_DATAOBJECT_STR,
+ 'lease' => DB_DATAOBJECT_INT,
+ 'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+ 'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
+ 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+ 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+ }
+
+ static function schemaDef()
+ {
+ return array(new ColumnDef('hashkey', 'char',
+ /*size*/40,
+ /*nullable*/false,
+ /*key*/'PRI'),
+ new ColumnDef('topic', 'varchar',
+ /*size*/255,
+ /*nullable*/false,
+ /*key*/'KEY'),
+ new ColumnDef('callback', 'varchar',
+ 255, false),
+ new ColumnDef('secret', 'text',
+ null, true),
+ new ColumnDef('lease', 'int',
+ null, true),
+ new ColumnDef('sub_start', 'datetime',
+ null, true),
+ new ColumnDef('sub_end', 'datetime',
+ null, true),
+ new ColumnDef('created', 'datetime',
+ null, false),
+ new ColumnDef('modified', 'datetime',
+ null, false));
+ }
+
+ function keys()
+ {
+ return array_keys($this->keyTypes());
+ }
+
+ function sequenceKeys()
+ {
+ return array(false, false, false);
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has; this function
+ * defines them.
+ *
+ * @return array key definitions
+ */
+
+ function keyTypes()
+ {
+ return array('hashkey' => 'K');
+ }
+
+ /**
+ * Validates a requested lease length, sets length plus
+ * subscription start & end dates.
+ *
+ * Does not save to database -- use before insert() or update().
+ *
+ * @param int $length in seconds
+ */
+ function setLease($length)
+ {
+ assert(is_int($length));
+
+ $min = 86400;
+ $max = 86400 * 30;
+
+ if ($length == 0) {
+ // We want to garbage collect dead subscriptions!
+ $length = $max;
+ } elseif( $length < $min) {
+ $length = $min;
+ } else if ($length > $max) {
+ $length = $max;
+ }
+
+ $this->lease = $length;
+ $this->start_sub = common_sql_now();
+ $this->end_sub = common_sql_date(time() + $length);
+ }
+
+ /**
+ * Schedule a future verification ping to the subscriber.
+ * If queues are disabled, will be immediate.
+ *
+ * @param string $mode 'subscribe' or 'unsubscribe'
+ * @param string $token hub.verify_token value, if provided by client
+ */
+ function scheduleVerify($mode, $token=null, $retries=null)
+ {
+ if ($retries === null) {
+ $retries = intval(common_config('ostatus', 'hub_retries'));
+ }
+ $data = array('sub' => clone($this),
+ 'mode' => $mode,
+ 'token' => $token,
+ 'retries' => $retries);
+ $qm = QueueManager::get();
+ $qm->enqueue($data, 'hubverify');
+ }
+
+ /**
+ * Send a verification ping to subscriber, and if confirmed apply the changes.
+ * This may create, update, or delete the database record.
+ *
+ * @param string $mode 'subscribe' or 'unsubscribe'
+ * @param string $token hub.verify_token value, if provided by client
+ * @throws ClientException on failure
+ */
+ function verify($mode, $token=null)
+ {
+ assert($mode == 'subscribe' || $mode == 'unsubscribe');
+
+ $challenge = common_good_rand(32);
+ $params = array('hub.mode' => $mode,
+ 'hub.topic' => $this->topic,
+ 'hub.challenge' => $challenge);
+ if ($mode == 'subscribe') {
+ $params['hub.lease_seconds'] = $this->lease;
+ }
+ if ($token !== null) {
+ $params['hub.verify_token'] = $token;
+ }
+
+ // Any existing query string parameters must be preserved
+ $url = $this->callback;
+ if (strpos('?', $url) !== false) {
+ $url .= '&';
+ } else {
+ $url .= '?';
+ }
+ $url .= http_build_query($params, '', '&');
+
+ $request = new HTTPClient();
+ $response = $request->get($url);
+ $status = $response->getStatus();
+
+ if ($status >= 200 && $status < 300) {
+ common_log(LOG_INFO, "Verified $mode of $this->callback:$this->topic");
+ } else {
+ throw new ClientException("Hub subscriber verification returned HTTP $status");
+ }
+
+ $old = HubSub::staticGet($this->topic, $this->callback);
+ if ($mode == 'subscribe') {
+ if ($old) {
+ $this->update($old);
+ } else {
+ $ok = $this->insert();
+ }
+ } else if ($mode == 'unsubscribe') {
+ if ($old) {
+ $old->delete();
+ } else {
+ // That's ok, we're already unsubscribed.
+ }
+ }
+ }
+
+ /**
+ * Insert wrapper; transparently set the hash key from topic and callback columns.
+ * @return mixed success
+ */
+ function insert()
+ {
+ $this->hashkey = self::hashkey($this->topic, $this->callback);
+ $this->created = common_sql_now();
+ $this->modified = common_sql_now();
+ return parent::insert();
+ }
+
+ /**
+ * Update wrapper; transparently update modified column.
+ * @return boolean success
+ */
+ function update($old=null)
+ {
+ $this->modified = common_sql_now();
+ return parent::update($old);
+ }
+
+ /**
+ * Schedule delivery of a 'fat ping' to the subscriber's callback
+ * endpoint. If queues are disabled, this will run immediately.
+ *
+ * @param string $atom well-formed Atom feed
+ * @param int $retries optional count of retries if POST fails; defaults to hub_retries from config or 0 if unset
+ */
+ function distribute($atom, $retries=null)
+ {
+ if ($retries === null) {
+ $retries = intval(common_config('ostatus', 'hub_retries'));
+ }
+
+ $data = array('sub' => clone($this),
+ 'atom' => $atom,
+ 'retries' => $retries);
+ $qm = QueueManager::get();
+ $qm->enqueue($data, 'hubout');
+ }
+
+ /**
+ * Send a 'fat ping' to the subscriber's callback endpoint
+ * containing the given Atom feed chunk.
+ *
+ * Determination of which items to send should be done at
+ * a higher level; don't just shove in a complete feed!
+ *
+ * @param string $atom well-formed Atom feed
+ * @throws Exception (HTTP or general)
+ */
+ function push($atom)
+ {
+ $headers = array('Content-Type: application/atom+xml');
+ if ($this->secret) {
+ $hmac = hash_hmac('sha1', $atom, $this->secret);
+ $headers[] = "X-Hub-Signature: sha1=$hmac";
+ } else {
+ $hmac = '(none)';
+ }
+ common_log(LOG_INFO, "About to push feed to $this->callback for $this->topic, HMAC $hmac");
+
+ $request = new HTTPClient();
+ $request->setBody($atom);
+ $response = $request->post($this->callback, $headers);
+
+ if ($response->isOk()) {
+ return true;
+ } else {
+ throw new Exception("Callback returned status: " .
+ $response->getStatus() .
+ "; body: " .
+ trim($response->getBody()));
+ }
+ }
+}
+
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php
new file mode 100644
index 000000000..700168c11
--- /dev/null
+++ b/plugins/OStatus/classes/Ostatus_profile.php
@@ -0,0 +1,951 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009-2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @maintainer Brion Vibber <brion@status.net>
+ */
+
+class Ostatus_profile extends Memcached_DataObject
+{
+ public $__table = 'ostatus_profile';
+
+ public $uri;
+
+ public $profile_id;
+ public $group_id;
+
+ public $feeduri;
+ public $salmonuri;
+
+ public $created;
+ public $modified;
+
+ public /*static*/ function staticGet($k, $v=null)
+ {
+ return parent::staticGet(__CLASS__, $k, $v);
+ }
+
+ /**
+ * return table definition for DB_DataObject
+ *
+ * DB_DataObject needs to know something about the table to manipulate
+ * instances. This method provides all the DB_DataObject needs to know.
+ *
+ * @return array array of column definitions
+ */
+
+ function table()
+ {
+ return array('uri' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
+ 'profile_id' => DB_DATAOBJECT_INT,
+ 'group_id' => DB_DATAOBJECT_INT,
+ 'feeduri' => DB_DATAOBJECT_STR,
+ 'salmonuri' => DB_DATAOBJECT_STR,
+ 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
+ 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+ }
+
+ static function schemaDef()
+ {
+ return array(new ColumnDef('uri', 'varchar',
+ 255, false, 'PRI'),
+ new ColumnDef('profile_id', 'integer',
+ null, true, 'UNI'),
+ new ColumnDef('group_id', 'integer',
+ null, true, 'UNI'),
+ new ColumnDef('feeduri', 'varchar',
+ 255, true, 'UNI'),
+ new ColumnDef('salmonuri', 'text',
+ null, true),
+ new ColumnDef('created', 'datetime',
+ null, false),
+ new ColumnDef('modified', 'datetime',
+ null, false));
+ }
+
+ /**
+ * return key definitions for DB_DataObject
+ *
+ * DB_DataObject needs to know about keys that the table has; this function
+ * defines them.
+ *
+ * @return array key definitions
+ */
+
+ function keys()
+ {
+ return array_keys($this->keyTypes());
+ }
+
+ /**
+ * return key definitions for Memcached_DataObject
+ *
+ * Our caching system uses the same key definitions, but uses a different
+ * method to get them.
+ *
+ * @return array key definitions
+ */
+
+ function keyTypes()
+ {
+ return array('uri' => 'K', 'profile_id' => 'U', 'group_id' => 'U', 'feeduri' => 'U');
+ }
+
+ function sequenceKey()
+ {
+ return array(false, false, false);
+ }
+
+ /**
+ * Fetch the StatusNet-side profile for this feed
+ * @return Profile
+ */
+ public function localProfile()
+ {
+ if ($this->profile_id) {
+ return Profile::staticGet('id', $this->profile_id);
+ }
+ return null;
+ }
+
+ /**
+ * Fetch the StatusNet-side profile for this feed
+ * @return Profile
+ */
+ public function localGroup()
+ {
+ if ($this->group_id) {
+ return User_group::staticGet('id', $this->group_id);
+ }
+ return null;
+ }
+
+ /**
+ * Returns an XML string fragment with profile information as an
+ * Activity Streams noun object with the given element type.
+ *
+ * Assumes that 'activity' namespace has been previously defined.
+ *
+ * @param string $element one of 'actor', 'subject', 'object', 'target'
+ * @return string
+ */
+ function asActivityNoun($element)
+ {
+ $xs = new XMLStringer(true);
+ $avatarHref = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
+ $avatarType = 'image/png';
+ if ($this->isGroup()) {
+ $type = 'http://activitystrea.ms/schema/1.0/group';
+ $self = $this->localGroup();
+
+ // @fixme put a standard getAvatar() interface on groups too
+ if ($self->homepage_logo) {
+ $avatarHref = $self->homepage_logo;
+ $map = array('png' => 'image/png',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'gif' => 'image/gif');
+ $extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION);
+ if (isset($map[$extension])) {
+ $avatarType = $map[$extension];
+ }
+ }
+ } else {
+ $type = 'http://activitystrea.ms/schema/1.0/person';
+ $self = $this->localProfile();
+ $avatar = $self->getAvatar(AVATAR_PROFILE_SIZE);
+ if ($avatar) {
+ $avatarHref = $avatar->url;
+ $avatarType = $avatar->mediatype;
+ }
+ }
+ $xs->elementStart('activity:' . $element);
+ $xs->element(
+ 'activity:object-type',
+ null,
+ $type
+ );
+ $xs->element(
+ 'id',
+ null,
+ $this->uri); // ?
+ $xs->element('title', null, $self->getBestName());
+
+ $xs->element(
+ 'link', array(
+ 'type' => $avatarType,
+ 'href' => $avatarHref
+ ),
+ ''
+ );
+
+ $xs->elementEnd('activity:' . $element);
+
+ return $xs->getString();
+ }
+
+ /**
+ * Damn dirty hack!
+ */
+ function isGroup()
+ {
+ return (strpos($this->feeduri, '/groups/') !== false);
+ }
+
+ /**
+ * Subscribe a local user to this remote user.
+ * PuSH subscription will be started if necessary, and we'll
+ * send a Salmon notification to the remote server if available
+ * notifying them of the sub.
+ *
+ * @param User $user
+ * @return boolean success
+ * @throws FeedException
+ */
+ public function subscribeLocalToRemote(User $user)
+ {
+ if ($this->isGroup()) {
+ throw new ServerException("Can't subscribe to a remote group");
+ }
+
+ if ($this->subscribe()) {
+ if ($user->subscribeTo($this->localProfile())) {
+ $this->notify($user->getProfile(), ActivityVerb::FOLLOW, $this);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Mark this remote profile as subscribing to the given local user,
+ * and send appropriate notifications to the user.
+ *
+ * This will generally be in response to a subscription notification
+ * from a foreign site to our local Salmon response channel.
+ *
+ * @param User $user
+ * @return boolean success
+ */
+ public function subscribeRemoteToLocal(User $user)
+ {
+ if ($this->isGroup()) {
+ throw new ServerException("Remote groups can't subscribe to local users");
+ }
+
+ // @fixme use regular channels for subbing, once they accept remote profiles
+ $sub = new Subscription();
+ $sub->subscriber = $this->profile_id;
+ $sub->subscribed = $user->id;
+ $sub->created = common_sql_now(); // current time
+
+ if ($sub->insert()) {
+ // @fixme use subs_notify() if refactored to take profiles?
+ mail_subscribe_notify_profile($user, $this->localProfile());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Send a subscription request to the hub for this feed.
+ * The hub will later send us a confirmation POST to /main/push/callback.
+ *
+ * @return bool true on success, false on failure
+ * @throws ServerException if feed state is not valid
+ */
+ public function subscribe()
+ {
+ $feedsub = FeedSub::ensureFeed($this->feeduri);
+ if ($feedsub->sub_state == 'active' || $feedsub->sub_state == 'subscribe') {
+ return true;
+ } else if ($feedsub->sub_state == '' || $feedsub->sub_state == 'inactive') {
+ return $feedsub->subscribe();
+ } else if ('unsubscribe') {
+ throw new FeedSubException("Unsub is pending, can't subscribe...");
+ }
+ }
+
+ /**
+ * Send a PuSH unsubscription request to the hub for this feed.
+ * The hub will later send us a confirmation POST to /main/push/callback.
+ *
+ * @return bool true on success, false on failure
+ * @throws ServerException if feed state is not valid
+ */
+ public function unsubscribe() {
+ $feedsub = FeedSub::staticGet('uri', $this->feeduri);
+ if ($feedsub->sub_state == 'active') {
+ return $feedsub->unsubscribe();
+ } else if ($feedsub->sub_state == '' || $feedsub->sub_state == 'inactive' || $feedsub->sub_state == 'unsubscribe') {
+ return true;
+ } else if ($feedsub->sub_state == 'subscribe') {
+ throw new FeedSubException("Feed is awaiting subscription, can't unsub...");
+ }
+ }
+
+ /**
+ * Send an Activity Streams notification to the remote Salmon endpoint,
+ * if so configured.
+ *
+ * @param Profile $actor Actor who did the activity
+ * @param string $verb Activity::SUBSCRIBE or Activity::JOIN
+ * @param Object $object object of the action; must define asActivityNoun($tag)
+ */
+ public function notify($actor, $verb, $object=null)
+ {
+ if (!($actor instanceof Profile)) {
+ $type = gettype($actor);
+ if ($type == 'object') {
+ $type = get_class($actor);
+ }
+ throw new ServerException("Invalid actor passed to " . __METHOD__ . ": " . $type);
+ }
+ if ($object == null) {
+ $object = $this;
+ }
+ if ($this->salmonuri) {
+
+ $text = 'update';
+ $id = TagURI::mint('%s:%s:%s',
+ $verb,
+ $actor->getURI(),
+ common_date_iso8601(time()));
+
+ // @fixme consolidate all these NS settings somewhere
+ $attributes = array('xmlns' => Activity::ATOM,
+ 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
+ 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
+ 'xmlns:georss' => 'http://www.georss.org/georss',
+ 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
+
+ $entry = new XMLStringer();
+ $entry->elementStart('entry', $attributes);
+ $entry->element('id', null, $id);
+ $entry->element('title', null, $text);
+ $entry->element('summary', null, $text);
+ $entry->element('published', null, common_date_w3dtf(common_sql_now()));
+
+ $entry->element('activity:verb', null, $verb);
+ $entry->raw($actor->asAtomAuthor());
+ $entry->raw($actor->asActivityActor());
+ $entry->raw($object->asActivityNoun('object'));
+ $entry->elementEnd('entry');
+
+ $xml = $entry->getString();
+ common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml");
+
+ $salmon = new Salmon(); // ?
+ $salmon->post($this->salmonuri, $xml);
+ }
+ }
+
+ public function notifyActivity($activity)
+ {
+ if ($this->salmonuri) {
+
+ $xml = $activity->asString(true);
+
+ $salmon = new Salmon(); // ?
+
+ $salmon->post($this->salmonuri, $xml);
+ }
+
+ return;
+ }
+
+ function getBestName()
+ {
+ if ($this->isGroup()) {
+ return $this->localGroup()->getBestName();
+ } else {
+ return $this->localProfile()->getBestName();
+ }
+ }
+
+ function atomFeed($actor)
+ {
+ $feed = new Atom10Feed();
+ // @fixme should these be set up somewhere else?
+ $feed->addNamespace('activity', 'http://activitystrea.ms/spec/1.0/');
+ $feed->addNamespace('thr', 'http://purl.org/syndication/thread/1.0');
+ $feed->addNamespace('georss', 'http://www.georss.org/georss');
+ $feed->addNamespace('ostatus', 'http://ostatus.org/schema/1.0');
+
+ $taguribase = common_config('integration', 'taguri');
+ $feed->setId("tag:{$taguribase}:UserTimeline:{$actor->id}"); // ???
+
+ $feed->setTitle($actor->getBestName() . ' timeline'); // @fixme
+ $feed->setUpdated(time());
+ $feed->setPublished(time());
+
+ $feed->addLink(common_local_url('ApiTimelineUser',
+ array('id' => $actor->id,
+ 'type' => 'atom')),
+ array('rel' => 'self',
+ 'type' => 'application/atom+xml'));
+
+ $feed->addLink(common_local_url('userbyid',
+ array('id' => $actor->id)),
+ array('rel' => 'alternate',
+ 'type' => 'text/html'));
+
+ return $feed;
+ }
+
+ /**
+ * Read and post notices for updates from the feed.
+ * Currently assumes that all items in the feed are new,
+ * coming from a PuSH hub.
+ *
+ * @param DOMDocument $feed
+ */
+ public function processFeed($feed)
+ {
+ $entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
+ if ($entries->length == 0) {
+ common_log(LOG_ERR, __METHOD__ . ": no entries in feed update, ignoring");
+ return;
+ }
+
+ for ($i = 0; $i < $entries->length; $i++) {
+ $entry = $entries->item($i);
+ $this->processEntry($entry, $feed);
+ }
+ }
+
+ /**
+ * Process a posted entry from this feed source.
+ *
+ * @param DOMElement $entry
+ * @param DOMElement $feed for context
+ */
+ protected function processEntry($entry, $feed)
+ {
+ $activity = new Activity($entry, $feed);
+
+ $debug = var_export($activity, true);
+ common_log(LOG_DEBUG, $debug);
+
+ if ($activity->verb == ActivityVerb::POST) {
+ $this->processPost($activity);
+ } else {
+ common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
+ }
+ }
+
+ /**
+ * Process an incoming post activity from this remote feed.
+ * @param Activity $activity
+ */
+ protected function processPost($activity)
+ {
+ if ($this->isGroup()) {
+ // @fixme validate these profiles in some way!
+ $oprofile = self::ensureActorProfile($activity);
+ } else {
+ $actorUri = self::getActorProfileURI($activity);
+ if ($actorUri == $this->uri) {
+ // @fixme check if profile info has changed and update it
+ } else {
+ // @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely
+ common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->uri");
+ //return;
+ }
+ $oprofile = $this;
+ }
+
+ $sourceUri = $activity->object->id;
+
+ $dupe = Notice::staticGet('uri', $sourceUri);
+
+ if ($dupe) {
+ common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri");
+ return;
+ }
+
+ $sourceUrl = null;
+
+ if ($activity->object->link) {
+ $sourceUrl = $activity->object->link;
+ } else if (preg_match('!^https?://!', $activity->object->id)) {
+ $sourceUrl = $activity->object->id;
+ }
+
+ // @fixme sanitize and save HTML content if available
+
+ $content = $activity->object->title;
+
+ $params = array('is_local' => Notice::REMOTE_OMB,
+ 'url' => $sourceUrl,
+ 'uri' => $sourceUri);
+
+ $location = $activity->context->location;
+
+ if ($location) {
+ $params['lat'] = $location->lat;
+ $params['lon'] = $location->lon;
+ if ($location->location_id) {
+ $params['location_ns'] = $location->location_ns;
+ $params['location_id'] = $location->location_id;
+ }
+ }
+
+ // @fixme save detailed ostatus source info
+ // @fixme ensure that groups get handled correctly
+
+ $saved = Notice::saveNew($oprofile->localProfile()->id,
+ $content,
+ 'ostatus',
+ $params);
+ }
+
+ /**
+ * @param string $profile_url
+ * @return Ostatus_profile
+ * @throws FeedSubException
+ */
+ public static function ensureProfile($profile_uri)
+ {
+ // Get the canonical feed URI and check it
+ $discover = new FeedDiscovery();
+ $feeduri = $discover->discoverFromURL($profile_uri);
+
+ //$feedsub = FeedSub::ensureFeed($feeduri, $discover->feed);
+ $huburi = $discover->getAtomLink('hub');
+ $salmonuri = $discover->getAtomLink('salmon');
+
+ if (!$huburi) {
+ // We can only deal with folks with a PuSH hub
+ throw new FeedSubNoHubException();
+ }
+
+ // Try to get a profile from the feed activity:subject
+
+ $feedEl = $discover->feed->documentElement;
+
+ $subject = ActivityUtils::child($feedEl, Activity::SUBJECT, Activity::SPEC);
+
+ if (!empty($subject)) {
+ $subjObject = new ActivityObject($subject);
+ return self::ensureActivityObjectProfile($subjObject, $feeduri, $salmonuri);
+ }
+
+ // Otherwise, try the feed author
+
+ $author = ActivityUtils::child($feedEl, Activity::AUTHOR, Activity::ATOM);
+
+ if (!empty($author)) {
+ $authorObject = new ActivityObject($author);
+ return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri);
+ }
+
+ // Sheesh. Not a very nice feed! Let's try fingerpoken in the
+ // entries.
+
+ $entries = $discover->feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
+
+ if (!empty($entries) && $entries->length > 0) {
+
+ $entry = $entries->item(0);
+
+ $actor = ActivityUtils::child($entry, Activity::ACTOR, Activity::SPEC);
+
+ if (!empty($actor)) {
+ $actorObject = new ActivityObject($actor);
+ return self::ensureActivityObjectProfile($actorObject, $feeduri, $salmonuri);
+
+ }
+
+ $author = ActivityUtils::child($entry, Activity::AUTHOR, Activity::ATOM);
+
+ if (!empty($author)) {
+ $authorObject = new ActivityObject($author);
+ return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri);
+ }
+ }
+
+ // XXX: make some educated guesses here
+
+ throw new FeedSubException("Can't find enough profile information to make a feed.");
+ }
+
+ /**
+ *
+ * Download and update given avatar image
+ * @param string $url
+ * @throws Exception in various failure cases
+ */
+ protected function updateAvatar($url)
+ {
+ // @fixme this should be better encapsulated
+ // ripped from oauthstore.php (for old OMB client)
+ $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
+ copy($url, $temp_filename);
+
+ if ($this->isGroup()) {
+ $id = $this->group_id;
+ } else {
+ $id = $this->profile_id;
+ }
+ // @fixme should we be using different ids?
+ $imagefile = new ImageFile($id, $temp_filename);
+ $filename = Avatar::filename($id,
+ image_type_to_extension($imagefile->type),
+ null,
+ common_timestamp());
+ rename($temp_filename, Avatar::path($filename));
+ if ($this->isGroup()) {
+ $group = $this->localGroup();
+ $group->setOriginal($filename);
+ } else {
+ $profile = $this->localProfile();
+ $profile->setOriginal($filename);
+ }
+ }
+
+ protected static function getActivityObjectAvatar($object)
+ {
+ // XXX: go poke around in the feed
+ return $object->avatar;
+ }
+
+ /**
+ * Get an appropriate avatar image source URL, if available.
+ *
+ * @param ActivityObject $actor
+ * @param DOMElement $feed
+ * @return string
+ */
+
+ protected static function getAvatar($actor, $feed)
+ {
+ $url = '';
+ $icon = '';
+ if ($actor->avatar) {
+ $url = trim($actor->avatar);
+ }
+ if (!$url) {
+ // Check <atom:logo> and <atom:icon> on the feed
+ $els = $feed->childNodes();
+ if ($els && $els->length) {
+ for ($i = 0; $i < $els->length; $i++) {
+ $el = $els->item($i);
+ if ($el->namespaceURI == Activity::ATOM) {
+ if (empty($url) && $el->localName == 'logo') {
+ $url = trim($el->textContent);
+ break;
+ }
+ if (empty($icon) && $el->localName == 'icon') {
+ // Use as a fallback
+ $icon = trim($el->textContent);
+ }
+ }
+ }
+ }
+ if ($icon && !$url) {
+ $url = $icon;
+ }
+ }
+ if ($url) {
+ $opts = array('allowed_schemes' => array('http', 'https'));
+ if (Validate::uri($url, $opts)) {
+ return $url;
+ }
+ }
+ return common_path('plugins/OStatus/images/96px-Feed-icon.svg.png');
+ }
+
+ /**
+ * Fetch, or build if necessary, an Ostatus_profile for the actor
+ * in a given Activity Streams activity.
+ *
+ * @param Activity $activity
+ * @param string $feeduri if we already know the canonical feed URI!
+ * @param string $salmonuri if we already know the salmon return channel URI
+ * @return Ostatus_profile
+ */
+
+ public static function ensureActorProfile($activity, $feeduri=null, $salmonuri=null)
+ {
+ return self::ensureActivityObjectProfile($activity->actor, $feeduri, $salmonuri);
+ }
+
+ public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null)
+ {
+ $profile = self::getActivityObjectProfile($object);
+ if (!$profile) {
+ $profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri);
+ }
+ return $profile;
+ }
+
+ /**
+ * @param Activity $activity
+ * @return mixed matching Ostatus_profile or false if none known
+ */
+ protected static function getActorProfile($activity)
+ {
+ return self::getActivityObjectProfile($activity->actor);
+ }
+
+ protected static function getActivityObjectProfile($object)
+ {
+ $uri = self::getActivityObjectProfileURI($object);
+ return Ostatus_profile::staticGet('uri', $uri);
+ }
+
+ protected static function getActorProfileURI($activity)
+ {
+ return self::getActivityObjectProfileURI($activity->actor);
+ }
+
+ /**
+ * @param Activity $activity
+ * @return string
+ * @throws ServerException
+ */
+ protected static function getActivityObjectProfileURI($object)
+ {
+ $opts = array('allowed_schemes' => array('http', 'https'));
+ if ($object->id && Validate::uri($object->id, $opts)) {
+ return $object->id;
+ }
+ if ($object->link && Validate::uri($object->link, $opts)) {
+ return $object->link;
+ }
+ throw new ServerException("No author ID URI found");
+ }
+
+ /**
+ * @fixme validate stuff somewhere
+ */
+
+ protected static function createActorProfile($activity, $feeduri=null, $salmonuri=null)
+ {
+ $actor = $activity->actor;
+
+ self::createActivityObjectProfile($actor, $feeduri, $salmonuri);
+ }
+
+ protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null)
+ {
+ $homeuri = $object->id;
+ $nickname = self::getActivityObjectNickname($object);
+ $avatar = self::getActivityObjectAvatar($object);
+
+ if (!$homeuri) {
+ common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true));
+ throw new ServerException("No profile URI");
+ }
+
+ if (!$feeduri || !$salmonuri) {
+ // Get the canonical feed URI and check it
+ $discover = new FeedDiscovery();
+ $feeduri = $discover->discoverFromURL($homeuri);
+
+ $huburi = $discover->getAtomLink('hub');
+ $salmonuri = $discover->getAtomLink('salmon');
+
+ if (!$huburi) {
+ // We can only deal with folks with a PuSH hub
+ throw new FeedSubNoHubException();
+ }
+ }
+
+ $profile = new Profile();
+ $profile->nickname = $nickname;
+ $profile->fullname = $object->title;
+ $profile->profileurl = $object->link;
+ $profile->created = common_sql_now();
+
+ // @fixme bio
+ // @fixme tags/categories
+ // @fixme location?
+ // @todo tags from categories
+ // @todo lat/lon/location?
+
+ $profile_id = $profile->insert();
+
+ if (!$profile_id) {
+ throw new ServerException("Can't save local profile");
+ }
+
+ // @fixme either need to do feed discovery here
+ // or need to split out some of the feed stuff
+ // so we can leave it empty until later.
+
+ $oprofile = new Ostatus_profile();
+
+ $oprofile->uri = $homeuri;
+ $oprofile->feeduri = $feeduri;
+ $oprofile->salmonuri = $salmonuri;
+ $oprofile->profile_id = $profile_id;
+
+ $oprofile->created = common_sql_now();
+ $oprofile->modified = common_sql_now();
+
+ $ok = $oprofile->insert();
+
+ if ($ok) {
+ $oprofile->updateAvatar($avatar);
+ return $oprofile;
+ } else {
+ throw new ServerException("Can't save OStatus profile");
+ }
+ }
+
+ protected static function getActivityObjectNickname($object)
+ {
+ // XXX: check whatever PoCo calls a nickname first
+
+ $nickname = self::nicknameFromURI($object->id);
+
+ if (empty($nickname)) {
+ $nickname = common_nicknamize($object->title);
+ }
+
+ return $nickname;
+ }
+
+ protected static function nicknameFromURI($uri)
+ {
+ preg_match('/(\w+):/', $uri, $matches);
+
+ $protocol = $matches[1];
+
+ switch ($protocol) {
+ case 'acct':
+ case 'mailto':
+ if (preg_match("/^$protocol:(.*)?@.*\$/", $uri, $matches)) {
+ return common_canonical_nickname($matches[1]);
+ }
+ return null;
+ case 'http':
+ return common_url_to_nickname($uri);
+ break;
+ default:
+ return null;
+ }
+ }
+
+ public static function ensureWebfinger($addr)
+ {
+ // First, look it up
+
+ $oprofile = Ostatus_profile::staticGet('uri', 'acct:'.$addr);
+
+ if (!empty($oprofile)) {
+ return $oprofile;
+ }
+
+ // Now, try some discovery
+
+ $wf = new Webfinger();
+
+ $result = $wf->lookup($addr);
+
+ if (!$result) {
+ return null;
+ }
+
+ foreach ($result->links as $link) {
+ switch ($link['rel']) {
+ case Webfinger::PROFILEPAGE:
+ $profileUrl = $link['href'];
+ break;
+ case 'salmon':
+ $salmonEndpoint = $link['href'];
+ break;
+ case Webfinger::UPDATESFROM:
+ $feedUrl = $link['href'];
+ break;
+ default:
+ common_log(LOG_NOTICE, "Don't know what to do with rel = '{$link['rel']}'");
+ break;
+ }
+ }
+
+ // If we got a feed URL, try that
+
+ if (isset($feedUrl)) {
+ try {
+ $oprofile = self::ensureProfile($feedUrl);
+ return $oprofile;
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, "Failed creating profile from feed URL '$feedUrl': " . $e->getMessage());
+ // keep looking
+ }
+ }
+
+ // If we got a profile page, try that!
+
+ if (isset($profileUrl)) {
+ try {
+ $oprofile = self::ensureProfile($profileUrl);
+ return $oprofile;
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage());
+ // keep looking
+ }
+ }
+
+ // XXX: try hcard
+ // XXX: try FOAF
+
+ if (isset($salmonEndpoint)) {
+
+ // An account URL, a salmon endpoint, and a dream? Not much to go
+ // on, but let's give it a try
+
+ $uri = 'acct:'.$addr;
+
+ $profile = new Profile();
+
+ $profile->nickname = self::nicknameFromUri($uri);
+ $profile->created = common_sql_now();
+
+ $profile_id = $profile->insert();
+
+ if (!$profile_id) {
+ common_log_db_error($profile, 'INSERT', __FILE__);
+ throw new Exception("Couldn't save profile for '$addr'");
+ }
+
+ $oprofile = new Ostatus_profile();
+
+ $oprofile->uri = $uri;
+ $oprofile->salmonuri = $salmonEndpoint;
+ $oprofile->profile_id = $profile_id;
+ $oprofile->created = common_sql_now();
+
+ $result = $oprofile->insert();
+
+ if (!$result) {
+ common_log_db_error($oprofile, 'INSERT', __FILE__);
+ throw new Exception("Couldn't save ostatus_profile for '$addr'");
+ }
+
+ return $oprofile;
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/FeedSub/images/24px-Feed-icon.svg.png b/plugins/OStatus/images/24px-Feed-icon.svg.png
index 317225814..317225814 100644
--- a/plugins/FeedSub/images/24px-Feed-icon.svg.png
+++ b/plugins/OStatus/images/24px-Feed-icon.svg.png
Binary files differ
diff --git a/plugins/FeedSub/images/48px-Feed-icon.svg.png b/plugins/OStatus/images/48px-Feed-icon.svg.png
index bd1da4f91..bd1da4f91 100644
--- a/plugins/FeedSub/images/48px-Feed-icon.svg.png
+++ b/plugins/OStatus/images/48px-Feed-icon.svg.png
Binary files differ
diff --git a/plugins/FeedSub/images/96px-Feed-icon.svg.png b/plugins/OStatus/images/96px-Feed-icon.svg.png
index bf16571ec..bf16571ec 100644
--- a/plugins/FeedSub/images/96px-Feed-icon.svg.png
+++ b/plugins/OStatus/images/96px-Feed-icon.svg.png
Binary files differ
diff --git a/plugins/FeedSub/images/README b/plugins/OStatus/images/README
index d9379c23e..d9379c23e 100644
--- a/plugins/FeedSub/images/README
+++ b/plugins/OStatus/images/README
diff --git a/plugins/OStatus/js/ostatus.js b/plugins/OStatus/js/ostatus.js
new file mode 100644
index 000000000..671795558
--- /dev/null
+++ b/plugins/OStatus/js/ostatus.js
@@ -0,0 +1,60 @@
+SN.U.DialogBox = {
+ Subscribe: function(a) {
+ var f = a.parent().find('#form_ostatus_connect');
+ if (f.length > 0) {
+ f.show();
+ }
+ else {
+ $.ajax({
+ type: 'GET',
+ dataType: 'xml',
+ url: a[0].href+'&ajax=1',
+ beforeSend: function(formData) {
+ a.addClass('processing');
+ },
+ error: function (xhr, textStatus, errorThrown) {
+ alert(errorThrown || textStatus);
+ },
+ success: function(data, textStatus, xhr) {
+ if (typeof($('form', data)[0]) != 'undefined') {
+ a.after(document._importNode($('form', data)[0], true));
+
+ var form = a.parent().find('#form_ostatus_connect');
+
+ form
+ .addClass('dialogbox')
+ .append('<button class="close">&#215;</button>');
+
+ form
+ .find('.submit')
+ .addClass('submit_dialogbox')
+ .removeClass('submit')
+ .bind('click', function() {
+ form.addClass('processing');
+ });
+
+ form.find('button.close').click(function(){
+ form.hide();
+
+ return false;
+ });
+
+ form.find('#acct').focus();
+ }
+
+ a.removeClass('processing');
+ }
+ });
+ }
+ }
+};
+
+SN.Init.Subscribe = function() {
+ $('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; });
+};
+
+$(document).ready(function() {
+ if ($('.entity_subscribe .entity_remote_subscribe').length > 0) {
+ SN.Init.Subscribe();
+ }
+});
diff --git a/plugins/OStatus/lib/activity.php b/plugins/OStatus/lib/activity.php
new file mode 100644
index 000000000..af83f8bc6
--- /dev/null
+++ b/plugins/OStatus/lib/activity.php
@@ -0,0 +1,716 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An activity
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Utilities for turning DOMish things into Activityish things
+ *
+ * Some common functions that I didn't have the bandwidth to try to factor
+ * into some kind of reasonable superclass, so just dumped here. Might
+ * be useful to have an ActivityObject parent class or something.
+ *
+ * @category OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class ActivityUtils
+{
+ const ATOM = 'http://www.w3.org/2005/Atom';
+
+ const LINK = 'link';
+ const REL = 'rel';
+ const TYPE = 'type';
+ const HREF = 'href';
+
+ const CONTENT = 'content';
+ const SRC = 'src';
+
+ /**
+ * Get the permalink for an Activity object
+ *
+ * @param DOMElement $element A DOM element
+ *
+ * @return string related link, if any
+ */
+
+ static function getPermalink($element)
+ {
+ return self::getLink($element, 'alternate', 'text/html');
+ }
+
+ /**
+ * Get the permalink for an Activity object
+ *
+ * @param DOMElement $element A DOM element
+ *
+ * @return string related link, if any
+ */
+
+ static function getLink($element, $rel, $type=null)
+ {
+ $links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
+
+ foreach ($links as $link) {
+
+ $linkRel = $link->getAttribute(self::REL);
+ $linkType = $link->getAttribute(self::TYPE);
+
+ if ($linkRel == $rel &&
+ (is_null($type) || $linkType == $type)) {
+ return $link->getAttribute(self::HREF);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the first child element with the given tag
+ *
+ * @param DOMElement $element element to pick at
+ * @param string $tag tag to look for
+ * @param string $namespace Namespace to look under
+ *
+ * @return DOMElement found element or null
+ */
+
+ static function child($element, $tag, $namespace=self::ATOM)
+ {
+ $els = $element->childNodes;
+ if (empty($els) || $els->length == 0) {
+ return null;
+ } else {
+ for ($i = 0; $i < $els->length; $i++) {
+ $el = $els->item($i);
+ if ($el->localName == $tag && $el->namespaceURI == $namespace) {
+ return $el;
+ }
+ }
+ }
+ }
+
+ /**
+ * Grab the text content of a DOM element child of the current element
+ *
+ * @param DOMElement $element Element whose children we examine
+ * @param string $tag Tag to look up
+ * @param string $namespace Namespace to use, defaults to Atom
+ *
+ * @return string content of the child
+ */
+
+ static function childContent($element, $tag, $namespace=self::ATOM)
+ {
+ $el = self::child($element, $tag, $namespace);
+
+ if (empty($el)) {
+ return null;
+ } else {
+ return $el->textContent;
+ }
+ }
+
+ /**
+ * Get the content of an atom:entry-like object
+ *
+ * @param DOMElement $element The element to examine.
+ *
+ * @return string unencoded HTML content of the element, like "This -&lt; is <b>HTML</b>."
+ *
+ * @todo handle remote content
+ * @todo handle embedded XML mime types
+ * @todo handle base64-encoded non-XML and non-text mime types
+ */
+
+ static function getContent($element)
+ {
+ $contentEl = ActivityUtils::child($element, self::CONTENT);
+
+ if (!empty($contentEl)) {
+
+ $src = $contentEl->getAttribute(self::SRC);
+
+ if (!empty($src)) {
+ throw new ClientException(_("Can't handle remote content yet."));
+ }
+
+ $type = $contentEl->getAttribute(self::TYPE);
+
+ // slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
+
+ if ($type == 'text') {
+ return $contentEl->textContent;
+ } else if ($type == 'html') {
+ $text = $contentEl->textContent;
+ return htmlspecialchars_decode($text, ENT_QUOTES);
+ } else if ($type == 'xhtml') {
+ $divEl = ActivityUtils::child($contentEl, 'div');
+ if (empty($divEl)) {
+ return null;
+ }
+ $doc = $divEl->ownerDocument;
+ $text = '';
+ $children = $divEl->childNodes;
+
+ for ($i = 0; $i < $children->length; $i++) {
+ $child = $children->item($i);
+ $text .= $doc->saveXML($child);
+ }
+ return trim($text);
+ } else if (in_array(array('text/xml', 'application/xml'), $type) ||
+ preg_match('#(+|/)xml$#', $type)) {
+ throw new ClientException(_("Can't handle embedded XML content yet."));
+ } else if (strncasecmp($type, 'text/', 5)) {
+ return $contentEl->textContent;
+ } else {
+ throw new ClientException(_("Can't handle embedded Base64 content yet."));
+ }
+ }
+ }
+}
+
+/**
+ * A noun-ish thing in the activity universe
+ *
+ * The activity streams spec talks about activity objects, while also having
+ * a tag activity:object, which is in fact an activity object. Aaaaaah!
+ *
+ * This is just a thing in the activity universe. Can be the subject, object,
+ * or indirect object (target!) of an activity verb. Rotten name, and I'm
+ * propagating it. *sigh*
+ *
+ * @category OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class ActivityObject
+{
+ const ARTICLE = 'http://activitystrea.ms/schema/1.0/article';
+ const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
+ const NOTE = 'http://activitystrea.ms/schema/1.0/note';
+ const STATUS = 'http://activitystrea.ms/schema/1.0/status';
+ const FILE = 'http://activitystrea.ms/schema/1.0/file';
+ const PHOTO = 'http://activitystrea.ms/schema/1.0/photo';
+ const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album';
+ const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist';
+ const VIDEO = 'http://activitystrea.ms/schema/1.0/video';
+ const AUDIO = 'http://activitystrea.ms/schema/1.0/audio';
+ const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark';
+ const PERSON = 'http://activitystrea.ms/schema/1.0/person';
+ const GROUP = 'http://activitystrea.ms/schema/1.0/group';
+ const PLACE = 'http://activitystrea.ms/schema/1.0/place';
+ const COMMENT = 'http://activitystrea.ms/schema/1.0/comment';
+ // ^^^^^^^^^^ tea!
+
+ // Atom elements we snarf
+
+ const TITLE = 'title';
+ const SUMMARY = 'summary';
+ const ID = 'id';
+ const SOURCE = 'source';
+
+ const NAME = 'name';
+ const URI = 'uri';
+ const EMAIL = 'email';
+
+ public $element;
+ public $type;
+ public $id;
+ public $title;
+ public $summary;
+ public $content;
+ public $link;
+ public $source;
+ public $avatar;
+
+ /**
+ * Constructor
+ *
+ * This probably needs to be refactored
+ * to generate a local class (ActivityPerson, ActivityFile, ...)
+ * based on the object type.
+ *
+ * @param DOMElement $element DOM thing to turn into an Activity thing
+ */
+
+ function __construct($element = null)
+ {
+ if (empty($element)) {
+ return;
+ }
+
+ $this->element = $element;
+
+ if ($element->tagName == 'author') {
+
+ $this->type = self::PERSON; // XXX: is this fair?
+ $this->title = $this->_childContent($element, self::NAME);
+ $this->id = $this->_childContent($element, self::URI);
+
+ if (empty($this->id)) {
+ $email = $this->_childContent($element, self::EMAIL);
+ if (!empty($email)) {
+ // XXX: acct: ?
+ $this->id = 'mailto:'.$email;
+ }
+ }
+
+ } else {
+
+ $this->type = $this->_childContent($element, Activity::OBJECTTYPE,
+ Activity::SPEC);
+
+ if (empty($this->type)) {
+ $this->type = ActivityObject::NOTE;
+ }
+
+ $this->id = $this->_childContent($element, self::ID);
+ $this->title = $this->_childContent($element, self::TITLE);
+ $this->summary = $this->_childContent($element, self::SUMMARY);
+
+ $this->source = $this->_getSource($element);
+
+ $this->content = ActivityUtils::getContent($element);
+
+ $this->link = ActivityUtils::getPermalink($element);
+
+ // XXX: grab PoCo stuff
+ }
+
+ // Some per-type attributes...
+ if ($this->type == self::PERSON || $this->type == self::GROUP) {
+ $this->displayName = $this->title;
+
+ // @fixme we may have multiple avatars with different resolutions specified
+ $this->avatar = ActivityUtils::getLink($element, 'avatar');
+ }
+ }
+
+ private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
+ {
+ return ActivityUtils::childContent($element, $tag, $namespace);
+ }
+
+ // Try to get a unique id for the source feed
+
+ private function _getSource($element)
+ {
+ $sourceEl = ActivityUtils::child($element, 'source');
+
+ if (empty($sourceEl)) {
+ return null;
+ } else {
+ $href = ActivityUtils::getLink($sourceEl, 'self');
+ if (!empty($href)) {
+ return $href;
+ } else {
+ return ActivityUtils::childContent($sourceEl, 'id');
+ }
+ }
+ }
+
+ static function fromNotice($notice)
+ {
+ $object = new ActivityObject();
+
+ $object->type = ActivityObject::NOTE;
+
+ $object->id = $notice->uri;
+ $object->title = $notice->content;
+ $object->content = $notice->rendered;
+ $object->link = $notice->bestUrl();
+
+ return $object;
+ }
+
+ static function fromProfile($profile)
+ {
+ $object = new ActivityObject();
+
+ $object->type = ActivityObject::PERSON;
+ $object->id = $profile->getUri();
+ $object->title = $profile->getBestName();
+ $object->link = $profile->profileurl;
+
+ return $object;
+ }
+
+ function asString($tag='activity:object')
+ {
+ $xs = new XMLStringer(true);
+
+ $xs->elementStart($tag);
+
+ $xs->element('activity:object-type', null, $this->type);
+
+ $xs->element(self::ID, null, $this->id);
+
+ if (!empty($this->title)) {
+ $xs->element(self::TITLE, null, $this->title);
+ }
+
+ if (!empty($this->summary)) {
+ $xs->element(self::SUMMARY, null, $this->summary);
+ }
+
+ if (!empty($this->content)) {
+ // XXX: assuming HTML content here
+ $xs->element(ActivityUtils::CONTENT, array('type' => 'html'), $this->content);
+ }
+
+ if (!empty($this->link)) {
+ $xs->element('link', array('rel' => 'alternate', 'type' => 'text/html'),
+ $this->content);
+ }
+
+ $xs->elementEnd($tag);
+
+ return $xs->getString();
+ }
+}
+
+/**
+ * Utility class to hold a bunch of constant defining default verb types
+ *
+ * @category OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class ActivityVerb
+{
+ const POST = 'http://activitystrea.ms/schema/1.0/post';
+ const SHARE = 'http://activitystrea.ms/schema/1.0/share';
+ const SAVE = 'http://activitystrea.ms/schema/1.0/save';
+ const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
+ const PLAY = 'http://activitystrea.ms/schema/1.0/play';
+ const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow';
+ const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend';
+ const JOIN = 'http://activitystrea.ms/schema/1.0/join';
+ const TAG = 'http://activitystrea.ms/schema/1.0/tag';
+
+ // Custom OStatus verbs for the flipside until they're standardized
+ const DELETE = 'http://ostatus.org/schema/1.0/unfollow';
+ const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
+ const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow';
+ const LEAVE = 'http://ostatus.org/schema/1.0/leave';
+}
+
+class ActivityContext
+{
+ public $replyToID;
+ public $replyToUrl;
+ public $location;
+ public $attention = array();
+ public $conversation;
+
+ const THR = 'http://purl.org/syndication/thread/1.0';
+ const GEORSS = 'http://www.georss.org/georss';
+ const OSTATUS = 'http://ostatus.org/schema/1.0';
+
+ const INREPLYTO = 'in-reply-to';
+ const REF = 'ref';
+ const HREF = 'href';
+
+ const POINT = 'point';
+
+ const ATTENTION = 'ostatus:attention';
+ const CONVERSATION = 'ostatus:conversation';
+
+ function __construct($element)
+ {
+ $replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR);
+
+ if (!empty($replyToEl)) {
+ $this->replyToID = $replyToEl->getAttribute(self::REF);
+ $this->replyToUrl = $replyToEl->getAttribute(self::HREF);
+ }
+
+ $this->location = $this->getLocation($element);
+
+ $this->conversation = ActivityUtils::getLink($element, self::CONVERSATION);
+
+ // Multiple attention links allowed
+
+ $links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK);
+
+ for ($i = 0; $i < $links->length; $i++) {
+
+ $link = $links->item($i);
+
+ $linkRel = $link->getAttribute(ActivityUtils::REL);
+
+ if ($linkRel == self::ATTENTION) {
+ $this->attention[] = $link->getAttribute(self::HREF);
+ }
+ }
+ }
+
+ /**
+ * Parse location given as a GeoRSS-simple point, if provided.
+ * http://www.georss.org/simple
+ *
+ * @param feed item $entry
+ * @return mixed Location or false
+ */
+ function getLocation($dom)
+ {
+ $points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT);
+
+ for ($i = 0; $i < $points->length; $i++) {
+ $point = $points->item($i)->textContent;
+ $point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
+ $point = preg_replace('/\s+/', ' ', $point);
+ $point = trim($point);
+ $coords = explode(' ', $point);
+ if (count($coords) == 2) {
+ list($lat, $lon) = $coords;
+ if (is_numeric($lat) && is_numeric($lon)) {
+ common_log(LOG_INFO, "Looking up location for $lat $lon from georss");
+ return Location::fromLatLon($lat, $lon);
+ }
+ }
+ common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
+ }
+
+ return null;
+ }
+}
+
+/**
+ * An activity in the ActivityStrea.ms world
+ *
+ * An activity is kind of like a sentence: someone did something
+ * to something else.
+ *
+ * 'someone' is the 'actor'; 'did something' is the verb;
+ * 'something else' is the object.
+ *
+ * @category OStatus
+ * @package StatusNet
+ * @author Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
+ * @link http://status.net/
+ */
+
+class Activity
+{
+ const SPEC = 'http://activitystrea.ms/spec/1.0/';
+ const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
+
+ const VERB = 'verb';
+ const OBJECT = 'object';
+ const ACTOR = 'actor';
+ const SUBJECT = 'subject';
+ const OBJECTTYPE = 'object-type';
+ const CONTEXT = 'context';
+ const TARGET = 'target';
+
+ const ATOM = 'http://www.w3.org/2005/Atom';
+
+ const AUTHOR = 'author';
+ const PUBLISHED = 'published';
+ const UPDATED = 'updated';
+
+ public $actor; // an ActivityObject
+ public $verb; // a string (the URL)
+ public $object; // an ActivityObject
+ public $target; // an ActivityObject
+ public $context; // an ActivityObject
+ public $time; // Time of the activity
+ public $link; // an ActivityObject
+ public $entry; // the source entry
+ public $feed; // the source feed
+
+ public $summary; // summary of activity
+ public $content; // HTML content of activity
+ public $id; // ID of the activity
+ public $title; // title of the activity
+
+ /**
+ * Turns a regular old Atom <entry> into a magical activity
+ *
+ * @param DOMElement $entry Atom entry to poke at
+ * @param DOMElement $feed Atom feed, for context
+ */
+
+ function __construct($entry = null, $feed = null)
+ {
+ if (is_null($entry)) {
+ return;
+ }
+
+ $this->entry = $entry;
+ $this->feed = $feed;
+
+ $pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
+
+ if (!empty($pubEl)) {
+ $this->time = strtotime($pubEl->textContent);
+ } else {
+ // XXX technically an error; being liberal. Good idea...?
+ $updateEl = $this->_child($entry, self::UPDATED, self::ATOM);
+ if (!empty($updateEl)) {
+ $this->time = strtotime($updateEl->textContent);
+ } else {
+ $this->time = null;
+ }
+ }
+
+ $this->link = ActivityUtils::getPermalink($entry);
+
+ $verbEl = $this->_child($entry, self::VERB);
+
+ if (!empty($verbEl)) {
+ $this->verb = trim($verbEl->textContent);
+ } else {
+ $this->verb = ActivityVerb::POST;
+ // XXX: do other implied stuff here
+ }
+
+ $objectEl = $this->_child($entry, self::OBJECT);
+
+ if (!empty($objectEl)) {
+ $this->object = new ActivityObject($objectEl);
+ } else {
+ $this->object = new ActivityObject($entry);
+ }
+
+ $actorEl = $this->_child($entry, self::ACTOR);
+
+ if (!empty($actorEl)) {
+
+ $this->actor = new ActivityObject($actorEl);
+
+ } else if (!empty($feed) &&
+ $subjectEl = $this->_child($feed, self::SUBJECT)) {
+
+ $this->actor = new ActivityObject($subjectEl);
+
+ } else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
+
+ $this->actor = new ActivityObject($authorEl);
+
+ } else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
+ self::ATOM)) {
+
+ $this->actor = new ActivityObject($authorEl);
+ }
+
+ $contextEl = $this->_child($entry, self::CONTEXT);
+
+ if (!empty($contextEl)) {
+ $this->context = new ActivityContext($contextEl);
+ } else {
+ $this->context = new ActivityContext($entry);
+ }
+
+ $targetEl = $this->_child($entry, self::TARGET);
+
+ if (!empty($targetEl)) {
+ $this->target = new ActivityObject($targetEl);
+ }
+
+ $this->summary = ActivityUtils::childContent($entry, 'summary');
+ $this->id = ActivityUtils::childContent($entry, 'id');
+ $this->content = ActivityUtils::getContent($entry);
+ }
+
+ /**
+ * Returns an Atom <entry> based on this activity
+ *
+ * @return DOMElement Atom entry
+ */
+
+ function toAtomEntry()
+ {
+ return null;
+ }
+
+ function asString($namespace=false)
+ {
+ $xs = new XMLStringer(true);
+
+ if ($namespace) {
+ $attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
+ 'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
+ 'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
+ } else {
+ $attrs = array();
+ }
+
+ $xs->elementStart('entry', $attrs);
+
+ $xs->element('id', null, $this->id);
+ $xs->element('title', null, $this->title);
+ $xs->element('published', null, common_date_iso8601($this->time));
+ $xs->element('content', array('type' => 'html'), $this->content);
+
+ if (!empty($this->summary)) {
+ $xs->element('summary', null, $this->summary);
+ }
+
+ if (!empty($this->link)) {
+ $xs->element('link', array('rel' => 'alternate',
+ 'type' => 'text/html'),
+ $this->link);
+ }
+
+ // XXX: add context
+ // XXX: add target
+
+ $xs->raw($this->actor->asString('activity:actor'));
+ $xs->element('activity:verb', null, $this->verb);
+ $xs->raw($this->object->asString());
+
+ $xs->elementEnd('entry');
+
+ return $xs->getString();
+ }
+
+ private function _child($element, $tag, $namespace=self::SPEC)
+ {
+ return ActivityUtils::child($element, $tag, $namespace);
+ }
+} \ No newline at end of file
diff --git a/plugins/FeedSub/feeddiscovery.php b/plugins/OStatus/lib/feeddiscovery.php
index 35edaca33..7afb71bdc 100644
--- a/plugins/FeedSub/feeddiscovery.php
+++ b/plugins/OStatus/lib/feeddiscovery.php
@@ -48,23 +48,47 @@ class FeedSubNoFeedException extends FeedSubException
{
}
+class FeedSubBadXmlException extends FeedSubException
+{
+}
+
+class FeedSubNoHubException extends FeedSubException
+{
+}
+
+/**
+ * Given a web page or feed URL, discover the final location of the feed
+ * and return its current contents.
+ *
+ * @example
+ * $feed = new FeedDiscovery();
+ * if ($feed->discoverFromURL($url)) {
+ * print $feed->uri;
+ * print $feed->type;
+ * processFeed($feed->feed); // DOMDocument
+ * }
+ */
class FeedDiscovery
{
public $uri;
public $type;
- public $body;
+ public $feed;
+ /** Post-initialize query helper... */
+ public function getLink($rel, $type=null)
+ {
+ // @fixme check for non-Atom links in RSS2 feeds as well
+ return self::getAtomLink($rel, $type);
+ }
- public function feedMunger()
+ public function getAtomLink($rel, $type=null)
{
- require_once 'XML/Feed/Parser.php';
- $feed = new XML_Feed_Parser($this->body, false, false, true); // @fixme
- return new FeedMunger($feed, $this->uri);
+ return ActivityUtils::getLink($this->feed->documentElement, $rel, $type);
}
/**
* @param string $url
- * @param bool $htmlOk
+ * @param bool $htmlOk pass false here if you don't want to follow web pages.
* @return string with validated URL
* @throws FeedSubBadURLException
* @throws FeedSubBadHtmlException
@@ -78,6 +102,7 @@ class FeedDiscovery
$client = new HTTPClient();
$response = $client->get($url);
} catch (HTTP_Request2_Exception $e) {
+ common_log(LOG_ERR, __METHOD__ . " Failure for $url - " . $e->getMessage());
throw new FeedSubBadURLException($e);
}
@@ -95,7 +120,12 @@ class FeedDiscovery
return $this->initFromResponse($response);
}
-
+
+ function discoverFromFeedURL($url)
+ {
+ return $this->discoverFromURL($url, false);
+ }
+
function initFromResponse($response)
{
if (!$response->isOk()) {
@@ -110,16 +140,26 @@ class FeedDiscovery
$type = $response->getHeader('Content-Type');
if (preg_match('!^(text/xml|application/xml|application/(rss|atom)\+xml)!i', $type)) {
- $this->uri = $sourceurl;
- $this->type = $type;
- $this->body = $body;
- return true;
+ return $this->init($sourceurl, $type, $body);
} else {
common_log(LOG_WARNING, "Unrecognized feed type $type for $sourceurl");
throw new FeedSubUnrecognizedTypeException($type);
}
}
+ function init($sourceurl, $type, $body)
+ {
+ $feed = new DOMDocument();
+ if ($feed->loadXML($body)) {
+ $this->uri = $sourceurl;
+ $this->type = $type;
+ $this->feed = $feed;
+ return $this->uri;
+ } else {
+ throw new FeedSubBadXmlException($url);
+ }
+ }
+
/**
* @param string $url source URL, used to resolve relative links
* @param string $body HTML body text
@@ -156,7 +196,13 @@ class FeedDiscovery
}
// Ok... now on to the links!
+ // Types listed in order of priority -- we'll prefer Atom if available.
// @fixme merge with the munger link checks
+ $feeds = array(
+ 'application/atom+xml' => false,
+ 'application/rss+xml' => false,
+ );
+
$nodes = $dom->getElementsByTagName('link');
for ($i = 0; $i < $nodes->length; $i++) {
$node = $nodes->item($i);
@@ -169,17 +215,21 @@ class FeedDiscovery
$type = trim($type->value);
$href = trim($href->value);
- $feedTypes = array(
- 'application/rss+xml',
- 'application/atom+xml',
- );
- if (trim($rel) == 'alternate' && in_array($type, $feedTypes)) {
- return $this->resolveURI($href, $base);
+ if (trim($rel) == 'alternate' && array_key_exists($type, $feeds) && empty($feeds[$type])) {
+ // Save the first feed found of each type...
+ $feeds[$type] = $this->resolveURI($href, $base);
}
}
}
}
+ // Return the highest-priority feed found
+ foreach ($feeds as $type => $url) {
+ if ($url) {
+ return $url;
+ }
+ }
+
return false;
}
diff --git a/plugins/OStatus/lib/hubdistribqueuehandler.php b/plugins/OStatus/lib/hubdistribqueuehandler.php
new file mode 100644
index 000000000..30a427e3f
--- /dev/null
+++ b/plugins/OStatus/lib/hubdistribqueuehandler.php
@@ -0,0 +1,182 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Send a PuSH subscription verification from our internal hub.
+ * Queue up final distribution for
+ * @package Hub
+ * @author Brion Vibber <brion@status.net>
+ */
+class HubDistribQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'hubdistrib';
+ }
+
+ function handle($notice)
+ {
+ assert($notice instanceof Notice);
+
+ $this->pushUser($notice);
+ foreach ($notice->getGroups() as $group) {
+ $this->pushGroup($notice, $group->group_id);
+ }
+ return true;
+ }
+
+ function pushUser($notice)
+ {
+ // See if there's any PuSH subscriptions, including OStatus clients.
+ // @fixme handle group subscriptions as well
+ // http://identi.ca/api/statuses/user_timeline/1.atom
+ $feed = common_local_url('ApiTimelineUser',
+ array('id' => $notice->profile_id,
+ 'format' => 'atom'));
+ $this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
+ }
+
+ function pushGroup($notice, $group_id)
+ {
+ $feed = common_local_url('ApiTimelineGroup',
+ array('id' => $group_id,
+ 'format' => 'atom'));
+ $this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice);
+ }
+
+ /**
+ * @param string $feed URI to the feed
+ * @param callable $callback function to generate Atom feed update if needed
+ * any additional params are passed to the callback.
+ */
+ function pushFeed($feed, $callback)
+ {
+ $hub = common_config('ostatus', 'hub');
+ if ($hub) {
+ $this->pushFeedExternal($feed, $hub);
+ }
+
+ $sub = new HubSub();
+ $sub->topic = $feed;
+ if ($sub->find()) {
+ $args = array_slice(func_get_args(), 2);
+ $atom = call_user_func_array($callback, $args);
+ $this->pushFeedInternal($atom, $sub);
+ } else {
+ common_log(LOG_INFO, "No PuSH subscribers for $feed");
+ }
+ return true;
+ }
+
+ /**
+ * Ping external hub about this update.
+ * The hub will pull the feed and check for new items later.
+ * Not guaranteed safe in an environment with database replication.
+ *
+ * @param string $feed feed topic URI
+ * @param string $hub PuSH hub URI
+ * @fixme can consolidate pings for user & group posts
+ */
+ function pushFeedExternal($feed, $hub)
+ {
+ $client = new HTTPClient();
+ try {
+ $data = array('hub.mode' => 'publish',
+ 'hub.url' => $feed);
+ $response = $client->post($hub, array(), $data);
+ if ($response->getStatus() == 204) {
+ common_log(LOG_INFO, "PuSH ping to hub $hub for $feed ok");
+ return true;
+ } else {
+ common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed with HTTP " .
+ $response->getStatus() . ': ' .
+ $response->getBody());
+ }
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "PuSH ping to hub $hub for $feed failed: " . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Queue up direct feed update pushes to subscribers on our internal hub.
+ * @param string $atom update feed, containing only new/changed items
+ * @param HubSub $sub open query of subscribers
+ */
+ function pushFeedInternal($atom, $sub)
+ {
+ common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic");
+ $qm = QueueManager::get();
+ while ($sub->fetch()) {
+ $sub->distribute($atom);
+ }
+ }
+
+ /**
+ * Build a single-item version of the sending user's Atom feed.
+ * @param Notice $notice
+ * @return string
+ */
+ function userFeedForNotice($notice)
+ {
+ // @fixme this feels VERY hacky...
+ // should probably be a cleaner way to do it
+
+ ob_start();
+ $api = new ApiTimelineUserAction();
+ $api->prepare(array('id' => $notice->profile_id,
+ 'format' => 'atom',
+ 'max_id' => $notice->id,
+ 'since_id' => $notice->id - 1));
+ $api->showTimeline();
+ $feed = ob_get_clean();
+
+ // ...and override the content-type back to something normal... eww!
+ // hope there's no other headers that got set while we weren't looking.
+ header('Content-Type: text/html; charset=utf-8');
+
+ common_log(LOG_DEBUG, $feed);
+ return $feed;
+ }
+
+ function groupFeedForNotice($group_id, $notice)
+ {
+ // @fixme this feels VERY hacky...
+ // should probably be a cleaner way to do it
+
+ ob_start();
+ $api = new ApiTimelineGroupAction();
+ $args = array('id' => $group_id,
+ 'format' => 'atom',
+ 'max_id' => $notice->id,
+ 'since_id' => $notice->id - 1);
+ $api->prepare($args);
+ $api->handle($args);
+ $feed = ob_get_clean();
+
+ // ...and override the content-type back to something normal... eww!
+ // hope there's no other headers that got set while we weren't looking.
+ header('Content-Type: text/html; charset=utf-8');
+
+ common_log(LOG_DEBUG, $feed);
+ return $feed;
+ }
+
+}
+
diff --git a/plugins/OStatus/lib/huboutqueuehandler.php b/plugins/OStatus/lib/huboutqueuehandler.php
new file mode 100644
index 000000000..3ad94646e
--- /dev/null
+++ b/plugins/OStatus/lib/huboutqueuehandler.php
@@ -0,0 +1,60 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Send a raw PuSH atom update from our internal hub.
+ * @package Hub
+ * @author Brion Vibber <brion@status.net>
+ */
+class HubOutQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'hubout';
+ }
+
+ function handle($data)
+ {
+ $sub = $data['sub'];
+ $atom = $data['atom'];
+ $retries = $data['retries'];
+
+ assert($sub instanceof HubSub);
+ assert(is_string($atom));
+
+ try {
+ $sub->push($atom);
+ } catch (Exception $e) {
+ $retries--;
+ $msg = "Failed PuSH to $sub->callback for $sub->topic: " .
+ $e->getMessage();
+ if ($retries > 0) {
+ common_log(LOG_ERR, "$msg; scheduling for $retries more tries");
+
+ // @fixme when we have infrastructure to schedule a retry
+ // after a delay, use it.
+ $sub->distribute($atom, $retries);
+ } else {
+ common_log(LOG_ERR, "$msg; discarding");
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/OStatus/lib/hubverifyqueuehandler.php b/plugins/OStatus/lib/hubverifyqueuehandler.php
new file mode 100644
index 000000000..7ce9e1431
--- /dev/null
+++ b/plugins/OStatus/lib/hubverifyqueuehandler.php
@@ -0,0 +1,54 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Send a PuSH subscription verification from our internal hub.
+ * @package Hub
+ * @author Brion Vibber <brion@status.net>
+ */
+class HubVerifyQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'hubverify';
+ }
+
+ function handle($data)
+ {
+ $sub = $data['sub'];
+ $mode = $data['mode'];
+ $token = $data['token'];
+
+ assert($sub instanceof HubSub);
+ assert($mode === 'subscribe' || $mode === 'unsubscribe');
+
+ common_log(LOG_INFO, __METHOD__ . ": $mode $sub->callback $sub->topic");
+ try {
+ $sub->verify($mode, $token);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "Failed PuSH $mode verify to $sub->callback for $sub->topic: " .
+ $e->getMessage());
+ // @fixme schedule retry?
+ // @fixme just kill it?
+ }
+
+ return true;
+ }
+}
+
diff --git a/plugins/OStatus/lib/pushinputqueuehandler.php b/plugins/OStatus/lib/pushinputqueuehandler.php
new file mode 100644
index 000000000..cbd9139b5
--- /dev/null
+++ b/plugins/OStatus/lib/pushinputqueuehandler.php
@@ -0,0 +1,49 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Process a feed distribution POST from a PuSH hub.
+ * @package FeedSub
+ * @author Brion Vibber <brion@status.net>
+ */
+
+class PushInputQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'pushinput';
+ }
+
+ function handle($data)
+ {
+ assert(is_array($data));
+
+ $feedsub_id = $data['feedsub_id'];
+ $post = $data['post'];
+ $hmac = $data['hmac'];
+
+ $feedsub = FeedSub::staticGet('id', $feedsub_id);
+ if ($feedsub) {
+ $feedsub->receive($post, $hmac);
+ } else {
+ common_log(LOG_ERR, "Discarding POST to unknown feed subscription id $feedsub_id");
+ }
+ return true;
+ }
+}
diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php
new file mode 100644
index 000000000..df17a7006
--- /dev/null
+++ b/plugins/OStatus/lib/salmon.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+class Salmon
+{
+ public function post($endpoint_uri, $xml)
+ {
+ if (empty($endpoint_uri)) {
+ return FALSE;
+ }
+
+ $headers = array('Content-type: application/atom+xml');
+
+ try {
+ $client = new HTTPClient();
+ $client->setBody($xml);
+ $response = $client->post($endpoint_uri, $headers);
+ } catch (HTTP_Request2_Exception $e) {
+ common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage());
+ return false;
+ }
+ if ($response->getStatus() != 200) {
+ common_log(LOG_ERR, "Salmon at $endpoint_uri returned status " .
+ $response->getStatus() . ': ' . $response->getBody());
+ return false;
+ }
+
+ }
+
+ public function createMagicEnv($text, $userid)
+ {
+
+
+ }
+
+
+ public function verifyMagicEnv($env)
+ {
+
+
+ }
+}
diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php
new file mode 100644
index 000000000..11c411c7d
--- /dev/null
+++ b/plugins/OStatus/lib/salmonaction.php
@@ -0,0 +1,210 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @package OStatusPlugin
+ * @author James Walker <james@status.net>
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class SalmonAction extends Action
+{
+ var $xml = null;
+ var $activity = null;
+
+ function prepare($args)
+ {
+ StatusNet::setApi(true); // Send smaller error pages
+
+ parent::prepare($args);
+
+ if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+ $this->clientError(_('This method requires a POST.'));
+ }
+
+ if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
+ $this->clientError(_('Salmon requires application/atom+xml'));
+ }
+
+ $xml = file_get_contents('php://input');
+
+ $dom = DOMDocument::loadXML($xml);
+
+ if ($dom->documentElement->namespaceURI != Activity::ATOM ||
+ $dom->documentElement->localName != 'entry') {
+ common_log(LOG_DEBUG, "Got invalid Salmon post: $xml");
+ $this->clientError(_m('Salmon post must be an Atom entry.'));
+ }
+ // XXX: check the signature
+
+ $this->act = new Activity($dom->documentElement);
+ return true;
+ }
+
+ /**
+ * Check the posted activity type and break out to appropriate processing.
+ */
+
+ function handle($args)
+ {
+ StatusNet::setApi(true); // Send smaller error pages
+
+ // TODO : Insert new $xml -> notice code
+
+ if (Event::handle('StartHandleSalmon', array($this->activity))) {
+ switch ($this->act->verb)
+ {
+ case ActivityVerb::POST:
+ $this->handlePost();
+ break;
+ case ActivityVerb::SHARE:
+ $this->handleShare();
+ break;
+ case ActivityVerb::FAVORITE:
+ $this->handleFavorite();
+ break;
+ case ActivityVerb::UNFAVORITE:
+ $this->handleUnfavorite();
+ break;
+ case ActivityVerb::FOLLOW:
+ case ActivityVerb::FRIEND:
+ $this->handleFollow();
+ break;
+ case ActivityVerb::UNFOLLOW:
+ $this->handleUnfollow();
+ break;
+ case ActivityVerb::JOIN:
+ $this->handleJoin();
+ break;
+ default:
+ throw new ClientException(_("Unimplemented."));
+ }
+ Event::handle('EndHandleSalmon', array($this->activity));
+ }
+ }
+
+ function handlePost()
+ {
+ throw new ClientException(_("Unimplemented!"));
+ }
+
+ function handleFollow()
+ {
+ throw new ClientException(_("Unimplemented!"));
+ }
+
+ function handleUnfollow()
+ {
+ throw new ClientException(_("Unimplemented!"));
+ }
+
+ function handleFavorite()
+ {
+ throw new ClientException(_("Unimplemented!"));
+ }
+
+ /**
+ * Remote user doesn't like one of our posts after all!
+ * Confirm the post is ours, and delete a local favorite event.
+ */
+
+ function handleUnfavorite()
+ {
+ throw new ClientException(_("Unimplemented!"));
+ }
+
+ /**
+ * Hmmmm
+ */
+ function handleShare()
+ {
+ throw new ClientException(_("Unimplemented!"));
+ }
+
+ /**
+ * Hmmmm
+ */
+ function handleJoin()
+ {
+ throw new ClientException(_("Unimplemented!"));
+ }
+
+ /**
+ * @return Ostatus_profile
+ */
+ function ensureProfile()
+ {
+ $actor = $this->act->actor;
+ if (empty($actor->id)) {
+ common_log(LOG_ERR, "broken actor: " . var_export($actor, true));
+ common_log(LOG_ERR, "activity with no actor: " . var_export($this->act, true));
+ throw new Exception("Received a salmon slap from unidentified actor.");
+ }
+
+ return Ostatus_profile::ensureActivityObjectProfile($actor);
+ }
+
+ function saveNotice()
+ {
+ $oprofile = $this->ensureProfile();
+
+ // Get (safe!) HTML and text versions of the content
+
+ require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php');
+
+ $html = $this->act->object->content;
+
+ $rendered = HTMLPurifier::purify($html);
+ $content = html_entity_decode(strip_tags($rendered));
+
+ $options = array('is_local' => Notice::REMOTE_OMB,
+ 'uri' => $this->act->object->id,
+ 'url' => $this->act->object->link,
+ 'rendered' => $rendered);
+
+ if (!empty($this->act->context->location)) {
+ $options['lat'] = $location->lat;
+ $options['lon'] = $location->lon;
+ if ($location->location_id) {
+ $options['location_ns'] = $location->location_ns;
+ $options['location_id'] = $location->location_id;
+ }
+ }
+
+ if (!empty($this->act->context->replyToID)) {
+ $orig = Notice::staticGet('uri',
+ $this->act->context->replyToID);
+ if (!empty($orig)) {
+ $options['reply_to'] = $orig->id;
+ }
+ }
+
+ if (!empty($this->act->time)) {
+ $options['created'] = common_sql_time($this->act->time);
+ }
+
+ return Notice::saveNew($oprofile->profile_id,
+ $content,
+ 'ostatus+salmon',
+ $options);
+ }
+}
diff --git a/plugins/OStatus/lib/webfinger.php b/plugins/OStatus/lib/webfinger.php
new file mode 100644
index 000000000..0386881d1
--- /dev/null
+++ b/plugins/OStatus/lib/webfinger.php
@@ -0,0 +1,147 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd');
+
+/**
+ * Implement the webfinger protocol.
+ */
+
+class Webfinger
+{
+ const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
+ const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
+
+ /**
+ * Perform a webfinger lookup given an account.
+ */
+
+ public function lookup($id)
+ {
+ $id = $this->normalize($id);
+ list($name, $domain) = explode('@', $id);
+
+ $links = $this->getServiceLinks($domain);
+ if (!$links) {
+ return false;
+ }
+
+ $services = array();
+ foreach ($links as $link) {
+ if ($link['template']) {
+ return $this->getServiceDescription($link['template'], $id);
+ }
+ if ($link['href']) {
+ return $this->getServiceDescription($link['href'], $id);
+ }
+ }
+ }
+
+ /**
+ * Normalize an account ID
+ */
+ function normalize($id)
+ {
+ if (substr($id, 0, 7) == 'acct://') {
+ return substr($id, 7);
+ } else if (substr($id, 0, 5) == 'acct:') {
+ return substr($id, 5);
+ }
+
+ return $id;
+ }
+
+ function getServiceLinks($domain)
+ {
+ $url = 'http://'. $domain .'/.well-known/host-meta';
+ $content = $this->fetchURL($url);
+ if (empty($content)) {
+ common_log(LOG_DEBUG, 'Error fetching host-meta');
+ return false;
+ }
+ $result = XRD::parse($content);
+
+ // Ensure that the host == domain (spec may include signing later)
+ if ($result->host != $domain) {
+ return false;
+ }
+
+ $links = array();
+ foreach ($result->links as $link) {
+ if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) {
+ $links[] = $link;
+ }
+
+ }
+ return $links;
+ }
+
+ function getServiceDescription($template, $id)
+ {
+ $url = $this->applyTemplate($template, 'acct:' . $id);
+
+ $content = $this->fetchURL($url);
+
+ return XRD::parse($content);
+ }
+
+ function fetchURL($url)
+ {
+ try {
+ $client = new HTTPClient();
+ $response = $client->get($url);
+ } catch (HTTP_Request2_Exception $e) {
+ return false;
+ }
+
+ if ($response->getStatus() != 200) {
+ return false;
+ }
+
+ return $response->getBody();
+ }
+
+ function applyTemplate($template, $id)
+ {
+ $template = str_replace('{uri}', urlencode($id), $template);
+
+ return $template;
+ }
+
+ function getHostMeta($domain, $template) {
+ $xrd = new XRD();
+ $xrd->host = $domain;
+ $xrd->links[] = array('rel' => 'lrdd',
+ 'template' => $template,
+ 'title' => array('Resource Descriptor'));
+
+ return $xrd->toXML();
+ }
+}
+
diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php
new file mode 100644
index 000000000..16d27f8eb
--- /dev/null
+++ b/plugins/OStatus/lib/xrd.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A sample module to show best practices for StatusNet plugins
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package StatusNet
+ * @author James Walker <james@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+
+class XRD
+{
+ const XML_NS = 'http://www.w3.org/2000/xmlns/';
+
+ const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0';
+
+ const HOST_META_NS = 'http://host-meta.net/xrd/1.0';
+
+ public $expires;
+
+ public $subject;
+
+ public $host;
+
+ public $alias = array();
+
+ public $types = array();
+
+ public $links = array();
+
+ public static function parse($xml)
+ {
+ $xrd = new XRD();
+
+ $dom = new DOMDocument();
+ $dom->loadXML($xml);
+ $xrd_element = $dom->getElementsByTagName('XRD')->item(0);
+
+ // Check for host-meta host
+ $host = $xrd_element->getElementsByTagName('Host')->item(0)->nodeValue;
+ if ($host) {
+ $xrd->host = $host;
+ }
+
+ // Loop through other elements
+ foreach ($xrd_element->childNodes as $node) {
+ switch ($node->tagName) {
+ case 'Expires':
+ $xrd->expires = $node->nodeValue;
+ break;
+ case 'Subject':
+ $xrd->subject = $node->nodeValue;
+ break;
+
+ case 'Alias':
+ $xrd->alias[] = $node->nodeValue;
+ break;
+
+ case 'Link':
+ $xrd->links[] = $xrd->parseLink($node);
+ break;
+
+ case 'Type':
+ $xrd->types[] = $xrd->parseType($node);
+ break;
+
+ }
+ }
+ return $xrd;
+ }
+
+ public function toXML()
+ {
+ $dom = new DOMDocument('1.0', 'UTF-8');
+ $dom->formatOutput = true;
+
+ $xrd_dom = $dom->createElementNS(XRD::XRD_NS, 'XRD');
+ $dom->appendChild($xrd_dom);
+
+ if ($this->host) {
+ $host_dom = $dom->createElement('hm:Host', $this->host);
+ $xrd_dom->setAttributeNS(XRD::XML_NS, 'xmlns:hm', XRD::HOST_META_NS);
+ $xrd_dom->appendChild($host_dom);
+ }
+
+ if ($this->expires) {
+ $expires_dom = $dom->createElement('Expires', $this->expires);
+ $xrd_dom->appendChild($expires_dom);
+ }
+
+ if ($this->subject) {
+ $subject_dom = $dom->createElement('Subject', $this->subject);
+ $xrd_dom->appendChild($subject_dom);
+ }
+
+ foreach ($this->alias as $alias) {
+ $alias_dom = $dom->createElement('Alias', $alias);
+ $xrd_dom->appendChild($alias_dom);
+ }
+
+ foreach ($this->types as $type) {
+ $type_dom = $dom->createElement('Type', $type);
+ $xrd_dom->appendChild($type_dom);
+ }
+
+ foreach ($this->links as $link) {
+ $link_dom = $this->saveLink($dom, $link);
+ $xrd_dom->appendChild($link_dom);
+ }
+
+ return $dom->saveXML();
+ }
+
+ function parseType($element)
+ {
+ return array();
+ }
+
+ function parseLink($element)
+ {
+ $link = array();
+ $link['rel'] = $element->getAttribute('rel');
+ $link['type'] = $element->getAttribute('type');
+ $link['href'] = $element->getAttribute('href');
+ $link['template'] = $element->getAttribute('template');
+ foreach ($element->childNodes as $node) {
+ switch($node->tagName) {
+ case 'Title':
+ $link['title'][] = $node->nodeValue;
+ }
+ }
+
+ return $link;
+ }
+
+ function saveLink($doc, $link)
+ {
+ $link_element = $doc->createElement('Link');
+ if ($link['rel']) {
+ $link_element->setAttribute('rel', $link['rel']);
+ }
+ if ($link['type']) {
+ $link_element->setAttribute('type', $link['type']);
+ }
+ if ($link['href']) {
+ $link_element->setAttribute('href', $link['href']);
+ }
+ if ($link['template']) {
+ $link_element->setAttribute('template', $link['template']);
+ }
+
+ if (is_array($link['title'])) {
+ foreach($link['title'] as $title) {
+ $title = $doc->createElement('Title', $title);
+ $link_element->appendChild($title);
+ }
+ }
+
+
+ return $link_element;
+ }
+}
+
diff --git a/plugins/FeedSub/locale/FeedSub.po b/plugins/OStatus/locale/OStatus.po
index dedc018e3..dedc018e3 100644
--- a/plugins/FeedSub/locale/FeedSub.po
+++ b/plugins/OStatus/locale/OStatus.po
diff --git a/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po b/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po
index f17dfa50a..f17dfa50a 100644
--- a/plugins/FeedSub/locale/fr/LC_MESSAGES/FeedSub.po
+++ b/plugins/OStatus/locale/fr/LC_MESSAGES/OStatus.po
diff --git a/plugins/OStatus/tests/ActivityParseTests.php b/plugins/OStatus/tests/ActivityParseTests.php
new file mode 100644
index 000000000..d7305dede
--- /dev/null
+++ b/plugins/OStatus/tests/ActivityParseTests.php
@@ -0,0 +1,209 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ print "This script must be run from the command line\n";
+ exit();
+}
+
+// XXX: we should probably have some common source for this stuff
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
+define('STATUSNET', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php';
+
+class ActivityParseTests extends PHPUnit_Framework_TestCase
+{
+ public function testExample1()
+ {
+ global $_example1;
+ $dom = DOMDocument::loadXML($_example1);
+ $act = new Activity($dom->documentElement);
+
+ $this->assertFalse(empty($act));
+
+ $this->assertEquals($act->time, 1243860840);
+ $this->assertEquals($act->verb, ActivityVerb::POST);
+
+ $this->assertFalse(empty($act->object));
+ $this->assertEquals($act->object->title, 'Punctuation Changeset');
+ $this->assertEquals($act->object->type, 'http://versioncentral.example.org/activity/changeset');
+ $this->assertEquals($act->object->summary, 'Fixing punctuation because it makes it more readable.');
+ $this->assertEquals($act->object->id, 'tag:versioncentral.example.org,2009:/change/1643245');
+ }
+
+ public function testExample3()
+ {
+ global $_example3;
+ $dom = DOMDocument::loadXML($_example3);
+
+ $feed = $dom->documentElement;
+
+ $entries = $feed->getElementsByTagName('entry');
+
+ $entry = $entries->item(0);
+
+ $act = new Activity($entry, $feed);
+
+ $this->assertFalse(empty($act));
+ $this->assertEquals($act->time, 1071340202);
+ $this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html');
+
+ $this->assertEquals($act->verb, ActivityVerb::POST);
+
+ $this->assertFalse(empty($act->actor));
+ $this->assertEquals($act->actor->type, ActivityObject::PERSON);
+ $this->assertEquals($act->actor->title, 'John Doe');
+ $this->assertEquals($act->actor->id, 'mailto:johndoe@example.com');
+
+ $this->assertFalse(empty($act->object));
+ $this->assertEquals($act->object->type, ActivityObject::NOTE);
+ $this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a');
+ $this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok');
+ $this->assertEquals($act->object->summary, 'Some text.');
+ $this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html');
+
+ $this->assertFalse(empty($act->context));
+
+ $this->assertTrue(empty($act->target));
+
+ $this->assertEquals($act->entry, $entry);
+ $this->assertEquals($act->feed, $feed);
+ }
+
+ public function testExample4()
+ {
+ global $_example4;
+ $dom = DOMDocument::loadXML($_example4);
+
+ $entry = $dom->documentElement;
+
+ $act = new Activity($entry);
+
+ $this->assertFalse(empty($act));
+ $this->assertEquals(1266547958, $act->time);
+ $this->assertEquals('http://example.net/notice/14', $act->link);
+
+ $this->assertFalse(empty($act->context));
+ $this->assertEquals('http://example.net/notice/12', $act->context->replyToID);
+ $this->assertEquals('http://example.net/notice/12', $act->context->replyToUrl);
+ $this->assertEquals('http://example.net/conversation/11', $act->context->conversation);
+ $this->assertEquals(array('http://example.net/user/1'), $act->context->attention);
+
+ $this->assertFalse(empty($act->object));
+ $this->assertEquals($act->object->content,
+ '@<span class="vcard"><a href="http://example.net/user/1" class="url"><span class="fn nickname">evan</span></a></span> now is the time for all good men to come to the aid of their country. #<span class="tag"><a href="http://example.net/tag/thetime" rel="tag">thetime</a></span>');
+
+ $this->assertFalse(empty($act->actor));
+ }
+}
+
+$_example1 = <<<EXAMPLE1
+<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
+ <id>tag:versioncentral.example.org,2009:/commit/1643245</id>
+ <published>2009-06-01T12:54:00Z</published>
+ <title>Geraldine committed a change to yate</title>
+ <content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content>
+ <link rel="alternate" type="text/html"
+ href="http://versioncentral.example.org/geraldine/yate/commit/1643245" />
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <activity:verb>http://versioncentral.example.org/activity/commit</activity:verb>
+ <activity:object>
+ <activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type>
+ <id>tag:versioncentral.example.org,2009:/change/1643245</id>
+ <title>Punctuation Changeset</title>
+ <summary>Fixing punctuation because it makes it more readable.</summary>
+ <link rel="alternate" type="text/html" href="..." />
+ </activity:object>
+</entry>
+EXAMPLE1;
+
+$_example2 = <<<EXAMPLE2
+<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
+ <id>tag:photopanic.example.com,2008:activity01</id>
+ <title>Geraldine posted a Photo on PhotoPanic</title>
+ <published>2008-11-02T15:29:00Z</published>
+ <link rel="alternate" type="text/html" href="/geraldine/activities/1" />
+ <activity:verb>
+ http://activitystrea.ms/schema/1.0/post
+ </activity:verb>
+ <activity:object>
+ <id>tag:photopanic.example.com,2008:photo01</id>
+ <title>My Cat</title>
+ <published>2008-11-02T15:29:00Z</published>
+ <link rel="alternate" type="text/html" href="/geraldine/photos/1" />
+ <activity:object-type>
+ tag:atomactivity.example.com,2008:photo
+ </activity:object-type>
+ <source>
+ <title>Geraldine's Photos</title>
+ <link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" />
+ <link rel="alternate" type="text/html" href="/geraldine/" />
+ </source>
+ </activity:object>
+ <content type="html">
+ &lt;p&gt;Geraldine posted a Photo on PhotoPanic&lt;/p&gt;
+ &lt;img src="/geraldine/photo1.jpg"&gt;
+ </content>
+</entry>
+EXAMPLE2;
+
+$_example3 = <<<EXAMPLE3
+<?xml version="1.0" encoding="utf-8"?>
+
+<feed xmlns="http://www.w3.org/2005/Atom">
+
+ <title>Example Feed</title>
+ <subtitle>A subtitle.</subtitle>
+ <link href="http://example.org/feed/" rel="self" />
+ <link href="http://example.org/" />
+ <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <author>
+ <name>John Doe</name>
+ <email>johndoe@example.com</email>
+ </author>
+
+ <entry>
+ <title>Atom-Powered Robots Run Amok</title>
+ <link href="http://example.org/2003/12/13/atom03" />
+ <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
+ <link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
+ <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
+ <updated>2003-12-13T18:30:02Z</updated>
+ <summary>Some text.</summary>
+ </entry>
+
+</feed>
+EXAMPLE3;
+
+$_example4 = <<<EXAMPLE4
+<?xml version='1.0' encoding='UTF-8'?>
+<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:ostatus="http://ostatus.org/schema/1.0">
+ <title>@evan now is the time for all good men to come to the aid of their country. #thetime</title>
+ <summary>@evan now is the time for all good men to come to the aid of their country. #thetime</summary>
+<author>
+ <name>spock</name>
+ <uri>http://example.net/user/2</uri>
+</author>
+<activity:actor>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+ <id>http://example.net/user/2</id>
+ <title>spock</title>
+ <link type="image/png" rel="avatar" href="http://example.net/theme/identica/default-avatar-profile.png"></link>
+</activity:actor>
+ <link rel="alternate" type="text/html" href="http://example.net/notice/14"/>
+ <id>http://example.net/notice/14</id>
+ <published>2010-02-19T02:52:38+00:00</published>
+ <updated>2010-02-19T02:52:38+00:00</updated>
+ <link rel="related" href="http://example.net/notice/12"/>
+ <thr:in-reply-to ref="http://example.net/notice/12" href="http://example.net/notice/12"></thr:in-reply-to>
+ <link rel="ostatus:conversation" href="http://example.net/conversation/11"/>
+ <link rel="ostatus:attention" href="http://example.net/user/1"/>
+ <content type="html">@&lt;span class=&quot;vcard&quot;&gt;&lt;a href=&quot;http://example.net/user/1&quot; class=&quot;url&quot;&gt;&lt;span class=&quot;fn nickname&quot;&gt;evan&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; now is the time for all good men to come to the aid of their country. #&lt;span class=&quot;tag&quot;&gt;&lt;a href=&quot;http://example.net/tag/thetime&quot; rel=&quot;tag&quot;&gt;thetime&lt;/a&gt;&lt;/span&gt;</content>
+ <category term="thetime"></category>
+</entry>
+EXAMPLE4;
diff --git a/plugins/FeedSub/tests/FeedDiscoveryTest.php b/plugins/OStatus/tests/FeedDiscoveryTest.php
index 1c5249701..1c5249701 100644
--- a/plugins/FeedSub/tests/FeedDiscoveryTest.php
+++ b/plugins/OStatus/tests/FeedDiscoveryTest.php
diff --git a/plugins/FeedSub/tests/gettext-speedtest.php b/plugins/OStatus/tests/gettext-speedtest.php
index 8bbdf5e89..8bbdf5e89 100644
--- a/plugins/FeedSub/tests/gettext-speedtest.php
+++ b/plugins/OStatus/tests/gettext-speedtest.php
diff --git a/plugins/OStatus/theme/base/css/ostatus.css b/plugins/OStatus/theme/base/css/ostatus.css
new file mode 100644
index 000000000..9bc90a731
--- /dev/null
+++ b/plugins/OStatus/theme/base/css/ostatus.css
@@ -0,0 +1,30 @@
+/** theme: base for OStatus
+ *
+ * @package StatusNet
+ * @author Sarven Capadisli <csarven@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+#form_ostatus_connect.dialogbox {
+width:70%;
+background-image:none;
+}
+#form_ostatus_connect.dialogbox .form_data label {
+width:34%;
+}
+#form_ostatus_connect.dialogbox .form_data input {
+width:57%;
+}
+#form_ostatus_connect.dialogbox .form_data .form_guide {
+margin-left:36%;
+}
+
+#form_ostatus_connect.dialogbox #ostatus_nickname {
+display:none;
+}
+
+#form_ostatus_connect.dialogbox .submit_dialogbox {
+min-width:96px;
+}
diff --git a/plugins/OpenID/finishopenidlogin.php b/plugins/OpenID/finishopenidlogin.php
index d25ce696c..438a728d8 100644
--- a/plugins/OpenID/finishopenidlogin.php
+++ b/plugins/OpenID/finishopenidlogin.php
@@ -438,49 +438,7 @@ class FinishopenidloginAction extends Action
function urlToNickname($openid)
{
- static $bad = array('query', 'user', 'password', 'port', 'fragment');
-
- $parts = parse_url($openid);
-
- # If any of these parts exist, this won't work
-
- foreach ($bad as $badpart) {
- if (array_key_exists($badpart, $parts)) {
- return null;
- }
- }
-
- # We just have host and/or path
-
- # If it's just a host...
- if (array_key_exists('host', $parts) &&
- (!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0))
- {
- $hostparts = explode('.', $parts['host']);
-
- # Try to catch common idiom of nickname.service.tld
-
- if ((count($hostparts) > 2) &&
- (strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au
- (strcmp($hostparts[0], 'www') != 0))
- {
- return $this->nicknamize($hostparts[0]);
- } else {
- # Do the whole hostname
- return $this->nicknamize($parts['host']);
- }
- } else {
- if (array_key_exists('path', $parts)) {
- # Strip starting, ending slashes
- $path = preg_replace('@/$@', '', $parts['path']);
- $path = preg_replace('@^/@', '', $path);
- if (strpos($path, '/') === false) {
- return $this->nicknamize($path);
- }
- }
- }
-
- return null;
+ return common_url_to_nickname($openid);
}
function xriToNickname($xri)
@@ -510,7 +468,6 @@ class FinishopenidloginAction extends Action
function nicknamize($str)
{
- $str = preg_replace('/\W/', '', $str);
- return strtolower($str);
+ return common_nicknamize($str);
}
}
diff --git a/plugins/PostDebug/PostDebugPlugin.php b/plugins/PostDebug/PostDebugPlugin.php
new file mode 100644
index 000000000..48fe28eab
--- /dev/null
+++ b/plugins/PostDebug/PostDebugPlugin.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Debugging helper plugin -- records detailed data on POSTs to log
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Sample
+ * @package StatusNet
+ * @author Brion Vibber <brionv@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+class PostDebugPlugin extends Plugin
+{
+ /**
+ * Set to a directory to dump individual items instead of
+ * sending to the debug log
+ */
+ public $dir=false;
+
+ public function onArgsInitialize(&$args)
+ {
+ if (isset($_SERVER['REQUEST_METHOD']) &&
+ $_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->doDebug();
+ }
+ }
+
+ public function onPluginVersion(&$versions)
+ {
+ $versions[] = array('name' => 'PostDebug',
+ 'version' => STATUSNET_VERSION,
+ 'author' => 'Brion Vibber',
+ 'homepage' => 'http://status.net/wiki/Plugin:PostDebug',
+ 'rawdescription' =>
+ _m('Debugging tool to record request details on POST.'));
+ return true;
+ }
+
+ protected function doDebug()
+ {
+ $data = array('timestamp' => gmdate('r'),
+ 'remote_addr' => @$_SERVER['REMOTE_ADDR'],
+ 'url' => @$_SERVER['REQUEST_URI'],
+ 'have_session' => common_have_session(),
+ 'logged_in' => common_logged_in(),
+ 'is_real_login' => common_is_real_login(),
+ 'user' => common_logged_in() ? common_current_user()->nickname : null,
+ 'headers' => $this->getHttpHeaders(),
+ 'post_data' => $this->sanitizePostData($_POST));
+ $this->saveDebug($data);
+ }
+
+ protected function saveDebug($data)
+ {
+ $output = var_export($data, true);
+ if ($this->dir) {
+ $file = $this->dir . DIRECTORY_SEPARATOR . $this->logFileName();
+ file_put_contents($file, $output);
+ } else {
+ common_log(LOG_DEBUG, "PostDebug: $output");
+ }
+ }
+
+ protected function logFileName()
+ {
+ $base = common_request_id();
+ $base = preg_replace('/^(.+?) .*$/', '$1', $base);
+ $base = str_replace(':', '-', $base);
+ $base = rawurlencode($base);
+ return $base;
+ }
+
+ protected function getHttpHeaders()
+ {
+ if (function_exists('getallheaders')) {
+ $headers = getallheaders();
+ } else {
+ $headers = array();
+ $prefix = 'HTTP_';
+ $prefixLen = strlen($prefix);
+ foreach ($_SERVER as $key => $val) {
+ if (substr($key, 0, $prefixLen) == $prefix) {
+ $header = $this->normalizeHeader(substr($key, $prefixLen));
+ $headers[$header] = $val;
+ }
+ }
+ }
+ foreach ($headers as $header => $val) {
+ if (strtolower($header) == 'cookie') {
+ $headers[$header] = $this->sanitizeCookies($val);
+ }
+ }
+ return $headers;
+ }
+
+ protected function normalizeHeader($key)
+ {
+ return implode('-',
+ array_map('ucfirst',
+ explode("_",
+ strtolower($key))));
+ }
+
+ function sanitizeCookies($val)
+ {
+ $blacklist = array(session_name(), 'rememberme');
+ foreach ($blacklist as $name) {
+ $val = preg_replace("/(^|;\s*)({$name}=)(.*?)(;|$)/",
+ "$1$2########$4",
+ $val);
+ }
+ return $val;
+ }
+
+ function sanitizePostData($data)
+ {
+ $blacklist = array('password', 'confirm', 'token');
+ foreach ($data as $key => $val) {
+ if (in_array($key, $blacklist)) {
+ $data[$key] = '########';
+ }
+ }
+ return $data;
+ }
+
+}
+
diff --git a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
index 14d1608d3..fb4eff738 100644
--- a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
+++ b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
@@ -45,6 +45,7 @@ class PoweredByStatusNetPlugin extends Plugin
{
function onEndAddressData($action)
{
+ $action->text(' ');
$action->elementStart('span', 'poweredby');
$action->raw(sprintf(_m('powered by %s'),
sprintf('<a href="http://status.net/">%s</a>',
diff --git a/plugins/Realtime/realtimeupdate.js b/plugins/Realtime/realtimeupdate.js
index 52151f9de..2e5851ae5 100644
--- a/plugins/Realtime/realtimeupdate.js
+++ b/plugins/Realtime/realtimeupdate.js
@@ -95,9 +95,7 @@ RealtimeUpdate = {
$("#notices_primary .notice:first").css({display:"none"});
$("#notices_primary .notice:first").fadeIn(1000);
- SN.U.FormXHR($('#'+noticeItemID+' .form_favor'));
SN.U.NoticeReplyTo($('#'+noticeItemID));
- SN.U.FormXHR($('#'+noticeItemID+' .form_repeat'));
SN.U.NoticeWithAttachment($('#'+noticeItemID));
},
@@ -136,7 +134,7 @@ RealtimeUpdate = {
ni = "<li class=\"hentry notice\" id=\"notice-"+unique+"\">"+
"<div class=\"entry-title\">"+
"<span class=\"vcard author\">"+
- "<a href=\""+user['profile_url']+"\" class=\"url\">"+
+ "<a href=\""+user['profile_url']+"\" class=\"url\" title=\""+user['name']+"\">"+
"<img src=\""+user['profile_image_url']+"\" class=\"avatar photo\" width=\"48\" height=\"48\" alt=\""+user['screen_name']+"\"/>"+
"<span class=\"nickname fn\">"+user['screen_name']+"</span>"+
"</a>"+
@@ -180,7 +178,7 @@ RealtimeUpdate = {
ni = ni+"</div>";
- "</li>";
+ ni = ni+"</li>";
return ni;
},
@@ -211,10 +209,10 @@ RealtimeUpdate = {
var rf;
rf = "<form id=\"repeat-"+id+"\" class=\"form_repeat\" method=\"post\" action=\""+RealtimeUpdate._repeaturl+"\">"+
"<fieldset>"+
- "<legend>Favor this notice</legend>"+
+ "<legend>Repeat this notice?</legend>"+
"<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
- "<input name=\"notice\" type=\"hidden\" id=\"notice-n"+id+"\" value=\""+id+"\"/>"+
- "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Favor\" title=\"Repeat this notice\"/>"+
+ "<input name=\"notice\" type=\"hidden\" id=\"notice-"+id+"\" value=\""+id+"\"/>"+
+ "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Yes\" title=\"Repeat this notice\"/>"+
"</fieldset>"+
"</form>";
diff --git a/plugins/UserFlag/UserFlagPlugin.php b/plugins/UserFlag/UserFlagPlugin.php
index a33869c19..ae3dfe036 100644
--- a/plugins/UserFlag/UserFlagPlugin.php
+++ b/plugins/UserFlag/UserFlagPlugin.php
@@ -183,21 +183,6 @@ class UserFlagPlugin extends Plugin
}
/**
- * Add our plugin's CSS to page output
- *
- * @param Action $action action being shown
- *
- * @return boolean hook result
- */
-
- function onEndShowStatusNetStyles($action)
- {
- $action->cssLink(common_path('plugins/UserFlag/userflag.css'),
- null, 'screen, projection, tv');
- return true;
- }
-
- /**
* Initialize any flagging buttons on the page
*
* @param Action $action action being shown
@@ -208,8 +193,8 @@ class UserFlagPlugin extends Plugin
function onEndShowScripts($action)
{
$action->inlineScript('if ($(".form_entity_flag").length > 0) { '.
- 'SN.U.FormXHR($(".form_entity_flag")); '.
- '}');
+ '$(".form_entity_flag").bind("click", function() {'.
+ 'SN.U.FormXHR($(this)); return false; }); }');
return true;
}
diff --git a/plugins/UserFlag/clearflagform.php b/plugins/UserFlag/clearflagform.php
index 5ad6055d3..eefd15c36 100644
--- a/plugins/UserFlag/clearflagform.php
+++ b/plugins/UserFlag/clearflagform.php
@@ -54,7 +54,7 @@ class ClearFlagForm extends ProfileActionForm
function formClass()
{
- return 'form_entity_clearflag';
+ return 'form_user_clearflag';
}
/**
diff --git a/plugins/UserFlag/userflag.css b/plugins/UserFlag/userflag.css
deleted file mode 100644
index 98da24cc9..000000000
--- a/plugins/UserFlag/userflag.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.entity_flag input.submit,
-.entity_flag p {
-background:url(icon_flag.gif) 5px 5px no-repeat;
-}
diff --git a/scripts/setconfig.php b/scripts/setconfig.php
index b102f99b1..fe0e46d65 100644..100755
--- a/scripts/setconfig.php
+++ b/scripts/setconfig.php
@@ -28,6 +28,7 @@ setconfig.php [options] [section] [setting] <value>
With three args, set the setting to the value.
With two args, just show the setting.
With -d, delete the setting.
+With no args, lists all currently set values.
[section] section to use (required)
[setting] setting to use (required)
@@ -39,6 +40,21 @@ END_OF_SETCONFIG_HELP;
require_once INSTALLDIR.'/scripts/commandline.inc';
+if (empty($args)) {
+ $count = 0;
+ $config = new Config();
+ $config->find();
+ while ($config->fetch()) {
+ $count++;
+ printf("%-20s %-20s %s\n", $config->section, $config->setting,
+ var_export($config->value, true));
+ }
+ if ($count == 0) {
+ print "No configuration set in database for this site.\n";
+ }
+ exit(0);
+}
+
if (count($args) < 2 || count($args) > 3) {
show_help();
exit(1);
diff --git a/tests/TagURITest.php b/tests/TagURITest.php
new file mode 100644
index 000000000..d23f8bfe6
--- /dev/null
+++ b/tests/TagURITest.php
@@ -0,0 +1,36 @@
+<?php
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ print "This script must be run from the command line\n";
+ exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('STATUSNET', true);
+
+require_once INSTALLDIR . '/lib/common.php';
+
+$config['site']['server'] = 'example.net';
+$config['site']['path'] = '/apps/statusnet';
+
+class TagURITest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider provider
+ */
+ public function testProduction($format, $args, $uri)
+ {
+ $minted = call_user_func_array(array('TagURI', 'mint'),
+ array_merge(array($format), $args));
+
+ $this->assertEquals($uri, $minted);
+ }
+
+ static public function provider()
+ {
+ return array(array('favorite:%d:%d',
+ array(1, 3),
+ 'tag:example.net,'.date('Y-m-d').':apps:statusnet:favorite:1:3'));
+ }
+}
+
diff --git a/theme/base/css/display.css b/theme/base/css/display.css
index ed8853e57..380975e32 100644
--- a/theme/base/css/display.css
+++ b/theme/base/css/display.css
@@ -288,7 +288,7 @@ margin-left:18px;
}
#site_nav_global_primary li {
display:inline;
-margin-left:11px;
+margin-left:18px;
}
.system_notice dt {
@@ -370,7 +370,7 @@ margin-bottom:11px;
#site_nav_global_secondary ul li {
display:inline;
-margin-right:11px;
+margin-right:18px;
}
#export_data li a {
padding-left:20px;
@@ -383,15 +383,13 @@ padding-left:28px;
}
#export_data ul {
-display:inline;
+width:100%;
+float:left;
}
#export_data li {
list-style-type:none;
-display:inline;
-margin-left:11px;
-}
-#export_data li:first-child {
-margin-left:0;
+float:left;
+margin-right:11px;
}
#licenses {
@@ -801,8 +799,8 @@ list-style-type:none;
display:inline;
}
.entity_tags li {
-display:inline;
-margin-right:4px;
+float:left;
+margin-right:11px;
}
.aside .section {
@@ -820,6 +818,7 @@ font-size:1em;
#entity_statistics dt,
#entity_statistics dd {
display:inline;
+margin-right:11px;
}
#entity_statistics dt:after {
content: ":";
@@ -1036,6 +1035,7 @@ text-decoration:underline;
.notice .entry-title {
overflow:hidden;
+word-wrap:break-word;
}
.notice .entry-title.ov {
overflow:visible;
@@ -1104,25 +1104,22 @@ left:0;
.dialogbox {
position:absolute;
-top:-4px;
-right:29px;
+top:-1px;
+right:-1px;
z-index:9;
-min-width:199px;
float:none;
-background-color:#FFF;
padding:11px;
border-radius:7px;
-moz-border-radius:7px;
-webkit-border-radius:7px;
border-style:solid;
border-width:1px;
-border-color:#DDDDDD;
--moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
}
.dialogbox legend {
display:block !important;
margin-right:18px;
+margin-bottom:18px;
}
.dialogbox button.close {
@@ -1131,11 +1128,22 @@ right:3px;
top:3px;
}
+.dialogbox .form_guide {
+font-weight:normal;
+padding:0;
+}
+
.dialogbox .submit_dialogbox {
font-weight:bold;
text-indent:0;
min-width:46px;
}
+.dialogbox input {
+padding-left:4px;
+}
+.dialogbox fieldset {
+margin-bottom:0;
+}
#wrap form.processing input.submit,
.entity_actions a.processing,
@@ -1145,6 +1153,12 @@ outline:none;
text-indent:-9999px;
}
+.form_repeat.dialogbox {
+top:-4px;
+right:29px;
+min-width:199px;
+}
+
.notice-options {
position:relative;
font-size:0.95em;
@@ -1413,6 +1427,9 @@ margin-bottom:18px;
.hentry .entry-content li li {
margin-left:18px;
}
+.hentry .entry-content .form_settings ul {
+margin-left:0;
+}
#content #plugin_authors {
min-width:122px;
@@ -1479,6 +1496,11 @@ display:inline;
margin-right:7px;
line-height:1.25;
}
+
+.tag-cloud li:before {
+content:'\0009';
+}
+
.aside .tag-cloud li {
line-height:1.5;
}
diff --git a/theme/base/images/icons/icons-01.gif b/theme/base/images/icons/icons-01.gif
index f93d33d79..6f284f023 100644
--- a/theme/base/images/icons/icons-01.gif
+++ b/theme/base/images/icons/icons-01.gif
Binary files differ
diff --git a/theme/base/images/icons/twotone/green/against.gif b/theme/base/images/icons/twotone/green/against.gif
new file mode 100644
index 000000000..ca796c8a3
--- /dev/null
+++ b/theme/base/images/icons/twotone/green/against.gif
Binary files differ
diff --git a/theme/base/images/icons/twotone/green/checkmark.gif b/theme/base/images/icons/twotone/green/checkmark.gif
new file mode 100644
index 000000000..892429d48
--- /dev/null
+++ b/theme/base/images/icons/twotone/green/checkmark.gif
Binary files differ
diff --git a/theme/base/images/icons/twotone/green/clear.gif b/theme/base/images/icons/twotone/green/clear.gif
new file mode 100644
index 000000000..2666430ef
--- /dev/null
+++ b/theme/base/images/icons/twotone/green/clear.gif
Binary files differ
diff --git a/plugins/UserFlag/icon_flag.gif b/theme/base/images/icons/twotone/green/flag.gif
index 68c8aee25..68c8aee25 100644
--- a/plugins/UserFlag/icon_flag.gif
+++ b/theme/base/images/icons/twotone/green/flag.gif
Binary files differ
diff --git a/theme/base/logo.png b/theme/base/logo.png
index 7c68b34f6..cf1839194 100644
--- a/theme/base/logo.png
+++ b/theme/base/logo.png
Binary files differ
diff --git a/theme/default/css/display.css b/theme/default/css/display.css
index 06711850f..71470d55d 100644
--- a/theme/default/css/display.css
+++ b/theme/default/css/display.css
@@ -1,7 +1,7 @@
/** theme: default
*
* @package StatusNet
- * @author Sarven Capadisli <csarven@status.net>
+ * @author Sarven Capadisli <csarven@status.net>
* @copyright 2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
@@ -18,7 +18,7 @@ font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
font-size:1em;
}
address {
-margin-right:5.7%;
+margin-right:5.3%;
}
input, textarea, select {
border-width:2px;
@@ -30,7 +30,9 @@ border-radius:4px;
input, textarea, select, option {
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
}
-input, textarea, select {
+input, textarea, select,
+.entity_actions .dialogbox input,
+.mark-top {
border-color:#AAAAAA;
}
@@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
.pagination .nav_prev a,
.pagination .nav_next a,
.form_settings fieldset fieldset,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
border-color:#DDDDDD;
}
@@ -78,7 +81,8 @@ background-color:transparent;
input:focus, textarea:focus, select:focus,
.form_notice.warning #notice_data-text,
.form_notice.warning #notice_text-count,
-.form_settings .form_note {
+.form_settings .form_note,
+.entity_actions .dialogbox .form_data input:focus {
border-color:#9BB43E;
}
input.submit {
@@ -133,9 +137,6 @@ color:#002FA7;
#content tbody tr {
border-top-color:#C8D1D5;
}
-.mark-top {
-border-color:#AAAAAA;
-}
#aside_primary {
background-color:#C8D1D5;
@@ -144,7 +145,9 @@ background-color:#C8D1D5;
#notice_text-count {
color:#333333;
}
-.form_notice.warning #notice_text-count {
+.form_notice.warning #notice_text-count,
+.dialogbox,
+.entity_actions .dialogbox input {
color:#000000;
}
.form_notice label[for=notice_data-attach] {
@@ -189,7 +192,11 @@ button.close,
.notice-options .repeated,
.form_notice label[for=notice_data-geo],
button.minimize,
-.form_reset_key input.submit {
+.form_reset_key input.submit,
+.entity_clear input.submit,
+.entity_flag input.submit,
+.entity_flag p,
+.entity_subscribe input.submit {
background-image:url(../../base/images/icons/icons-01.gif);
background-repeat:no-repeat;
background-color:transparent;
@@ -217,7 +224,8 @@ border-color:transparent;
#content,
#site_nav_local_views .current a,
.entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
background-color:#FFFFFF;
}
@@ -298,13 +306,14 @@ background-position:5px -1181px;
}
.entity_edit a {
-background-position: 5px -718px;
+background-position: 5px -719px;
}
.entity_send-a-message a {
background-position: 5px -852px;
}
.entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
@@ -338,6 +347,19 @@ background-position: 5px -1511px;
.form_reset_key input.submit {
background-position: 5px -1973px;
}
+.entity_clear input.submit {
+background-position: 5px -2039px;
+}
+.entity_flag input.submit,
+.entity_flag p {
+background-position: 5px -2105px;
+}
+.entity_subscribe input.accept {
+background-position: 5px -2171px;
+}
+.entity_subscribe input.reject {
+background-position: 5px -2237px;
+}
/* NOTICES */
.notice .attachment {
diff --git a/theme/default/logo.png b/theme/default/logo.png
index 550d373fe..cf1839194 100644
--- a/theme/default/logo.png
+++ b/theme/default/logo.png
Binary files differ
diff --git a/theme/default/mobilelogo.png b/theme/default/mobilelogo.png
index b72f7aff5..66bb5f678 100644
--- a/theme/default/mobilelogo.png
+++ b/theme/default/mobilelogo.png
Binary files differ
diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css
index 1ac96ab5b..14a82a8de 100644
--- a/theme/identica/css/display.css
+++ b/theme/identica/css/display.css
@@ -1,7 +1,7 @@
/** theme: identica
*
* @package StatusNet
- * @author Sarven Capadisli <csarven@status.net>
+ * @author Sarven Capadisli <csarven@status.net>
* @copyright 2009 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
@@ -30,7 +30,9 @@ border-radius:4px;
input, textarea, select, option {
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
}
-input, textarea, select {
+input, textarea, select,
+.entity_actions .dialogbox input,
+.mark-top {
border-color:#AAAAAA;
}
@@ -46,7 +48,8 @@ box-shadow:3px 3px 7px rgba(194, 194, 194, 0.3);
.pagination .nav_prev a,
.pagination .nav_next a,
.form_settings fieldset fieldset,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
border-color:#DDDDDD;
}
@@ -88,6 +91,7 @@ color:#FFFFFF;
border-color:transparent;
text-shadow:none;
}
+
.dialogbox .submit_dialogbox,
input.submit,
.form_notice input.submit {
@@ -133,9 +137,6 @@ color:#002FA7;
#content tbody tr {
border-top-color:#CEE1E9;
}
-.mark-top {
-border-color:#AAAAAA;
-}
#aside_primary {
background-color:#CEE1E9;
@@ -144,7 +145,9 @@ background-color:#CEE1E9;
#notice_text-count {
color:#333333;
}
-.form_notice.warning #notice_text-count {
+.form_notice.warning #notice_text-count,
+.dialogbox,
+.entity_actions .dialogbox input {
color:#000000;
}
.form_notice label[for=notice_data-attach] {
@@ -189,7 +192,11 @@ button.close,
.notice-options .repeated,
.form_notice label[for=notice_data-geo],
button.minimize,
-.form_reset_key input.submit {
+.form_reset_key input.submit,
+.entity_clear input.submit,
+.entity_flag input.submit,
+.entity_flag p,
+.entity_subscribe input.submit {
background-image:url(../../base/images/icons/icons-01.gif);
background-repeat:no-repeat;
background-color:transparent;
@@ -217,7 +224,8 @@ border-color:transparent;
#content,
#site_nav_local_views .current a,
.entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
background-color:#FFFFFF;
}
@@ -297,13 +305,14 @@ background-position:5px -1181px;
}
.entity_edit a {
-background-position: 5px -718px;
+background-position: 5px -719px;
}
.entity_send-a-message a {
background-position: 5px -852px;
}
.entity_send-a-message .form_notice,
-.entity_moderation:hover ul {
+.entity_moderation:hover ul,
+.dialogbox {
box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-moz-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
-webkit-box-shadow:3px 7px 5px rgba(194, 194, 194, 0.7);
@@ -337,6 +346,19 @@ background-position: 5px -1511px;
.form_reset_key input.submit {
background-position: 5px -1973px;
}
+.entity_clear input.submit {
+background-position: 5px -2039px;
+}
+.entity_flag input.submit,
+.entity_flag p {
+background-position: 5px -2105px;
+}
+.entity_subscribe input.accept {
+background-position: 5px -2171px;
+}
+.entity_subscribe input.reject {
+background-position: 5px -2237px;
+}
/* NOTICES */
.notice .attachment {
diff --git a/theme/identica/mobilelogo.png b/theme/identica/mobilelogo.png
index b72f7aff5..66bb5f678 100644
--- a/theme/identica/mobilelogo.png
+++ b/theme/identica/mobilelogo.png
Binary files differ