From 8ca6cc32b2c789a3149861159ad258f2cb9491e3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 14:39:39 +0200 Subject: Adding upstream version 2.11.4. Signed-off-by: Daniel Baumann --- .../Icinga/Application/ApplicationBootstrap.php | 758 +++ library/Icinga/Application/Benchmark.php | 299 + library/Icinga/Application/ClassLoader.php | 334 + library/Icinga/Application/Cli.php | 210 + library/Icinga/Application/Config.php | 497 ++ library/Icinga/Application/EmbeddedWeb.php | 114 + library/Icinga/Application/Hook.php | 330 + .../Application/Hook/ApplicationStateHook.php | 90 + library/Icinga/Application/Hook/AuditHook.php | 123 + .../Icinga/Application/Hook/AuthenticationHook.php | 75 + .../Application/Hook/ConfigFormEventsHook.php | 137 + library/Icinga/Application/Hook/GrapherHook.php | 111 + library/Icinga/Application/Hook/HealthHook.php | 222 + library/Icinga/Application/Hook/PdfexportHook.php | 25 + .../Icinga/Application/Hook/ThemeLoaderHook.php | 22 + .../Application/Hook/Ticket/TicketPattern.php | 140 + library/Icinga/Application/Hook/TicketHook.php | 210 + library/Icinga/Application/Hook/WebBaseHook.php | 54 + library/Icinga/Application/Icinga.php | 49 + library/Icinga/Application/LegacyWeb.php | 31 + library/Icinga/Application/Libraries.php | 91 + library/Icinga/Application/Libraries/Library.php | 259 + library/Icinga/Application/Logger.php | 349 + library/Icinga/Application/Logger/LogWriter.php | 30 + .../Application/Logger/Writer/FileWriter.php | 80 + .../Icinga/Application/Logger/Writer/PhpWriter.php | 39 + .../Application/Logger/Writer/StderrWriter.php | 61 + .../Application/Logger/Writer/StdoutWriter.php | 13 + .../Application/Logger/Writer/SyslogWriter.php | 90 + .../Application/Modules/DashboardContainer.php | 58 + library/Icinga/Application/Modules/Manager.php | 698 ++ .../Application/Modules/MenuItemContainer.php | 55 + library/Icinga/Application/Modules/Module.php | 1444 +++++ .../Modules/NavigationItemContainer.php | 117 + library/Icinga/Application/Platform.php | 435 ++ library/Icinga/Application/StaticWeb.php | 21 + library/Icinga/Application/Version.php | 65 + library/Icinga/Application/Web.php | 505 ++ library/Icinga/Application/functions.php | 110 + library/Icinga/Application/webrouter.php | 106 + library/Icinga/Authentication/AdmissionLoader.php | 249 + library/Icinga/Authentication/Auth.php | 453 ++ library/Icinga/Authentication/AuthChain.php | 269 + library/Icinga/Authentication/Authenticatable.php | 21 + library/Icinga/Authentication/Role.php | 334 + library/Icinga/Authentication/RolesConfig.php | 43 + .../Icinga/Authentication/User/DbUserBackend.php | 256 + .../Authentication/User/DomainAwareInterface.php | 17 + .../Icinga/Authentication/User/ExternalBackend.php | 124 + .../Icinga/Authentication/User/LdapUserBackend.php | 477 ++ library/Icinga/Authentication/User/UserBackend.php | 257 + .../Authentication/User/UserBackendInterface.php | 39 + .../UserGroup/DbUserGroupBackend.php | 325 + .../UserGroup/LdapUserGroupBackend.php | 944 +++ .../Authentication/UserGroup/UserGroupBackend.php | 188 + .../UserGroup/UserGroupBackendInterface.php | 56 + library/Icinga/Chart/Axis.php | 485 ++ library/Icinga/Chart/Chart.php | 162 + library/Icinga/Chart/Donut.php | 465 ++ library/Icinga/Chart/Format.php | 21 + library/Icinga/Chart/Graph/BarGraph.php | 162 + library/Icinga/Chart/Graph/LineGraph.php | 195 + library/Icinga/Chart/Graph/StackedGraph.php | 88 + library/Icinga/Chart/Graph/Tooltip.php | 143 + library/Icinga/Chart/GridChart.php | 446 ++ library/Icinga/Chart/Inline/Inline.php | 96 + library/Icinga/Chart/Inline/PieChart.php | 41 + library/Icinga/Chart/Legend.php | 102 + library/Icinga/Chart/Palette.php | 65 + library/Icinga/Chart/PieChart.php | 306 + library/Icinga/Chart/Primitive/Animatable.php | 43 + library/Icinga/Chart/Primitive/Animation.php | 87 + library/Icinga/Chart/Primitive/Canvas.php | 140 + library/Icinga/Chart/Primitive/Circle.php | 68 + library/Icinga/Chart/Primitive/Drawable.php | 22 + library/Icinga/Chart/Primitive/Line.php | 87 + library/Icinga/Chart/Primitive/Path.php | 174 + library/Icinga/Chart/Primitive/PieSlice.php | 293 + library/Icinga/Chart/Primitive/RawElement.php | 43 + library/Icinga/Chart/Primitive/Rect.php | 105 + library/Icinga/Chart/Primitive/Styleable.php | 154 + library/Icinga/Chart/Primitive/Text.php | 168 + library/Icinga/Chart/Render/LayoutBox.php | 200 + library/Icinga/Chart/Render/RenderContext.php | 225 + library/Icinga/Chart/Render/Rotator.php | 80 + library/Icinga/Chart/SVGRenderer.php | 331 + library/Icinga/Chart/Unit/AxisUnit.php | 56 + library/Icinga/Chart/Unit/CalendarUnit.php | 167 + library/Icinga/Chart/Unit/LinearUnit.php | 227 + library/Icinga/Chart/Unit/LogarithmicUnit.php | 263 + library/Icinga/Chart/Unit/StaticAxis.php | 129 + library/Icinga/Cli/AnsiScreen.php | 122 + library/Icinga/Cli/Command.php | 208 + library/Icinga/Cli/Documentation.php | 162 + library/Icinga/Cli/Documentation/CommentParser.php | 85 + library/Icinga/Cli/Loader.php | 499 ++ library/Icinga/Cli/Params.php | 320 + library/Icinga/Cli/Screen.php | 106 + library/Icinga/Common/Database.php | 56 + library/Icinga/Common/PdfExport.php | 105 + library/Icinga/Crypt/AesCrypt.php | 337 + library/Icinga/Data/ConfigObject.php | 289 + library/Icinga/Data/ConnectionInterface.php | 8 + library/Icinga/Data/DataArray/ArrayDatasource.php | 292 + library/Icinga/Data/Db/DbConnection.php | 655 ++ library/Icinga/Data/Db/DbQuery.php | 564 ++ library/Icinga/Data/Extensible.php | 22 + library/Icinga/Data/Fetchable.php | 47 + library/Icinga/Data/Filter/Filter.php | 255 + library/Icinga/Data/Filter/FilterAnd.php | 42 + library/Icinga/Data/Filter/FilterChain.php | 286 + library/Icinga/Data/Filter/FilterEqual.php | 16 + .../Data/Filter/FilterEqualOrGreaterThan.php | 16 + .../Icinga/Data/Filter/FilterEqualOrLessThan.php | 26 + library/Icinga/Data/Filter/FilterException.php | 15 + library/Icinga/Data/Filter/FilterExpression.php | 224 + library/Icinga/Data/Filter/FilterGreaterThan.php | 16 + library/Icinga/Data/Filter/FilterLessThan.php | 26 + library/Icinga/Data/Filter/FilterMatch.php | 8 + .../Data/Filter/FilterMatchCaseInsensitive.php | 13 + library/Icinga/Data/Filter/FilterMatchNot.php | 12 + .../Data/Filter/FilterMatchNotCaseInsensitive.php | 13 + library/Icinga/Data/Filter/FilterNot.php | 58 + library/Icinga/Data/Filter/FilterNotEqual.php | 12 + library/Icinga/Data/Filter/FilterOr.php | 39 + .../Icinga/Data/Filter/FilterParseException.php | 10 + library/Icinga/Data/Filter/FilterQueryString.php | 320 + library/Icinga/Data/FilterColumns.php | 21 + library/Icinga/Data/Filterable.php | 27 + library/Icinga/Data/Identifiable.php | 17 + library/Icinga/Data/Inspectable.php | 20 + library/Icinga/Data/Inspection.php | 129 + library/Icinga/Data/Limitable.php | 48 + library/Icinga/Data/Paginatable.php | 10 + library/Icinga/Data/PivotTable.php | 396 ++ library/Icinga/Data/QueryInterface.php | 8 + library/Icinga/Data/Queryable.php | 20 + library/Icinga/Data/Reducible.php | 23 + library/Icinga/Data/ResourceFactory.php | 138 + library/Icinga/Data/Selectable.php | 17 + library/Icinga/Data/SimpleQuery.php | 650 ++ library/Icinga/Data/SortRules.php | 14 + library/Icinga/Data/Sortable.php | 49 + library/Icinga/Data/Tree/SimpleTree.php | 90 + library/Icinga/Data/Tree/TreeNode.php | 109 + library/Icinga/Data/Tree/TreeNodeIterator.php | 75 + library/Icinga/Data/Updatable.php | 24 + library/Icinga/Date/DateFormatter.php | 259 + .../Icinga/Exception/AlreadyExistsException.php | 11 + .../Icinga/Exception/AuthenticationException.php | 11 + library/Icinga/Exception/ConfigurationError.php | 12 + .../Icinga/Exception/Http/BaseHttpException.php | 73 + .../Exception/Http/HttpBadRequestException.php | 12 + library/Icinga/Exception/Http/HttpException.php | 25 + .../Exception/Http/HttpExceptionInterface.php | 21 + .../Http/HttpMethodNotAllowedException.php | 36 + .../Exception/Http/HttpNotFoundException.php | 12 + library/Icinga/Exception/IcingaException.php | 107 + .../Icinga/Exception/InvalidPropertyException.php | 11 + .../Icinga/Exception/Json/JsonDecodeException.php | 11 + .../Icinga/Exception/Json/JsonEncodeException.php | 11 + library/Icinga/Exception/Json/JsonException.php | 13 + .../Icinga/Exception/MissingParameterException.php | 40 + library/Icinga/Exception/NotFoundError.php | 8 + library/Icinga/Exception/NotImplementedError.php | 12 + library/Icinga/Exception/NotReadableError.php | 8 + library/Icinga/Exception/NotWritableError.php | 8 + library/Icinga/Exception/ProgrammingError.php | 12 + library/Icinga/Exception/QueryException.php | 11 + library/Icinga/Exception/StatementException.php | 8 + .../Icinga/Exception/SystemPermissionException.php | 11 + library/Icinga/File/Csv.php | 47 + library/Icinga/File/Ini/Dom/Comment.php | 37 + library/Icinga/File/Ini/Dom/Directive.php | 166 + library/Icinga/File/Ini/Dom/Document.php | 132 + library/Icinga/File/Ini/Dom/Section.php | 190 + library/Icinga/File/Ini/IniParser.php | 310 + library/Icinga/File/Ini/IniWriter.php | 205 + library/Icinga/File/Pdf.php | 96 + library/Icinga/File/Storage/LocalFileStorage.php | 158 + library/Icinga/File/Storage/StorageInterface.php | 94 + .../File/Storage/TemporaryLocalFileStorage.php | 59 + library/Icinga/Legacy/DashboardConfig.php | 137 + library/Icinga/Less/Call.php | 77 + library/Icinga/Less/ColorProp.php | 109 + library/Icinga/Less/ColorPropOrVariable.php | 71 + library/Icinga/Less/DeferredColorProp.php | 136 + library/Icinga/Less/LightMode.php | 128 + library/Icinga/Less/LightModeCall.php | 38 + library/Icinga/Less/LightModeDefinition.php | 75 + library/Icinga/Less/LightModeTrait.php | 30 + library/Icinga/Less/LightModeVisitor.php | 26 + library/Icinga/Less/Visitor.php | 243 + library/Icinga/Protocol/Dns.php | 89 + .../File/Exception/FileReaderException.php | 12 + library/Icinga/Protocol/File/FileIterator.php | 81 + library/Icinga/Protocol/File/FileQuery.php | 86 + library/Icinga/Protocol/File/FileReader.php | 208 + library/Icinga/Protocol/File/LogFileIterator.php | 149 + library/Icinga/Protocol/Ldap/Discovery.php | 143 + library/Icinga/Protocol/Ldap/LdapCapabilities.php | 439 ++ library/Icinga/Protocol/Ldap/LdapConnection.php | 1584 +++++ library/Icinga/Protocol/Ldap/LdapException.php | 14 + library/Icinga/Protocol/Ldap/LdapQuery.php | 361 ++ library/Icinga/Protocol/Ldap/LdapUtils.php | 148 + library/Icinga/Protocol/Ldap/Node.php | 69 + library/Icinga/Protocol/Ldap/Root.php | 241 + library/Icinga/Protocol/Nrpe/Connection.php | 111 + library/Icinga/Protocol/Nrpe/Packet.php | 69 + library/Icinga/Repository/DbRepository.php | 1077 ++++ library/Icinga/Repository/IniRepository.php | 420 ++ library/Icinga/Repository/LdapRepository.php | 71 + library/Icinga/Repository/Repository.php | 1261 ++++ library/Icinga/Repository/RepositoryQuery.php | 797 +++ library/Icinga/Security/SecurityException.php | 13 + library/Icinga/Test/BaseTestCase.php | 361 ++ library/Icinga/Test/ClassLoader.php | 113 + library/Icinga/Test/DbTest.php | 47 + library/Icinga/User.php | 649 ++ library/Icinga/User/Preferences.php | 169 + .../Icinga/User/Preferences/PreferencesStore.php | 344 + library/Icinga/Util/ASN1.php | 102 + library/Icinga/Util/Color.php | 121 + library/Icinga/Util/ConfigAwareFactory.php | 18 + library/Icinga/Util/Dimension.php | 123 + library/Icinga/Util/DirectoryIterator.php | 213 + library/Icinga/Util/EnumeratingFilterIterator.php | 30 + library/Icinga/Util/Environment.php | 42 + library/Icinga/Util/File.php | 195 + library/Icinga/Util/Format.php | 197 + library/Icinga/Util/GlobFilter.php | 182 + library/Icinga/Util/Json.php | 151 + library/Icinga/Util/LessParser.php | 17 + library/Icinga/Util/StringHelper.php | 184 + library/Icinga/Util/TimezoneDetect.php | 107 + library/Icinga/Web/Announcement.php | 158 + .../Icinga/Web/Announcement/AnnouncementCookie.php | 138 + .../Web/Announcement/AnnouncementIniRepository.php | 152 + library/Icinga/Web/ApplicationStateCookie.php | 74 + library/Icinga/Web/Controller.php | 264 + library/Icinga/Web/Controller/ActionController.php | 587 ++ .../Web/Controller/AuthBackendController.php | 149 + .../Web/Controller/BasePreferenceController.php | 39 + .../Web/Controller/ControllerTabCollector.php | 97 + library/Icinga/Web/Controller/Dispatcher.php | 93 + .../Web/Controller/ModuleActionController.php | 80 + library/Icinga/Web/Controller/StaticController.php | 87 + library/Icinga/Web/Cookie.php | 299 + library/Icinga/Web/CookieSet.php | 58 + library/Icinga/Web/Dom/DomNodeIterator.php | 84 + library/Icinga/Web/FileCache.php | 293 + library/Icinga/Web/Form.php | 1666 +++++ library/Icinga/Web/Form/Decorator/Autosubmit.php | 133 + .../Web/Form/Decorator/ConditionalHidden.php | 35 + .../Icinga/Web/Form/Decorator/ElementDoubler.php | 63 + .../Icinga/Web/Form/Decorator/FormDescriptions.php | 76 + library/Icinga/Web/Form/Decorator/FormHints.php | 142 + .../Web/Form/Decorator/FormNotifications.php | 125 + library/Icinga/Web/Form/Decorator/Help.php | 113 + library/Icinga/Web/Form/Decorator/Spinner.php | 48 + library/Icinga/Web/Form/Element/Button.php | 80 + library/Icinga/Web/Form/Element/Checkbox.php | 9 + .../Icinga/Web/Form/Element/CsrfCounterMeasure.php | 99 + library/Icinga/Web/Form/Element/Date.php | 19 + library/Icinga/Web/Form/Element/DateTimePicker.php | 80 + library/Icinga/Web/Form/Element/Note.php | 55 + library/Icinga/Web/Form/Element/Number.php | 144 + library/Icinga/Web/Form/Element/Textarea.php | 20 + library/Icinga/Web/Form/Element/Time.php | 19 + library/Icinga/Web/Form/ErrorLabeller.php | 71 + library/Icinga/Web/Form/FormElement.php | 61 + .../Icinga/Web/Form/InvalidCSRFTokenException.php | 11 + .../Web/Form/Validator/DateFormatValidator.php | 61 + .../Web/Form/Validator/DateTimeValidator.php | 77 + library/Icinga/Web/Form/Validator/InArray.php | 28 + .../Web/Form/Validator/InternalUrlValidator.php | 41 + .../Web/Form/Validator/ReadablePathValidator.php | 53 + .../Web/Form/Validator/TimeFormatValidator.php | 58 + library/Icinga/Web/Form/Validator/UrlValidator.php | 40 + .../Web/Form/Validator/WritablePathValidator.php | 72 + library/Icinga/Web/Helper/CookieHelper.php | 81 + library/Icinga/Web/Helper/HtmlPurifier.php | 99 + library/Icinga/Web/Helper/Markdown.php | 38 + .../Icinga/Web/Helper/Markdown/LinkTransformer.php | 73 + library/Icinga/Web/Hook.php | 16 + library/Icinga/Web/JavaScript.php | 269 + library/Icinga/Web/LessCompiler.php | 257 + library/Icinga/Web/Menu.php | 152 + library/Icinga/Web/Navigation/ConfigMenu.php | 302 + library/Icinga/Web/Navigation/DashboardPane.php | 84 + library/Icinga/Web/Navigation/DropdownItem.php | 20 + library/Icinga/Web/Navigation/Navigation.php | 571 ++ library/Icinga/Web/Navigation/NavigationItem.php | 947 +++ .../Renderer/BadgeNavigationItemRenderer.php | 139 + .../Renderer/HealthNavigationRenderer.php | 44 + .../Navigation/Renderer/NavigationItemRenderer.php | 233 + .../Web/Navigation/Renderer/NavigationRenderer.php | 356 ++ .../Renderer/NavigationRendererInterface.php | 142 + .../Renderer/RecursiveNavigationRenderer.php | 186 + .../Renderer/SummaryNavigationItemRenderer.php | 72 + library/Icinga/Web/Notification.php | 220 + .../Icinga/Web/Paginator/Adapter/QueryAdapter.php | 84 + .../Paginator/ScrollingStyle/SlidingWithBorder.php | 78 + library/Icinga/Web/RememberMe.php | 363 ++ library/Icinga/Web/RememberMeUserDevicesList.php | 144 + library/Icinga/Web/RememberMeUserList.php | 106 + library/Icinga/Web/Request.php | 142 + library/Icinga/Web/Response.php | 429 ++ library/Icinga/Web/Response/JsonResponse.php | 239 + library/Icinga/Web/Session.php | 54 + library/Icinga/Web/Session/Php72Session.php | 37 + library/Icinga/Web/Session/PhpSession.php | 256 + library/Icinga/Web/Session/Session.php | 126 + library/Icinga/Web/Session/SessionNamespace.php | 201 + library/Icinga/Web/StyleSheet.php | 341 + library/Icinga/Web/Url.php | 806 +++ library/Icinga/Web/UrlParams.php | 433 ++ library/Icinga/Web/UserAgent.php | 86 + library/Icinga/Web/View.php | 254 + library/Icinga/Web/View/AppHealth.php | 89 + library/Icinga/Web/View/Helper/IcingaCheckbox.php | 30 + library/Icinga/Web/View/PrivilegeAudit.php | 622 ++ library/Icinga/Web/View/helpers/format.php | 72 + library/Icinga/Web/View/helpers/generic.php | 15 + library/Icinga/Web/View/helpers/string.php | 36 + library/Icinga/Web/View/helpers/url.php | 158 + library/Icinga/Web/Widget.php | 48 + library/Icinga/Web/Widget/AbstractWidget.php | 120 + library/Icinga/Web/Widget/Announcements.php | 55 + .../Icinga/Web/Widget/ApplicationStateMessages.php | 74 + .../Icinga/Web/Widget/Chart/HistoryColorGrid.php | 373 ++ library/Icinga/Web/Widget/Chart/InlinePie.php | 257 + library/Icinga/Web/Widget/Dashboard.php | 475 ++ library/Icinga/Web/Widget/Dashboard/Dashlet.php | 315 + library/Icinga/Web/Widget/Dashboard/Pane.php | 335 + library/Icinga/Web/Widget/Dashboard/UserWidget.php | 36 + library/Icinga/Web/Widget/FilterEditor.php | 811 +++ library/Icinga/Web/Widget/FilterWidget.php | 122 + library/Icinga/Web/Widget/Limiter.php | 54 + library/Icinga/Web/Widget/Paginator.php | 167 + library/Icinga/Web/Widget/SearchDashboard.php | 111 + .../Icinga/Web/Widget/SingleValueSearchControl.php | 200 + library/Icinga/Web/Widget/SortBox.php | 260 + library/Icinga/Web/Widget/Tab.php | 323 + .../Web/Widget/Tabextension/DashboardAction.php | 35 + .../Web/Widget/Tabextension/DashboardSettings.php | 39 + .../Icinga/Web/Widget/Tabextension/MenuAction.php | 35 + .../Web/Widget/Tabextension/OutputFormat.php | 114 + .../Web/Widget/Tabextension/Tabextension.php | 25 + library/Icinga/Web/Widget/Tabs.php | 445 ++ library/Icinga/Web/Widget/Widget.php | 24 + library/Icinga/Web/Window.php | 125 + library/Icinga/Web/Wizard.php | 719 +++ library/vendor/HTMLPurifier.autoload.php | 25 + library/vendor/HTMLPurifier.php | 297 + library/vendor/HTMLPurifier/Arborize.php | 71 + library/vendor/HTMLPurifier/AttrCollections.php | 148 + library/vendor/HTMLPurifier/AttrDef.php | 144 + library/vendor/HTMLPurifier/AttrDef/CSS.php | 136 + .../vendor/HTMLPurifier/AttrDef/CSS/AlphaValue.php | 34 + .../vendor/HTMLPurifier/AttrDef/CSS/Background.php | 113 + .../AttrDef/CSS/BackgroundPosition.php | 157 + library/vendor/HTMLPurifier/AttrDef/CSS/Border.php | 56 + library/vendor/HTMLPurifier/AttrDef/CSS/Color.php | 161 + .../vendor/HTMLPurifier/AttrDef/CSS/Composite.php | 48 + .../AttrDef/CSS/DenyElementDecorator.php | 44 + library/vendor/HTMLPurifier/AttrDef/CSS/Filter.php | 77 + library/vendor/HTMLPurifier/AttrDef/CSS/Font.php | 176 + .../vendor/HTMLPurifier/AttrDef/CSS/FontFamily.php | 219 + library/vendor/HTMLPurifier/AttrDef/CSS/Ident.php | 32 + .../AttrDef/CSS/ImportantDecorator.php | 56 + library/vendor/HTMLPurifier/AttrDef/CSS/Length.php | 77 + .../vendor/HTMLPurifier/AttrDef/CSS/ListStyle.php | 112 + .../vendor/HTMLPurifier/AttrDef/CSS/Multiple.php | 71 + library/vendor/HTMLPurifier/AttrDef/CSS/Number.php | 90 + .../vendor/HTMLPurifier/AttrDef/CSS/Percentage.php | 54 + .../HTMLPurifier/AttrDef/CSS/TextDecoration.php | 46 + library/vendor/HTMLPurifier/AttrDef/CSS/URI.php | 77 + library/vendor/HTMLPurifier/AttrDef/Clone.php | 44 + library/vendor/HTMLPurifier/AttrDef/Enum.php | 73 + library/vendor/HTMLPurifier/AttrDef/HTML/Bool.php | 48 + library/vendor/HTMLPurifier/AttrDef/HTML/Class.php | 48 + library/vendor/HTMLPurifier/AttrDef/HTML/Color.php | 51 + .../HTMLPurifier/AttrDef/HTML/ContentEditable.php | 16 + .../HTMLPurifier/AttrDef/HTML/FrameTarget.php | 38 + library/vendor/HTMLPurifier/AttrDef/HTML/ID.php | 113 + .../vendor/HTMLPurifier/AttrDef/HTML/Length.php | 56 + .../vendor/HTMLPurifier/AttrDef/HTML/LinkTypes.php | 72 + .../HTMLPurifier/AttrDef/HTML/MultiLength.php | 60 + .../vendor/HTMLPurifier/AttrDef/HTML/Nmtokens.php | 70 + .../vendor/HTMLPurifier/AttrDef/HTML/Pixels.php | 76 + library/vendor/HTMLPurifier/AttrDef/Integer.php | 91 + library/vendor/HTMLPurifier/AttrDef/Lang.php | 86 + library/vendor/HTMLPurifier/AttrDef/Switch.php | 53 + library/vendor/HTMLPurifier/AttrDef/Text.php | 21 + library/vendor/HTMLPurifier/AttrDef/URI.php | 111 + library/vendor/HTMLPurifier/AttrDef/URI/Email.php | 20 + .../HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php | 29 + library/vendor/HTMLPurifier/AttrDef/URI/Host.php | 142 + library/vendor/HTMLPurifier/AttrDef/URI/IPv4.php | 45 + library/vendor/HTMLPurifier/AttrDef/URI/IPv6.php | 89 + library/vendor/HTMLPurifier/AttrTransform.php | 60 + .../HTMLPurifier/AttrTransform/Background.php | 28 + .../vendor/HTMLPurifier/AttrTransform/BdoDir.php | 27 + .../vendor/HTMLPurifier/AttrTransform/BgColor.php | 28 + .../HTMLPurifier/AttrTransform/BoolToCSS.php | 47 + .../vendor/HTMLPurifier/AttrTransform/Border.php | 26 + .../HTMLPurifier/AttrTransform/EnumToCSS.php | 68 + .../HTMLPurifier/AttrTransform/ImgRequired.php | 47 + .../vendor/HTMLPurifier/AttrTransform/ImgSpace.php | 61 + .../vendor/HTMLPurifier/AttrTransform/Input.php | 56 + library/vendor/HTMLPurifier/AttrTransform/Lang.php | 31 + .../vendor/HTMLPurifier/AttrTransform/Length.php | 45 + library/vendor/HTMLPurifier/AttrTransform/Name.php | 33 + .../vendor/HTMLPurifier/AttrTransform/NameSync.php | 46 + .../vendor/HTMLPurifier/AttrTransform/Nofollow.php | 52 + .../HTMLPurifier/AttrTransform/SafeEmbed.php | 25 + .../HTMLPurifier/AttrTransform/SafeObject.php | 28 + .../HTMLPurifier/AttrTransform/SafeParam.php | 84 + .../HTMLPurifier/AttrTransform/ScriptRequired.php | 23 + .../HTMLPurifier/AttrTransform/TargetBlank.php | 45 + .../HTMLPurifier/AttrTransform/TargetNoopener.php | 37 + .../AttrTransform/TargetNoreferrer.php | 37 + .../vendor/HTMLPurifier/AttrTransform/Textarea.php | 27 + library/vendor/HTMLPurifier/AttrTypes.php | 97 + library/vendor/HTMLPurifier/AttrValidator.php | 178 + library/vendor/HTMLPurifier/Bootstrap.php | 124 + library/vendor/HTMLPurifier/CSSDefinition.php | 549 ++ library/vendor/HTMLPurifier/ChildDef.php | 52 + library/vendor/HTMLPurifier/ChildDef/Chameleon.php | 67 + library/vendor/HTMLPurifier/ChildDef/Custom.php | 102 + library/vendor/HTMLPurifier/ChildDef/Empty.php | 38 + library/vendor/HTMLPurifier/ChildDef/List.php | 94 + library/vendor/HTMLPurifier/ChildDef/Optional.php | 45 + library/vendor/HTMLPurifier/ChildDef/Required.php | 118 + .../HTMLPurifier/ChildDef/StrictBlockquote.php | 110 + library/vendor/HTMLPurifier/ChildDef/Table.php | 224 + library/vendor/HTMLPurifier/Config.php | 920 +++ library/vendor/HTMLPurifier/ConfigSchema.php | 176 + .../ConfigSchema/Builder/ConfigSchema.php | 48 + .../HTMLPurifier/ConfigSchema/Builder/Xml.php | 144 + .../vendor/HTMLPurifier/ConfigSchema/Exception.php | 11 + .../HTMLPurifier/ConfigSchema/Interchange.php | 47 + .../ConfigSchema/Interchange/Directive.php | 89 + .../HTMLPurifier/ConfigSchema/Interchange/Id.php | 58 + .../ConfigSchema/InterchangeBuilder.php | 226 + .../vendor/HTMLPurifier/ConfigSchema/Validator.php | 248 + .../HTMLPurifier/ConfigSchema/ValidatorAtom.php | 130 + .../vendor/HTMLPurifier/ConfigSchema/schema.ser | 1 + .../ConfigSchema/schema/Attr.AllowedClasses.txt | 8 + .../schema/Attr.AllowedFrameTargets.txt | 12 + .../ConfigSchema/schema/Attr.AllowedRel.txt | 9 + .../ConfigSchema/schema/Attr.AllowedRev.txt | 9 + .../ConfigSchema/schema/Attr.ClassUseCDATA.txt | 19 + .../ConfigSchema/schema/Attr.DefaultImageAlt.txt | 11 + .../schema/Attr.DefaultInvalidImage.txt | 9 + .../schema/Attr.DefaultInvalidImageAlt.txt | 8 + .../ConfigSchema/schema/Attr.DefaultTextDir.txt | 10 + .../ConfigSchema/schema/Attr.EnableID.txt | 16 + .../ConfigSchema/schema/Attr.ForbiddenClasses.txt | 8 + .../ConfigSchema/schema/Attr.ID.HTML5.txt | 10 + .../ConfigSchema/schema/Attr.IDBlacklist.txt | 5 + .../ConfigSchema/schema/Attr.IDBlacklistRegexp.txt | 9 + .../ConfigSchema/schema/Attr.IDPrefix.txt | 12 + .../ConfigSchema/schema/Attr.IDPrefixLocal.txt | 14 + .../schema/AutoFormat.AutoParagraph.txt | 31 + .../ConfigSchema/schema/AutoFormat.Custom.txt | 12 + .../schema/AutoFormat.DisplayLinkURI.txt | 11 + .../ConfigSchema/schema/AutoFormat.Linkify.txt | 12 + .../schema/AutoFormat.PurifierLinkify.DocURL.txt | 12 + .../schema/AutoFormat.PurifierLinkify.txt | 12 + .../schema/AutoFormat.RemoveEmpty.Predicate.txt | 14 + ...utoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt | 11 + .../schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt | 15 + .../ConfigSchema/schema/AutoFormat.RemoveEmpty.txt | 46 + .../AutoFormat.RemoveSpansWithoutAttributes.txt | 11 + .../ConfigSchema/schema/CSS.AllowDuplicates.txt | 11 + .../ConfigSchema/schema/CSS.AllowImportant.txt | 8 + .../ConfigSchema/schema/CSS.AllowTricky.txt | 11 + .../ConfigSchema/schema/CSS.AllowedFonts.txt | 12 + .../ConfigSchema/schema/CSS.AllowedProperties.txt | 18 + .../ConfigSchema/schema/CSS.DefinitionRev.txt | 11 + .../schema/CSS.ForbiddenProperties.txt | 13 + .../ConfigSchema/schema/CSS.MaxImgLength.txt | 16 + .../ConfigSchema/schema/CSS.Proprietary.txt | 10 + .../ConfigSchema/schema/CSS.Trusted.txt | 9 + .../ConfigSchema/schema/Cache.DefinitionImpl.txt | 14 + .../ConfigSchema/schema/Cache.SerializerPath.txt | 13 + .../schema/Cache.SerializerPermissions.txt | 16 + .../ConfigSchema/schema/Core.AggressivelyFixLt.txt | 18 + .../schema/Core.AggressivelyRemoveScript.txt | 16 + .../schema/Core.AllowHostnameUnderscore.txt | 16 + .../schema/Core.AllowParseManyTags.txt | 12 + .../ConfigSchema/schema/Core.CollectErrors.txt | 12 + .../ConfigSchema/schema/Core.ColorKeywords.txt | 160 + .../schema/Core.ConvertDocumentToFragment.txt | 14 + .../Core.DirectLexLineNumberSyncInterval.txt | 17 + .../ConfigSchema/schema/Core.DisableExcludes.txt | 14 + .../ConfigSchema/schema/Core.EnableIDNA.txt | 9 + .../ConfigSchema/schema/Core.Encoding.txt | 15 + .../schema/Core.EscapeInvalidChildren.txt | 12 + .../ConfigSchema/schema/Core.EscapeInvalidTags.txt | 7 + .../schema/Core.EscapeNonASCIICharacters.txt | 13 + .../ConfigSchema/schema/Core.HiddenElements.txt | 19 + .../ConfigSchema/schema/Core.Language.txt | 10 + .../schema/Core.LegacyEntityDecoder.txt | 36 + .../ConfigSchema/schema/Core.LexerImpl.txt | 34 + .../schema/Core.MaintainLineNumbers.txt | 16 + .../ConfigSchema/schema/Core.NormalizeNewlines.txt | 11 + .../ConfigSchema/schema/Core.RemoveInvalidImg.txt | 12 + .../schema/Core.RemoveProcessingInstructions.txt | 11 + .../schema/Core.RemoveScriptContents.txt | 12 + .../ConfigSchema/schema/Filter.Custom.txt | 11 + .../schema/Filter.ExtractStyleBlocks.Escaping.txt | 14 + .../schema/Filter.ExtractStyleBlocks.Scope.txt | 29 + .../schema/Filter.ExtractStyleBlocks.TidyImpl.txt | 16 + .../schema/Filter.ExtractStyleBlocks.txt | 74 + .../ConfigSchema/schema/Filter.YouTube.txt | 16 + .../ConfigSchema/schema/HTML.Allowed.txt | 25 + .../ConfigSchema/schema/HTML.AllowedAttributes.txt | 19 + .../ConfigSchema/schema/HTML.AllowedComments.txt | 10 + .../schema/HTML.AllowedCommentsRegexp.txt | 15 + .../ConfigSchema/schema/HTML.AllowedElements.txt | 23 + .../ConfigSchema/schema/HTML.AllowedModules.txt | 20 + .../schema/HTML.Attr.Name.UseCDATA.txt | 11 + .../ConfigSchema/schema/HTML.BlockWrapper.txt | 18 + .../ConfigSchema/schema/HTML.CoreModules.txt | 23 + .../ConfigSchema/schema/HTML.CustomDoctype.txt | 9 + .../ConfigSchema/schema/HTML.DefinitionID.txt | 33 + .../ConfigSchema/schema/HTML.DefinitionRev.txt | 16 + .../ConfigSchema/schema/HTML.Doctype.txt | 11 + .../schema/HTML.FlashAllowFullScreen.txt | 11 + .../schema/HTML.ForbiddenAttributes.txt | 21 + .../ConfigSchema/schema/HTML.ForbiddenElements.txt | 20 + .../ConfigSchema/schema/HTML.Forms.txt | 11 + .../ConfigSchema/schema/HTML.MaxImgLength.txt | 14 + .../ConfigSchema/schema/HTML.Nofollow.txt | 7 + .../ConfigSchema/schema/HTML.Parent.txt | 12 + .../ConfigSchema/schema/HTML.Proprietary.txt | 12 + .../ConfigSchema/schema/HTML.SafeEmbed.txt | 13 + .../ConfigSchema/schema/HTML.SafeIframe.txt | 13 + .../ConfigSchema/schema/HTML.SafeObject.txt | 13 + .../ConfigSchema/schema/HTML.SafeScripting.txt | 10 + .../ConfigSchema/schema/HTML.Strict.txt | 9 + .../ConfigSchema/schema/HTML.TargetBlank.txt | 8 + .../ConfigSchema/schema/HTML.TargetNoopener.txt | 10 + .../ConfigSchema/schema/HTML.TargetNoreferrer.txt | 9 + .../ConfigSchema/schema/HTML.TidyAdd.txt | 8 + .../ConfigSchema/schema/HTML.TidyLevel.txt | 24 + .../ConfigSchema/schema/HTML.TidyRemove.txt | 8 + .../ConfigSchema/schema/HTML.Trusted.txt | 9 + .../ConfigSchema/schema/HTML.XHTML.txt | 11 + .../schema/Output.CommentScriptContents.txt | 10 + .../ConfigSchema/schema/Output.FixInnerHTML.txt | 15 + .../ConfigSchema/schema/Output.FlashCompat.txt | 11 + .../ConfigSchema/schema/Output.Newline.txt | 13 + .../ConfigSchema/schema/Output.SortAttr.txt | 14 + .../ConfigSchema/schema/Output.TidyFormat.txt | 25 + .../ConfigSchema/schema/Test.ForceNoIconv.txt | 7 + .../ConfigSchema/schema/URI.AllowedSchemes.txt | 18 + .../HTMLPurifier/ConfigSchema/schema/URI.Base.txt | 17 + .../ConfigSchema/schema/URI.DefaultScheme.txt | 15 + .../ConfigSchema/schema/URI.DefinitionID.txt | 11 + .../ConfigSchema/schema/URI.DefinitionRev.txt | 11 + .../ConfigSchema/schema/URI.Disable.txt | 14 + .../ConfigSchema/schema/URI.DisableExternal.txt | 11 + .../schema/URI.DisableExternalResources.txt | 13 + .../ConfigSchema/schema/URI.DisableResources.txt | 15 + .../HTMLPurifier/ConfigSchema/schema/URI.Host.txt | 19 + .../ConfigSchema/schema/URI.HostBlacklist.txt | 9 + .../ConfigSchema/schema/URI.MakeAbsolute.txt | 13 + .../HTMLPurifier/ConfigSchema/schema/URI.Munge.txt | 83 + .../ConfigSchema/schema/URI.MungeResources.txt | 17 + .../ConfigSchema/schema/URI.MungeSecretKey.txt | 30 + .../schema/URI.OverrideAllowedSchemes.txt | 9 + .../ConfigSchema/schema/URI.SafeIframeRegexp.txt | 22 + .../HTMLPurifier/ConfigSchema/schema/info.ini | 3 + library/vendor/HTMLPurifier/ContentSets.php | 170 + library/vendor/HTMLPurifier/Context.php | 95 + library/vendor/HTMLPurifier/Definition.php | 55 + library/vendor/HTMLPurifier/DefinitionCache.php | 129 + .../HTMLPurifier/DefinitionCache/Decorator.php | 112 + .../DefinitionCache/Decorator/Cleanup.php | 78 + .../DefinitionCache/Decorator/Memory.php | 85 + .../DefinitionCache/Decorator/Template.php.in | 82 + .../vendor/HTMLPurifier/DefinitionCache/Null.php | 76 + .../HTMLPurifier/DefinitionCache/Serializer.php | 311 + .../HTMLPurifier/DefinitionCache/Serializer/README | 3 + .../vendor/HTMLPurifier/DefinitionCacheFactory.php | 106 + library/vendor/HTMLPurifier/Doctype.php | 73 + library/vendor/HTMLPurifier/DoctypeRegistry.php | 142 + library/vendor/HTMLPurifier/ElementDef.php | 216 + library/vendor/HTMLPurifier/Encoder.php | 617 ++ library/vendor/HTMLPurifier/EntityLookup.php | 48 + .../vendor/HTMLPurifier/EntityLookup/entities.ser | 1 + library/vendor/HTMLPurifier/EntityParser.php | 285 + library/vendor/HTMLPurifier/ErrorCollector.php | 244 + library/vendor/HTMLPurifier/ErrorStruct.php | 74 + library/vendor/HTMLPurifier/Exception.php | 12 + library/vendor/HTMLPurifier/Filter.php | 56 + .../HTMLPurifier/Filter/ExtractStyleBlocks.php | 341 + library/vendor/HTMLPurifier/Filter/YouTube.php | 65 + library/vendor/HTMLPurifier/Generator.php | 286 + library/vendor/HTMLPurifier/HTMLDefinition.php | 493 ++ library/vendor/HTMLPurifier/HTMLModule.php | 285 + library/vendor/HTMLPurifier/HTMLModule/Bdo.php | 44 + .../HTMLPurifier/HTMLModule/CommonAttributes.php | 32 + library/vendor/HTMLPurifier/HTMLModule/Edit.php | 55 + library/vendor/HTMLPurifier/HTMLModule/Forms.php | 194 + .../vendor/HTMLPurifier/HTMLModule/Hypertext.php | 40 + library/vendor/HTMLPurifier/HTMLModule/Iframe.php | 51 + library/vendor/HTMLPurifier/HTMLModule/Image.php | 49 + library/vendor/HTMLPurifier/HTMLModule/Legacy.php | 186 + library/vendor/HTMLPurifier/HTMLModule/List.php | 51 + library/vendor/HTMLPurifier/HTMLModule/Name.php | 26 + .../vendor/HTMLPurifier/HTMLModule/Nofollow.php | 25 + .../HTMLModule/NonXMLCommonAttributes.php | 20 + library/vendor/HTMLPurifier/HTMLModule/Object.php | 62 + .../HTMLPurifier/HTMLModule/Presentation.php | 42 + .../vendor/HTMLPurifier/HTMLModule/Proprietary.php | 40 + library/vendor/HTMLPurifier/HTMLModule/Ruby.php | 36 + .../vendor/HTMLPurifier/HTMLModule/SafeEmbed.php | 40 + .../vendor/HTMLPurifier/HTMLModule/SafeObject.php | 62 + .../HTMLPurifier/HTMLModule/SafeScripting.php | 40 + .../vendor/HTMLPurifier/HTMLModule/Scripting.php | 73 + .../HTMLPurifier/HTMLModule/StyleAttribute.php | 33 + library/vendor/HTMLPurifier/HTMLModule/Tables.php | 75 + library/vendor/HTMLPurifier/HTMLModule/Target.php | 28 + .../vendor/HTMLPurifier/HTMLModule/TargetBlank.php | 24 + .../HTMLPurifier/HTMLModule/TargetNoopener.php | 21 + .../HTMLPurifier/HTMLModule/TargetNoreferrer.php | 21 + library/vendor/HTMLPurifier/HTMLModule/Text.php | 87 + library/vendor/HTMLPurifier/HTMLModule/Tidy.php | 227 + .../vendor/HTMLPurifier/HTMLModule/Tidy/Name.php | 33 + .../HTMLPurifier/HTMLModule/Tidy/Proprietary.php | 34 + .../vendor/HTMLPurifier/HTMLModule/Tidy/Strict.php | 43 + .../HTMLPurifier/HTMLModule/Tidy/Transitional.php | 16 + .../vendor/HTMLPurifier/HTMLModule/Tidy/XHTML.php | 26 + .../HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php | 182 + .../HTMLModule/XMLCommonAttributes.php | 20 + library/vendor/HTMLPurifier/HTMLModuleManager.php | 467 ++ library/vendor/HTMLPurifier/IDAccumulator.php | 57 + library/vendor/HTMLPurifier/Injector.php | 283 + .../vendor/HTMLPurifier/Injector/AutoParagraph.php | 356 ++ .../HTMLPurifier/Injector/DisplayLinkURI.php | 40 + library/vendor/HTMLPurifier/Injector/Linkify.php | 67 + .../HTMLPurifier/Injector/PurifierLinkify.php | 71 + .../vendor/HTMLPurifier/Injector/RemoveEmpty.php | 112 + .../Injector/RemoveSpansWithoutAttributes.php | 95 + .../vendor/HTMLPurifier/Injector/SafeObject.php | 124 + library/vendor/HTMLPurifier/LICENSE | 504 ++ library/vendor/HTMLPurifier/Language.php | 204 + .../vendor/HTMLPurifier/Language/messages/en.php | 55 + library/vendor/HTMLPurifier/LanguageFactory.php | 209 + library/vendor/HTMLPurifier/Length.php | 162 + library/vendor/HTMLPurifier/Lexer.php | 387 ++ library/vendor/HTMLPurifier/Lexer/DOMLex.php | 338 + library/vendor/HTMLPurifier/Lexer/DirectLex.php | 539 ++ library/vendor/HTMLPurifier/Lexer/PH5P.php | 4788 ++++++++++++++ library/vendor/HTMLPurifier/Node.php | 49 + library/vendor/HTMLPurifier/Node/Comment.php | 36 + library/vendor/HTMLPurifier/Node/Element.php | 59 + library/vendor/HTMLPurifier/Node/Text.php | 54 + library/vendor/HTMLPurifier/PercentEncoder.php | 111 + library/vendor/HTMLPurifier/Printer.php | 218 + .../vendor/HTMLPurifier/Printer/CSSDefinition.php | 44 + library/vendor/HTMLPurifier/Printer/ConfigForm.css | 10 + library/vendor/HTMLPurifier/Printer/ConfigForm.js | 5 + library/vendor/HTMLPurifier/Printer/ConfigForm.php | 451 ++ .../vendor/HTMLPurifier/Printer/HTMLDefinition.php | 324 + library/vendor/HTMLPurifier/PropertyList.php | 122 + .../vendor/HTMLPurifier/PropertyListIterator.php | 43 + library/vendor/HTMLPurifier/Queue.php | 56 + library/vendor/HTMLPurifier/SOURCE | 10 + library/vendor/HTMLPurifier/Strategy.php | 26 + library/vendor/HTMLPurifier/Strategy/Composite.php | 30 + library/vendor/HTMLPurifier/Strategy/Core.php | 17 + .../vendor/HTMLPurifier/Strategy/FixNesting.php | 181 + .../HTMLPurifier/Strategy/MakeWellFormed.php | 659 ++ .../Strategy/RemoveForeignElements.php | 207 + .../HTMLPurifier/Strategy/ValidateAttributes.php | 45 + library/vendor/HTMLPurifier/StringHash.php | 48 + library/vendor/HTMLPurifier/StringHashParser.php | 136 + library/vendor/HTMLPurifier/TagTransform.php | 37 + library/vendor/HTMLPurifier/TagTransform/Font.php | 114 + .../vendor/HTMLPurifier/TagTransform/Simple.php | 44 + library/vendor/HTMLPurifier/Token.php | 100 + library/vendor/HTMLPurifier/Token/Comment.php | 38 + library/vendor/HTMLPurifier/Token/Empty.php | 15 + library/vendor/HTMLPurifier/Token/End.php | 24 + library/vendor/HTMLPurifier/Token/Start.php | 10 + library/vendor/HTMLPurifier/Token/Tag.php | 68 + library/vendor/HTMLPurifier/Token/Text.php | 53 + library/vendor/HTMLPurifier/TokenFactory.php | 118 + library/vendor/HTMLPurifier/URI.php | 316 + library/vendor/HTMLPurifier/URIDefinition.php | 112 + library/vendor/HTMLPurifier/URIFilter.php | 74 + .../HTMLPurifier/URIFilter/DisableExternal.php | 54 + .../URIFilter/DisableExternalResources.php | 25 + .../HTMLPurifier/URIFilter/DisableResources.php | 22 + .../HTMLPurifier/URIFilter/HostBlacklist.php | 46 + .../vendor/HTMLPurifier/URIFilter/MakeAbsolute.php | 158 + library/vendor/HTMLPurifier/URIFilter/Munge.php | 115 + .../vendor/HTMLPurifier/URIFilter/SafeIframe.php | 68 + library/vendor/HTMLPurifier/URIParser.php | 71 + library/vendor/HTMLPurifier/URIScheme.php | 102 + library/vendor/HTMLPurifier/URIScheme/data.php | 136 + library/vendor/HTMLPurifier/URIScheme/file.php | 44 + library/vendor/HTMLPurifier/URIScheme/ftp.php | 58 + library/vendor/HTMLPurifier/URIScheme/http.php | 36 + library/vendor/HTMLPurifier/URIScheme/https.php | 18 + library/vendor/HTMLPurifier/URIScheme/mailto.php | 40 + library/vendor/HTMLPurifier/URIScheme/news.php | 35 + library/vendor/HTMLPurifier/URIScheme/nntp.php | 32 + library/vendor/HTMLPurifier/URIScheme/tel.php | 46 + library/vendor/HTMLPurifier/URISchemeRegistry.php | 81 + library/vendor/HTMLPurifier/UnitConverter.php | 307 + library/vendor/HTMLPurifier/VERSION | 1 + library/vendor/HTMLPurifier/VarParser.php | 198 + library/vendor/HTMLPurifier/VarParser/Flexible.php | 130 + library/vendor/HTMLPurifier/VarParser/Native.php | 38 + library/vendor/HTMLPurifier/VarParserException.php | 11 + library/vendor/HTMLPurifier/Zipper.php | 157 + library/vendor/JShrink/LICENSE | 29 + library/vendor/JShrink/Minifier.php | 613 ++ library/vendor/JShrink/SOURCE | 7 + library/vendor/Parsedown/LICENSE | 20 + library/vendor/Parsedown/Parsedown.php | 1693 +++++ library/vendor/Parsedown/SOURCE | 7 + library/vendor/Zend/Cache.php | 245 + library/vendor/Zend/Cache/Backend.php | 285 + library/vendor/Zend/Cache/Backend/Apc.php | 353 ++ library/vendor/Zend/Cache/Backend/BlackHole.php | 248 + .../Zend/Cache/Backend/ExtendedInterface.php | 125 + library/vendor/Zend/Cache/Backend/File.php | 1035 +++ library/vendor/Zend/Cache/Backend/Interface.php | 99 + library/vendor/Zend/Cache/Backend/Libmemcached.php | 482 ++ library/vendor/Zend/Cache/Backend/Memcached.php | 507 ++ library/vendor/Zend/Cache/Backend/Sqlite.php | 676 ++ library/vendor/Zend/Cache/Backend/Static.php | 577 ++ library/vendor/Zend/Cache/Backend/Test.php | 414 ++ library/vendor/Zend/Cache/Backend/TwoLevels.php | 546 ++ library/vendor/Zend/Cache/Backend/WinCache.php | 347 + library/vendor/Zend/Cache/Backend/Xcache.php | 219 + library/vendor/Zend/Cache/Backend/ZendPlatform.php | 315 + library/vendor/Zend/Cache/Backend/ZendServer.php | 205 + .../vendor/Zend/Cache/Backend/ZendServer/Disk.php | 99 + .../vendor/Zend/Cache/Backend/ZendServer/ShMem.php | 99 + library/vendor/Zend/Cache/Core.php | 762 +++ library/vendor/Zend/Cache/Exception.php | 31 + library/vendor/Zend/Cache/Frontend/Capture.php | 87 + library/vendor/Zend/Cache/Frontend/Class.php | 274 + library/vendor/Zend/Cache/Frontend/File.php | 221 + library/vendor/Zend/Cache/Frontend/Function.php | 178 + library/vendor/Zend/Cache/Frontend/Output.php | 104 + library/vendor/Zend/Cache/Frontend/Page.php | 403 ++ library/vendor/Zend/Cache/Manager.php | 304 + library/vendor/Zend/Config.php | 481 ++ library/vendor/Zend/Config/Exception.php | 32 + library/vendor/Zend/Config/Ini.php | 301 + library/vendor/Zend/Config/Writer.php | 101 + library/vendor/Zend/Config/Writer/Array.php | 54 + library/vendor/Zend/Config/Writer/FileAbstract.php | 130 + library/vendor/Zend/Config/Writer/Ini.php | 191 + library/vendor/Zend/Controller/Action.php | 789 +++ .../vendor/Zend/Controller/Action/Exception.php | 37 + .../Zend/Controller/Action/Helper/Abstract.php | 155 + .../Zend/Controller/Action/Helper/ActionStack.php | 133 + .../Zend/Controller/Action/Helper/AjaxContext.php | 79 + .../Action/Helper/AutoComplete/Abstract.php | 146 + .../vendor/Zend/Controller/Action/Helper/Cache.php | 286 + .../Controller/Action/Helper/ContextSwitch.php | 1377 ++++ .../vendor/Zend/Controller/Action/Helper/Json.php | 130 + .../Zend/Controller/Action/Helper/Redirector.php | 531 ++ .../vendor/Zend/Controller/Action/Helper/Url.php | 116 + .../Zend/Controller/Action/Helper/ViewRenderer.php | 996 +++ .../vendor/Zend/Controller/Action/HelperBroker.php | 373 ++ .../Action/HelperBroker/PriorityStack.php | 272 + .../vendor/Zend/Controller/Action/Interface.php | 69 + .../vendor/Zend/Controller/Dispatcher/Abstract.php | 435 ++ .../Zend/Controller/Dispatcher/Exception.php | 36 + .../Zend/Controller/Dispatcher/Interface.php | 204 + .../vendor/Zend/Controller/Dispatcher/Standard.php | 504 ++ library/vendor/Zend/Controller/Exception.php | 34 + library/vendor/Zend/Controller/Front.php | 977 +++ library/vendor/Zend/Controller/Plugin/Abstract.php | 151 + .../vendor/Zend/Controller/Plugin/ActionStack.php | 277 + library/vendor/Zend/Controller/Plugin/Broker.php | 361 ++ .../vendor/Zend/Controller/Plugin/ErrorHandler.php | 299 + .../vendor/Zend/Controller/Plugin/PutHandler.php | 58 + .../vendor/Zend/Controller/Request/Abstract.php | 356 ++ .../vendor/Zend/Controller/Request/Apache404.php | 80 + .../vendor/Zend/Controller/Request/Exception.php | 36 + library/vendor/Zend/Controller/Request/Http.php | 1087 ++++ .../Zend/Controller/Request/HttpTestCase.php | 275 + library/vendor/Zend/Controller/Request/Simple.php | 54 + .../vendor/Zend/Controller/Response/Abstract.php | 790 +++ library/vendor/Zend/Controller/Response/Cli.php | 67 + .../vendor/Zend/Controller/Response/Exception.php | 35 + library/vendor/Zend/Controller/Response/Http.php | 37 + .../Zend/Controller/Response/HttpTestCase.php | 129 + library/vendor/Zend/Controller/Router/Abstract.php | 176 + .../vendor/Zend/Controller/Router/Exception.php | 34 + .../vendor/Zend/Controller/Router/Interface.php | 123 + library/vendor/Zend/Controller/Router/Rewrite.php | 542 ++ library/vendor/Zend/Controller/Router/Route.php | 603 ++ .../Zend/Controller/Router/Route/Abstract.php | 119 + .../vendor/Zend/Controller/Router/Route/Chain.php | 228 + .../Zend/Controller/Router/Route/Hostname.php | 377 ++ .../Zend/Controller/Router/Route/Interface.php | 38 + .../vendor/Zend/Controller/Router/Route/Module.php | 322 + .../vendor/Zend/Controller/Router/Route/Regex.php | 316 + .../vendor/Zend/Controller/Router/Route/Static.php | 148 + library/vendor/Zend/Crypt.php | 167 + library/vendor/Zend/Crypt/DiffieHellman.php | 378 ++ .../vendor/Zend/Crypt/DiffieHellman/Exception.php | 35 + library/vendor/Zend/Crypt/Exception.php | 34 + library/vendor/Zend/Crypt/Hmac.php | 178 + library/vendor/Zend/Crypt/Hmac/Exception.php | 35 + library/vendor/Zend/Crypt/Math.php | 186 + library/vendor/Zend/Crypt/Math/BigInteger.php | 113 + .../vendor/Zend/Crypt/Math/BigInteger/Bcmath.php | 226 + .../Zend/Crypt/Math/BigInteger/Exception.php | 35 + library/vendor/Zend/Crypt/Math/BigInteger/Gmp.php | 219 + .../Zend/Crypt/Math/BigInteger/Interface.php | 51 + library/vendor/Zend/Crypt/Math/Exception.php | 35 + library/vendor/Zend/Crypt/Rsa.php | 334 + library/vendor/Zend/Crypt/Rsa/Exception.php | 35 + library/vendor/Zend/Crypt/Rsa/Key.php | 94 + library/vendor/Zend/Crypt/Rsa/Key/Private.php | 72 + library/vendor/Zend/Crypt/Rsa/Key/Public.php | 72 + library/vendor/Zend/Date.php | 4872 ++++++++++++++ library/vendor/Zend/Date/Cities.php | 321 + library/vendor/Zend/Date/DateObject.php | 1094 ++++ library/vendor/Zend/Date/Exception.php | 48 + library/vendor/Zend/Db.php | 282 + library/vendor/Zend/Db/Adapter/Abstract.php | 1267 ++++ library/vendor/Zend/Db/Adapter/Db2.php | 827 +++ library/vendor/Zend/Db/Adapter/Db2/Exception.php | 44 + library/vendor/Zend/Db/Adapter/Exception.php | 56 + library/vendor/Zend/Db/Adapter/Mysqli.php | 543 ++ .../vendor/Zend/Db/Adapter/Mysqli/Exception.php | 39 + library/vendor/Zend/Db/Adapter/Oracle.php | 631 ++ .../vendor/Zend/Db/Adapter/Oracle/Exception.php | 59 + library/vendor/Zend/Db/Adapter/Pdo/Abstract.php | 397 ++ library/vendor/Zend/Db/Adapter/Pdo/Ibm.php | 354 ++ library/vendor/Zend/Db/Adapter/Pdo/Ibm/Db2.php | 224 + library/vendor/Zend/Db/Adapter/Pdo/Ibm/Ids.php | 297 + library/vendor/Zend/Db/Adapter/Pdo/Mssql.php | 435 ++ library/vendor/Zend/Db/Adapter/Pdo/Mysql.php | 269 + library/vendor/Zend/Db/Adapter/Pdo/Oci.php | 375 ++ library/vendor/Zend/Db/Adapter/Pdo/Pgsql.php | 333 + library/vendor/Zend/Db/Adapter/Pdo/Sqlite.php | 305 + library/vendor/Zend/Db/Adapter/Sqlsrv.php | 662 ++ .../vendor/Zend/Db/Adapter/Sqlsrv/Exception.php | 62 + library/vendor/Zend/Db/Exception.php | 34 + library/vendor/Zend/Db/Expr.php | 77 + library/vendor/Zend/Db/Profiler.php | 469 ++ library/vendor/Zend/Db/Profiler/Exception.php | 39 + library/vendor/Zend/Db/Profiler/Query.php | 213 + library/vendor/Zend/Db/Select.php | 1368 ++++ library/vendor/Zend/Db/Select/Exception.php | 38 + library/vendor/Zend/Db/Statement.php | 488 ++ library/vendor/Zend/Db/Statement/Db2.php | 354 ++ library/vendor/Zend/Db/Statement/Db2/Exception.php | 57 + library/vendor/Zend/Db/Statement/Exception.php | 55 + library/vendor/Zend/Db/Statement/Interface.php | 203 + library/vendor/Zend/Db/Statement/Mysqli.php | 356 ++ .../vendor/Zend/Db/Statement/Mysqli/Exception.php | 37 + library/vendor/Zend/Db/Statement/Oracle.php | 561 ++ .../vendor/Zend/Db/Statement/Oracle/Exception.php | 58 + library/vendor/Zend/Db/Statement/Pdo.php | 426 ++ library/vendor/Zend/Db/Statement/Pdo/Ibm.php | 92 + library/vendor/Zend/Db/Statement/Pdo/Oci.php | 90 + library/vendor/Zend/Db/Statement/Sqlsrv.php | 430 ++ .../vendor/Zend/Db/Statement/Sqlsrv/Exception.php | 60 + library/vendor/Zend/Db/Table.php | 77 + library/vendor/Zend/Db/Table/Abstract.php | 1599 +++++ library/vendor/Zend/Db/Table/Definition.php | 131 + library/vendor/Zend/Db/Table/Exception.php | 37 + library/vendor/Zend/Db/Table/Row.php | 41 + library/vendor/Zend/Db/Table/Row/Abstract.php | 1160 ++++ library/vendor/Zend/Db/Table/Row/Exception.php | 37 + library/vendor/Zend/Db/Table/Rowset.php | 42 + library/vendor/Zend/Db/Table/Rowset/Abstract.php | 443 ++ library/vendor/Zend/Db/Table/Rowset/Exception.php | 36 + library/vendor/Zend/Db/Table/Select.php | 221 + library/vendor/Zend/Db/Table/Select/Exception.php | 38 + library/vendor/Zend/Exception.php | 96 + library/vendor/Zend/File/ClassFileLocator.php | 177 + library/vendor/Zend/File/PhpClassFile.php | 56 + library/vendor/Zend/File/Transfer.php | 122 + .../vendor/Zend/File/Transfer/Adapter/Abstract.php | 1548 +++++ library/vendor/Zend/File/Transfer/Adapter/Http.php | 480 ++ library/vendor/Zend/File/Transfer/Exception.php | 54 + library/vendor/Zend/Filter.php | 236 + library/vendor/Zend/Filter/Alnum.php | 144 + library/vendor/Zend/Filter/Alpha.php | 144 + library/vendor/Zend/Filter/BaseName.php | 49 + library/vendor/Zend/Filter/Boolean.php | 369 ++ library/vendor/Zend/Filter/Callback.php | 149 + library/vendor/Zend/Filter/Compress.php | 192 + library/vendor/Zend/Filter/Compress/Bz2.php | 181 + .../Zend/Filter/Compress/CompressAbstract.php | 88 + .../Zend/Filter/Compress/CompressInterface.php | 54 + library/vendor/Zend/Filter/Compress/Gz.php | 220 + library/vendor/Zend/Filter/Compress/Lzf.php | 87 + library/vendor/Zend/Filter/Compress/Rar.php | 243 + library/vendor/Zend/Filter/Compress/Tar.php | 234 + library/vendor/Zend/Filter/Compress/Zip.php | 343 + library/vendor/Zend/Filter/Decompress.php | 48 + library/vendor/Zend/Filter/Decrypt.php | 48 + library/vendor/Zend/Filter/Digits.php | 81 + library/vendor/Zend/Filter/Dir.php | 49 + library/vendor/Zend/Filter/Encrypt.php | 134 + library/vendor/Zend/Filter/Encrypt/Interface.php | 47 + library/vendor/Zend/Filter/Encrypt/Mcrypt.php | 352 ++ library/vendor/Zend/Filter/Encrypt/Openssl.php | 480 ++ library/vendor/Zend/Filter/Exception.php | 36 + library/vendor/Zend/Filter/File/Decrypt.php | 101 + library/vendor/Zend/Filter/File/Encrypt.php | 101 + library/vendor/Zend/Filter/File/LowerCase.php | 79 + library/vendor/Zend/Filter/File/Rename.php | 304 + library/vendor/Zend/Filter/File/UpperCase.php | 79 + library/vendor/Zend/Filter/HtmlEntities.php | 213 + library/vendor/Zend/Filter/Inflector.php | 523 ++ library/vendor/Zend/Filter/Input.php | 1196 ++++ library/vendor/Zend/Filter/Int.php | 49 + library/vendor/Zend/Filter/Interface.php | 40 + .../vendor/Zend/Filter/LocalizedToNormalized.php | 110 + .../vendor/Zend/Filter/NormalizedToLocalized.php | 108 + library/vendor/Zend/Filter/Null.php | 181 + library/vendor/Zend/Filter/PregReplace.php | 172 + library/vendor/Zend/Filter/RealPath.php | 133 + library/vendor/Zend/Filter/StringToLower.php | 118 + library/vendor/Zend/Filter/StringToUpper.php | 118 + library/vendor/Zend/Filter/StringTrim.php | 123 + library/vendor/Zend/Filter/StripNewlines.php | 47 + library/vendor/Zend/Filter/StripTags.php | 351 ++ .../vendor/Zend/Filter/Word/CamelCaseToDash.php | 43 + .../Zend/Filter/Word/CamelCaseToSeparator.php | 48 + .../Zend/Filter/Word/CamelCaseToUnderscore.php | 43 + .../vendor/Zend/Filter/Word/DashToCamelCase.php | 43 + .../vendor/Zend/Filter/Word/DashToSeparator.php | 41 + .../vendor/Zend/Filter/Word/DashToUnderscore.php | 44 + .../vendor/Zend/Filter/Word/Separator/Abstract.php | 74 + .../Zend/Filter/Word/SeparatorToCamelCase.php | 63 + .../vendor/Zend/Filter/Word/SeparatorToDash.php | 45 + .../Zend/Filter/Word/SeparatorToSeparator.php | 127 + .../Zend/Filter/Word/UnderscoreToCamelCase.php | 43 + .../vendor/Zend/Filter/Word/UnderscoreToDash.php | 44 + .../Zend/Filter/Word/UnderscoreToSeparator.php | 44 + library/vendor/Zend/Form.php | 3472 ++++++++++ library/vendor/Zend/Form/Decorator/Abstract.php | 251 + library/vendor/Zend/Form/Decorator/Callback.php | 126 + library/vendor/Zend/Form/Decorator/Captcha.php | 71 + library/vendor/Zend/Form/Decorator/Description.php | 197 + library/vendor/Zend/Form/Decorator/DtDdWrapper.php | 69 + library/vendor/Zend/Form/Decorator/Errors.php | 76 + library/vendor/Zend/Form/Decorator/Exception.php | 36 + library/vendor/Zend/Form/Decorator/Fieldset.php | 156 + library/vendor/Zend/Form/Decorator/File.php | 140 + library/vendor/Zend/Form/Decorator/Form.php | 133 + .../vendor/Zend/Form/Decorator/FormElements.php | 140 + library/vendor/Zend/Form/Decorator/FormErrors.php | 514 ++ library/vendor/Zend/Form/Decorator/HtmlTag.php | 253 + library/vendor/Zend/Form/Decorator/Image.php | 152 + library/vendor/Zend/Form/Decorator/Interface.php | 123 + library/vendor/Zend/Form/Decorator/Label.php | 461 ++ .../Zend/Form/Decorator/Marker/File/Interface.php | 33 + .../vendor/Zend/Form/Decorator/PrepareElements.php | 89 + library/vendor/Zend/Form/Decorator/Tooltip.php | 57 + library/vendor/Zend/Form/Decorator/ViewHelper.php | 266 + library/vendor/Zend/Form/Decorator/ViewScript.php | 190 + library/vendor/Zend/Form/DisplayGroup.php | 1172 ++++ library/vendor/Zend/Form/Element.php | 2280 +++++++ library/vendor/Zend/Form/Element/Button.php | 55 + library/vendor/Zend/Form/Element/Checkbox.php | 202 + library/vendor/Zend/Form/Element/Exception.php | 36 + library/vendor/Zend/Form/Element/File.php | 910 +++ library/vendor/Zend/Form/Element/Hidden.php | 41 + library/vendor/Zend/Form/Element/Image.php | 131 + library/vendor/Zend/Form/Element/Multi.php | 316 + library/vendor/Zend/Form/Element/MultiCheckbox.php | 72 + library/vendor/Zend/Form/Element/Multiselect.php | 53 + library/vendor/Zend/Form/Element/Note.php | 63 + library/vendor/Zend/Form/Element/Password.php | 87 + library/vendor/Zend/Form/Element/Radio.php | 65 + library/vendor/Zend/Form/Element/Reset.php | 41 + library/vendor/Zend/Form/Element/Select.php | 47 + library/vendor/Zend/Form/Element/Submit.php | 126 + library/vendor/Zend/Form/Element/Text.php | 41 + library/vendor/Zend/Form/Element/Textarea.php | 41 + library/vendor/Zend/Form/Element/Xhtml.php | 36 + library/vendor/Zend/Form/Exception.php | 34 + library/vendor/Zend/Form/SubForm.php | 60 + library/vendor/Zend/Json.php | 433 ++ library/vendor/Zend/Json/Decoder.php | 570 ++ library/vendor/Zend/Json/Encoder.php | 576 ++ library/vendor/Zend/Json/Exception.php | 36 + library/vendor/Zend/Json/Expr.php | 80 + library/vendor/Zend/LICENSE.txt | 27 + library/vendor/Zend/Layout.php | 788 +++ .../Layout/Controller/Action/Helper/Layout.php | 181 + .../Zend/Layout/Controller/Plugin/Layout.php | 155 + library/vendor/Zend/Layout/Exception.php | 34 + library/vendor/Zend/Loader.php | 338 + library/vendor/Zend/Loader/Autoloader.php | 589 ++ .../vendor/Zend/Loader/Autoloader/Interface.php | 43 + library/vendor/Zend/Loader/Exception.php | 34 + .../Loader/Exception/InvalidArgumentException.php | 33 + library/vendor/Zend/Loader/PluginLoader.php | 497 ++ .../vendor/Zend/Loader/PluginLoader/Exception.php | 38 + .../vendor/Zend/Loader/PluginLoader/Interface.php | 75 + library/vendor/Zend/Locale.php | 1996 ++++++ library/vendor/Zend/Locale/Data.php | 1609 +++++ library/vendor/Zend/Locale/Data/Translation.php | 285 + library/vendor/Zend/Locale/Exception.php | 36 + library/vendor/Zend/Locale/Format.php | 1321 ++++ library/vendor/Zend/Locale/Math.php | 354 ++ library/vendor/Zend/Locale/Math/Exception.php | 52 + library/vendor/Zend/Locale/Math/PhpMath.php | 239 + library/vendor/Zend/Log.php | 645 ++ library/vendor/Zend/Log/Exception.php | 32 + library/vendor/Zend/Log/FactoryInterface.php | 38 + library/vendor/Zend/Log/Filter/Abstract.php | 57 + library/vendor/Zend/Log/Filter/Interface.php | 40 + library/vendor/Zend/Log/Filter/Message.php | 83 + library/vendor/Zend/Log/Filter/Priority.php | 99 + library/vendor/Zend/Log/Filter/Suppress.php | 76 + library/vendor/Zend/Log/Formatter/Abstract.php | 38 + library/vendor/Zend/Log/Formatter/Interface.php | 41 + library/vendor/Zend/Log/Formatter/Simple.php | 106 + library/vendor/Zend/Log/Formatter/Xml.php | 164 + library/vendor/Zend/Log/Writer/Abstract.php | 138 + library/vendor/Zend/Log/Writer/Db.php | 144 + library/vendor/Zend/Log/Writer/Mail.php | 426 ++ library/vendor/Zend/Log/Writer/Mock.php | 80 + library/vendor/Zend/Log/Writer/Null.php | 55 + library/vendor/Zend/Log/Writer/Stream.php | 132 + library/vendor/Zend/Log/Writer/Syslog.php | 263 + library/vendor/Zend/Log/Writer/ZendMonitor.php | 130 + library/vendor/Zend/Mail.php | 1265 ++++ library/vendor/Zend/Mail/Exception.php | 36 + library/vendor/Zend/Mail/Header/HeaderName.php | 91 + library/vendor/Zend/Mail/Header/HeaderValue.php | 135 + library/vendor/Zend/Mail/Message.php | 110 + library/vendor/Zend/Mail/Message/File.php | 94 + library/vendor/Zend/Mail/Message/Interface.php | 55 + library/vendor/Zend/Mail/Part.php | 590 ++ library/vendor/Zend/Mail/Part/File.php | 191 + library/vendor/Zend/Mail/Part/Interface.php | 136 + library/vendor/Zend/Mail/Protocol/Abstract.php | 436 ++ library/vendor/Zend/Mail/Protocol/Exception.php | 38 + library/vendor/Zend/Mail/Protocol/Imap.php | 830 +++ library/vendor/Zend/Mail/Protocol/Pop3.php | 466 ++ library/vendor/Zend/Mail/Protocol/Smtp.php | 433 ++ .../Zend/Mail/Protocol/Smtp/Auth/Crammd5.php | 107 + .../vendor/Zend/Mail/Protocol/Smtp/Auth/Login.php | 97 + .../vendor/Zend/Mail/Protocol/Smtp/Auth/Plain.php | 95 + library/vendor/Zend/Mail/Storage.php | 40 + library/vendor/Zend/Mail/Storage/Abstract.php | 364 ++ library/vendor/Zend/Mail/Storage/Exception.php | 38 + library/vendor/Zend/Mail/Storage/Folder.php | 235 + .../vendor/Zend/Mail/Storage/Folder/Interface.php | 60 + .../vendor/Zend/Mail/Storage/Folder/Maildir.php | 255 + library/vendor/Zend/Mail/Storage/Folder/Mbox.php | 255 + library/vendor/Zend/Mail/Storage/Imap.php | 620 ++ library/vendor/Zend/Mail/Storage/Maildir.php | 462 ++ library/vendor/Zend/Mail/Storage/Mbox.php | 437 ++ library/vendor/Zend/Mail/Storage/Pop3.php | 321 + .../Zend/Mail/Storage/Writable/Interface.php | 108 + .../vendor/Zend/Mail/Storage/Writable/Maildir.php | 1014 +++ library/vendor/Zend/Mail/Transport/Abstract.php | 345 + library/vendor/Zend/Mail/Transport/Exception.php | 38 + library/vendor/Zend/Mail/Transport/File.php | 131 + library/vendor/Zend/Mail/Transport/Sendmail.php | 214 + library/vendor/Zend/Mail/Transport/Smtp.php | 238 + library/vendor/Zend/Mime.php | 670 ++ library/vendor/Zend/Mime/Decode.php | 275 + library/vendor/Zend/Mime/Exception.php | 35 + library/vendor/Zend/Mime/Message.php | 302 + library/vendor/Zend/Mime/Part.php | 329 + library/vendor/Zend/Paginator.php | 1164 ++++ library/vendor/Zend/Paginator/Adapter/Array.php | 80 + library/vendor/Zend/Paginator/Adapter/DbSelect.php | 285 + .../Zend/Paginator/Adapter/DbTableSelect.php | 47 + .../vendor/Zend/Paginator/Adapter/Interface.php | 40 + library/vendor/Zend/Paginator/Adapter/Iterator.php | 99 + library/vendor/Zend/Paginator/Adapter/Null.php | 79 + library/vendor/Zend/Paginator/AdapterAggregate.php | 40 + library/vendor/Zend/Paginator/Exception.php | 34 + .../vendor/Zend/Paginator/ScrollingStyle/All.php | 49 + .../Zend/Paginator/ScrollingStyle/Elastic.php | 62 + .../Zend/Paginator/ScrollingStyle/Interface.php | 38 + .../Zend/Paginator/ScrollingStyle/Jumping.php | 62 + .../Zend/Paginator/ScrollingStyle/Sliding.php | 77 + .../Zend/Paginator/SerializableLimitIterator.php | 159 + library/vendor/Zend/ProgressBar.php | 207 + library/vendor/Zend/ProgressBar/Adapter.php | 113 + .../vendor/Zend/ProgressBar/Adapter/Exception.php | 37 + library/vendor/Zend/ProgressBar/Adapter/JsPull.php | 115 + library/vendor/Zend/ProgressBar/Adapter/JsPush.php | 146 + library/vendor/Zend/ProgressBar/Exception.php | 37 + library/vendor/Zend/README.md | 8 + library/vendor/Zend/Registry.php | 192 + library/vendor/Zend/Server/Abstract.php | 235 + library/vendor/Zend/Server/Cache.php | 147 + library/vendor/Zend/Server/Definition.php | 263 + library/vendor/Zend/Server/Exception.php | 34 + library/vendor/Zend/Server/Interface.php | 118 + library/vendor/Zend/Server/Method/Callback.php | 204 + library/vendor/Zend/Server/Method/Definition.php | 288 + library/vendor/Zend/Server/Method/Parameter.php | 214 + library/vendor/Zend/Server/Method/Prototype.php | 207 + library/vendor/Zend/Server/Reflection.php | 105 + library/vendor/Zend/Server/Reflection/Class.php | 195 + .../vendor/Zend/Server/Reflection/Exception.php | 37 + library/vendor/Zend/Server/Reflection/Function.php | 38 + .../Zend/Server/Reflection/Function/Abstract.php | 506 ++ library/vendor/Zend/Server/Reflection/Method.php | 109 + library/vendor/Zend/Server/Reflection/Node.php | 201 + .../vendor/Zend/Server/Reflection/Parameter.php | 158 + .../vendor/Zend/Server/Reflection/Prototype.php | 99 + .../vendor/Zend/Server/Reflection/ReturnValue.php | 108 + library/vendor/Zend/Session.php | 895 +++ library/vendor/Zend/Session/Abstract.php | 182 + library/vendor/Zend/Session/Exception.php | 73 + library/vendor/Zend/Session/Namespace.php | 511 ++ .../vendor/Zend/Session/SaveHandler/DbTable.php | 579 ++ .../vendor/Zend/Session/SaveHandler/Exception.php | 36 + .../vendor/Zend/Session/SaveHandler/Interface.php | 81 + library/vendor/Zend/Session/Validator/Abstract.php | 70 + .../vendor/Zend/Session/Validator/Exception.php | 42 + .../Zend/Session/Validator/HttpUserAgent.php | 65 + .../vendor/Zend/Session/Validator/Interface.php | 52 + library/vendor/Zend/Soap/AutoDiscover.php | 597 ++ .../vendor/Zend/Soap/AutoDiscover/Exception.php | 33 + library/vendor/Zend/Soap/Client.php | 1223 ++++ library/vendor/Zend/Soap/Client/Common.php | 76 + library/vendor/Zend/Soap/Client/DotNet.php | 93 + library/vendor/Zend/Soap/Client/Exception.php | 34 + library/vendor/Zend/Soap/Client/Local.php | 97 + library/vendor/Zend/Soap/Server.php | 999 +++ library/vendor/Zend/Soap/Server/Exception.php | 36 + library/vendor/Zend/Soap/Server/Proxy.php | 75 + library/vendor/Zend/Soap/Wsdl.php | 661 ++ library/vendor/Zend/Soap/Wsdl/Exception.php | 36 + .../vendor/Zend/Soap/Wsdl/Strategy/Abstract.php | 65 + library/vendor/Zend/Soap/Wsdl/Strategy/AnyType.php | 58 + .../Zend/Soap/Wsdl/Strategy/ArrayOfTypeComplex.php | 142 + .../Soap/Wsdl/Strategy/ArrayOfTypeSequence.php | 154 + .../vendor/Zend/Soap/Wsdl/Strategy/Composite.php | 183 + .../Zend/Soap/Wsdl/Strategy/DefaultComplexType.php | 89 + .../vendor/Zend/Soap/Wsdl/Strategy/Interface.php | 48 + library/vendor/Zend/TimeSync.php | 296 + library/vendor/Zend/TimeSync/Exception.php | 63 + library/vendor/Zend/TimeSync/Ntp.php | 430 ++ library/vendor/Zend/TimeSync/Protocol.php | 148 + library/vendor/Zend/TimeSync/Sntp.php | 118 + library/vendor/Zend/Translate.php | 220 + library/vendor/Zend/Translate/Adapter.php | 988 +++ library/vendor/Zend/Translate/Exception.php | 36 + library/vendor/Zend/Translate/Plural.php | 223 + library/vendor/Zend/Uri.php | 201 + library/vendor/Zend/Uri/Exception.php | 36 + library/vendor/Zend/Uri/Http.php | 745 +++ library/vendor/Zend/VERSION | 1 + library/vendor/Zend/Validate.php | 283 + library/vendor/Zend/Validate/Abstract.php | 477 ++ library/vendor/Zend/Validate/Alnum.php | 147 + library/vendor/Zend/Validate/Alpha.php | 147 + library/vendor/Zend/Validate/Barcode.php | 222 + .../Zend/Validate/Barcode/AdapterAbstract.php | 314 + .../Zend/Validate/Barcode/AdapterInterface.php | 68 + library/vendor/Zend/Validate/Barcode/Code25.php | 61 + .../Zend/Validate/Barcode/Code25interleaved.php | 61 + library/vendor/Zend/Validate/Barcode/Code39.php | 97 + library/vendor/Zend/Validate/Barcode/Code39ext.php | 55 + library/vendor/Zend/Validate/Barcode/Code93.php | 117 + library/vendor/Zend/Validate/Barcode/Code93ext.php | 55 + library/vendor/Zend/Validate/Barcode/Ean12.php | 51 + library/vendor/Zend/Validate/Barcode/Ean13.php | 51 + library/vendor/Zend/Validate/Barcode/Ean14.php | 51 + library/vendor/Zend/Validate/Barcode/Ean18.php | 51 + library/vendor/Zend/Validate/Barcode/Ean2.php | 55 + library/vendor/Zend/Validate/Barcode/Ean5.php | 55 + library/vendor/Zend/Validate/Barcode/Ean8.php | 68 + library/vendor/Zend/Validate/Barcode/Gtin12.php | 51 + library/vendor/Zend/Validate/Barcode/Gtin13.php | 51 + library/vendor/Zend/Validate/Barcode/Gtin14.php | 51 + library/vendor/Zend/Validate/Barcode/Identcode.php | 51 + .../Zend/Validate/Barcode/Intelligentmail.php | 55 + library/vendor/Zend/Validate/Barcode/Issn.php | 118 + library/vendor/Zend/Validate/Barcode/Itf14.php | 51 + library/vendor/Zend/Validate/Barcode/Leitcode.php | 51 + library/vendor/Zend/Validate/Barcode/Planet.php | 51 + library/vendor/Zend/Validate/Barcode/Postnet.php | 51 + library/vendor/Zend/Validate/Barcode/Royalmail.php | 120 + library/vendor/Zend/Validate/Barcode/Sscc.php | 51 + library/vendor/Zend/Validate/Barcode/Upca.php | 51 + library/vendor/Zend/Validate/Barcode/Upce.php | 68 + library/vendor/Zend/Validate/Between.php | 222 + library/vendor/Zend/Validate/Callback.php | 170 + library/vendor/Zend/Validate/Ccnum.php | 110 + library/vendor/Zend/Validate/CreditCard.php | 316 + library/vendor/Zend/Validate/Date.php | 253 + library/vendor/Zend/Validate/Db/Abstract.php | 350 + library/vendor/Zend/Validate/Db/NoRecordExists.php | 50 + library/vendor/Zend/Validate/Db/RecordExists.php | 50 + library/vendor/Zend/Validate/Digits.php | 89 + library/vendor/Zend/Validate/EmailAddress.php | 571 ++ library/vendor/Zend/Validate/Exception.php | 33 + library/vendor/Zend/Validate/File/Count.php | 279 + library/vendor/Zend/Validate/File/Crc32.php | 177 + .../vendor/Zend/Validate/File/ExcludeExtension.php | 92 + .../vendor/Zend/Validate/File/ExcludeMimeType.php | 99 + library/vendor/Zend/Validate/File/Exists.php | 201 + library/vendor/Zend/Validate/File/Extension.php | 235 + library/vendor/Zend/Validate/File/FilesSize.php | 161 + library/vendor/Zend/Validate/File/Hash.php | 190 + library/vendor/Zend/Validate/File/ImageSize.php | 357 ++ library/vendor/Zend/Validate/File/IsCompressed.php | 148 + library/vendor/Zend/Validate/File/IsImage.php | 171 + library/vendor/Zend/Validate/File/Md5.php | 179 + library/vendor/Zend/Validate/File/MimeType.php | 468 ++ library/vendor/Zend/Validate/File/NotExists.php | 83 + library/vendor/Zend/Validate/File/Sha1.php | 179 + library/vendor/Zend/Validate/File/Size.php | 398 ++ library/vendor/Zend/Validate/File/Upload.php | 249 + library/vendor/Zend/Validate/File/WordCount.php | 99 + library/vendor/Zend/Validate/Float.php | 131 + library/vendor/Zend/Validate/GreaterThan.php | 122 + library/vendor/Zend/Validate/Hex.php | 71 + library/vendor/Zend/Validate/Hostname.php | 1987 ++++++ library/vendor/Zend/Validate/Hostname/Biz.php | 2917 +++++++++ library/vendor/Zend/Validate/Hostname/Cn.php | 2199 +++++++ library/vendor/Zend/Validate/Hostname/Com.php | 196 + library/vendor/Zend/Validate/Hostname/Jp.php | 739 +++ library/vendor/Zend/Validate/Iban.php | 246 + library/vendor/Zend/Validate/Identical.php | 163 + library/vendor/Zend/Validate/InArray.php | 202 + library/vendor/Zend/Validate/Int.php | 145 + library/vendor/Zend/Validate/Interface.php | 54 + library/vendor/Zend/Validate/Ip.php | 190 + library/vendor/Zend/Validate/Isbn.php | 274 + library/vendor/Zend/Validate/LessThan.php | 120 + library/vendor/Zend/Validate/NotEmpty.php | 277 + library/vendor/Zend/Validate/PostCode.php | 202 + library/vendor/Zend/Validate/Regex.php | 143 + .../vendor/Zend/Validate/Sitemap/Changefreq.php | 94 + library/vendor/Zend/Validate/Sitemap/Lastmod.php | 87 + library/vendor/Zend/Validate/Sitemap/Loc.php | 85 + library/vendor/Zend/Validate/Sitemap/Priority.php | 81 + library/vendor/Zend/Validate/StringLength.php | 263 + library/vendor/Zend/View.php | 158 + library/vendor/Zend/View/Abstract.php | 1186 ++++ library/vendor/Zend/View/Exception.php | 50 + library/vendor/Zend/View/Helper/Abstract.php | 63 + library/vendor/Zend/View/Helper/Action.php | 161 + library/vendor/Zend/View/Helper/BaseUrl.php | 114 + library/vendor/Zend/View/Helper/Cycle.php | 225 + library/vendor/Zend/View/Helper/DeclareVars.php | 94 + library/vendor/Zend/View/Helper/Doctype.php | 239 + library/vendor/Zend/View/Helper/Fieldset.php | 78 + library/vendor/Zend/View/Helper/Form.php | 85 + library/vendor/Zend/View/Helper/FormButton.php | 104 + library/vendor/Zend/View/Helper/FormCheckbox.php | 163 + library/vendor/Zend/View/Helper/FormElement.php | 202 + library/vendor/Zend/View/Helper/FormErrors.php | 166 + library/vendor/Zend/View/Helper/FormFile.php | 74 + library/vendor/Zend/View/Helper/FormHidden.php | 65 + library/vendor/Zend/View/Helper/FormImage.php | 94 + library/vendor/Zend/View/Helper/FormLabel.php | 71 + .../vendor/Zend/View/Helper/FormMultiCheckbox.php | 73 + library/vendor/Zend/View/Helper/FormNote.php | 60 + library/vendor/Zend/View/Helper/FormPassword.php | 88 + library/vendor/Zend/View/Helper/FormRadio.php | 185 + library/vendor/Zend/View/Helper/FormReset.php | 81 + library/vendor/Zend/View/Helper/FormSelect.php | 199 + library/vendor/Zend/View/Helper/FormSubmit.php | 80 + library/vendor/Zend/View/Helper/FormText.php | 77 + library/vendor/Zend/View/Helper/FormTextarea.php | 103 + library/vendor/Zend/View/Helper/Gravatar.php | 361 ++ library/vendor/Zend/View/Helper/HeadLink.php | 471 ++ library/vendor/Zend/View/Helper/HeadMeta.php | 440 ++ library/vendor/Zend/View/Helper/HeadScript.php | 512 ++ library/vendor/Zend/View/Helper/HeadStyle.php | 426 ++ library/vendor/Zend/View/Helper/HeadTitle.php | 218 + library/vendor/Zend/View/Helper/HtmlElement.php | 165 + library/vendor/Zend/View/Helper/HtmlFlash.php | 59 + library/vendor/Zend/View/Helper/HtmlList.php | 88 + library/vendor/Zend/View/Helper/HtmlObject.php | 79 + library/vendor/Zend/View/Helper/HtmlPage.php | 74 + library/vendor/Zend/View/Helper/HtmlQuicktime.php | 81 + library/vendor/Zend/View/Helper/InlineScript.php | 60 + library/vendor/Zend/View/Helper/Interface.php | 46 + library/vendor/Zend/View/Helper/Json.php | 86 + library/vendor/Zend/View/Helper/Layout.php | 79 + .../vendor/Zend/View/Helper/PaginationControl.php | 142 + library/vendor/Zend/View/Helper/Partial.php | 150 + .../vendor/Zend/View/Helper/Partial/Exception.php | 38 + library/vendor/Zend/View/Helper/PartialLoop.php | 98 + library/vendor/Zend/View/Helper/Placeholder.php | 85 + .../Zend/View/Helper/Placeholder/Container.php | 35 + .../View/Helper/Placeholder/Container/Abstract.php | 384 ++ .../Helper/Placeholder/Container/Exception.php | 38 + .../Helper/Placeholder/Container/Standalone.php | 322 + .../Zend/View/Helper/Placeholder/Registry.php | 183 + .../View/Helper/Placeholder/Registry/Exception.php | 38 + .../Zend/View/Helper/RenderToPlaceholder.php | 52 + library/vendor/Zend/View/Helper/ServerUrl.php | 148 + library/vendor/Zend/View/Helper/Translate.php | 174 + library/vendor/Zend/View/Helper/Url.php | 50 + library/vendor/Zend/View/Interface.php | 137 + library/vendor/Zend/View/Stream.php | 183 + library/vendor/Zend/Xml/Exception.php | 35 + library/vendor/Zend/Xml/Security.php | 486 ++ library/vendor/dompdf/AUTHORS.md | 24 + library/vendor/dompdf/LICENSE.LGPL | 456 ++ library/vendor/dompdf/README.md | 232 + library/vendor/dompdf/VERSION | 1 + library/vendor/dompdf/autoload.inc.php | 1 + library/vendor/dompdf/vendor/autoload.php | 7 + .../vendor/dompdf/vendor/composer/ClassLoader.php | 572 ++ .../dompdf/vendor/composer/InstalledVersions.php | 350 + library/vendor/dompdf/vendor/composer/LICENSE | 21 + .../dompdf/vendor/composer/autoload_classmap.php | 11 + .../dompdf/vendor/composer/autoload_namespaces.php | 9 + .../dompdf/vendor/composer/autoload_psr4.php | 14 + .../dompdf/vendor/composer/autoload_real.php | 57 + .../dompdf/vendor/composer/autoload_static.php | 66 + .../vendor/dompdf/vendor/composer/installed.json | 295 + .../vendor/dompdf/vendor/composer/installed.php | 68 + .../dompdf/vendor/composer/platform_check.php | 26 + .../vendor/dompdf/vendor/dompdf/dompdf/AUTHORS.md | 24 + .../dompdf/vendor/dompdf/dompdf/LICENSE.LGPL | 456 ++ .../vendor/dompdf/vendor/dompdf/dompdf/README.md | 232 + library/vendor/dompdf/vendor/dompdf/dompdf/VERSION | 1 + .../dompdf/vendor/dompdf/dompdf/composer.json | 47 + .../dompdf/vendor/dompdf/dompdf/lib/Cpdf.php | 6501 +++++++++++++++++++ .../dompdf/dompdf/lib/fonts/Courier-Bold.afm | 344 + .../dompdf/lib/fonts/Courier-BoldOblique.afm | 344 + .../dompdf/dompdf/lib/fonts/Courier-Oblique.afm | 344 + .../vendor/dompdf/dompdf/lib/fonts/Courier.afm | 344 + .../dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ttf | Bin 0 -> 705684 bytes .../dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ufm | 6067 ++++++++++++++++++ .../dompdf/lib/fonts/DejaVuSans-BoldOblique.ttf | Bin 0 -> 643292 bytes .../dompdf/lib/fonts/DejaVuSans-BoldOblique.ufm | 5712 +++++++++++++++++ .../dompdf/dompdf/lib/fonts/DejaVuSans-Oblique.ttf | Bin 0 -> 635416 bytes .../dompdf/dompdf/lib/fonts/DejaVuSans-Oblique.ufm | 5268 ++++++++++++++++ .../vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ttf | Bin 0 -> 757076 bytes .../vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ufm | 6661 ++++++++++++++++++++ .../dompdf/lib/fonts/DejaVuSansMono-Bold.ttf | Bin 0 -> 331992 bytes .../dompdf/lib/fonts/DejaVuSansMono-Bold.ufm | 3285 ++++++++++ .../lib/fonts/DejaVuSansMono-BoldOblique.ttf | Bin 0 -> 253580 bytes .../lib/fonts/DejaVuSansMono-BoldOblique.ufm | 2707 ++++++++ .../dompdf/lib/fonts/DejaVuSansMono-Oblique.ttf | Bin 0 -> 251932 bytes .../dompdf/lib/fonts/DejaVuSansMono-Oblique.ufm | 2707 ++++++++ .../dompdf/dompdf/lib/fonts/DejaVuSansMono.ttf | Bin 0 -> 340712 bytes .../dompdf/dompdf/lib/fonts/DejaVuSansMono.ufm | 3284 ++++++++++ .../dompdf/dompdf/lib/fonts/DejaVuSerif-Bold.ttf | Bin 0 -> 356088 bytes .../dompdf/dompdf/lib/fonts/DejaVuSerif-Bold.ufm | 4013 ++++++++++++ .../dompdf/lib/fonts/DejaVuSerif-BoldItalic.ttf | Bin 0 -> 347460 bytes .../dompdf/lib/fonts/DejaVuSerif-BoldItalic.ufm | 3892 ++++++++++++ .../dompdf/dompdf/lib/fonts/DejaVuSerif-Italic.ttf | Bin 0 -> 345996 bytes .../dompdf/dompdf/lib/fonts/DejaVuSerif-Italic.ufm | 3883 ++++++++++++ .../vendor/dompdf/dompdf/lib/fonts/DejaVuSerif.ttf | Bin 0 -> 380132 bytes .../vendor/dompdf/dompdf/lib/fonts/DejaVuSerif.ufm | 4012 ++++++++++++ .../dompdf/dompdf/lib/fonts/Helvetica-Bold.afm | 2829 +++++++++ .../dompdf/lib/fonts/Helvetica-BoldOblique.afm | 2829 +++++++++ .../dompdf/dompdf/lib/fonts/Helvetica-Oblique.afm | 3053 +++++++++ .../vendor/dompdf/dompdf/lib/fonts/Helvetica.afm | 3053 +++++++++ .../vendor/dompdf/dompdf/lib/fonts/Symbol.afm | 213 + .../vendor/dompdf/dompdf/lib/fonts/Times-Bold.afm | 2590 ++++++++ .../dompdf/dompdf/lib/fonts/Times-BoldItalic.afm | 2386 +++++++ .../dompdf/dompdf/lib/fonts/Times-Italic.afm | 2669 ++++++++ .../vendor/dompdf/dompdf/lib/fonts/Times-Roman.afm | 2421 +++++++ .../dompdf/dompdf/lib/fonts/ZapfDingbats.afm | 225 + .../dompdf/lib/fonts/installed-fonts.dist.json | 80 + .../vendor/dompdf/dompdf/lib/fonts/mustRead.html | 17 + .../vendor/dompdf/dompdf/lib/res/broken_image.png | Bin 0 -> 618 bytes .../vendor/dompdf/dompdf/lib/res/broken_image.svg | 8 + .../dompdf/vendor/dompdf/dompdf/lib/res/html.css | 518 ++ .../vendor/dompdf/dompdf/src/Adapter/CPDF.php | 944 +++ .../dompdf/vendor/dompdf/dompdf/src/Adapter/GD.php | 929 +++ .../vendor/dompdf/dompdf/src/Adapter/PDFLib.php | 1450 +++++ .../dompdf/vendor/dompdf/dompdf/src/Canvas.php | 477 ++ .../vendor/dompdf/dompdf/src/CanvasFactory.php | 58 + .../dompdf/vendor/dompdf/dompdf/src/Cellmap.php | 999 +++ .../dompdf/dompdf/src/Css/AttributeTranslator.php | 652 ++ .../dompdf/vendor/dompdf/dompdf/src/Css/Color.php | 339 + .../dompdf/vendor/dompdf/dompdf/src/Css/Style.php | 3743 +++++++++++ .../vendor/dompdf/dompdf/src/Css/Stylesheet.php | 1689 +++++ .../dompdf/vendor/dompdf/dompdf/src/Dompdf.php | 1470 +++++ .../dompdf/vendor/dompdf/dompdf/src/Exception.php | 27 + .../dompdf/dompdf/src/Exception/ImageException.php | 30 + .../vendor/dompdf/dompdf/src/FontMetrics.php | 635 ++ .../dompdf/vendor/dompdf/dompdf/src/Frame.php | 1217 ++++ .../vendor/dompdf/dompdf/src/Frame/Factory.php | 263 + .../dompdf/dompdf/src/Frame/FrameListIterator.php | 100 + .../vendor/dompdf/dompdf/src/Frame/FrameTree.php | 324 + .../dompdf/dompdf/src/Frame/FrameTreeIterator.php | 88 + .../src/FrameDecorator/AbstractFrameDecorator.php | 923 +++ .../dompdf/dompdf/src/FrameDecorator/Block.php | 256 + .../dompdf/dompdf/src/FrameDecorator/Image.php | 120 + .../dompdf/dompdf/src/FrameDecorator/Inline.php | 121 + .../dompdf/src/FrameDecorator/ListBullet.php | 117 + .../dompdf/src/FrameDecorator/ListBulletImage.php | 112 + .../src/FrameDecorator/NullFrameDecorator.php | 33 + .../dompdf/dompdf/src/FrameDecorator/Page.php | 753 +++ .../dompdf/dompdf/src/FrameDecorator/Table.php | 343 + .../dompdf/dompdf/src/FrameDecorator/TableCell.php | 143 + .../dompdf/dompdf/src/FrameDecorator/TableRow.php | 28 + .../dompdf/src/FrameDecorator/TableRowGroup.php | 74 + .../dompdf/dompdf/src/FrameDecorator/Text.php | 191 + .../src/FrameReflower/AbstractFrameReflower.php | 705 +++ .../dompdf/dompdf/src/FrameReflower/Block.php | 949 +++ .../dompdf/dompdf/src/FrameReflower/Image.php | 213 + .../dompdf/dompdf/src/FrameReflower/Inline.php | 188 + .../dompdf/dompdf/src/FrameReflower/ListBullet.php | 47 + .../dompdf/src/FrameReflower/NullFrameReflower.php | 37 + .../dompdf/dompdf/src/FrameReflower/Page.php | 199 + .../dompdf/dompdf/src/FrameReflower/Table.php | 523 ++ .../dompdf/dompdf/src/FrameReflower/TableCell.php | 161 + .../dompdf/dompdf/src/FrameReflower/TableRow.php | 82 + .../dompdf/src/FrameReflower/TableRowGroup.php | 71 + .../dompdf/dompdf/src/FrameReflower/Text.php | 605 ++ .../dompdf/vendor/dompdf/dompdf/src/Helpers.php | 1093 ++++ .../vendor/dompdf/dompdf/src/Image/Cache.php | 254 + .../dompdf/dompdf/src/JavascriptEmbedder.php | 51 + .../dompdf/vendor/dompdf/dompdf/src/LineBox.php | 412 ++ .../dompdf/vendor/dompdf/dompdf/src/Options.php | 1159 ++++ .../vendor/dompdf/dompdf/src/PhpEvaluator.php | 62 + .../dompdf/dompdf/src/Positioner/Absolute.php | 128 + .../dompdf/src/Positioner/AbstractPositioner.php | 48 + .../vendor/dompdf/dompdf/src/Positioner/Block.php | 40 + .../vendor/dompdf/dompdf/src/Positioner/Fixed.php | 92 + .../vendor/dompdf/dompdf/src/Positioner/Inline.php | 52 + .../dompdf/dompdf/src/Positioner/ListBullet.php | 42 + .../dompdf/src/Positioner/NullPositioner.php | 26 + .../dompdf/dompdf/src/Positioner/TableCell.php | 29 + .../dompdf/dompdf/src/Positioner/TableRow.php | 34 + .../dompdf/vendor/dompdf/dompdf/src/Renderer.php | 291 + .../dompdf/src/Renderer/AbstractRenderer.php | 1244 ++++ .../vendor/dompdf/dompdf/src/Renderer/Block.php | 88 + .../vendor/dompdf/dompdf/src/Renderer/Image.php | 90 + .../vendor/dompdf/dompdf/src/Renderer/Inline.php | 126 + .../dompdf/dompdf/src/Renderer/ListBullet.php | 235 + .../dompdf/dompdf/src/Renderer/TableCell.php | 188 + .../dompdf/dompdf/src/Renderer/TableRowGroup.php | 40 + .../vendor/dompdf/dompdf/src/Renderer/Text.php | 158 + .../vendor/dompdf/vendor/masterminds/html5/CREDITS | 11 + .../dompdf/vendor/masterminds/html5/LICENSE.txt | 66 + .../dompdf/vendor/masterminds/html5/README.md | 270 + .../dompdf/vendor/masterminds/html5/UPGRADING.md | 21 + .../vendor/masterminds/html5/bin/entities.php | 26 + .../dompdf/vendor/masterminds/html5/composer.json | 42 + .../dompdf/vendor/masterminds/html5/src/HTML5.php | 246 + .../masterminds/html5/src/HTML5/Elements.php | 619 ++ .../masterminds/html5/src/HTML5/Entities.php | 2236 +++++++ .../masterminds/html5/src/HTML5/Exception.php | 10 + .../html5/src/HTML5/InstructionProcessor.php | 41 + .../html5/src/HTML5/Parser/CharacterReference.php | 61 + .../html5/src/HTML5/Parser/DOMTreeBuilder.php | 705 +++ .../html5/src/HTML5/Parser/EventHandler.php | 114 + .../html5/src/HTML5/Parser/FileInputStream.php | 33 + .../html5/src/HTML5/Parser/InputStream.php | 87 + .../html5/src/HTML5/Parser/ParseError.php | 10 + .../masterminds/html5/src/HTML5/Parser/README.md | 53 + .../masterminds/html5/src/HTML5/Parser/Scanner.php | 416 ++ .../html5/src/HTML5/Parser/StringInputStream.php | 331 + .../html5/src/HTML5/Parser/Tokenizer.php | 1197 ++++ .../html5/src/HTML5/Parser/TreeBuildingRules.php | 127 + .../html5/src/HTML5/Parser/UTF8Utils.php | 183 + .../html5/src/HTML5/Serializer/HTML5Entities.php | 1533 +++++ .../html5/src/HTML5/Serializer/OutputRules.php | 553 ++ .../html5/src/HTML5/Serializer/README.md | 33 + .../html5/src/HTML5/Serializer/RulesInterface.php | 99 + .../html5/src/HTML5/Serializer/Traverser.php | 142 + .../dompdf/vendor/phenx/php-font-lib/LICENSE | 456 ++ .../dompdf/vendor/phenx/php-font-lib/README.md | 28 + .../dompdf/vendor/phenx/php-font-lib/bower.json | 23 + .../dompdf/vendor/phenx/php-font-lib/composer.json | 32 + .../dompdf/vendor/phenx/php-font-lib/index.php | 1 + .../php-font-lib/maps/adobe-standard-encoding.map | 231 + .../vendor/phenx/php-font-lib/maps/cp1250.map | 251 + .../vendor/phenx/php-font-lib/maps/cp1251.map | 255 + .../vendor/phenx/php-font-lib/maps/cp1252.map | 251 + .../vendor/phenx/php-font-lib/maps/cp1253.map | 239 + .../vendor/phenx/php-font-lib/maps/cp1254.map | 249 + .../vendor/phenx/php-font-lib/maps/cp1255.map | 233 + .../vendor/phenx/php-font-lib/maps/cp1257.map | 244 + .../vendor/phenx/php-font-lib/maps/cp1258.map | 247 + .../vendor/phenx/php-font-lib/maps/cp874.map | 225 + .../vendor/phenx/php-font-lib/maps/iso-8859-1.map | 256 + .../vendor/phenx/php-font-lib/maps/iso-8859-11.map | 248 + .../vendor/phenx/php-font-lib/maps/iso-8859-15.map | 256 + .../vendor/phenx/php-font-lib/maps/iso-8859-16.map | 256 + .../vendor/phenx/php-font-lib/maps/iso-8859-2.map | 256 + .../vendor/phenx/php-font-lib/maps/iso-8859-4.map | 256 + .../vendor/phenx/php-font-lib/maps/iso-8859-5.map | 256 + .../vendor/phenx/php-font-lib/maps/iso-8859-7.map | 250 + .../vendor/phenx/php-font-lib/maps/iso-8859-9.map | 256 + .../vendor/phenx/php-font-lib/maps/koi8-r.map | 256 + .../vendor/phenx/php-font-lib/maps/koi8-u.map | 256 + .../php-font-lib/src/FontLib/AdobeFontMetrics.php | 217 + .../phenx/php-font-lib/src/FontLib/Autoloader.php | 43 + .../php-font-lib/src/FontLib/BinaryStream.php | 449 ++ .../phenx/php-font-lib/src/FontLib/EOT/File.php | 159 + .../phenx/php-font-lib/src/FontLib/EOT/Header.php | 113 + .../phenx/php-font-lib/src/FontLib/EncodingMap.php | 37 + .../FontLib/Exception/FontNotFoundException.php | 11 + .../vendor/phenx/php-font-lib/src/FontLib/Font.php | 89 + .../php-font-lib/src/FontLib/Glyph/Outline.php | 109 + .../src/FontLib/Glyph/OutlineComponent.php | 31 + .../src/FontLib/Glyph/OutlineComposite.php | 242 + .../src/FontLib/Glyph/OutlineSimple.php | 335 + .../phenx/php-font-lib/src/FontLib/Header.php | 37 + .../php-font-lib/src/FontLib/OpenType/File.php | 18 + .../src/FontLib/OpenType/TableDirectoryEntry.php | 18 + .../src/FontLib/Table/DirectoryEntry.php | 134 + .../phenx/php-font-lib/src/FontLib/Table/Table.php | 93 + .../php-font-lib/src/FontLib/Table/Type/cmap.php | 298 + .../php-font-lib/src/FontLib/Table/Type/glyf.php | 154 + .../php-font-lib/src/FontLib/Table/Type/head.php | 46 + .../php-font-lib/src/FontLib/Table/Type/hhea.php | 44 + .../php-font-lib/src/FontLib/Table/Type/hmtx.php | 59 + .../php-font-lib/src/FontLib/Table/Type/kern.php | 80 + .../php-font-lib/src/FontLib/Table/Type/loca.php | 80 + .../php-font-lib/src/FontLib/Table/Type/maxp.php | 42 + .../php-font-lib/src/FontLib/Table/Type/name.php | 193 + .../src/FontLib/Table/Type/nameRecord.php | 53 + .../php-font-lib/src/FontLib/Table/Type/os2.php | 47 + .../php-font-lib/src/FontLib/Table/Type/post.php | 143 + .../src/FontLib/TrueType/Collection.php | 100 + .../php-font-lib/src/FontLib/TrueType/File.php | 471 ++ .../php-font-lib/src/FontLib/TrueType/Header.php | 31 + .../src/FontLib/TrueType/TableDirectoryEntry.php | 33 + .../phenx/php-font-lib/src/FontLib/WOFF/File.php | 81 + .../phenx/php-font-lib/src/FontLib/WOFF/Header.php | 32 + .../src/FontLib/WOFF/TableDirectoryEntry.php | 34 + .../vendor/dompdf/vendor/phenx/php-svg-lib/LICENSE | 165 + .../dompdf/vendor/phenx/php-svg-lib/README.md | 13 + .../dompdf/vendor/phenx/php-svg-lib/composer.json | 31 + .../vendor/phenx/php-svg-lib/src/Svg/CssLength.php | 135 + .../phenx/php-svg-lib/src/Svg/DefaultStyle.php | 29 + .../vendor/phenx/php-svg-lib/src/Svg/Document.php | 406 ++ .../phenx/php-svg-lib/src/Svg/Gradient/Stop.php | 16 + .../vendor/phenx/php-svg-lib/src/Svg/Style.php | 541 ++ .../phenx/php-svg-lib/src/Svg/Surface/CPdf.php | 6418 +++++++++++++++++++ .../php-svg-lib/src/Svg/Surface/SurfaceCpdf.php | 495 ++ .../src/Svg/Surface/SurfaceInterface.php | 90 + .../php-svg-lib/src/Svg/Surface/SurfacePDFLib.php | 430 ++ .../phenx/php-svg-lib/src/Svg/Tag/AbstractTag.php | 236 + .../phenx/php-svg-lib/src/Svg/Tag/Anchor.php | 14 + .../phenx/php-svg-lib/src/Svg/Tag/Circle.php | 36 + .../phenx/php-svg-lib/src/Svg/Tag/ClipPath.php | 33 + .../phenx/php-svg-lib/src/Svg/Tag/Ellipse.php | 42 + .../vendor/phenx/php-svg-lib/src/Svg/Tag/Group.php | 33 + .../vendor/phenx/php-svg-lib/src/Svg/Tag/Image.php | 68 + .../vendor/phenx/php-svg-lib/src/Svg/Tag/Line.php | 43 + .../php-svg-lib/src/Svg/Tag/LinearGradient.php | 83 + .../vendor/phenx/php-svg-lib/src/Svg/Tag/Path.php | 576 ++ .../phenx/php-svg-lib/src/Svg/Tag/Polygon.php | 42 + .../phenx/php-svg-lib/src/Svg/Tag/Polyline.php | 40 + .../php-svg-lib/src/Svg/Tag/RadialGradient.php | 17 + .../vendor/phenx/php-svg-lib/src/Svg/Tag/Rect.php | 50 + .../vendor/phenx/php-svg-lib/src/Svg/Tag/Shape.php | 63 + .../vendor/phenx/php-svg-lib/src/Svg/Tag/Stop.php | 17 + .../phenx/php-svg-lib/src/Svg/Tag/StyleTag.php | 27 + .../vendor/phenx/php-svg-lib/src/Svg/Tag/Text.php | 72 + .../phenx/php-svg-lib/src/Svg/Tag/UseTag.php | 102 + .../vendor/sabberworm/php-css-parser/CHANGELOG.md | 241 + .../vendor/sabberworm/php-css-parser/LICENSE | 21 + .../vendor/sabberworm/php-css-parser/README.md | 632 ++ .../vendor/sabberworm/php-css-parser/composer.json | 69 + .../php-css-parser/src/CSSList/AtRuleBlockList.php | 83 + .../php-css-parser/src/CSSList/CSSBlockList.php | 143 + .../php-css-parser/src/CSSList/CSSList.php | 479 ++ .../php-css-parser/src/CSSList/Document.php | 172 + .../php-css-parser/src/CSSList/KeyFrame.php | 104 + .../php-css-parser/src/Comment/Comment.php | 71 + .../php-css-parser/src/Comment/Commentable.php | 25 + .../sabberworm/php-css-parser/src/OutputFormat.php | 334 + .../php-css-parser/src/OutputFormatter.php | 231 + .../sabberworm/php-css-parser/src/Parser.php | 60 + .../php-css-parser/src/Parsing/OutputException.php | 18 + .../php-css-parser/src/Parsing/ParserState.php | 516 ++ .../php-css-parser/src/Parsing/SourceException.php | 32 + .../src/Parsing/UnexpectedEOFException.php | 12 + .../src/Parsing/UnexpectedTokenException.php | 51 + .../php-css-parser/src/Property/AtRule.php | 34 + .../php-css-parser/src/Property/CSSNamespace.php | 154 + .../php-css-parser/src/Property/Charset.php | 129 + .../php-css-parser/src/Property/Import.php | 137 + .../src/Property/KeyframeSelector.php | 23 + .../php-css-parser/src/Property/Selector.php | 138 + .../sabberworm/php-css-parser/src/Renderable.php | 21 + .../sabberworm/php-css-parser/src/Rule/Rule.php | 392 ++ .../php-css-parser/src/RuleSet/AtRuleSet.php | 73 + .../src/RuleSet/DeclarationBlock.php | 831 +++ .../php-css-parser/src/RuleSet/RuleSet.php | 326 + .../sabberworm/php-css-parser/src/Settings.php | 89 + .../php-css-parser/src/Value/CSSFunction.php | 73 + .../php-css-parser/src/Value/CSSString.php | 105 + .../php-css-parser/src/Value/CalcFunction.php | 89 + .../php-css-parser/src/Value/CalcRuleValueList.php | 24 + .../sabberworm/php-css-parser/src/Value/Color.php | 166 + .../php-css-parser/src/Value/LineName.php | 65 + .../php-css-parser/src/Value/PrimitiveValue.php | 14 + .../php-css-parser/src/Value/RuleValueList.php | 15 + .../sabberworm/php-css-parser/src/Value/Size.php | 209 + .../sabberworm/php-css-parser/src/Value/URL.php | 82 + .../sabberworm/php-css-parser/src/Value/Value.php | 198 + .../php-css-parser/src/Value/ValueList.php | 100 + library/vendor/lessphp/CHANGES.md | 72 + library/vendor/lessphp/LICENSE | 178 + library/vendor/lessphp/README.md | 332 + library/vendor/lessphp/SECURITY.md | 5 + library/vendor/lessphp/bin/lessc | 191 + library/vendor/lessphp/composer.json | 52 + library/vendor/lessphp/lessc.inc.php | 274 + library/vendor/lessphp/lib/Less/Autoloader.php | 77 + library/vendor/lessphp/lib/Less/Cache.php | 290 + library/vendor/lessphp/lib/Less/Colors.php | 169 + library/vendor/lessphp/lib/Less/Configurable.php | 65 + library/vendor/lessphp/lib/Less/Environment.php | 156 + .../vendor/lessphp/lib/Less/Exception/Chunk.php | 200 + .../vendor/lessphp/lib/Less/Exception/Compiler.php | 11 + .../vendor/lessphp/lib/Less/Exception/Parser.php | 107 + library/vendor/lessphp/lib/Less/Functions.php | 1186 ++++ library/vendor/lessphp/lib/Less/Less.php.combine | 17 + library/vendor/lessphp/lib/Less/Mime.php | 41 + library/vendor/lessphp/lib/Less/Output.php | 48 + library/vendor/lessphp/lib/Less/Output/Mapped.php | 119 + library/vendor/lessphp/lib/Less/Parser.php | 2676 ++++++++ .../lessphp/lib/Less/SourceMap/Base64VLQ.php | 187 + .../lessphp/lib/Less/SourceMap/Generator.php | 354 ++ library/vendor/lessphp/lib/Less/Tree.php | 84 + library/vendor/lessphp/lib/Less/Tree/Alpha.php | 48 + library/vendor/lessphp/lib/Less/Tree/Anonymous.php | 58 + .../vendor/lessphp/lib/Less/Tree/Assignment.php | 39 + library/vendor/lessphp/lib/Less/Tree/Attribute.php | 53 + library/vendor/lessphp/lib/Less/Tree/Call.php | 117 + library/vendor/lessphp/lib/Less/Tree/Color.php | 230 + library/vendor/lessphp/lib/Less/Tree/Comment.php | 51 + library/vendor/lessphp/lib/Less/Tree/Condition.php | 72 + .../vendor/lessphp/lib/Less/Tree/DefaultFunc.php | 34 + .../lessphp/lib/Less/Tree/DetachedRuleset.php | 39 + library/vendor/lessphp/lib/Less/Tree/Dimension.php | 198 + library/vendor/lessphp/lib/Less/Tree/Directive.php | 96 + library/vendor/lessphp/lib/Less/Tree/Element.php | 70 + .../vendor/lessphp/lib/Less/Tree/Expression.php | 95 + library/vendor/lessphp/lib/Less/Tree/Extend.php | 80 + library/vendor/lessphp/lib/Less/Tree/Import.php | 289 + .../vendor/lessphp/lib/Less/Tree/Javascript.php | 30 + library/vendor/lessphp/lib/Less/Tree/Keyword.php | 43 + library/vendor/lessphp/lib/Less/Tree/Media.php | 173 + .../vendor/lessphp/lib/Less/Tree/Mixin/Call.php | 193 + .../lessphp/lib/Less/Tree/Mixin/Definition.php | 233 + library/vendor/lessphp/lib/Less/Tree/NameValue.php | 49 + library/vendor/lessphp/lib/Less/Tree/Negative.php | 37 + library/vendor/lessphp/lib/Less/Tree/Operation.php | 68 + library/vendor/lessphp/lib/Less/Tree/Paren.php | 35 + library/vendor/lessphp/lib/Less/Tree/Quoted.php | 79 + library/vendor/lessphp/lib/Less/Tree/Rule.php | 112 + library/vendor/lessphp/lib/Less/Tree/Ruleset.php | 620 ++ .../vendor/lessphp/lib/Less/Tree/RulesetCall.php | 26 + library/vendor/lessphp/lib/Less/Tree/Selector.php | 172 + .../lessphp/lib/Less/Tree/UnicodeDescriptor.php | 28 + library/vendor/lessphp/lib/Less/Tree/Unit.php | 142 + .../lessphp/lib/Less/Tree/UnitConversions.php | 35 + library/vendor/lessphp/lib/Less/Tree/Url.php | 76 + library/vendor/lessphp/lib/Less/Tree/Value.php | 47 + library/vendor/lessphp/lib/Less/Tree/Variable.php | 51 + library/vendor/lessphp/lib/Less/Version.php | 15 + library/vendor/lessphp/lib/Less/Visitor.php | 46 + .../lessphp/lib/Less/Visitor/extendFinder.php | 109 + library/vendor/lessphp/lib/Less/Visitor/import.php | 137 + .../lessphp/lib/Less/Visitor/joinSelector.php | 68 + .../lessphp/lib/Less/Visitor/processExtends.php | 441 ++ library/vendor/lessphp/lib/Less/Visitor/toCSS.php | 280 + .../vendor/lessphp/lib/Less/VisitorReplacing.php | 70 + 1684 files changed, 405841 insertions(+) create mode 100644 library/Icinga/Application/ApplicationBootstrap.php create mode 100644 library/Icinga/Application/Benchmark.php create mode 100644 library/Icinga/Application/ClassLoader.php create mode 100644 library/Icinga/Application/Cli.php create mode 100644 library/Icinga/Application/Config.php create mode 100644 library/Icinga/Application/EmbeddedWeb.php create mode 100644 library/Icinga/Application/Hook.php create mode 100644 library/Icinga/Application/Hook/ApplicationStateHook.php create mode 100644 library/Icinga/Application/Hook/AuditHook.php create mode 100644 library/Icinga/Application/Hook/AuthenticationHook.php create mode 100644 library/Icinga/Application/Hook/ConfigFormEventsHook.php create mode 100644 library/Icinga/Application/Hook/GrapherHook.php create mode 100644 library/Icinga/Application/Hook/HealthHook.php create mode 100644 library/Icinga/Application/Hook/PdfexportHook.php create mode 100644 library/Icinga/Application/Hook/ThemeLoaderHook.php create mode 100644 library/Icinga/Application/Hook/Ticket/TicketPattern.php create mode 100644 library/Icinga/Application/Hook/TicketHook.php create mode 100644 library/Icinga/Application/Hook/WebBaseHook.php create mode 100644 library/Icinga/Application/Icinga.php create mode 100644 library/Icinga/Application/LegacyWeb.php create mode 100644 library/Icinga/Application/Libraries.php create mode 100644 library/Icinga/Application/Libraries/Library.php create mode 100644 library/Icinga/Application/Logger.php create mode 100644 library/Icinga/Application/Logger/LogWriter.php create mode 100644 library/Icinga/Application/Logger/Writer/FileWriter.php create mode 100644 library/Icinga/Application/Logger/Writer/PhpWriter.php create mode 100644 library/Icinga/Application/Logger/Writer/StderrWriter.php create mode 100644 library/Icinga/Application/Logger/Writer/StdoutWriter.php create mode 100644 library/Icinga/Application/Logger/Writer/SyslogWriter.php create mode 100644 library/Icinga/Application/Modules/DashboardContainer.php create mode 100644 library/Icinga/Application/Modules/Manager.php create mode 100644 library/Icinga/Application/Modules/MenuItemContainer.php create mode 100644 library/Icinga/Application/Modules/Module.php create mode 100644 library/Icinga/Application/Modules/NavigationItemContainer.php create mode 100644 library/Icinga/Application/Platform.php create mode 100644 library/Icinga/Application/StaticWeb.php create mode 100644 library/Icinga/Application/Version.php create mode 100644 library/Icinga/Application/Web.php create mode 100644 library/Icinga/Application/functions.php create mode 100644 library/Icinga/Application/webrouter.php create mode 100644 library/Icinga/Authentication/AdmissionLoader.php create mode 100644 library/Icinga/Authentication/Auth.php create mode 100644 library/Icinga/Authentication/AuthChain.php create mode 100644 library/Icinga/Authentication/Authenticatable.php create mode 100644 library/Icinga/Authentication/Role.php create mode 100644 library/Icinga/Authentication/RolesConfig.php create mode 100644 library/Icinga/Authentication/User/DbUserBackend.php create mode 100644 library/Icinga/Authentication/User/DomainAwareInterface.php create mode 100644 library/Icinga/Authentication/User/ExternalBackend.php create mode 100644 library/Icinga/Authentication/User/LdapUserBackend.php create mode 100644 library/Icinga/Authentication/User/UserBackend.php create mode 100644 library/Icinga/Authentication/User/UserBackendInterface.php create mode 100644 library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php create mode 100644 library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php create mode 100644 library/Icinga/Authentication/UserGroup/UserGroupBackend.php create mode 100644 library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php create mode 100644 library/Icinga/Chart/Axis.php create mode 100644 library/Icinga/Chart/Chart.php create mode 100644 library/Icinga/Chart/Donut.php create mode 100644 library/Icinga/Chart/Format.php create mode 100644 library/Icinga/Chart/Graph/BarGraph.php create mode 100644 library/Icinga/Chart/Graph/LineGraph.php create mode 100644 library/Icinga/Chart/Graph/StackedGraph.php create mode 100644 library/Icinga/Chart/Graph/Tooltip.php create mode 100644 library/Icinga/Chart/GridChart.php create mode 100644 library/Icinga/Chart/Inline/Inline.php create mode 100644 library/Icinga/Chart/Inline/PieChart.php create mode 100644 library/Icinga/Chart/Legend.php create mode 100644 library/Icinga/Chart/Palette.php create mode 100644 library/Icinga/Chart/PieChart.php create mode 100644 library/Icinga/Chart/Primitive/Animatable.php create mode 100644 library/Icinga/Chart/Primitive/Animation.php create mode 100644 library/Icinga/Chart/Primitive/Canvas.php create mode 100644 library/Icinga/Chart/Primitive/Circle.php create mode 100644 library/Icinga/Chart/Primitive/Drawable.php create mode 100644 library/Icinga/Chart/Primitive/Line.php create mode 100644 library/Icinga/Chart/Primitive/Path.php create mode 100644 library/Icinga/Chart/Primitive/PieSlice.php create mode 100644 library/Icinga/Chart/Primitive/RawElement.php create mode 100644 library/Icinga/Chart/Primitive/Rect.php create mode 100644 library/Icinga/Chart/Primitive/Styleable.php create mode 100644 library/Icinga/Chart/Primitive/Text.php create mode 100644 library/Icinga/Chart/Render/LayoutBox.php create mode 100644 library/Icinga/Chart/Render/RenderContext.php create mode 100644 library/Icinga/Chart/Render/Rotator.php create mode 100644 library/Icinga/Chart/SVGRenderer.php create mode 100644 library/Icinga/Chart/Unit/AxisUnit.php create mode 100644 library/Icinga/Chart/Unit/CalendarUnit.php create mode 100644 library/Icinga/Chart/Unit/LinearUnit.php create mode 100644 library/Icinga/Chart/Unit/LogarithmicUnit.php create mode 100644 library/Icinga/Chart/Unit/StaticAxis.php create mode 100644 library/Icinga/Cli/AnsiScreen.php create mode 100644 library/Icinga/Cli/Command.php create mode 100644 library/Icinga/Cli/Documentation.php create mode 100644 library/Icinga/Cli/Documentation/CommentParser.php create mode 100644 library/Icinga/Cli/Loader.php create mode 100644 library/Icinga/Cli/Params.php create mode 100644 library/Icinga/Cli/Screen.php create mode 100644 library/Icinga/Common/Database.php create mode 100644 library/Icinga/Common/PdfExport.php create mode 100644 library/Icinga/Crypt/AesCrypt.php create mode 100644 library/Icinga/Data/ConfigObject.php create mode 100644 library/Icinga/Data/ConnectionInterface.php create mode 100644 library/Icinga/Data/DataArray/ArrayDatasource.php create mode 100644 library/Icinga/Data/Db/DbConnection.php create mode 100644 library/Icinga/Data/Db/DbQuery.php create mode 100644 library/Icinga/Data/Extensible.php create mode 100644 library/Icinga/Data/Fetchable.php create mode 100644 library/Icinga/Data/Filter/Filter.php create mode 100644 library/Icinga/Data/Filter/FilterAnd.php create mode 100644 library/Icinga/Data/Filter/FilterChain.php create mode 100644 library/Icinga/Data/Filter/FilterEqual.php create mode 100644 library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php create mode 100644 library/Icinga/Data/Filter/FilterEqualOrLessThan.php create mode 100644 library/Icinga/Data/Filter/FilterException.php create mode 100644 library/Icinga/Data/Filter/FilterExpression.php create mode 100644 library/Icinga/Data/Filter/FilterGreaterThan.php create mode 100644 library/Icinga/Data/Filter/FilterLessThan.php create mode 100644 library/Icinga/Data/Filter/FilterMatch.php create mode 100644 library/Icinga/Data/Filter/FilterMatchCaseInsensitive.php create mode 100644 library/Icinga/Data/Filter/FilterMatchNot.php create mode 100644 library/Icinga/Data/Filter/FilterMatchNotCaseInsensitive.php create mode 100644 library/Icinga/Data/Filter/FilterNot.php create mode 100644 library/Icinga/Data/Filter/FilterNotEqual.php create mode 100644 library/Icinga/Data/Filter/FilterOr.php create mode 100644 library/Icinga/Data/Filter/FilterParseException.php create mode 100644 library/Icinga/Data/Filter/FilterQueryString.php create mode 100644 library/Icinga/Data/FilterColumns.php create mode 100644 library/Icinga/Data/Filterable.php create mode 100644 library/Icinga/Data/Identifiable.php create mode 100644 library/Icinga/Data/Inspectable.php create mode 100644 library/Icinga/Data/Inspection.php create mode 100644 library/Icinga/Data/Limitable.php create mode 100644 library/Icinga/Data/Paginatable.php create mode 100644 library/Icinga/Data/PivotTable.php create mode 100644 library/Icinga/Data/QueryInterface.php create mode 100644 library/Icinga/Data/Queryable.php create mode 100644 library/Icinga/Data/Reducible.php create mode 100644 library/Icinga/Data/ResourceFactory.php create mode 100644 library/Icinga/Data/Selectable.php create mode 100644 library/Icinga/Data/SimpleQuery.php create mode 100644 library/Icinga/Data/SortRules.php create mode 100644 library/Icinga/Data/Sortable.php create mode 100644 library/Icinga/Data/Tree/SimpleTree.php create mode 100644 library/Icinga/Data/Tree/TreeNode.php create mode 100644 library/Icinga/Data/Tree/TreeNodeIterator.php create mode 100644 library/Icinga/Data/Updatable.php create mode 100644 library/Icinga/Date/DateFormatter.php create mode 100644 library/Icinga/Exception/AlreadyExistsException.php create mode 100644 library/Icinga/Exception/AuthenticationException.php create mode 100644 library/Icinga/Exception/ConfigurationError.php create mode 100644 library/Icinga/Exception/Http/BaseHttpException.php create mode 100644 library/Icinga/Exception/Http/HttpBadRequestException.php create mode 100644 library/Icinga/Exception/Http/HttpException.php create mode 100644 library/Icinga/Exception/Http/HttpExceptionInterface.php create mode 100644 library/Icinga/Exception/Http/HttpMethodNotAllowedException.php create mode 100644 library/Icinga/Exception/Http/HttpNotFoundException.php create mode 100644 library/Icinga/Exception/IcingaException.php create mode 100644 library/Icinga/Exception/InvalidPropertyException.php create mode 100644 library/Icinga/Exception/Json/JsonDecodeException.php create mode 100644 library/Icinga/Exception/Json/JsonEncodeException.php create mode 100644 library/Icinga/Exception/Json/JsonException.php create mode 100644 library/Icinga/Exception/MissingParameterException.php create mode 100644 library/Icinga/Exception/NotFoundError.php create mode 100644 library/Icinga/Exception/NotImplementedError.php create mode 100644 library/Icinga/Exception/NotReadableError.php create mode 100644 library/Icinga/Exception/NotWritableError.php create mode 100644 library/Icinga/Exception/ProgrammingError.php create mode 100644 library/Icinga/Exception/QueryException.php create mode 100644 library/Icinga/Exception/StatementException.php create mode 100644 library/Icinga/Exception/SystemPermissionException.php create mode 100644 library/Icinga/File/Csv.php create mode 100644 library/Icinga/File/Ini/Dom/Comment.php create mode 100644 library/Icinga/File/Ini/Dom/Directive.php create mode 100644 library/Icinga/File/Ini/Dom/Document.php create mode 100644 library/Icinga/File/Ini/Dom/Section.php create mode 100644 library/Icinga/File/Ini/IniParser.php create mode 100644 library/Icinga/File/Ini/IniWriter.php create mode 100644 library/Icinga/File/Pdf.php create mode 100644 library/Icinga/File/Storage/LocalFileStorage.php create mode 100644 library/Icinga/File/Storage/StorageInterface.php create mode 100644 library/Icinga/File/Storage/TemporaryLocalFileStorage.php create mode 100644 library/Icinga/Legacy/DashboardConfig.php create mode 100644 library/Icinga/Less/Call.php create mode 100644 library/Icinga/Less/ColorProp.php create mode 100644 library/Icinga/Less/ColorPropOrVariable.php create mode 100644 library/Icinga/Less/DeferredColorProp.php create mode 100644 library/Icinga/Less/LightMode.php create mode 100644 library/Icinga/Less/LightModeCall.php create mode 100644 library/Icinga/Less/LightModeDefinition.php create mode 100644 library/Icinga/Less/LightModeTrait.php create mode 100644 library/Icinga/Less/LightModeVisitor.php create mode 100644 library/Icinga/Less/Visitor.php create mode 100644 library/Icinga/Protocol/Dns.php create mode 100644 library/Icinga/Protocol/File/Exception/FileReaderException.php create mode 100644 library/Icinga/Protocol/File/FileIterator.php create mode 100644 library/Icinga/Protocol/File/FileQuery.php create mode 100644 library/Icinga/Protocol/File/FileReader.php create mode 100644 library/Icinga/Protocol/File/LogFileIterator.php create mode 100644 library/Icinga/Protocol/Ldap/Discovery.php create mode 100644 library/Icinga/Protocol/Ldap/LdapCapabilities.php create mode 100644 library/Icinga/Protocol/Ldap/LdapConnection.php create mode 100644 library/Icinga/Protocol/Ldap/LdapException.php create mode 100644 library/Icinga/Protocol/Ldap/LdapQuery.php create mode 100644 library/Icinga/Protocol/Ldap/LdapUtils.php create mode 100644 library/Icinga/Protocol/Ldap/Node.php create mode 100644 library/Icinga/Protocol/Ldap/Root.php create mode 100644 library/Icinga/Protocol/Nrpe/Connection.php create mode 100644 library/Icinga/Protocol/Nrpe/Packet.php create mode 100644 library/Icinga/Repository/DbRepository.php create mode 100644 library/Icinga/Repository/IniRepository.php create mode 100644 library/Icinga/Repository/LdapRepository.php create mode 100644 library/Icinga/Repository/Repository.php create mode 100644 library/Icinga/Repository/RepositoryQuery.php create mode 100644 library/Icinga/Security/SecurityException.php create mode 100644 library/Icinga/Test/BaseTestCase.php create mode 100644 library/Icinga/Test/ClassLoader.php create mode 100644 library/Icinga/Test/DbTest.php create mode 100644 library/Icinga/User.php create mode 100644 library/Icinga/User/Preferences.php create mode 100644 library/Icinga/User/Preferences/PreferencesStore.php create mode 100644 library/Icinga/Util/ASN1.php create mode 100644 library/Icinga/Util/Color.php create mode 100644 library/Icinga/Util/ConfigAwareFactory.php create mode 100644 library/Icinga/Util/Dimension.php create mode 100644 library/Icinga/Util/DirectoryIterator.php create mode 100644 library/Icinga/Util/EnumeratingFilterIterator.php create mode 100644 library/Icinga/Util/Environment.php create mode 100644 library/Icinga/Util/File.php create mode 100644 library/Icinga/Util/Format.php create mode 100644 library/Icinga/Util/GlobFilter.php create mode 100644 library/Icinga/Util/Json.php create mode 100644 library/Icinga/Util/LessParser.php create mode 100644 library/Icinga/Util/StringHelper.php create mode 100644 library/Icinga/Util/TimezoneDetect.php create mode 100644 library/Icinga/Web/Announcement.php create mode 100644 library/Icinga/Web/Announcement/AnnouncementCookie.php create mode 100644 library/Icinga/Web/Announcement/AnnouncementIniRepository.php create mode 100644 library/Icinga/Web/ApplicationStateCookie.php create mode 100644 library/Icinga/Web/Controller.php create mode 100644 library/Icinga/Web/Controller/ActionController.php create mode 100644 library/Icinga/Web/Controller/AuthBackendController.php create mode 100644 library/Icinga/Web/Controller/BasePreferenceController.php create mode 100644 library/Icinga/Web/Controller/ControllerTabCollector.php create mode 100644 library/Icinga/Web/Controller/Dispatcher.php create mode 100644 library/Icinga/Web/Controller/ModuleActionController.php create mode 100644 library/Icinga/Web/Controller/StaticController.php create mode 100644 library/Icinga/Web/Cookie.php create mode 100644 library/Icinga/Web/CookieSet.php create mode 100644 library/Icinga/Web/Dom/DomNodeIterator.php create mode 100644 library/Icinga/Web/FileCache.php create mode 100644 library/Icinga/Web/Form.php create mode 100644 library/Icinga/Web/Form/Decorator/Autosubmit.php create mode 100644 library/Icinga/Web/Form/Decorator/ConditionalHidden.php create mode 100644 library/Icinga/Web/Form/Decorator/ElementDoubler.php create mode 100644 library/Icinga/Web/Form/Decorator/FormDescriptions.php create mode 100644 library/Icinga/Web/Form/Decorator/FormHints.php create mode 100644 library/Icinga/Web/Form/Decorator/FormNotifications.php create mode 100644 library/Icinga/Web/Form/Decorator/Help.php create mode 100644 library/Icinga/Web/Form/Decorator/Spinner.php create mode 100644 library/Icinga/Web/Form/Element/Button.php create mode 100644 library/Icinga/Web/Form/Element/Checkbox.php create mode 100644 library/Icinga/Web/Form/Element/CsrfCounterMeasure.php create mode 100644 library/Icinga/Web/Form/Element/Date.php create mode 100644 library/Icinga/Web/Form/Element/DateTimePicker.php create mode 100644 library/Icinga/Web/Form/Element/Note.php create mode 100644 library/Icinga/Web/Form/Element/Number.php create mode 100644 library/Icinga/Web/Form/Element/Textarea.php create mode 100644 library/Icinga/Web/Form/Element/Time.php create mode 100644 library/Icinga/Web/Form/ErrorLabeller.php create mode 100644 library/Icinga/Web/Form/FormElement.php create mode 100644 library/Icinga/Web/Form/InvalidCSRFTokenException.php create mode 100644 library/Icinga/Web/Form/Validator/DateFormatValidator.php create mode 100644 library/Icinga/Web/Form/Validator/DateTimeValidator.php create mode 100644 library/Icinga/Web/Form/Validator/InArray.php create mode 100644 library/Icinga/Web/Form/Validator/InternalUrlValidator.php create mode 100644 library/Icinga/Web/Form/Validator/ReadablePathValidator.php create mode 100644 library/Icinga/Web/Form/Validator/TimeFormatValidator.php create mode 100644 library/Icinga/Web/Form/Validator/UrlValidator.php create mode 100644 library/Icinga/Web/Form/Validator/WritablePathValidator.php create mode 100644 library/Icinga/Web/Helper/CookieHelper.php create mode 100644 library/Icinga/Web/Helper/HtmlPurifier.php create mode 100644 library/Icinga/Web/Helper/Markdown.php create mode 100644 library/Icinga/Web/Helper/Markdown/LinkTransformer.php create mode 100644 library/Icinga/Web/Hook.php create mode 100644 library/Icinga/Web/JavaScript.php create mode 100644 library/Icinga/Web/LessCompiler.php create mode 100644 library/Icinga/Web/Menu.php create mode 100644 library/Icinga/Web/Navigation/ConfigMenu.php create mode 100644 library/Icinga/Web/Navigation/DashboardPane.php create mode 100644 library/Icinga/Web/Navigation/DropdownItem.php create mode 100644 library/Icinga/Web/Navigation/Navigation.php create mode 100644 library/Icinga/Web/Navigation/NavigationItem.php create mode 100644 library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php create mode 100644 library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php create mode 100644 library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php create mode 100644 library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php create mode 100644 library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php create mode 100644 library/Icinga/Web/Navigation/Renderer/RecursiveNavigationRenderer.php create mode 100644 library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php create mode 100644 library/Icinga/Web/Notification.php create mode 100644 library/Icinga/Web/Paginator/Adapter/QueryAdapter.php create mode 100644 library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php create mode 100644 library/Icinga/Web/RememberMe.php create mode 100644 library/Icinga/Web/RememberMeUserDevicesList.php create mode 100644 library/Icinga/Web/RememberMeUserList.php create mode 100644 library/Icinga/Web/Request.php create mode 100644 library/Icinga/Web/Response.php create mode 100644 library/Icinga/Web/Response/JsonResponse.php create mode 100644 library/Icinga/Web/Session.php create mode 100644 library/Icinga/Web/Session/Php72Session.php create mode 100644 library/Icinga/Web/Session/PhpSession.php create mode 100644 library/Icinga/Web/Session/Session.php create mode 100644 library/Icinga/Web/Session/SessionNamespace.php create mode 100644 library/Icinga/Web/StyleSheet.php create mode 100644 library/Icinga/Web/Url.php create mode 100644 library/Icinga/Web/UrlParams.php create mode 100644 library/Icinga/Web/UserAgent.php create mode 100644 library/Icinga/Web/View.php create mode 100644 library/Icinga/Web/View/AppHealth.php create mode 100644 library/Icinga/Web/View/Helper/IcingaCheckbox.php create mode 100644 library/Icinga/Web/View/PrivilegeAudit.php create mode 100644 library/Icinga/Web/View/helpers/format.php create mode 100644 library/Icinga/Web/View/helpers/generic.php create mode 100644 library/Icinga/Web/View/helpers/string.php create mode 100644 library/Icinga/Web/View/helpers/url.php create mode 100644 library/Icinga/Web/Widget.php create mode 100644 library/Icinga/Web/Widget/AbstractWidget.php create mode 100644 library/Icinga/Web/Widget/Announcements.php create mode 100644 library/Icinga/Web/Widget/ApplicationStateMessages.php create mode 100644 library/Icinga/Web/Widget/Chart/HistoryColorGrid.php create mode 100644 library/Icinga/Web/Widget/Chart/InlinePie.php create mode 100644 library/Icinga/Web/Widget/Dashboard.php create mode 100644 library/Icinga/Web/Widget/Dashboard/Dashlet.php create mode 100644 library/Icinga/Web/Widget/Dashboard/Pane.php create mode 100644 library/Icinga/Web/Widget/Dashboard/UserWidget.php create mode 100644 library/Icinga/Web/Widget/FilterEditor.php create mode 100644 library/Icinga/Web/Widget/FilterWidget.php create mode 100644 library/Icinga/Web/Widget/Limiter.php create mode 100644 library/Icinga/Web/Widget/Paginator.php create mode 100644 library/Icinga/Web/Widget/SearchDashboard.php create mode 100644 library/Icinga/Web/Widget/SingleValueSearchControl.php create mode 100644 library/Icinga/Web/Widget/SortBox.php create mode 100644 library/Icinga/Web/Widget/Tab.php create mode 100644 library/Icinga/Web/Widget/Tabextension/DashboardAction.php create mode 100644 library/Icinga/Web/Widget/Tabextension/DashboardSettings.php create mode 100644 library/Icinga/Web/Widget/Tabextension/MenuAction.php create mode 100644 library/Icinga/Web/Widget/Tabextension/OutputFormat.php create mode 100644 library/Icinga/Web/Widget/Tabextension/Tabextension.php create mode 100644 library/Icinga/Web/Widget/Tabs.php create mode 100644 library/Icinga/Web/Widget/Widget.php create mode 100644 library/Icinga/Web/Window.php create mode 100644 library/Icinga/Web/Wizard.php create mode 100644 library/vendor/HTMLPurifier.autoload.php create mode 100644 library/vendor/HTMLPurifier.php create mode 100644 library/vendor/HTMLPurifier/Arborize.php create mode 100644 library/vendor/HTMLPurifier/AttrCollections.php create mode 100644 library/vendor/HTMLPurifier/AttrDef.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/AlphaValue.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Background.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Border.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Color.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Composite.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Filter.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Font.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/FontFamily.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Ident.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/ImportantDecorator.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Length.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/ListStyle.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Multiple.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Number.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/Percentage.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/TextDecoration.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/CSS/URI.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/Clone.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/Enum.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/Bool.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/Class.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/Color.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/ContentEditable.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/FrameTarget.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/ID.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/Length.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/LinkTypes.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/MultiLength.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/Nmtokens.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/HTML/Pixels.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/Integer.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/Lang.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/Switch.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/Text.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/URI.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/URI/Email.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/URI/Email/SimpleCheck.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/URI/Host.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/URI/IPv4.php create mode 100644 library/vendor/HTMLPurifier/AttrDef/URI/IPv6.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/Background.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/BdoDir.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/BgColor.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/BoolToCSS.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/Border.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/EnumToCSS.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/ImgRequired.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/ImgSpace.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/Input.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/Lang.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/Length.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/Name.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/NameSync.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/Nofollow.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/SafeEmbed.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/SafeObject.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/SafeParam.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/ScriptRequired.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/TargetBlank.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/TargetNoopener.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/TargetNoreferrer.php create mode 100644 library/vendor/HTMLPurifier/AttrTransform/Textarea.php create mode 100644 library/vendor/HTMLPurifier/AttrTypes.php create mode 100644 library/vendor/HTMLPurifier/AttrValidator.php create mode 100644 library/vendor/HTMLPurifier/Bootstrap.php create mode 100644 library/vendor/HTMLPurifier/CSSDefinition.php create mode 100644 library/vendor/HTMLPurifier/ChildDef.php create mode 100644 library/vendor/HTMLPurifier/ChildDef/Chameleon.php create mode 100644 library/vendor/HTMLPurifier/ChildDef/Custom.php create mode 100644 library/vendor/HTMLPurifier/ChildDef/Empty.php create mode 100644 library/vendor/HTMLPurifier/ChildDef/List.php create mode 100644 library/vendor/HTMLPurifier/ChildDef/Optional.php create mode 100644 library/vendor/HTMLPurifier/ChildDef/Required.php create mode 100644 library/vendor/HTMLPurifier/ChildDef/StrictBlockquote.php create mode 100644 library/vendor/HTMLPurifier/ChildDef/Table.php create mode 100644 library/vendor/HTMLPurifier/Config.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/Builder/Xml.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/Exception.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/Interchange.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/Interchange/Directive.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/Interchange/Id.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/InterchangeBuilder.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/Validator.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/ValidatorAtom.php create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema.ser create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.Language.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Base.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Host.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt create mode 100644 library/vendor/HTMLPurifier/ConfigSchema/schema/info.ini create mode 100644 library/vendor/HTMLPurifier/ContentSets.php create mode 100644 library/vendor/HTMLPurifier/Context.php create mode 100644 library/vendor/HTMLPurifier/Definition.php create mode 100644 library/vendor/HTMLPurifier/DefinitionCache.php create mode 100644 library/vendor/HTMLPurifier/DefinitionCache/Decorator.php create mode 100644 library/vendor/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php create mode 100644 library/vendor/HTMLPurifier/DefinitionCache/Decorator/Memory.php create mode 100644 library/vendor/HTMLPurifier/DefinitionCache/Decorator/Template.php.in create mode 100644 library/vendor/HTMLPurifier/DefinitionCache/Null.php create mode 100644 library/vendor/HTMLPurifier/DefinitionCache/Serializer.php create mode 100644 library/vendor/HTMLPurifier/DefinitionCache/Serializer/README create mode 100644 library/vendor/HTMLPurifier/DefinitionCacheFactory.php create mode 100644 library/vendor/HTMLPurifier/Doctype.php create mode 100644 library/vendor/HTMLPurifier/DoctypeRegistry.php create mode 100644 library/vendor/HTMLPurifier/ElementDef.php create mode 100644 library/vendor/HTMLPurifier/Encoder.php create mode 100644 library/vendor/HTMLPurifier/EntityLookup.php create mode 100644 library/vendor/HTMLPurifier/EntityLookup/entities.ser create mode 100644 library/vendor/HTMLPurifier/EntityParser.php create mode 100644 library/vendor/HTMLPurifier/ErrorCollector.php create mode 100644 library/vendor/HTMLPurifier/ErrorStruct.php create mode 100644 library/vendor/HTMLPurifier/Exception.php create mode 100644 library/vendor/HTMLPurifier/Filter.php create mode 100644 library/vendor/HTMLPurifier/Filter/ExtractStyleBlocks.php create mode 100644 library/vendor/HTMLPurifier/Filter/YouTube.php create mode 100644 library/vendor/HTMLPurifier/Generator.php create mode 100644 library/vendor/HTMLPurifier/HTMLDefinition.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Bdo.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/CommonAttributes.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Edit.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Forms.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Hypertext.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Iframe.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Image.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Legacy.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/List.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Name.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Nofollow.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Object.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Presentation.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Proprietary.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Ruby.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/SafeEmbed.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/SafeObject.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/SafeScripting.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Scripting.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/StyleAttribute.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Tables.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Target.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/TargetBlank.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/TargetNoopener.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/TargetNoreferrer.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Text.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Tidy.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Tidy/Name.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Tidy/Proprietary.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Tidy/Strict.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Tidy/Transitional.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Tidy/XHTML.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/Tidy/XHTMLAndHTML4.php create mode 100644 library/vendor/HTMLPurifier/HTMLModule/XMLCommonAttributes.php create mode 100644 library/vendor/HTMLPurifier/HTMLModuleManager.php create mode 100644 library/vendor/HTMLPurifier/IDAccumulator.php create mode 100644 library/vendor/HTMLPurifier/Injector.php create mode 100644 library/vendor/HTMLPurifier/Injector/AutoParagraph.php create mode 100644 library/vendor/HTMLPurifier/Injector/DisplayLinkURI.php create mode 100644 library/vendor/HTMLPurifier/Injector/Linkify.php create mode 100644 library/vendor/HTMLPurifier/Injector/PurifierLinkify.php create mode 100644 library/vendor/HTMLPurifier/Injector/RemoveEmpty.php create mode 100644 library/vendor/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php create mode 100644 library/vendor/HTMLPurifier/Injector/SafeObject.php create mode 100644 library/vendor/HTMLPurifier/LICENSE create mode 100644 library/vendor/HTMLPurifier/Language.php create mode 100644 library/vendor/HTMLPurifier/Language/messages/en.php create mode 100644 library/vendor/HTMLPurifier/LanguageFactory.php create mode 100644 library/vendor/HTMLPurifier/Length.php create mode 100644 library/vendor/HTMLPurifier/Lexer.php create mode 100644 library/vendor/HTMLPurifier/Lexer/DOMLex.php create mode 100644 library/vendor/HTMLPurifier/Lexer/DirectLex.php create mode 100644 library/vendor/HTMLPurifier/Lexer/PH5P.php create mode 100644 library/vendor/HTMLPurifier/Node.php create mode 100644 library/vendor/HTMLPurifier/Node/Comment.php create mode 100644 library/vendor/HTMLPurifier/Node/Element.php create mode 100644 library/vendor/HTMLPurifier/Node/Text.php create mode 100644 library/vendor/HTMLPurifier/PercentEncoder.php create mode 100644 library/vendor/HTMLPurifier/Printer.php create mode 100644 library/vendor/HTMLPurifier/Printer/CSSDefinition.php create mode 100644 library/vendor/HTMLPurifier/Printer/ConfigForm.css create mode 100644 library/vendor/HTMLPurifier/Printer/ConfigForm.js create mode 100644 library/vendor/HTMLPurifier/Printer/ConfigForm.php create mode 100644 library/vendor/HTMLPurifier/Printer/HTMLDefinition.php create mode 100644 library/vendor/HTMLPurifier/PropertyList.php create mode 100644 library/vendor/HTMLPurifier/PropertyListIterator.php create mode 100644 library/vendor/HTMLPurifier/Queue.php create mode 100644 library/vendor/HTMLPurifier/SOURCE create mode 100644 library/vendor/HTMLPurifier/Strategy.php create mode 100644 library/vendor/HTMLPurifier/Strategy/Composite.php create mode 100644 library/vendor/HTMLPurifier/Strategy/Core.php create mode 100644 library/vendor/HTMLPurifier/Strategy/FixNesting.php create mode 100644 library/vendor/HTMLPurifier/Strategy/MakeWellFormed.php create mode 100644 library/vendor/HTMLPurifier/Strategy/RemoveForeignElements.php create mode 100644 library/vendor/HTMLPurifier/Strategy/ValidateAttributes.php create mode 100644 library/vendor/HTMLPurifier/StringHash.php create mode 100644 library/vendor/HTMLPurifier/StringHashParser.php create mode 100644 library/vendor/HTMLPurifier/TagTransform.php create mode 100644 library/vendor/HTMLPurifier/TagTransform/Font.php create mode 100644 library/vendor/HTMLPurifier/TagTransform/Simple.php create mode 100644 library/vendor/HTMLPurifier/Token.php create mode 100644 library/vendor/HTMLPurifier/Token/Comment.php create mode 100644 library/vendor/HTMLPurifier/Token/Empty.php create mode 100644 library/vendor/HTMLPurifier/Token/End.php create mode 100644 library/vendor/HTMLPurifier/Token/Start.php create mode 100644 library/vendor/HTMLPurifier/Token/Tag.php create mode 100644 library/vendor/HTMLPurifier/Token/Text.php create mode 100644 library/vendor/HTMLPurifier/TokenFactory.php create mode 100644 library/vendor/HTMLPurifier/URI.php create mode 100644 library/vendor/HTMLPurifier/URIDefinition.php create mode 100644 library/vendor/HTMLPurifier/URIFilter.php create mode 100644 library/vendor/HTMLPurifier/URIFilter/DisableExternal.php create mode 100644 library/vendor/HTMLPurifier/URIFilter/DisableExternalResources.php create mode 100644 library/vendor/HTMLPurifier/URIFilter/DisableResources.php create mode 100644 library/vendor/HTMLPurifier/URIFilter/HostBlacklist.php create mode 100644 library/vendor/HTMLPurifier/URIFilter/MakeAbsolute.php create mode 100644 library/vendor/HTMLPurifier/URIFilter/Munge.php create mode 100644 library/vendor/HTMLPurifier/URIFilter/SafeIframe.php create mode 100644 library/vendor/HTMLPurifier/URIParser.php create mode 100644 library/vendor/HTMLPurifier/URIScheme.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/data.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/file.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/ftp.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/http.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/https.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/mailto.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/news.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/nntp.php create mode 100644 library/vendor/HTMLPurifier/URIScheme/tel.php create mode 100644 library/vendor/HTMLPurifier/URISchemeRegistry.php create mode 100644 library/vendor/HTMLPurifier/UnitConverter.php create mode 100644 library/vendor/HTMLPurifier/VERSION create mode 100644 library/vendor/HTMLPurifier/VarParser.php create mode 100644 library/vendor/HTMLPurifier/VarParser/Flexible.php create mode 100644 library/vendor/HTMLPurifier/VarParser/Native.php create mode 100644 library/vendor/HTMLPurifier/VarParserException.php create mode 100644 library/vendor/HTMLPurifier/Zipper.php create mode 100644 library/vendor/JShrink/LICENSE create mode 100644 library/vendor/JShrink/Minifier.php create mode 100644 library/vendor/JShrink/SOURCE create mode 100644 library/vendor/Parsedown/LICENSE create mode 100644 library/vendor/Parsedown/Parsedown.php create mode 100644 library/vendor/Parsedown/SOURCE create mode 100644 library/vendor/Zend/Cache.php create mode 100644 library/vendor/Zend/Cache/Backend.php create mode 100644 library/vendor/Zend/Cache/Backend/Apc.php create mode 100644 library/vendor/Zend/Cache/Backend/BlackHole.php create mode 100644 library/vendor/Zend/Cache/Backend/ExtendedInterface.php create mode 100644 library/vendor/Zend/Cache/Backend/File.php create mode 100644 library/vendor/Zend/Cache/Backend/Interface.php create mode 100644 library/vendor/Zend/Cache/Backend/Libmemcached.php create mode 100644 library/vendor/Zend/Cache/Backend/Memcached.php create mode 100644 library/vendor/Zend/Cache/Backend/Sqlite.php create mode 100644 library/vendor/Zend/Cache/Backend/Static.php create mode 100644 library/vendor/Zend/Cache/Backend/Test.php create mode 100644 library/vendor/Zend/Cache/Backend/TwoLevels.php create mode 100644 library/vendor/Zend/Cache/Backend/WinCache.php create mode 100644 library/vendor/Zend/Cache/Backend/Xcache.php create mode 100644 library/vendor/Zend/Cache/Backend/ZendPlatform.php create mode 100644 library/vendor/Zend/Cache/Backend/ZendServer.php create mode 100644 library/vendor/Zend/Cache/Backend/ZendServer/Disk.php create mode 100644 library/vendor/Zend/Cache/Backend/ZendServer/ShMem.php create mode 100644 library/vendor/Zend/Cache/Core.php create mode 100644 library/vendor/Zend/Cache/Exception.php create mode 100644 library/vendor/Zend/Cache/Frontend/Capture.php create mode 100644 library/vendor/Zend/Cache/Frontend/Class.php create mode 100644 library/vendor/Zend/Cache/Frontend/File.php create mode 100644 library/vendor/Zend/Cache/Frontend/Function.php create mode 100644 library/vendor/Zend/Cache/Frontend/Output.php create mode 100644 library/vendor/Zend/Cache/Frontend/Page.php create mode 100644 library/vendor/Zend/Cache/Manager.php create mode 100644 library/vendor/Zend/Config.php create mode 100644 library/vendor/Zend/Config/Exception.php create mode 100644 library/vendor/Zend/Config/Ini.php create mode 100644 library/vendor/Zend/Config/Writer.php create mode 100644 library/vendor/Zend/Config/Writer/Array.php create mode 100644 library/vendor/Zend/Config/Writer/FileAbstract.php create mode 100644 library/vendor/Zend/Config/Writer/Ini.php create mode 100644 library/vendor/Zend/Controller/Action.php create mode 100644 library/vendor/Zend/Controller/Action/Exception.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/Abstract.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/ActionStack.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/AjaxContext.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/AutoComplete/Abstract.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/Cache.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/ContextSwitch.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/Json.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/Redirector.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/Url.php create mode 100644 library/vendor/Zend/Controller/Action/Helper/ViewRenderer.php create mode 100644 library/vendor/Zend/Controller/Action/HelperBroker.php create mode 100644 library/vendor/Zend/Controller/Action/HelperBroker/PriorityStack.php create mode 100644 library/vendor/Zend/Controller/Action/Interface.php create mode 100644 library/vendor/Zend/Controller/Dispatcher/Abstract.php create mode 100644 library/vendor/Zend/Controller/Dispatcher/Exception.php create mode 100644 library/vendor/Zend/Controller/Dispatcher/Interface.php create mode 100644 library/vendor/Zend/Controller/Dispatcher/Standard.php create mode 100644 library/vendor/Zend/Controller/Exception.php create mode 100644 library/vendor/Zend/Controller/Front.php create mode 100644 library/vendor/Zend/Controller/Plugin/Abstract.php create mode 100644 library/vendor/Zend/Controller/Plugin/ActionStack.php create mode 100644 library/vendor/Zend/Controller/Plugin/Broker.php create mode 100644 library/vendor/Zend/Controller/Plugin/ErrorHandler.php create mode 100644 library/vendor/Zend/Controller/Plugin/PutHandler.php create mode 100644 library/vendor/Zend/Controller/Request/Abstract.php create mode 100644 library/vendor/Zend/Controller/Request/Apache404.php create mode 100644 library/vendor/Zend/Controller/Request/Exception.php create mode 100644 library/vendor/Zend/Controller/Request/Http.php create mode 100644 library/vendor/Zend/Controller/Request/HttpTestCase.php create mode 100644 library/vendor/Zend/Controller/Request/Simple.php create mode 100644 library/vendor/Zend/Controller/Response/Abstract.php create mode 100644 library/vendor/Zend/Controller/Response/Cli.php create mode 100644 library/vendor/Zend/Controller/Response/Exception.php create mode 100644 library/vendor/Zend/Controller/Response/Http.php create mode 100644 library/vendor/Zend/Controller/Response/HttpTestCase.php create mode 100644 library/vendor/Zend/Controller/Router/Abstract.php create mode 100644 library/vendor/Zend/Controller/Router/Exception.php create mode 100644 library/vendor/Zend/Controller/Router/Interface.php create mode 100644 library/vendor/Zend/Controller/Router/Rewrite.php create mode 100644 library/vendor/Zend/Controller/Router/Route.php create mode 100644 library/vendor/Zend/Controller/Router/Route/Abstract.php create mode 100644 library/vendor/Zend/Controller/Router/Route/Chain.php create mode 100644 library/vendor/Zend/Controller/Router/Route/Hostname.php create mode 100644 library/vendor/Zend/Controller/Router/Route/Interface.php create mode 100644 library/vendor/Zend/Controller/Router/Route/Module.php create mode 100644 library/vendor/Zend/Controller/Router/Route/Regex.php create mode 100644 library/vendor/Zend/Controller/Router/Route/Static.php create mode 100644 library/vendor/Zend/Crypt.php create mode 100644 library/vendor/Zend/Crypt/DiffieHellman.php create mode 100644 library/vendor/Zend/Crypt/DiffieHellman/Exception.php create mode 100644 library/vendor/Zend/Crypt/Exception.php create mode 100644 library/vendor/Zend/Crypt/Hmac.php create mode 100644 library/vendor/Zend/Crypt/Hmac/Exception.php create mode 100644 library/vendor/Zend/Crypt/Math.php create mode 100644 library/vendor/Zend/Crypt/Math/BigInteger.php create mode 100644 library/vendor/Zend/Crypt/Math/BigInteger/Bcmath.php create mode 100644 library/vendor/Zend/Crypt/Math/BigInteger/Exception.php create mode 100644 library/vendor/Zend/Crypt/Math/BigInteger/Gmp.php create mode 100644 library/vendor/Zend/Crypt/Math/BigInteger/Interface.php create mode 100644 library/vendor/Zend/Crypt/Math/Exception.php create mode 100644 library/vendor/Zend/Crypt/Rsa.php create mode 100644 library/vendor/Zend/Crypt/Rsa/Exception.php create mode 100644 library/vendor/Zend/Crypt/Rsa/Key.php create mode 100644 library/vendor/Zend/Crypt/Rsa/Key/Private.php create mode 100644 library/vendor/Zend/Crypt/Rsa/Key/Public.php create mode 100644 library/vendor/Zend/Date.php create mode 100644 library/vendor/Zend/Date/Cities.php create mode 100644 library/vendor/Zend/Date/DateObject.php create mode 100644 library/vendor/Zend/Date/Exception.php create mode 100644 library/vendor/Zend/Db.php create mode 100644 library/vendor/Zend/Db/Adapter/Abstract.php create mode 100644 library/vendor/Zend/Db/Adapter/Db2.php create mode 100644 library/vendor/Zend/Db/Adapter/Db2/Exception.php create mode 100644 library/vendor/Zend/Db/Adapter/Exception.php create mode 100644 library/vendor/Zend/Db/Adapter/Mysqli.php create mode 100644 library/vendor/Zend/Db/Adapter/Mysqli/Exception.php create mode 100644 library/vendor/Zend/Db/Adapter/Oracle.php create mode 100644 library/vendor/Zend/Db/Adapter/Oracle/Exception.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Abstract.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Ibm.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Ibm/Db2.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Ibm/Ids.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Mssql.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Mysql.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Oci.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Pgsql.php create mode 100644 library/vendor/Zend/Db/Adapter/Pdo/Sqlite.php create mode 100644 library/vendor/Zend/Db/Adapter/Sqlsrv.php create mode 100644 library/vendor/Zend/Db/Adapter/Sqlsrv/Exception.php create mode 100644 library/vendor/Zend/Db/Exception.php create mode 100644 library/vendor/Zend/Db/Expr.php create mode 100644 library/vendor/Zend/Db/Profiler.php create mode 100644 library/vendor/Zend/Db/Profiler/Exception.php create mode 100644 library/vendor/Zend/Db/Profiler/Query.php create mode 100644 library/vendor/Zend/Db/Select.php create mode 100644 library/vendor/Zend/Db/Select/Exception.php create mode 100644 library/vendor/Zend/Db/Statement.php create mode 100644 library/vendor/Zend/Db/Statement/Db2.php create mode 100644 library/vendor/Zend/Db/Statement/Db2/Exception.php create mode 100644 library/vendor/Zend/Db/Statement/Exception.php create mode 100644 library/vendor/Zend/Db/Statement/Interface.php create mode 100644 library/vendor/Zend/Db/Statement/Mysqli.php create mode 100644 library/vendor/Zend/Db/Statement/Mysqli/Exception.php create mode 100644 library/vendor/Zend/Db/Statement/Oracle.php create mode 100644 library/vendor/Zend/Db/Statement/Oracle/Exception.php create mode 100644 library/vendor/Zend/Db/Statement/Pdo.php create mode 100644 library/vendor/Zend/Db/Statement/Pdo/Ibm.php create mode 100644 library/vendor/Zend/Db/Statement/Pdo/Oci.php create mode 100644 library/vendor/Zend/Db/Statement/Sqlsrv.php create mode 100644 library/vendor/Zend/Db/Statement/Sqlsrv/Exception.php create mode 100644 library/vendor/Zend/Db/Table.php create mode 100644 library/vendor/Zend/Db/Table/Abstract.php create mode 100644 library/vendor/Zend/Db/Table/Definition.php create mode 100644 library/vendor/Zend/Db/Table/Exception.php create mode 100644 library/vendor/Zend/Db/Table/Row.php create mode 100644 library/vendor/Zend/Db/Table/Row/Abstract.php create mode 100644 library/vendor/Zend/Db/Table/Row/Exception.php create mode 100644 library/vendor/Zend/Db/Table/Rowset.php create mode 100644 library/vendor/Zend/Db/Table/Rowset/Abstract.php create mode 100644 library/vendor/Zend/Db/Table/Rowset/Exception.php create mode 100644 library/vendor/Zend/Db/Table/Select.php create mode 100644 library/vendor/Zend/Db/Table/Select/Exception.php create mode 100644 library/vendor/Zend/Exception.php create mode 100644 library/vendor/Zend/File/ClassFileLocator.php create mode 100644 library/vendor/Zend/File/PhpClassFile.php create mode 100644 library/vendor/Zend/File/Transfer.php create mode 100644 library/vendor/Zend/File/Transfer/Adapter/Abstract.php create mode 100644 library/vendor/Zend/File/Transfer/Adapter/Http.php create mode 100644 library/vendor/Zend/File/Transfer/Exception.php create mode 100644 library/vendor/Zend/Filter.php create mode 100644 library/vendor/Zend/Filter/Alnum.php create mode 100644 library/vendor/Zend/Filter/Alpha.php create mode 100644 library/vendor/Zend/Filter/BaseName.php create mode 100644 library/vendor/Zend/Filter/Boolean.php create mode 100644 library/vendor/Zend/Filter/Callback.php create mode 100644 library/vendor/Zend/Filter/Compress.php create mode 100644 library/vendor/Zend/Filter/Compress/Bz2.php create mode 100644 library/vendor/Zend/Filter/Compress/CompressAbstract.php create mode 100644 library/vendor/Zend/Filter/Compress/CompressInterface.php create mode 100644 library/vendor/Zend/Filter/Compress/Gz.php create mode 100644 library/vendor/Zend/Filter/Compress/Lzf.php create mode 100644 library/vendor/Zend/Filter/Compress/Rar.php create mode 100644 library/vendor/Zend/Filter/Compress/Tar.php create mode 100644 library/vendor/Zend/Filter/Compress/Zip.php create mode 100644 library/vendor/Zend/Filter/Decompress.php create mode 100644 library/vendor/Zend/Filter/Decrypt.php create mode 100644 library/vendor/Zend/Filter/Digits.php create mode 100644 library/vendor/Zend/Filter/Dir.php create mode 100644 library/vendor/Zend/Filter/Encrypt.php create mode 100644 library/vendor/Zend/Filter/Encrypt/Interface.php create mode 100644 library/vendor/Zend/Filter/Encrypt/Mcrypt.php create mode 100644 library/vendor/Zend/Filter/Encrypt/Openssl.php create mode 100644 library/vendor/Zend/Filter/Exception.php create mode 100644 library/vendor/Zend/Filter/File/Decrypt.php create mode 100644 library/vendor/Zend/Filter/File/Encrypt.php create mode 100644 library/vendor/Zend/Filter/File/LowerCase.php create mode 100644 library/vendor/Zend/Filter/File/Rename.php create mode 100644 library/vendor/Zend/Filter/File/UpperCase.php create mode 100644 library/vendor/Zend/Filter/HtmlEntities.php create mode 100644 library/vendor/Zend/Filter/Inflector.php create mode 100644 library/vendor/Zend/Filter/Input.php create mode 100644 library/vendor/Zend/Filter/Int.php create mode 100644 library/vendor/Zend/Filter/Interface.php create mode 100644 library/vendor/Zend/Filter/LocalizedToNormalized.php create mode 100644 library/vendor/Zend/Filter/NormalizedToLocalized.php create mode 100644 library/vendor/Zend/Filter/Null.php create mode 100644 library/vendor/Zend/Filter/PregReplace.php create mode 100644 library/vendor/Zend/Filter/RealPath.php create mode 100644 library/vendor/Zend/Filter/StringToLower.php create mode 100644 library/vendor/Zend/Filter/StringToUpper.php create mode 100644 library/vendor/Zend/Filter/StringTrim.php create mode 100644 library/vendor/Zend/Filter/StripNewlines.php create mode 100644 library/vendor/Zend/Filter/StripTags.php create mode 100644 library/vendor/Zend/Filter/Word/CamelCaseToDash.php create mode 100644 library/vendor/Zend/Filter/Word/CamelCaseToSeparator.php create mode 100644 library/vendor/Zend/Filter/Word/CamelCaseToUnderscore.php create mode 100644 library/vendor/Zend/Filter/Word/DashToCamelCase.php create mode 100644 library/vendor/Zend/Filter/Word/DashToSeparator.php create mode 100644 library/vendor/Zend/Filter/Word/DashToUnderscore.php create mode 100644 library/vendor/Zend/Filter/Word/Separator/Abstract.php create mode 100644 library/vendor/Zend/Filter/Word/SeparatorToCamelCase.php create mode 100644 library/vendor/Zend/Filter/Word/SeparatorToDash.php create mode 100644 library/vendor/Zend/Filter/Word/SeparatorToSeparator.php create mode 100644 library/vendor/Zend/Filter/Word/UnderscoreToCamelCase.php create mode 100644 library/vendor/Zend/Filter/Word/UnderscoreToDash.php create mode 100644 library/vendor/Zend/Filter/Word/UnderscoreToSeparator.php create mode 100644 library/vendor/Zend/Form.php create mode 100644 library/vendor/Zend/Form/Decorator/Abstract.php create mode 100644 library/vendor/Zend/Form/Decorator/Callback.php create mode 100644 library/vendor/Zend/Form/Decorator/Captcha.php create mode 100644 library/vendor/Zend/Form/Decorator/Description.php create mode 100644 library/vendor/Zend/Form/Decorator/DtDdWrapper.php create mode 100644 library/vendor/Zend/Form/Decorator/Errors.php create mode 100644 library/vendor/Zend/Form/Decorator/Exception.php create mode 100644 library/vendor/Zend/Form/Decorator/Fieldset.php create mode 100644 library/vendor/Zend/Form/Decorator/File.php create mode 100644 library/vendor/Zend/Form/Decorator/Form.php create mode 100644 library/vendor/Zend/Form/Decorator/FormElements.php create mode 100644 library/vendor/Zend/Form/Decorator/FormErrors.php create mode 100644 library/vendor/Zend/Form/Decorator/HtmlTag.php create mode 100644 library/vendor/Zend/Form/Decorator/Image.php create mode 100644 library/vendor/Zend/Form/Decorator/Interface.php create mode 100644 library/vendor/Zend/Form/Decorator/Label.php create mode 100644 library/vendor/Zend/Form/Decorator/Marker/File/Interface.php create mode 100644 library/vendor/Zend/Form/Decorator/PrepareElements.php create mode 100644 library/vendor/Zend/Form/Decorator/Tooltip.php create mode 100644 library/vendor/Zend/Form/Decorator/ViewHelper.php create mode 100644 library/vendor/Zend/Form/Decorator/ViewScript.php create mode 100644 library/vendor/Zend/Form/DisplayGroup.php create mode 100644 library/vendor/Zend/Form/Element.php create mode 100644 library/vendor/Zend/Form/Element/Button.php create mode 100644 library/vendor/Zend/Form/Element/Checkbox.php create mode 100644 library/vendor/Zend/Form/Element/Exception.php create mode 100644 library/vendor/Zend/Form/Element/File.php create mode 100644 library/vendor/Zend/Form/Element/Hidden.php create mode 100644 library/vendor/Zend/Form/Element/Image.php create mode 100644 library/vendor/Zend/Form/Element/Multi.php create mode 100644 library/vendor/Zend/Form/Element/MultiCheckbox.php create mode 100644 library/vendor/Zend/Form/Element/Multiselect.php create mode 100644 library/vendor/Zend/Form/Element/Note.php create mode 100644 library/vendor/Zend/Form/Element/Password.php create mode 100644 library/vendor/Zend/Form/Element/Radio.php create mode 100644 library/vendor/Zend/Form/Element/Reset.php create mode 100644 library/vendor/Zend/Form/Element/Select.php create mode 100644 library/vendor/Zend/Form/Element/Submit.php create mode 100644 library/vendor/Zend/Form/Element/Text.php create mode 100644 library/vendor/Zend/Form/Element/Textarea.php create mode 100644 library/vendor/Zend/Form/Element/Xhtml.php create mode 100644 library/vendor/Zend/Form/Exception.php create mode 100644 library/vendor/Zend/Form/SubForm.php create mode 100644 library/vendor/Zend/Json.php create mode 100644 library/vendor/Zend/Json/Decoder.php create mode 100644 library/vendor/Zend/Json/Encoder.php create mode 100644 library/vendor/Zend/Json/Exception.php create mode 100644 library/vendor/Zend/Json/Expr.php create mode 100644 library/vendor/Zend/LICENSE.txt create mode 100644 library/vendor/Zend/Layout.php create mode 100644 library/vendor/Zend/Layout/Controller/Action/Helper/Layout.php create mode 100644 library/vendor/Zend/Layout/Controller/Plugin/Layout.php create mode 100644 library/vendor/Zend/Layout/Exception.php create mode 100644 library/vendor/Zend/Loader.php create mode 100644 library/vendor/Zend/Loader/Autoloader.php create mode 100644 library/vendor/Zend/Loader/Autoloader/Interface.php create mode 100644 library/vendor/Zend/Loader/Exception.php create mode 100644 library/vendor/Zend/Loader/Exception/InvalidArgumentException.php create mode 100644 library/vendor/Zend/Loader/PluginLoader.php create mode 100644 library/vendor/Zend/Loader/PluginLoader/Exception.php create mode 100644 library/vendor/Zend/Loader/PluginLoader/Interface.php create mode 100644 library/vendor/Zend/Locale.php create mode 100644 library/vendor/Zend/Locale/Data.php create mode 100644 library/vendor/Zend/Locale/Data/Translation.php create mode 100644 library/vendor/Zend/Locale/Exception.php create mode 100644 library/vendor/Zend/Locale/Format.php create mode 100644 library/vendor/Zend/Locale/Math.php create mode 100644 library/vendor/Zend/Locale/Math/Exception.php create mode 100644 library/vendor/Zend/Locale/Math/PhpMath.php create mode 100644 library/vendor/Zend/Log.php create mode 100644 library/vendor/Zend/Log/Exception.php create mode 100644 library/vendor/Zend/Log/FactoryInterface.php create mode 100644 library/vendor/Zend/Log/Filter/Abstract.php create mode 100644 library/vendor/Zend/Log/Filter/Interface.php create mode 100644 library/vendor/Zend/Log/Filter/Message.php create mode 100644 library/vendor/Zend/Log/Filter/Priority.php create mode 100644 library/vendor/Zend/Log/Filter/Suppress.php create mode 100644 library/vendor/Zend/Log/Formatter/Abstract.php create mode 100644 library/vendor/Zend/Log/Formatter/Interface.php create mode 100644 library/vendor/Zend/Log/Formatter/Simple.php create mode 100644 library/vendor/Zend/Log/Formatter/Xml.php create mode 100644 library/vendor/Zend/Log/Writer/Abstract.php create mode 100644 library/vendor/Zend/Log/Writer/Db.php create mode 100644 library/vendor/Zend/Log/Writer/Mail.php create mode 100644 library/vendor/Zend/Log/Writer/Mock.php create mode 100644 library/vendor/Zend/Log/Writer/Null.php create mode 100644 library/vendor/Zend/Log/Writer/Stream.php create mode 100644 library/vendor/Zend/Log/Writer/Syslog.php create mode 100644 library/vendor/Zend/Log/Writer/ZendMonitor.php create mode 100644 library/vendor/Zend/Mail.php create mode 100644 library/vendor/Zend/Mail/Exception.php create mode 100644 library/vendor/Zend/Mail/Header/HeaderName.php create mode 100644 library/vendor/Zend/Mail/Header/HeaderValue.php create mode 100644 library/vendor/Zend/Mail/Message.php create mode 100644 library/vendor/Zend/Mail/Message/File.php create mode 100644 library/vendor/Zend/Mail/Message/Interface.php create mode 100644 library/vendor/Zend/Mail/Part.php create mode 100644 library/vendor/Zend/Mail/Part/File.php create mode 100644 library/vendor/Zend/Mail/Part/Interface.php create mode 100644 library/vendor/Zend/Mail/Protocol/Abstract.php create mode 100644 library/vendor/Zend/Mail/Protocol/Exception.php create mode 100644 library/vendor/Zend/Mail/Protocol/Imap.php create mode 100644 library/vendor/Zend/Mail/Protocol/Pop3.php create mode 100644 library/vendor/Zend/Mail/Protocol/Smtp.php create mode 100644 library/vendor/Zend/Mail/Protocol/Smtp/Auth/Crammd5.php create mode 100644 library/vendor/Zend/Mail/Protocol/Smtp/Auth/Login.php create mode 100644 library/vendor/Zend/Mail/Protocol/Smtp/Auth/Plain.php create mode 100644 library/vendor/Zend/Mail/Storage.php create mode 100644 library/vendor/Zend/Mail/Storage/Abstract.php create mode 100644 library/vendor/Zend/Mail/Storage/Exception.php create mode 100644 library/vendor/Zend/Mail/Storage/Folder.php create mode 100644 library/vendor/Zend/Mail/Storage/Folder/Interface.php create mode 100644 library/vendor/Zend/Mail/Storage/Folder/Maildir.php create mode 100644 library/vendor/Zend/Mail/Storage/Folder/Mbox.php create mode 100644 library/vendor/Zend/Mail/Storage/Imap.php create mode 100644 library/vendor/Zend/Mail/Storage/Maildir.php create mode 100644 library/vendor/Zend/Mail/Storage/Mbox.php create mode 100644 library/vendor/Zend/Mail/Storage/Pop3.php create mode 100644 library/vendor/Zend/Mail/Storage/Writable/Interface.php create mode 100644 library/vendor/Zend/Mail/Storage/Writable/Maildir.php create mode 100644 library/vendor/Zend/Mail/Transport/Abstract.php create mode 100644 library/vendor/Zend/Mail/Transport/Exception.php create mode 100644 library/vendor/Zend/Mail/Transport/File.php create mode 100644 library/vendor/Zend/Mail/Transport/Sendmail.php create mode 100644 library/vendor/Zend/Mail/Transport/Smtp.php create mode 100644 library/vendor/Zend/Mime.php create mode 100644 library/vendor/Zend/Mime/Decode.php create mode 100644 library/vendor/Zend/Mime/Exception.php create mode 100644 library/vendor/Zend/Mime/Message.php create mode 100644 library/vendor/Zend/Mime/Part.php create mode 100644 library/vendor/Zend/Paginator.php create mode 100644 library/vendor/Zend/Paginator/Adapter/Array.php create mode 100644 library/vendor/Zend/Paginator/Adapter/DbSelect.php create mode 100644 library/vendor/Zend/Paginator/Adapter/DbTableSelect.php create mode 100644 library/vendor/Zend/Paginator/Adapter/Interface.php create mode 100644 library/vendor/Zend/Paginator/Adapter/Iterator.php create mode 100644 library/vendor/Zend/Paginator/Adapter/Null.php create mode 100644 library/vendor/Zend/Paginator/AdapterAggregate.php create mode 100644 library/vendor/Zend/Paginator/Exception.php create mode 100644 library/vendor/Zend/Paginator/ScrollingStyle/All.php create mode 100644 library/vendor/Zend/Paginator/ScrollingStyle/Elastic.php create mode 100644 library/vendor/Zend/Paginator/ScrollingStyle/Interface.php create mode 100644 library/vendor/Zend/Paginator/ScrollingStyle/Jumping.php create mode 100644 library/vendor/Zend/Paginator/ScrollingStyle/Sliding.php create mode 100644 library/vendor/Zend/Paginator/SerializableLimitIterator.php create mode 100644 library/vendor/Zend/ProgressBar.php create mode 100644 library/vendor/Zend/ProgressBar/Adapter.php create mode 100644 library/vendor/Zend/ProgressBar/Adapter/Exception.php create mode 100644 library/vendor/Zend/ProgressBar/Adapter/JsPull.php create mode 100644 library/vendor/Zend/ProgressBar/Adapter/JsPush.php create mode 100644 library/vendor/Zend/ProgressBar/Exception.php create mode 100644 library/vendor/Zend/README.md create mode 100644 library/vendor/Zend/Registry.php create mode 100644 library/vendor/Zend/Server/Abstract.php create mode 100644 library/vendor/Zend/Server/Cache.php create mode 100644 library/vendor/Zend/Server/Definition.php create mode 100644 library/vendor/Zend/Server/Exception.php create mode 100644 library/vendor/Zend/Server/Interface.php create mode 100644 library/vendor/Zend/Server/Method/Callback.php create mode 100644 library/vendor/Zend/Server/Method/Definition.php create mode 100644 library/vendor/Zend/Server/Method/Parameter.php create mode 100644 library/vendor/Zend/Server/Method/Prototype.php create mode 100644 library/vendor/Zend/Server/Reflection.php create mode 100644 library/vendor/Zend/Server/Reflection/Class.php create mode 100644 library/vendor/Zend/Server/Reflection/Exception.php create mode 100644 library/vendor/Zend/Server/Reflection/Function.php create mode 100644 library/vendor/Zend/Server/Reflection/Function/Abstract.php create mode 100644 library/vendor/Zend/Server/Reflection/Method.php create mode 100644 library/vendor/Zend/Server/Reflection/Node.php create mode 100644 library/vendor/Zend/Server/Reflection/Parameter.php create mode 100644 library/vendor/Zend/Server/Reflection/Prototype.php create mode 100644 library/vendor/Zend/Server/Reflection/ReturnValue.php create mode 100644 library/vendor/Zend/Session.php create mode 100644 library/vendor/Zend/Session/Abstract.php create mode 100644 library/vendor/Zend/Session/Exception.php create mode 100644 library/vendor/Zend/Session/Namespace.php create mode 100644 library/vendor/Zend/Session/SaveHandler/DbTable.php create mode 100644 library/vendor/Zend/Session/SaveHandler/Exception.php create mode 100644 library/vendor/Zend/Session/SaveHandler/Interface.php create mode 100644 library/vendor/Zend/Session/Validator/Abstract.php create mode 100644 library/vendor/Zend/Session/Validator/Exception.php create mode 100644 library/vendor/Zend/Session/Validator/HttpUserAgent.php create mode 100644 library/vendor/Zend/Session/Validator/Interface.php create mode 100644 library/vendor/Zend/Soap/AutoDiscover.php create mode 100644 library/vendor/Zend/Soap/AutoDiscover/Exception.php create mode 100644 library/vendor/Zend/Soap/Client.php create mode 100644 library/vendor/Zend/Soap/Client/Common.php create mode 100644 library/vendor/Zend/Soap/Client/DotNet.php create mode 100644 library/vendor/Zend/Soap/Client/Exception.php create mode 100644 library/vendor/Zend/Soap/Client/Local.php create mode 100644 library/vendor/Zend/Soap/Server.php create mode 100644 library/vendor/Zend/Soap/Server/Exception.php create mode 100644 library/vendor/Zend/Soap/Server/Proxy.php create mode 100644 library/vendor/Zend/Soap/Wsdl.php create mode 100644 library/vendor/Zend/Soap/Wsdl/Exception.php create mode 100644 library/vendor/Zend/Soap/Wsdl/Strategy/Abstract.php create mode 100644 library/vendor/Zend/Soap/Wsdl/Strategy/AnyType.php create mode 100644 library/vendor/Zend/Soap/Wsdl/Strategy/ArrayOfTypeComplex.php create mode 100644 library/vendor/Zend/Soap/Wsdl/Strategy/ArrayOfTypeSequence.php create mode 100644 library/vendor/Zend/Soap/Wsdl/Strategy/Composite.php create mode 100644 library/vendor/Zend/Soap/Wsdl/Strategy/DefaultComplexType.php create mode 100644 library/vendor/Zend/Soap/Wsdl/Strategy/Interface.php create mode 100644 library/vendor/Zend/TimeSync.php create mode 100644 library/vendor/Zend/TimeSync/Exception.php create mode 100644 library/vendor/Zend/TimeSync/Ntp.php create mode 100644 library/vendor/Zend/TimeSync/Protocol.php create mode 100644 library/vendor/Zend/TimeSync/Sntp.php create mode 100644 library/vendor/Zend/Translate.php create mode 100644 library/vendor/Zend/Translate/Adapter.php create mode 100644 library/vendor/Zend/Translate/Exception.php create mode 100644 library/vendor/Zend/Translate/Plural.php create mode 100644 library/vendor/Zend/Uri.php create mode 100644 library/vendor/Zend/Uri/Exception.php create mode 100644 library/vendor/Zend/Uri/Http.php create mode 100644 library/vendor/Zend/VERSION create mode 100644 library/vendor/Zend/Validate.php create mode 100644 library/vendor/Zend/Validate/Abstract.php create mode 100644 library/vendor/Zend/Validate/Alnum.php create mode 100644 library/vendor/Zend/Validate/Alpha.php create mode 100644 library/vendor/Zend/Validate/Barcode.php create mode 100644 library/vendor/Zend/Validate/Barcode/AdapterAbstract.php create mode 100644 library/vendor/Zend/Validate/Barcode/AdapterInterface.php create mode 100644 library/vendor/Zend/Validate/Barcode/Code25.php create mode 100644 library/vendor/Zend/Validate/Barcode/Code25interleaved.php create mode 100644 library/vendor/Zend/Validate/Barcode/Code39.php create mode 100644 library/vendor/Zend/Validate/Barcode/Code39ext.php create mode 100644 library/vendor/Zend/Validate/Barcode/Code93.php create mode 100644 library/vendor/Zend/Validate/Barcode/Code93ext.php create mode 100644 library/vendor/Zend/Validate/Barcode/Ean12.php create mode 100644 library/vendor/Zend/Validate/Barcode/Ean13.php create mode 100644 library/vendor/Zend/Validate/Barcode/Ean14.php create mode 100644 library/vendor/Zend/Validate/Barcode/Ean18.php create mode 100644 library/vendor/Zend/Validate/Barcode/Ean2.php create mode 100644 library/vendor/Zend/Validate/Barcode/Ean5.php create mode 100644 library/vendor/Zend/Validate/Barcode/Ean8.php create mode 100644 library/vendor/Zend/Validate/Barcode/Gtin12.php create mode 100644 library/vendor/Zend/Validate/Barcode/Gtin13.php create mode 100644 library/vendor/Zend/Validate/Barcode/Gtin14.php create mode 100644 library/vendor/Zend/Validate/Barcode/Identcode.php create mode 100644 library/vendor/Zend/Validate/Barcode/Intelligentmail.php create mode 100644 library/vendor/Zend/Validate/Barcode/Issn.php create mode 100644 library/vendor/Zend/Validate/Barcode/Itf14.php create mode 100644 library/vendor/Zend/Validate/Barcode/Leitcode.php create mode 100644 library/vendor/Zend/Validate/Barcode/Planet.php create mode 100644 library/vendor/Zend/Validate/Barcode/Postnet.php create mode 100644 library/vendor/Zend/Validate/Barcode/Royalmail.php create mode 100644 library/vendor/Zend/Validate/Barcode/Sscc.php create mode 100644 library/vendor/Zend/Validate/Barcode/Upca.php create mode 100644 library/vendor/Zend/Validate/Barcode/Upce.php create mode 100644 library/vendor/Zend/Validate/Between.php create mode 100644 library/vendor/Zend/Validate/Callback.php create mode 100644 library/vendor/Zend/Validate/Ccnum.php create mode 100644 library/vendor/Zend/Validate/CreditCard.php create mode 100644 library/vendor/Zend/Validate/Date.php create mode 100644 library/vendor/Zend/Validate/Db/Abstract.php create mode 100644 library/vendor/Zend/Validate/Db/NoRecordExists.php create mode 100644 library/vendor/Zend/Validate/Db/RecordExists.php create mode 100644 library/vendor/Zend/Validate/Digits.php create mode 100644 library/vendor/Zend/Validate/EmailAddress.php create mode 100644 library/vendor/Zend/Validate/Exception.php create mode 100644 library/vendor/Zend/Validate/File/Count.php create mode 100644 library/vendor/Zend/Validate/File/Crc32.php create mode 100644 library/vendor/Zend/Validate/File/ExcludeExtension.php create mode 100644 library/vendor/Zend/Validate/File/ExcludeMimeType.php create mode 100644 library/vendor/Zend/Validate/File/Exists.php create mode 100644 library/vendor/Zend/Validate/File/Extension.php create mode 100644 library/vendor/Zend/Validate/File/FilesSize.php create mode 100644 library/vendor/Zend/Validate/File/Hash.php create mode 100644 library/vendor/Zend/Validate/File/ImageSize.php create mode 100644 library/vendor/Zend/Validate/File/IsCompressed.php create mode 100644 library/vendor/Zend/Validate/File/IsImage.php create mode 100644 library/vendor/Zend/Validate/File/Md5.php create mode 100644 library/vendor/Zend/Validate/File/MimeType.php create mode 100644 library/vendor/Zend/Validate/File/NotExists.php create mode 100644 library/vendor/Zend/Validate/File/Sha1.php create mode 100644 library/vendor/Zend/Validate/File/Size.php create mode 100644 library/vendor/Zend/Validate/File/Upload.php create mode 100644 library/vendor/Zend/Validate/File/WordCount.php create mode 100644 library/vendor/Zend/Validate/Float.php create mode 100644 library/vendor/Zend/Validate/GreaterThan.php create mode 100644 library/vendor/Zend/Validate/Hex.php create mode 100644 library/vendor/Zend/Validate/Hostname.php create mode 100644 library/vendor/Zend/Validate/Hostname/Biz.php create mode 100644 library/vendor/Zend/Validate/Hostname/Cn.php create mode 100644 library/vendor/Zend/Validate/Hostname/Com.php create mode 100644 library/vendor/Zend/Validate/Hostname/Jp.php create mode 100644 library/vendor/Zend/Validate/Iban.php create mode 100644 library/vendor/Zend/Validate/Identical.php create mode 100644 library/vendor/Zend/Validate/InArray.php create mode 100644 library/vendor/Zend/Validate/Int.php create mode 100644 library/vendor/Zend/Validate/Interface.php create mode 100644 library/vendor/Zend/Validate/Ip.php create mode 100644 library/vendor/Zend/Validate/Isbn.php create mode 100644 library/vendor/Zend/Validate/LessThan.php create mode 100644 library/vendor/Zend/Validate/NotEmpty.php create mode 100644 library/vendor/Zend/Validate/PostCode.php create mode 100644 library/vendor/Zend/Validate/Regex.php create mode 100644 library/vendor/Zend/Validate/Sitemap/Changefreq.php create mode 100644 library/vendor/Zend/Validate/Sitemap/Lastmod.php create mode 100644 library/vendor/Zend/Validate/Sitemap/Loc.php create mode 100644 library/vendor/Zend/Validate/Sitemap/Priority.php create mode 100644 library/vendor/Zend/Validate/StringLength.php create mode 100644 library/vendor/Zend/View.php create mode 100644 library/vendor/Zend/View/Abstract.php create mode 100644 library/vendor/Zend/View/Exception.php create mode 100644 library/vendor/Zend/View/Helper/Abstract.php create mode 100644 library/vendor/Zend/View/Helper/Action.php create mode 100644 library/vendor/Zend/View/Helper/BaseUrl.php create mode 100644 library/vendor/Zend/View/Helper/Cycle.php create mode 100644 library/vendor/Zend/View/Helper/DeclareVars.php create mode 100644 library/vendor/Zend/View/Helper/Doctype.php create mode 100644 library/vendor/Zend/View/Helper/Fieldset.php create mode 100644 library/vendor/Zend/View/Helper/Form.php create mode 100644 library/vendor/Zend/View/Helper/FormButton.php create mode 100644 library/vendor/Zend/View/Helper/FormCheckbox.php create mode 100644 library/vendor/Zend/View/Helper/FormElement.php create mode 100644 library/vendor/Zend/View/Helper/FormErrors.php create mode 100644 library/vendor/Zend/View/Helper/FormFile.php create mode 100644 library/vendor/Zend/View/Helper/FormHidden.php create mode 100644 library/vendor/Zend/View/Helper/FormImage.php create mode 100644 library/vendor/Zend/View/Helper/FormLabel.php create mode 100644 library/vendor/Zend/View/Helper/FormMultiCheckbox.php create mode 100644 library/vendor/Zend/View/Helper/FormNote.php create mode 100644 library/vendor/Zend/View/Helper/FormPassword.php create mode 100644 library/vendor/Zend/View/Helper/FormRadio.php create mode 100644 library/vendor/Zend/View/Helper/FormReset.php create mode 100644 library/vendor/Zend/View/Helper/FormSelect.php create mode 100644 library/vendor/Zend/View/Helper/FormSubmit.php create mode 100644 library/vendor/Zend/View/Helper/FormText.php create mode 100644 library/vendor/Zend/View/Helper/FormTextarea.php create mode 100644 library/vendor/Zend/View/Helper/Gravatar.php create mode 100644 library/vendor/Zend/View/Helper/HeadLink.php create mode 100644 library/vendor/Zend/View/Helper/HeadMeta.php create mode 100644 library/vendor/Zend/View/Helper/HeadScript.php create mode 100644 library/vendor/Zend/View/Helper/HeadStyle.php create mode 100644 library/vendor/Zend/View/Helper/HeadTitle.php create mode 100644 library/vendor/Zend/View/Helper/HtmlElement.php create mode 100644 library/vendor/Zend/View/Helper/HtmlFlash.php create mode 100644 library/vendor/Zend/View/Helper/HtmlList.php create mode 100644 library/vendor/Zend/View/Helper/HtmlObject.php create mode 100644 library/vendor/Zend/View/Helper/HtmlPage.php create mode 100644 library/vendor/Zend/View/Helper/HtmlQuicktime.php create mode 100644 library/vendor/Zend/View/Helper/InlineScript.php create mode 100644 library/vendor/Zend/View/Helper/Interface.php create mode 100644 library/vendor/Zend/View/Helper/Json.php create mode 100644 library/vendor/Zend/View/Helper/Layout.php create mode 100644 library/vendor/Zend/View/Helper/PaginationControl.php create mode 100644 library/vendor/Zend/View/Helper/Partial.php create mode 100644 library/vendor/Zend/View/Helper/Partial/Exception.php create mode 100644 library/vendor/Zend/View/Helper/PartialLoop.php create mode 100644 library/vendor/Zend/View/Helper/Placeholder.php create mode 100644 library/vendor/Zend/View/Helper/Placeholder/Container.php create mode 100644 library/vendor/Zend/View/Helper/Placeholder/Container/Abstract.php create mode 100644 library/vendor/Zend/View/Helper/Placeholder/Container/Exception.php create mode 100644 library/vendor/Zend/View/Helper/Placeholder/Container/Standalone.php create mode 100644 library/vendor/Zend/View/Helper/Placeholder/Registry.php create mode 100644 library/vendor/Zend/View/Helper/Placeholder/Registry/Exception.php create mode 100644 library/vendor/Zend/View/Helper/RenderToPlaceholder.php create mode 100644 library/vendor/Zend/View/Helper/ServerUrl.php create mode 100644 library/vendor/Zend/View/Helper/Translate.php create mode 100644 library/vendor/Zend/View/Helper/Url.php create mode 100644 library/vendor/Zend/View/Interface.php create mode 100644 library/vendor/Zend/View/Stream.php create mode 100644 library/vendor/Zend/Xml/Exception.php create mode 100644 library/vendor/Zend/Xml/Security.php create mode 100644 library/vendor/dompdf/AUTHORS.md create mode 100644 library/vendor/dompdf/LICENSE.LGPL create mode 100644 library/vendor/dompdf/README.md create mode 100644 library/vendor/dompdf/VERSION create mode 100644 library/vendor/dompdf/autoload.inc.php create mode 100644 library/vendor/dompdf/vendor/autoload.php create mode 100644 library/vendor/dompdf/vendor/composer/ClassLoader.php create mode 100644 library/vendor/dompdf/vendor/composer/InstalledVersions.php create mode 100644 library/vendor/dompdf/vendor/composer/LICENSE create mode 100644 library/vendor/dompdf/vendor/composer/autoload_classmap.php create mode 100644 library/vendor/dompdf/vendor/composer/autoload_namespaces.php create mode 100644 library/vendor/dompdf/vendor/composer/autoload_psr4.php create mode 100644 library/vendor/dompdf/vendor/composer/autoload_real.php create mode 100644 library/vendor/dompdf/vendor/composer/autoload_static.php create mode 100644 library/vendor/dompdf/vendor/composer/installed.json create mode 100644 library/vendor/dompdf/vendor/composer/installed.php create mode 100644 library/vendor/dompdf/vendor/composer/platform_check.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/AUTHORS.md create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/LICENSE.LGPL create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/README.md create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/VERSION create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/composer.json create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/Cpdf.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Courier-Bold.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Courier-BoldOblique.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Courier-Oblique.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Courier.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Bold.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-BoldOblique.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-BoldOblique.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Oblique.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSans-Oblique.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSans.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSansMono-Bold.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSansMono-Bold.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSansMono-BoldOblique.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSansMono-BoldOblique.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSansMono-Oblique.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSansMono-Oblique.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSansMono.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSansMono.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSerif-Bold.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSerif-Bold.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSerif-BoldItalic.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSerif-BoldItalic.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSerif-Italic.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSerif-Italic.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSerif.ttf create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/DejaVuSerif.ufm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Helvetica-Bold.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Helvetica-BoldOblique.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Helvetica-Oblique.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Helvetica.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Symbol.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Times-Bold.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Times-BoldItalic.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Times-Italic.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/Times-Roman.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/ZapfDingbats.afm create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/installed-fonts.dist.json create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/fonts/mustRead.html create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/res/broken_image.png create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/res/broken_image.svg create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/lib/res/html.css create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Adapter/CPDF.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Adapter/GD.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Adapter/PDFLib.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Canvas.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/CanvasFactory.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Cellmap.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Css/AttributeTranslator.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Css/Color.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Css/Style.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Css/Stylesheet.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Dompdf.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Exception.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Exception/ImageException.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FontMetrics.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Frame.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Frame/Factory.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Frame/FrameListIterator.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Frame/FrameTree.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Frame/FrameTreeIterator.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/AbstractFrameDecorator.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Block.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Image.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Inline.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/ListBullet.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/ListBulletImage.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/NullFrameDecorator.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Page.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Table.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableCell.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableRow.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/TableRowGroup.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameDecorator/Text.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/AbstractFrameReflower.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Block.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Image.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Inline.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/ListBullet.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/NullFrameReflower.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Page.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Table.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableCell.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableRow.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/TableRowGroup.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/FrameReflower/Text.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Helpers.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Image/Cache.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/JavascriptEmbedder.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/LineBox.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Options.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/PhpEvaluator.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/Absolute.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/AbstractPositioner.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/Block.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/Fixed.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/Inline.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/ListBullet.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/NullPositioner.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/TableCell.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Positioner/TableRow.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer/AbstractRenderer.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer/Block.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer/Image.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer/Inline.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer/ListBullet.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer/TableCell.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer/TableRowGroup.php create mode 100644 library/vendor/dompdf/vendor/dompdf/dompdf/src/Renderer/Text.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/CREDITS create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/LICENSE.txt create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/README.md create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/UPGRADING.md create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/bin/entities.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/composer.json create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Elements.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Entities.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Exception.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/InstructionProcessor.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/CharacterReference.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/EventHandler.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/FileInputStream.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/InputStream.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/ParseError.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/README.md create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/Scanner.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/StringInputStream.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Parser/UTF8Utils.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Serializer/OutputRules.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Serializer/README.md create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Serializer/RulesInterface.php create mode 100644 library/vendor/dompdf/vendor/masterminds/html5/src/HTML5/Serializer/Traverser.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/LICENSE create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/README.md create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/bower.json create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/composer.json create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/index.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/adobe-standard-encoding.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp1250.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp1251.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp1252.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp1253.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp1254.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp1255.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp1257.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp1258.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/cp874.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-1.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-11.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-15.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-16.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-2.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-4.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-5.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-7.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/iso-8859-9.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/koi8-r.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/maps/koi8-u.map create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/AdobeFontMetrics.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Autoloader.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/BinaryStream.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/EOT/File.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/EOT/Header.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/EncodingMap.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Exception/FontNotFoundException.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Font.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Glyph/Outline.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Glyph/OutlineComponent.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Glyph/OutlineComposite.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Glyph/OutlineSimple.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Header.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/OpenType/File.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/OpenType/TableDirectoryEntry.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/DirectoryEntry.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Table.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/cmap.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/glyf.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/head.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/hhea.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/hmtx.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/kern.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/loca.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/maxp.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/name.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/nameRecord.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/os2.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/Table/Type/post.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/TrueType/Collection.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/TrueType/File.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/TrueType/Header.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/TrueType/TableDirectoryEntry.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/WOFF/File.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/WOFF/Header.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-font-lib/src/FontLib/WOFF/TableDirectoryEntry.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/LICENSE create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/README.md create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/composer.json create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/CssLength.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/DefaultStyle.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Document.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Gradient/Stop.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Style.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Surface/CPdf.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfaceCpdf.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfaceInterface.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Surface/SurfacePDFLib.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/AbstractTag.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Anchor.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Circle.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/ClipPath.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Ellipse.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Group.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Image.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Line.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/LinearGradient.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Path.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Polygon.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Polyline.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/RadialGradient.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Rect.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Shape.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Stop.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/StyleTag.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/Text.php create mode 100644 library/vendor/dompdf/vendor/phenx/php-svg-lib/src/Svg/Tag/UseTag.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/CHANGELOG.md create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/LICENSE create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/README.md create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/composer.json create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/CSSList/AtRuleBlockList.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/CSSList/CSSBlockList.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/CSSList/CSSList.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/CSSList/Document.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/CSSList/KeyFrame.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Comment/Comment.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Comment/Commentable.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/OutputFormat.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/OutputFormatter.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Parser.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Parsing/OutputException.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Parsing/ParserState.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Parsing/SourceException.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Parsing/UnexpectedEOFException.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Parsing/UnexpectedTokenException.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Property/AtRule.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Property/CSSNamespace.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Property/Charset.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Property/Import.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Property/KeyframeSelector.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Property/Selector.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Renderable.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Rule/Rule.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/RuleSet/AtRuleSet.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/RuleSet/DeclarationBlock.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/RuleSet/RuleSet.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Settings.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/CSSFunction.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/CSSString.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/CalcFunction.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/CalcRuleValueList.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/Color.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/LineName.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/PrimitiveValue.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/RuleValueList.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/Size.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/URL.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/Value.php create mode 100644 library/vendor/dompdf/vendor/sabberworm/php-css-parser/src/Value/ValueList.php create mode 100644 library/vendor/lessphp/CHANGES.md create mode 100644 library/vendor/lessphp/LICENSE create mode 100644 library/vendor/lessphp/README.md create mode 100644 library/vendor/lessphp/SECURITY.md create mode 100755 library/vendor/lessphp/bin/lessc create mode 100644 library/vendor/lessphp/composer.json create mode 100644 library/vendor/lessphp/lessc.inc.php create mode 100644 library/vendor/lessphp/lib/Less/Autoloader.php create mode 100644 library/vendor/lessphp/lib/Less/Cache.php create mode 100644 library/vendor/lessphp/lib/Less/Colors.php create mode 100644 library/vendor/lessphp/lib/Less/Configurable.php create mode 100644 library/vendor/lessphp/lib/Less/Environment.php create mode 100644 library/vendor/lessphp/lib/Less/Exception/Chunk.php create mode 100644 library/vendor/lessphp/lib/Less/Exception/Compiler.php create mode 100644 library/vendor/lessphp/lib/Less/Exception/Parser.php create mode 100644 library/vendor/lessphp/lib/Less/Functions.php create mode 100644 library/vendor/lessphp/lib/Less/Less.php.combine create mode 100644 library/vendor/lessphp/lib/Less/Mime.php create mode 100644 library/vendor/lessphp/lib/Less/Output.php create mode 100644 library/vendor/lessphp/lib/Less/Output/Mapped.php create mode 100644 library/vendor/lessphp/lib/Less/Parser.php create mode 100644 library/vendor/lessphp/lib/Less/SourceMap/Base64VLQ.php create mode 100644 library/vendor/lessphp/lib/Less/SourceMap/Generator.php create mode 100644 library/vendor/lessphp/lib/Less/Tree.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Alpha.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Anonymous.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Assignment.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Attribute.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Call.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Color.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Comment.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Condition.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/DefaultFunc.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/DetachedRuleset.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Dimension.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Directive.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Element.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Expression.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Extend.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Import.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Javascript.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Keyword.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Media.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Mixin/Call.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Mixin/Definition.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/NameValue.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Negative.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Operation.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Paren.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Quoted.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Rule.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Ruleset.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/RulesetCall.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Selector.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/UnicodeDescriptor.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Unit.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/UnitConversions.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Url.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Value.php create mode 100644 library/vendor/lessphp/lib/Less/Tree/Variable.php create mode 100644 library/vendor/lessphp/lib/Less/Version.php create mode 100644 library/vendor/lessphp/lib/Less/Visitor.php create mode 100644 library/vendor/lessphp/lib/Less/Visitor/extendFinder.php create mode 100644 library/vendor/lessphp/lib/Less/Visitor/import.php create mode 100644 library/vendor/lessphp/lib/Less/Visitor/joinSelector.php create mode 100644 library/vendor/lessphp/lib/Less/Visitor/processExtends.php create mode 100644 library/vendor/lessphp/lib/Less/Visitor/toCSS.php create mode 100644 library/vendor/lessphp/lib/Less/VisitorReplacing.php (limited to 'library') diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php new file mode 100644 index 0000000..b550495 --- /dev/null +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -0,0 +1,758 @@ + + * use Icinga\Application\Cli; + + * Cli::start(); + * + * + * Usage example for Icinga Web application: + * + * use Icinga\Application\Web; + * Web::start()->dispatch(); + * + * + * Usage example for Icinga-Web 1.x compatibility mode: + * + * use Icinga\Application\LegacyWeb; + * LegacyWeb::start()->setIcingaWebBasedir(ICINGAWEB_BASEDIR)->dispatch(); + * + */ +abstract class ApplicationBootstrap +{ + /** + * Base directory + * + * Parent folder for at least application, bin, modules, library/vendor and public + * + * @var string + */ + protected $baseDir; + + /** + * Application directory + * + * @var string + */ + protected $appDir; + + /** + * Vendor library directory + * + * @var string + */ + protected $vendorDir; + + /** + * Icinga library directory + * + * @var string + */ + protected $libDir; + + /** + * Configuration directory + * + * @var string + */ + protected $configDir; + + /** + * Locale directory + * + * @var string + */ + protected $localeDir; + + /** + * Common storage directory + * + * @var string + */ + protected $storageDir; + + /** + * External library paths + * + * @var string[] + */ + protected $libraryPaths; + + /** + * Loaded external libraries + * + * @var Libraries + */ + protected $libraries; + + /** + * Icinga class loader + * + * @var ClassLoader + */ + private $loader; + + /** + * Config object + * + * @var Config + */ + protected $config; + + /** + * Module manager + * + * @var ModuleManager + */ + private $moduleManager; + + /** + * Flag indicates we're on cli environment + * + * @var bool + */ + protected $isCli = false; + + /** + * Flag indicates we're on web environment + * + * @var bool + */ + protected $isWeb = false; + + /** + * Whether Icinga Web 2 requires setup + * + * @var bool + */ + protected $requiresSetup = false; + + /** + * Constructor + * + * @param string $baseDir Icinga Web 2 base directory + * @param string $configDir Path to Icinga Web 2's configuration files + * @param string $storageDir Path to Icinga Web 2's stored files + */ + protected function __construct($baseDir = null, $configDir = null, $storageDir = null) + { + if ($baseDir === null) { + $baseDir = dirname($this->getBootstrapDirectory()); + } + $this->baseDir = $baseDir; + $this->appDir = $baseDir . '/application'; + $this->vendorDir = $baseDir . '/library/vendor'; + if (substr(__DIR__, 0, 8) === 'phar:///') { + $this->libDir = dirname(dirname(__DIR__)); + } else { + $this->libDir = realpath(__DIR__ . '/../..'); + } + + $this->setupAutoloader(); + + if ($configDir === null) { + $configDir = getenv('ICINGAWEB_CONFIGDIR'); + if ($configDir === false) { + $configDir = Platform::isWindows() + ? $baseDir . '/config' + : '/etc/icingaweb2'; + } + } + $canonical = realpath($configDir); + $this->configDir = $canonical ? $canonical : $configDir; + + if ($storageDir === null) { + $storageDir = getenv('ICINGAWEB_STORAGEDIR'); + if ($storageDir === false) { + $storageDir = Platform::isWindows() + ? $baseDir . '/storage' + : '/var/lib/icingaweb2'; + } + } + $canonical = realpath($storageDir); + $this->storageDir = $canonical ? $canonical : $storageDir; + + if ($this->libraryPaths === null) { + $libraryPaths = getenv('ICINGAWEB_LIBDIR'); + if ($libraryPaths !== false) { + $this->libraryPaths = array_filter(array_map( + 'realpath', + explode(':', $libraryPaths) + ), 'is_dir'); + } else { + $this->libraryPaths = is_dir('/usr/share/icinga-php') + ? ['/usr/share/icinga-php'] + : []; + } + } + + set_include_path( + implode( + PATH_SEPARATOR, + array($this->vendorDir, get_include_path()) + ) + ); + + Benchmark::measure('Bootstrap, autoloader registered'); + + Icinga::setApp($this); + + require_once dirname(__FILE__) . '/functions.php'; + } + + /** + * Bootstrap interface method for concrete bootstrap objects + * + * @return mixed + */ + abstract protected function bootstrap(); + + /** + * Get loaded external libraries + * + * @return Libraries + */ + public function getLibraries() + { + return $this->libraries; + } + + /** + * Getter for module manager + * + * @return ModuleManager + */ + public function getModuleManager() + { + return $this->moduleManager; + } + + /** + * Getter for class loader + * + * @return ClassLoader + */ + public function getLoader() + { + return $this->loader; + } + + /** + * Getter for configuration object + * + * @return Config + */ + public function getConfig() + { + return $this->config; + } + + /** + * Flag indicates we're on cli environment + * + * @return bool + */ + public function isCli() + { + return $this->isCli; + } + + /** + * Flag indicates we're on web environment + * + * @return bool + */ + public function isWeb() + { + return $this->isWeb; + } + + /** + * Helper to glue directories together + * + * @param string $dir + * @param string $subdir + * + * @return string + */ + private function getDirWithSubDir($dir, $subdir = null) + { + if ($subdir !== null) { + $dir .= '/' . ltrim($subdir, '/'); + } + + return $dir; + } + + /** + * Get the base directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getBaseDir($subDir = null) + { + return $this->getDirWithSubDir($this->baseDir, $subDir); + } + + /** + * Get the application directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getApplicationDir($subDir = null) + { + return $this->getDirWithSubDir($this->appDir, $subDir); + } + + /** + * Get the vendor library directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getVendorDir($subDir = null) + { + return $this->getDirWithSubDir($this->vendorDir, $subDir); + } + + /** + * Get the configuration directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getConfigDir($subDir = null) + { + return $this->getDirWithSubDir($this->configDir, $subDir); + } + + /** + * Get the common storage directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getStorageDir($subDir = null) + { + return $this->getDirWithSubDir($this->storageDir, $subDir); + } + + /** + * Get the Icinga library directory + * + * @param string $subDir Optional sub directory to get + * + * @return string + */ + public function getLibraryDir($subDir = null) + { + return $this->getDirWithSubDir($this->libDir, $subDir); + } + + /** + * Get the path to the bootstrapping directory + * + * This is usually /public for Web and EmbeddedWeb and /bin for the CLI + * + * @return string + * + * @throws LogicException If the base directory can not be detected + */ + public function getBootstrapDirectory() + { + $script = $_SERVER['SCRIPT_FILENAME']; + $canonical = realpath($script); + if ($canonical !== false) { + $dir = dirname($canonical); + } elseif (substr($script, -14) === '/webrouter.php') { + // If Icinga Web 2 is served using PHP's built-in webserver with our webrouter.php script, the $_SERVER + // variable SCRIPT_FILENAME is set to DOCUMENT_ROOT/webrouter.php which is not a valid path to + // realpath but DOCUMENT_ROOT here still is the bootstrapping directory + $dir = dirname($script); + } else { + throw new LogicException('Can\'t detected base directory'); + } + return $dir; + } + + /** + * Start the bootstrap + * + * @param string $baseDir Icinga Web 2 base directory + * @param string $configDir Path to Icinga Web 2's configuration files + * + * @return static + */ + public static function start($baseDir = null, $configDir = null) + { + $application = new static($baseDir, $configDir); + $application->bootstrap(); + return $application; + } + + /** + * Setup Icinga class loader + * + * @return $this + */ + public function setupAutoloader() + { + require_once $this->libDir . '/Icinga/Application/ClassLoader.php'; + + $this->loader = new ClassLoader(); + $this->loader->registerNamespace('Icinga', $this->libDir . '/Icinga'); + $this->loader->registerNamespace('Icinga', $this->libDir . '/Icinga', $this->appDir); + $this->loader->register(); + + return $this; + } + + /** + * Setup module manager + * + * @return $this + */ + protected function setupModuleManager() + { + $paths = $this->getAvailableModulePaths(); + $this->moduleManager = new ModuleManager( + $this, + $this->configDir . '/enabledModules', + $paths + ); + return $this; + } + + protected function getAvailableModulePaths() + { + $paths = array(); + $configured = $this->config->get('global', 'module_path', $this->baseDir . '/modules'); + $nextIsPhar = false; + foreach (explode(':', $configured) as $path) { + if ($path === 'phar') { + $nextIsPhar = true; + continue; + } + + if ($nextIsPhar) { + $nextIsPhar = false; + $paths[] = 'phar:' . $path; + } else { + $paths[] = $path; + } + } + + return $paths; + } + + /** + * Load all enabled modules + * + * @return $this + */ + protected function loadEnabledModules() + { + try { + $this->moduleManager->loadEnabledModules(); + } catch (NotReadableError $e) { + Logger::error(new IcingaException('Cannot load enabled modules. An exception was thrown:', $e)); + } + return $this; + } + + /** + * Load the setup module if Icinga Web 2 requires setup or the setup token exists + * + * @return $this + */ + protected function loadSetupModuleIfNecessary() + { + if (! @file_exists($this->config->resolvePath('authentication.ini'))) { + $this->requiresSetup = true; + if ($this->moduleManager->hasInstalled('setup')) { + $this->moduleManager->loadModule('setup'); + } + } elseif ($this->setupTokenExists()) { + // Load setup module but do not require setup + if ($this->moduleManager->hasInstalled('setup')) { + $this->moduleManager->loadModule('setup'); + } + } + return $this; + } + + /** + * Get whether Icinga Web 2 requires setup + * + * @return bool + */ + public function requiresSetup() + { + return $this->requiresSetup; + } + + /** + * Get whether the setup token exists + * + * @return bool + */ + public function setupTokenExists() + { + return @file_exists($this->config->resolvePath('setup.token')); + } + + /** + * Load external libraries + * + * @return $this + */ + protected function loadLibraries() + { + $this->libraries = new Libraries(); + foreach ($this->libraryPaths as $libraryPath) { + foreach (new DirectoryIterator($libraryPath) as $path) { + if (! $path->isDot() && is_dir($path->getRealPath())) { + $this->libraries->registerPath($path->getPathname()) + ->registerAutoloader(); + } + } + } + + return $this; + } + + /** + * Setup default logging + * + * @return $this + */ + protected function setupLogging() + { + Logger::create( + new ConfigObject( + array( + 'log' => 'syslog' + ) + ) + ); + return $this; + } + + /** + * Load Configuration + * + * @return $this + */ + protected function loadConfig() + { + Config::$configDir = $this->configDir; + + try { + $this->config = Config::app(); + } catch (NotReadableError $e) { + Logger::error(new IcingaException('Cannot load application configuration. An exception was thrown:', $e)); + $this->config = new Config(); + } + + return $this; + } + + /** + * Error handling configuration + * + * @return $this + */ + protected function setupErrorHandling() + { + error_reporting(E_ALL | E_STRICT); + ini_set('display_startup_errors', 1); + ini_set('display_errors', 1); + set_error_handler(function ($errno, $errstr, $errfile, $errline) { + if (! (error_reporting() & $errno)) { + // Error was suppressed with the @-operator + return false; // Continue with the normal error handler + } + switch ($errno) { + case E_NOTICE: + case E_WARNING: + case E_STRICT: + case E_RECOVERABLE_ERROR: + throw new ErrorException($errstr, 0, $errno, $errfile, $errline); + } + return false; // Continue with the normal error handler + }); + return $this; + } + + /** + * Set up logger + * + * @return $this + */ + protected function setupLogger() + { + if ($this->config->hasSection('logging')) { + $loggingConfig = $this->config->getSection('logging'); + + try { + Logger::create($loggingConfig); + } catch (ConfigurationError $e) { + Logger::getInstance()->registerConfigError($e->getMessage()); + + try { + Logger::getInstance()->setLevel($loggingConfig->get('level', Logger::ERROR)); + } catch (ConfigurationError $e) { + Logger::getInstance()->registerConfigError($e->getMessage()); + } + } + } + + return $this; + } + + /** + * Set up the user backend factory + * + * @return $this + */ + protected function setupUserBackendFactory() + { + try { + UserBackend::setConfig(Config::app('authentication')); + } catch (NotReadableError $e) { + Logger::error( + new IcingaException('Cannot load user backend configuration. An exception was thrown:', $e) + ); + } + + return $this; + } + + /** + * Detect the timezone + * + * @return null|string + */ + protected function detectTimezone() + { + return null; + } + + /** + * Set up the timezone + * + * @return $this + */ + final protected function setupTimezone() + { + $timezone = $this->detectTimeZone(); + if ($timezone === null || @date_default_timezone_set($timezone) === false) { + date_default_timezone_set(@date_default_timezone_get()); + } + return $this; + } + + /** + * Detect the locale + * + * @return null|string + */ + protected function detectLocale() + { + return null; + } + + /** + * Prepare internationalization using gettext + * + * @return $this + */ + protected function prepareInternationalization() + { + StaticTranslator::$instance = (new GettextTranslator()) + ->setDefaultDomain('icinga'); + + return $this; + } + + /** + * Set up internationalization using gettext + * + * @return $this + */ + final protected function setupInternationalization() + { + /** @var GettextTranslator $translator */ + $translator = StaticTranslator::$instance; + + if ($this->hasLocales()) { + $translator->addTranslationDirectory($this->getLocaleDir(), 'icinga'); + } + + $locale = $this->detectLocale(); + if ($locale === null) { + $locale = $translator->getDefaultLocale(); + } + + try { + $translator->setLocale($locale); + } catch (Exception $error) { + Logger::error($error); + } + + return $this; + } + + /** + * @return string Our locale directory + */ + public function getLocaleDir() + { + if ($this->localeDir === null) { + $L10nLocales = getenv('ICINGAWEB_LOCALEDIR') ?: '/usr/share/icinga-L10n/locale'; + if (file_exists($L10nLocales) && is_dir($L10nLocales)) { + $this->localeDir = $L10nLocales; + } else { + $this->localeDir = false; + } + } + + return $this->localeDir; + } + + /** + * return bool Whether Icinga Web has translations + */ + public function hasLocales() + { + $localedir = $this->getLocaleDir(); + return $localedir !== false && file_exists($localedir) && is_dir($localedir); + } +} diff --git a/library/Icinga/Application/Benchmark.php b/library/Icinga/Application/Benchmark.php new file mode 100644 index 0000000..6514ad5 --- /dev/null +++ b/library/Icinga/Application/Benchmark.php @@ -0,0 +1,299 @@ + + * Benchmark::measure('Program started'); + * // ...do something... + * Benchmark::measure('Task finieshed'); + * Benchmark::dump(); + * + */ +class Benchmark +{ + const TIME = 0x01; + const MEMORY = 0x02; + + protected static $instance; + protected $start; + protected $measures = array(); + + /** + * Add a measurement to your benchmark + * + * The same identifier can also be used multiple times + * + * @param string A comment identifying the current measurement + * @return void + */ + public static function measure($message) + { + self::getInstance()->measures[] = (object) array( + 'timestamp' => microtime(true), + 'memory_real' => memory_get_usage(true), + 'memory' => memory_get_usage(), + 'message' => $message + ); + } + + /** + * Throws all measurements away + * + * This empties your measurement table and allows you to restart your + * benchmark from scratch + * + * @return void + */ + public static function reset() + { + self::$instance = null; + } + + /** + * Rerieve benchmark start time + * + * This will give you the timestamp of your first measurement + * + * @return float + */ + public static function getStartTime() + { + return self::getInstance()->start; + } + + /** + * Dump benchmark data + * + * Will dump a text table if running on CLI and a simple HTML table + * otherwise. Use Benchmark::TIME and Benchmark::MEMORY to choose whether + * you prefer to show either time or memory or both in your output + * + * @param int Whether to get time and/or memory summary + * @return string + */ + public static function dump($what = null) + { + if (Icinga::app()->isCli()) { + echo self::renderToText($what); + } else { + echo self::renderToHtml($what); + } + } + + /** + * Render benchmark data to a simple text table + * + * Use Benchmark::TIME and Icinga::MEMORY to choose whether you prefer to + * show either time or memory or both in your output + * + * @param int Whether to get time and/or memory summary + * @return string + */ + public static function renderToText($what = null) + { + $data = self::prepareDataForRendering($what); + $sep = '+'; + $title = '|'; + foreach ($data->columns as & $col) { + $col->format = ' %' + . ($col->align === 'right' ? '' : '-') + . $col->maxlen . 's |'; + + $sep .= str_repeat('-', $col->maxlen) . '--+'; + $title .= sprintf($col->format, $col->title); + } + + $out = $sep . "\n" . $title . "\n" . $sep . "\n"; + foreach ($data->rows as & $row) { + $r = '|'; + foreach ($data->columns as $key => & $col) { + $r .= sprintf($col->format, $row[$key]); + } + $out .= $r . "\n"; + } + + $out .= $sep . "\n"; + return $out; + } + + /** + * Render benchmark data to a simple HTML table + * + * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer + * to show either time or memory or both in your output + * + * @param int Whether to get time and/or memory summary + * @return string + */ + public static function renderToHtml($what = null) + { + $data = self::prepareDataForRendering($what); + + // TODO: Move formatting to CSS file + $html = '' . "\n" . ''; + foreach ($data->columns as & $col) { + if ($col->title === 'Time') { + continue; + } + $html .= sprintf( + '', + $col->align, + htmlspecialchars($col->title) + ); + } + $html .= "\n"; + + foreach ($data->rows as & $row) { + $html .= ''; + foreach ($data->columns as $key => & $col) { + if ($col->title === 'Time') { + continue; + } + $html .= sprintf( + '', + $col->align, + $row[$key] + ); + } + $html .= "\n"; + } + $html .= "
%s
%s
\n"; + return $html; + } + + /** + * Prepares benchmark data for output + * + * Use Benchmark::TIME and Benchmark::MEMORY to choose whether you prefer + * to have either time or memory or both in your output + * + * @param int Whether to get time and/or memory summary + * @return array + */ + protected static function prepareDataForRendering($what = null) + { + if ($what === null) { + $what = self::TIME | self::MEMORY; + } + + $columns = array( + (object) array( + 'title' => 'Time', + 'align' => 'left', + 'maxlen' => 4 + ), + (object) array( + 'title' => 'Description', + 'align' => 'left', + 'maxlen' => 11 + ) + ); + if ($what & self::TIME) { + $columns[] = (object) array( + 'title' => 'Off (ms)', + 'align' => 'right', + 'maxlen' => 11 + ); + $columns[] = (object) array( + 'title' => 'Dur (ms)', + 'align' => 'right', + 'maxlen' => 13 + ); + } + if ($what & self::MEMORY) { + $columns[] = (object) array( + 'title' => 'Mem (diff)', + 'align' => 'right', + 'maxlen' => 10 + ); + $columns[] = (object) array( + 'title' => 'Mem (total)', + 'align' => 'right', + 'maxlen' => 11 + ); + } + + $bench = self::getInstance(); + $last = $bench->start; + $rows = array(); + $lastmem = 0; + foreach ($bench->measures as $m) { + $micro = sprintf( + '%03d', + round(($m->timestamp - floor($m->timestamp)) * 1000) + ); + $vals = array( + date('H:i:s', (int) $m->timestamp) . '.' . $micro, + $m->message + ); + + if ($what & self::TIME) { + $m->relative = $m->timestamp - $bench->start; + $m->offset = $m->timestamp - $last; + $last = $m->timestamp; + $vals[] = sprintf('%0.3f', $m->relative * 1000); + $vals[] = sprintf('%0.3f', $m->offset * 1000); + } + + if ($what & self::MEMORY) { + $mem = $m->memory - $lastmem; + $lastmem = $m->memory; + $vals[] = Format::bytes($mem); + $vals[] = Format::bytes($m->memory); + } + + $row = & $rows[]; + foreach ($vals as $col => $val) { + $row[$col] = $val; + $columns[$col]->maxlen = max( + strlen($val), + $columns[$col]->maxlen + ); + } + } + + return (object) array( + 'columns' => $columns, + 'rows' => $rows + ); + } + + /** + * Singleton + * + * Benchmark is run only once, but you are not allowed to directly access + * the getInstance() method + * + * @return self + */ + protected static function getInstance() + { + if (self::$instance === null) { + self::$instance = new Benchmark(); + self::$instance->start = microtime(true); + } + + return self::$instance; + } + + /** + * Constructor + * + * Singleton usage is enforced, the only way to instantiate Benchmark is by + * starting your measurements + * + * @return void + */ + protected function __construct() + { + } +} diff --git a/library/Icinga/Application/ClassLoader.php b/library/Icinga/Application/ClassLoader.php new file mode 100644 index 0000000..aed26f7 --- /dev/null +++ b/library/Icinga/Application/ClassLoader.php @@ -0,0 +1,334 @@ + 'clicommands', + 'Controllers' => 'controllers', + 'Forms' => 'forms' + ); + + /** + * Whether we already instantiated the ZF autoloader + * + * @var boolean + */ + protected $gotZend = false; + + /** + * Namespaces + * + * @var array + */ + private $namespaces = array(); + + /** + * Application directories + * + * @var array + */ + private $applicationDirectories = array(); + + /** + * Register a base directory for a namespace prefix + * + * Application directory is optional and provides additional lookup + * logic for hardcoded namespaces like "Forms" + * + * @param string $namespace + * @param string $directory + * @param string $appDirectory + * + * @return $this + */ + public function registerNamespace($namespace, $directory, $appDirectory = null) + { + $this->namespaces[$namespace] = $directory; + + if ($appDirectory !== null) { + $this->applicationDirectories[$namespace] = $appDirectory; + } + + return $this; + } + + /** + * Test whether a namespace exists + * + * @param string $namespace + * + * @return bool + */ + public function hasNamespace($namespace) + { + return array_key_exists($namespace, $this->namespaces); + } + + /** + * Get the source file of the given class or interface + * + * @param string $class Name of the class or interface + * + * @return string|null + */ + public function getSourceFile($class) + { + if ($file = $this->getModuleSourceFile($class)) { + return $file; + } + + foreach ($this->namespaces as $namespace => $dir) { + if ($class === strstr($class, "$namespace\\")) { + return $this->buildClassFilename($class, $namespace); + } + } + + return null; + } + + /** + * Get the source file of the given module class or interface + * + * @param string $class Module class or interface name + * + * @return string|null + */ + protected function getModuleSourceFile($class) + { + if (! $this->classBelongsToModule($class)) { + return null; + } + + $modules = Icinga::app()->getModuleManager(); + $namespace = $this->extractModuleNamespace($class); + + if ($this->hasNamespace($namespace)) { + return $this->buildClassFilename($class, $namespace); + } elseif (! $modules->loadedAllEnabledModules()) { + $moduleName = $this->extractModuleName($class); + + if ($modules->hasEnabled($moduleName)) { + $modules->loadModule($moduleName); + + return $this->buildClassFilename($class, $namespace); + } + } + + return null; + } + + /** + * Extract the Icinga module namespace from a given namespaced class name + * + * Does no validation, prefix must have been checked before + * + * @return string + */ + protected function extractModuleNamespace($class) + { + return substr( + $class, + 0, + strpos($class, self::NAMESPACE_SEPARATOR, self::MODULE_PREFIX_LENGTH + 1) + ); + } + + /** + * Extract the Icinga module name from a given namespaced class name + * + * Does no validation, prefix must have been checked before + * + * @return string + */ + public static function extractModuleName($class) + { + return lcfirst( + substr( + $class, + self::MODULE_PREFIX_LENGTH, + strpos( + $class, + self::NAMESPACE_SEPARATOR, + self::MODULE_PREFIX_LENGTH + 1 + ) - self::MODULE_PREFIX_LENGTH + ) + ); + } + + /** + * Whether the given class name belongs to a module namespace + * + * @return boolean + */ + public static function classBelongsToModule($class) + { + return substr($class, 0, self::MODULE_PREFIX_LENGTH) === self::MODULE_PREFIX; + } + + /** + * Prepare a filename string for the given class + * + * Expects the given namespace to be registered with a path name + * + * @return string + */ + protected function buildClassFilename($class, $namespace) + { + $relNs = substr($class, strlen($namespace) + 1); + + if ($this->namespaceHasApplictionDirectory($namespace)) { + $prefixSeparator = strpos($relNs, self::NAMESPACE_SEPARATOR); + $prefix = substr($relNs, 0, $prefixSeparator); + + if ($this->isApplicationPrefix($prefix)) { + return $this->applicationDirectories[$namespace] + . DIRECTORY_SEPARATOR + . $this->applicationPrefixes[$prefix] + . $this->classToRelativePhpFilename(substr($relNs, $prefixSeparator)); + } + } + + return $this->namespaces[$namespace] . DIRECTORY_SEPARATOR . $this->classToRelativePhpFilename($relNs); + } + + /** + * Return the relative file name for the given (namespaces) class + * + * @param string $class + * + * @return string + */ + protected function classToRelativePhpFilename($class) + { + return str_replace( + self::NAMESPACE_SEPARATOR, + DIRECTORY_SEPARATOR, + $class + ) . '.php'; + } + + /** + * Whether given prefix (Forms, Controllers...) makes part of "application" + * + * @param string $prefix + * + * @return boolean + */ + protected function isApplicationPrefix($prefix) + { + return array_key_exists($prefix, $this->applicationPrefixes); + } + + /** + * Whether the given namespace registered an application directory + * + * @return boolean + */ + protected function namespaceHasApplictionDirectory($namespace) + { + return array_key_exists($namespace, $this->applicationDirectories); + } + + /** + * Require ZF autoloader + * + * @return Zend_Loader_Autoloader + */ + protected function requireZendAutoloader() + { + require_once 'Zend/Loader/Autoloader.php'; + $this->gotZend = true; + return Zend_Loader_Autoloader::getInstance(); + } + + /** + * Load the given class or interface + * + * @param string $class Name of the class or interface + * + * @return bool Whether the class or interface has been loaded + */ + public function loadClass($class) + { + // We are aware of the Zend_ prefix and lazyload it's autoloader. + // Return as fast as possible if we already did so. + if (substr($class, 0, 5) === 'Zend_') { + if (! $this->gotZend) { + $zendLoader = $this->requireZendAutoloader(); + if (version_compare(PHP_VERSION, '7.0.0') >= 0) { + // PHP7 seems to remember the autoload function stack before auto-loading. Thus + // autoload functions registered during autoload never get called + return $zendLoader::autoload($class); + } + } + return false; + } + + if ($file = $this->getSourceFile($class)) { + if (file_exists($file)) { + require $file; + return true; + } + } + + return false; + } + + /** + * Register {@link loadClass()} as an autoloader + */ + public function register() + { + spl_autoload_register(array($this, 'loadClass')); + } + + /** + * Unregister {@link loadClass()} as an autoloader + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Unregister this as an autoloader + */ + public function __destruct() + { + $this->unregister(); + } +} diff --git a/library/Icinga/Application/Cli.php b/library/Icinga/Application/Cli.php new file mode 100644 index 0000000..719bf92 --- /dev/null +++ b/library/Icinga/Application/Cli.php @@ -0,0 +1,210 @@ +assertRunningOnCli(); + $this->setupLogging() + ->setupErrorHandling() + ->loadLibraries() + ->loadConfig() + ->setupTimezone() + ->prepareInternationalization() + ->setupInternationalization() + ->parseBasicParams() + ->setupLogger() + ->setupModuleManager() + ->setupUserBackendFactory() + ->loadSetupModuleIfNecessary() + ->setupFakeAuthentication(); + } + + /** + * {@inheritdoc} + */ + protected function setupLogging() + { + Logger::create( + new ConfigObject( + array( + 'log' => 'stderr' + ) + ) + ); + + return $this; + } + + /** + * {@inheritdoc} + */ + protected function setupLogger() + { + $config = new ConfigObject(); + $config->log = $this->params->shift('log', 'stderr'); + if ($config->log === 'file') { + $config->file = $this->params->shiftRequired('log-path'); + } elseif ($config->log === 'syslog') { + $config->application = 'icingacli'; + } + + if ($this->params->get('verbose', false)) { + $config->level = Logger::INFO; + } elseif ($this->params->get('debug', false)) { + $config->level = Logger::DEBUG; + } else { + $config->level = Logger::WARNING; + } + + Logger::create($config); + return $this; + } + + protected function setupFakeAuthentication() + { + Auth::getInstance()->setUser(new User('cli')); + + return $this; + } + + public function cliLoader() + { + if ($this->cliLoader === null) { + $this->cliLoader = new Loader($this); + } + return $this->cliLoader; + } + + protected function parseBasicParams() + { + $this->params = Params::parse(); + if ($this->params->shift('help')) { + $this->params->unshift('help'); + } + if ($this->params->shift('version')) { + $this->params->unshift('version'); + } + if ($this->params->shift('autocomplete')) { + $this->params->unshift('autocomplete'); + } + + $watch = $this->params->shift('watch'); + if ($watch === true) { + $this->watchTimeout = 5; + } elseif (is_numeric($watch)) { + $this->watchTimeout = (int) $watch; + } + + $this->debug = (int) $this->params->get('debug'); + $this->verbose = (int) $this->params->get('verbose'); + + $this->showBenchmark = (bool) $this->params->shift('benchmark'); + return $this; + } + + public function getParams() + { + return $this->params; + } + + public function dispatchModule($name, $basedir = null) + { + $this->getModuleManager()->loadModule($name, $basedir); + $this->cliLoader()->setModuleName($name); + $this->dispatch(); + } + + public function dispatch() + { + Benchmark::measure('Dispatching CLI command'); + + if ($this->watchTimeout === null) { + $this->dispatchOnce(); + } else { + $this->dispatchEndless(); + } + } + + protected function dispatchOnce() + { + $loader = $this->cliLoader(); + $loader->parseParams(); + $result = $loader->dispatch(); + Benchmark::measure('All done'); + if ($this->showBenchmark) { + Benchmark::dump(); + } + if ($result === false) { + exit(3); + } + } + + protected function dispatchEndless() + { + $loader = $this->cliLoader(); + $loader->parseParams(); + $screen = Screen::instance(); + + while (true) { + Benchmark::measure('Watch mode - loop begins'); + ob_start(); + $params = clone($this->params); + $loader->dispatch($params); + Benchmark::measure('Dispatch done'); + if ($this->showBenchmark) { + Benchmark::dump(); + } + Benchmark::reset(); + $out = ob_get_contents(); + ob_end_clean(); + echo $screen->clear() . $out; + sleep($this->watchTimeout); + } + } + + /** + * Fail if Icinga has not been called on CLI + * + * @throws ProgrammingError + * @return void + */ + private function assertRunningOnCli() + { + if (Platform::isCli()) { + return; + } + throw new ProgrammingError('Icinga is not running on CLI'); + } +} diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php new file mode 100644 index 0000000..006c40f --- /dev/null +++ b/library/Icinga/Application/Config.php @@ -0,0 +1,497 @@ +config = $config !== null ? $config : new ConfigObject(); + } + + /** + * Return this config's file path + * + * @return string + */ + public function getConfigFile() + { + return $this->configFile; + } + + /** + * Set this config's file path + * + * @param string $filepath The path to the ini file + * + * @return $this + */ + public function setConfigFile($filepath) + { + $this->configFile = $filepath; + return $this; + } + + /** + * Return the internal ConfigObject + * + * @return ConfigObject + */ + public function getConfigObject() + { + return $this->config; + } + + /** + * Provide a query for the internal config object + * + * @return SimpleQuery + */ + public function select() + { + return $this->config->select(); + } + + /** + * Return the count of available sections + * + * @return int + */ + public function count(): int + { + return $this->select()->count(); + } + + /** + * Reset the current position of the internal config object + * + * @return void + */ + public function rewind(): void + { + $this->config->rewind(); + } + + /** + * Return the section of the current iteration + * + * @return ConfigObject + */ + public function current(): ConfigObject + { + return $this->config->current(); + } + + /** + * Return whether the position of the current iteration is valid + * + * @return bool + */ + public function valid(): bool + { + return $this->config->valid(); + } + + /** + * Return the section's name of the current iteration + * + * @return string + */ + public function key(): string + { + return $this->config->key(); + } + + /** + * Advance the position of the current iteration and return the new section + * + * @return void + */ + public function next(): void + { + $this->config->next(); + } + + /** + * Return whether this config has any sections + * + * @return bool + */ + public function isEmpty() + { + return $this->config->isEmpty(); + } + + /** + * Return this config's section names + * + * @return array + */ + public function keys() + { + return $this->config->keys(); + } + + /** + * Return this config's data as associative array + * + * @return array + */ + public function toArray() + { + return $this->config->toArray(); + } + + /** + * Return the value from a section's property + * + * @param string $section The section where the given property can be found + * @param string $key The section's property to fetch the value from + * @param mixed $default The value to return in case the section or the property is missing + * + * @return mixed + * + * @throws UnexpectedValueException In case the given section does not hold any configuration + */ + public function get($section, $key, $default = null) + { + $value = $this->config->$section; + if ($value instanceof ConfigObject) { + $value = $value->$key; + } elseif ($value !== null) { + throw new UnexpectedValueException( + sprintf('Value "%s" is not of type "%s" or a sub-type of it', $value, get_class($this->config)) + ); + } + + if ($value === null && $default !== null) { + $value = $default; + } + + return $value; + } + + /** + * Return the given section + * + * @param string $name The section's name + * + * @return ConfigObject + */ + public function getSection($name) + { + $section = $this->config->get($name); + return $section !== null ? $section : new ConfigObject(); + } + + /** + * Set or replace a section + * + * @param string $name + * @param array|ConfigObject $config + * + * @return $this + */ + public function setSection($name, $config = null) + { + if ($config === null) { + $config = new ConfigObject(); + } elseif (! $config instanceof ConfigObject) { + $config = new ConfigObject($config); + } + + $this->config->$name = $config; + return $this; + } + + /** + * Remove a section + * + * @param string $name + * + * @return $this + */ + public function removeSection($name) + { + unset($this->config->$name); + return $this; + } + + /** + * Return whether the given section exists + * + * @param string $name + * + * @return bool + */ + public function hasSection($name) + { + return isset($this->config->$name); + } + + /** + * Initialize a new config using the given array + * + * The returned config has no file associated to it. + * + * @param array $array The array to initialize the config with + * + * @return Config + */ + public static function fromArray(array $array) + { + return new static(new ConfigObject($array)); + } + + /** + * Load configuration from the given INI file + * + * @param string $file The file to parse + * + * @throws NotReadableError When the file cannot be read + */ + public static function fromIni($file) + { + $emptyConfig = new static(); + + $filepath = realpath($file); + if ($filepath === false) { + $emptyConfig->setConfigFile($file); + } elseif (is_readable($filepath)) { + return IniParser::parseIniFile($filepath); + } elseif (@file_exists($filepath)) { + throw new NotReadableError(t('Cannot read config file "%s". Permission denied'), $filepath); + } + + return $emptyConfig; + } + + /** + * Save configuration to the given INI file + * + * @param string|null $filePath The path to the INI file or null in case this config's path should be used + * @param int $fileMode The file mode to store the file with + * + * @throws LogicException In case this config has no path and none is passed in either + * @throws NotWritableError In case the INI file cannot be written + * + * @todo create basepath and throw NotWritableError in case its not possible + */ + public function saveIni($filePath = null, $fileMode = 0660) + { + if ($filePath === null && $this->configFile) { + $filePath = $this->configFile; + } elseif ($filePath === null) { + throw new LogicException('You need to pass $filePath or set a path using Config::setConfigFile()'); + } + + if (! file_exists($filePath)) { + File::create($filePath, $fileMode); + } + + $this->getIniWriter($filePath, $fileMode)->write(); + } + + /** + * Return a IniWriter for this config + * + * @param string|null $filePath + * @param int $fileMode + * + * @return IniWriter + */ + protected function getIniWriter($filePath = null, $fileMode = null) + { + return new IniWriter($this, $filePath, $fileMode); + } + + /** + * Prepend configuration base dir to the given relative path + * + * @param string $path A relative path + * + * @return string + */ + public static function resolvePath($path) + { + return self::$configDir . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR); + } + + /** + * Retrieve a application config + * + * @param string $configname The configuration name (without ini suffix) to read and return + * @param bool $fromDisk When set true, the configuration will be read from disk, even + * if it already has been read + * + * @return Config The requested configuration + */ + public static function app($configname = 'config', $fromDisk = false) + { + if (! isset(self::$app[$configname]) || $fromDisk) { + self::$app[$configname] = static::fromIni(static::resolvePath($configname . '.ini')); + } + + return self::$app[$configname]; + } + + /** + * Retrieve a module config + * + * @param string $modulename The name of the module where to look for the requested configuration + * @param string $configname The configuration name (without ini suffix) to read and return + * @param string $fromDisk When set true, the configuration will be read from disk, even + * if it already has been read + * + * @return Config The requested configuration + */ + public static function module($modulename, $configname = 'config', $fromDisk = false) + { + if (! isset(self::$modules[$modulename])) { + self::$modules[$modulename] = array(); + } + + if (! isset(self::$modules[$modulename][$configname]) || $fromDisk) { + self::$modules[$modulename][$configname] = static::fromIni( + static::resolvePath('modules/' . $modulename . '/' . $configname . '.ini') + ); + } + return self::$modules[$modulename][$configname]; + } + + /** + * Retrieve a navigation config + * + * @param string $type The type identifier of the navigation item for which to return its config + * @param string $username A user's name or null if the shared config is desired + * @param bool $fromDisk If true, the configuration will be read from disk + * + * @return Config The requested configuration + */ + public static function navigation($type, $username = null, $fromDisk = false) + { + if (! isset(self::$navigation[$type])) { + self::$navigation[$type] = array(); + } + + $branch = $username ?: 'shared'; + $typeConfigs = self::$navigation[$type]; + if (! isset($typeConfigs[$branch]) || $fromDisk) { + $typeConfigs[$branch] = static::fromIni(static::getNavigationConfigPath($type, $username)); + } + + return $typeConfigs[$branch]; + } + + /** + * Return the path to the configuration file for the given navigation item type and user + * + * @param string $type + * @param string $username + * + * @return string + * + * @throws IcingaException In case the given type is unknown + */ + protected static function getNavigationConfigPath($type, $username = null) + { + $itemTypeConfig = Navigation::getItemTypeConfiguration(); + if (! isset($itemTypeConfig[$type])) { + throw new IcingaException('Invalid navigation item type %s provided', $type); + } + + if (isset($itemTypeConfig[$type]['config'])) { + $filename = $itemTypeConfig[$type]['config'] . '.ini'; + } else { + $filename = $type . 's.ini'; + } + + if ($username) { + $path = static::resolvePath(implode(DIRECTORY_SEPARATOR, array('preferences', $username, $filename))); + if (realpath($path) === false) { + $path = static::resolvePath(implode( + DIRECTORY_SEPARATOR, + array('preferences', strtolower($username), $filename) + )); + } + } else { + $path = static::resolvePath('navigation' . DIRECTORY_SEPARATOR . $filename); + } + return $path; + } + + /** + * Return this config rendered as a INI structured string + * + * @return string + */ + public function __toString() + { + return $this->getIniWriter()->render(); + } +} diff --git a/library/Icinga/Application/EmbeddedWeb.php b/library/Icinga/Application/EmbeddedWeb.php new file mode 100644 index 0000000..8d03e11 --- /dev/null +++ b/library/Icinga/Application/EmbeddedWeb.php @@ -0,0 +1,114 @@ + + * use Icinga\Application\EmbeddedWeb; + * EmbeddedWeb::start(); + * + */ +class EmbeddedWeb extends ApplicationBootstrap +{ + /** + * Request + * + * @var Request + */ + protected $request; + + /** + * Response + * + * @var Response + */ + protected $response; + + /** + * Get the request + * + * @return Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * Get the response + * + * @return Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * Embedded bootstrap parts + * + * @see ApplicationBootstrap::bootstrap + * + * @return $this + */ + protected function bootstrap() + { + return $this + ->setupErrorHandling() + ->loadLibraries() + ->loadConfig() + ->setupLogging() + ->setupLogger() + ->setupRequest() + ->setupResponse() + ->setupTimezone() + ->prepareFakeInternationalization() + ->setupModuleManager() + ->loadEnabledModules(); + } + + /** + * Set the request + * + * @return $this + */ + protected function setupRequest() + { + $this->request = new Request(); + return $this; + } + + /** + * Set the response + * + * @return $this + */ + protected function setupResponse() + { + $this->response = new Response(); + return $this; + } + + /** + * Prepare fake internationalization + * + * @return $this + */ + protected function prepareFakeInternationalization() + { + StaticTranslator::$instance = new NoopTranslator(); + + return $this; + } +} diff --git a/library/Icinga/Application/Hook.php b/library/Icinga/Application/Hook.php new file mode 100644 index 0000000..b3d12fe --- /dev/null +++ b/library/Icinga/Application/Hook.php @@ -0,0 +1,330 @@ + + * Hook::register('grapher', 'My\\Grapher\\Class'); + * + */ +class Hook +{ + /** + * Our hook name registry + * + * @var array + */ + protected static $hooks = array(); + + /** + * Hooks that have already been instantiated + * + * @var array + */ + protected static $instances = array(); + + /** + * Namespace prefix + * + * @var string + */ + public static $BASE_NS = 'Icinga\\Application\\Hook\\'; + + /** + * Append this string to base class + * + * All base classes renamed to *Hook + * + * @var string + */ + public static $classSuffix = 'Hook'; + + /** + * Reset object state + */ + public static function clean() + { + self::$hooks = array(); + self::$instances = array(); + self::$BASE_NS = 'Icinga\\Application\\Hook\\'; + } + + /** + * Whether someone registered itself for the given hook name + * + * @param string $name One of the predefined hook names + * + * @return bool + */ + public static function has($name) + { + $name = self::normalizeHookName($name); + + if (! array_key_exists($name, self::$hooks)) { + return false; + } + + foreach (self::$hooks[$name] as $hook) { + list($class, $alwaysRun) = $hook; + if ($alwaysRun || self::hasPermission($class)) { + return true; + } + } + + return false; + } + + protected static function normalizeHookName($name) + { + if (strpos($name, '\\') === false) { + $parts = explode('/', $name); + foreach ($parts as & $part) { + $part = ucfirst($part); + } + + return implode('\\', $parts); + } + + return $name; + } + + /** + * Create or return an instance of a given hook + * + * TODO: Should return some kind of a hook interface + * + * @param string $name One of the predefined hook names + * @param string $key The identifier of a specific subtype + * + * @return mixed + */ + public static function createInstance($name, $key) + { + $name = self::normalizeHookName($name); + + if (!self::has($name, $key)) { + return null; + } + + if (isset(self::$instances[$name][$key])) { + return self::$instances[$name][$key]; + } + + $class = self::$hooks[$name][$key][0]; + + if (! class_exists($class)) { + throw new ProgrammingError( + 'Erraneous hook implementation, class "%s" does not exist', + $class + ); + } + try { + $instance = new $class(); + } catch (Exception $e) { + Logger::debug( + 'Hook "%s" (%s) (%s) failed, will be unloaded: %s', + $name, + $key, + $class, + $e->getMessage() + ); + // TODO: Persist unloading for "some time" or "current session" + unset(self::$hooks[$name][$key]); + return null; + } + + self::assertValidHook($instance, $name); + self::$instances[$name][$key] = $instance; + return $instance; + } + + protected static function splitHookName($name) + { + $sep = '\\'; + if (false === $module = strpos($name, $sep)) { + return array(null, $name); + } + return array( + substr($name, 0, $module), + substr($name, $module + 1) + ); + } + + /** + * Extract the Icinga module name from a given namespaced class name + * + * Does no validation, prefix must have been checked before + * + * Shameless copy of ClassLoader::extractModuleName() + * + * @param string $class The hook's class path + * + * @return string + */ + protected static function extractModuleName($class) + { + return lcfirst( + substr( + $class, + ClassLoader::MODULE_PREFIX_LENGTH, + strpos( + $class, + ClassLoader::NAMESPACE_SEPARATOR, + ClassLoader::MODULE_PREFIX_LENGTH + 1 + ) - ClassLoader::MODULE_PREFIX_LENGTH + ) + ); + } + + /** + * Return whether the user has the permission to access the module which provides the given hook + * + * @param string $class The hook's class path + * + * @return bool + */ + protected static function hasPermission($class) + { + if (Icinga::app()->isCli()) { + return true; + } + + return Auth::getInstance()->hasPermission( + Manager::MODULE_PERMISSION_NS . self::extractModuleName($class) + ); + } + + /** + * Test for a valid class name + * + * @param mixed $instance + * @param string $name + * + * @throws ProgrammingError + */ + private static function assertValidHook($instance, $name) + { + $name = self::normalizeHookName($name); + + $suffix = self::$classSuffix; // 'Hook' + $base = self::$BASE_NS; // 'Icinga\\Web\\Hook\\' + + list($module, $name) = self::splitHookName($name); + + if ($module === null) { + $base_class = $base . ucfirst($name) . 'Hook'; + + // I'm unsure whether this makes sense. Unused and Wrong. + if (strpos($base_class, $suffix) === false) { + $base_class .= $suffix; + } + } else { + $base_class = 'Icinga\\Module\\' + . ucfirst($module) + . '\\Hook\\' + . ucfirst($name) + . $suffix; + } + + if (!$instance instanceof $base_class) { + // This is a compatibility check. Should be removed one far day: + if ($module !== null) { + $compat_class = 'Icinga\\Module\\' + . ucfirst($module) + . '\\Web\\Hook\\' + . ucfirst($name) + . $suffix; + + if ($instance instanceof $compat_class) { + return; + } + } + + throw new ProgrammingError( + '%s is not an instance of %s', + get_class($instance), + $base_class + ); + } + } + + /** + * Return all instances of a specific name + * + * @param string $name One of the predefined hook names + * + * @return array + */ + public static function all($name) + { + $name = self::normalizeHookName($name); + if (! self::has($name)) { + return array(); + } + + foreach (self::$hooks[$name] as $key => $hook) { + list($class, $alwaysRun) = $hook; + if ($alwaysRun || self::hasPermission($class)) { + if (self::createInstance($name, $key) === null) { + return array(); + } + } + } + + return isset(self::$instances[$name]) ? self::$instances[$name] : array(); + } + + /** + * Get the first hook + * + * @param string $name One of the predefined hook names + * + * @return null|mixed + */ + public static function first($name) + { + $name = self::normalizeHookName($name); + + if (self::has($name)) { + foreach (self::$hooks[$name] as $key => $hook) { + list($class, $alwaysRun) = $hook; + if ($alwaysRun || self::hasPermission($class)) { + return self::createInstance($name, $key); + } + } + } + } + + /** + * Register a class + * + * @param string $name One of the predefined hook names + * @param string $key The identifier of a specific subtype + * @param string $class Your class name, must inherit one of the + * classes in the Icinga/Application/Hook folder + * @param bool $alwaysRun To run the hook always (e.g. without permission check) + */ + public static function register($name, $key, $class, $alwaysRun = false) + { + $name = self::normalizeHookName($name); + + if (!isset(self::$hooks[$name])) { + self::$hooks[$name] = array(); + } + + $class = ltrim($class, ClassLoader::NAMESPACE_SEPARATOR); + + self::$hooks[$name][$key] = [$class, $alwaysRun]; + } +} diff --git a/library/Icinga/Application/Hook/ApplicationStateHook.php b/library/Icinga/Application/Hook/ApplicationStateHook.php new file mode 100644 index 0000000..be973fe --- /dev/null +++ b/library/Icinga/Application/Hook/ApplicationStateHook.php @@ -0,0 +1,90 @@ +messages); + } + + final public function getMessages() + { + return $this->messages; + } + + /** + * Add an error message + * + * The timestamp of the message is used for deduplication and thus must refer to the time when the error first + * occurred. Don't use {@link time()} here! + * + * @param string $id ID of the message. The ID must be prefixed with the module name + * @param int $timestamp Timestamp when the error first occurred + * @param string $message Error message + * + * @return $this + */ + final public function addError($id, $timestamp, $message) + { + $id = trim($id); + $timestamp = (int) $timestamp; + + if (! strlen($id)) { + throw new \InvalidArgumentException('ID expected.'); + } + + if (! $timestamp) { + throw new \InvalidArgumentException('Timestamp expected.'); + } + + $this->messages[sha1($id . $timestamp)] = [self::ERROR, $timestamp, $message]; + + return $this; + } + + /** + * Override this method in order to provide application state messages + */ + abstract public function collectMessages(); + + final public static function getAllMessages() + { + $messages = []; + + if (! Hook::has('ApplicationState')) { + return $messages; + } + + foreach (Hook::all('ApplicationState') as $hook) { + /** @var self $hook */ + try { + $hook->collectMessages(); + } catch (\Exception $e) { + Logger::error( + "Failed to collect messages from hook '%s'. An error occurred: %s", + get_class($hook), + $e + ); + } + + if ($hook->hasMessages()) { + $messages += $hook->getMessages(); + } + } + + return $messages; + } +} diff --git a/library/Icinga/Application/Hook/AuditHook.php b/library/Icinga/Application/Hook/AuditHook.php new file mode 100644 index 0000000..e6209da --- /dev/null +++ b/library/Icinga/Application/Hook/AuditHook.php @@ -0,0 +1,123 @@ +getUser()->getUsername(); + } + + if ($time === null) { + $time = time(); + } + + foreach (Hook::all('audit') as $hook) { + /** @var self $hook */ + try { + $formattedMessage = $message; + if ($data !== null) { + // Calling formatMessage on each hook is intended and allows + // intercepting message formatting while keeping it implicit + $formattedMessage = $hook->formatMessage($message, $data); + } + + $hook->logMessage($time, $identity, $type, $formattedMessage, $data); + } catch (Exception $e) { + Logger::error( + 'Failed to propagate audit message to hook "%s". An error occurred: %s', + get_class($hook), + $e + ); + } + } + } + + /** + * Log a message to the audit log + * + * @param int $time A timestamp defining when the activity occurred + * @param string $identity An arbitrary name identifying the responsible subject + * @param string $type An arbitrary name identifying the type of activity + * @param string $message A detailed description of the activity + * @param array $data Additional activity information + */ + abstract public function logMessage($time, $identity, $type, $message, array $data = null); + + /** + * Substitute the given message with its accompanying data + * + * @param string $message + * @param array $messageData + * + * @return string + */ + public function formatMessage($message, array $messageData) + { + return preg_replace_callback('/{{(.+?)}}/', function ($match) use ($messageData) { + return $this->extractMessageValue(explode('.', $match[1]), $messageData); + }, $message); + } + + /** + * Extract the given value path from the given message data + * + * @param array $path + * @param array $messageData + * + * @return mixed + * + * @throws InvalidArgumentException In case of an invalid or missing format parameter + */ + protected function extractMessageValue(array $path, array $messageData) + { + $key = array_shift($path); + if (array_key_exists($key, $messageData)) { + $value = $messageData[$key]; + } else { + throw new InvalidArgumentException("Missing format parameter '$key'"); + } + + if (empty($path)) { + if (! is_scalar($value)) { + throw new InvalidArgumentException( + 'Invalid format parameter. Expected scalar for path "' . join('.', $path) . '".' + . ' Got "' . gettype($value) . '" instead' + ); + } + + return $value; + } elseif (! is_array($value)) { + throw new InvalidArgumentException( + 'Invalid format parameter. Expected array for path "'. join('.', $path) . '".' + . ' Got "' . gettype($value) . '" instead' + ); + } + + return $this->extractMessageValue($path, $value); + } +} diff --git a/library/Icinga/Application/Hook/AuthenticationHook.php b/library/Icinga/Application/Hook/AuthenticationHook.php new file mode 100644 index 0000000..41cc661 --- /dev/null +++ b/library/Icinga/Application/Hook/AuthenticationHook.php @@ -0,0 +1,75 @@ +onLogin($user); + } catch (\Exception $e) { + // Avoid error propagation if login failed in third party application + Logger::error($e); + } + } + } + + /** + * Call the onLogout() method of all registered AuthHook(s) + * + * @param User $user + */ + public static function triggerLogout(User $user) + { + /** @var AuthenticationHook $hook */ + foreach (Hook::all(self::NAME) as $hook) { + try { + $hook->onLogout($user); + } catch (\Exception $e) { + // Avoid error propagation if login failed in third party application + Logger::error($e); + } + } + } +} diff --git a/library/Icinga/Application/Hook/ConfigFormEventsHook.php b/library/Icinga/Application/Hook/ConfigFormEventsHook.php new file mode 100644 index 0000000..aa0cadc --- /dev/null +++ b/library/Icinga/Application/Hook/ConfigFormEventsHook.php @@ -0,0 +1,137 @@ +runAppliesTo($form)) { + continue; + } + + try { + $hook->$eventMethod($form); + } catch (\Exception $e) { + static::$lastErrors[] = $e->getMessage(); + + Logger::error("%s\n%s", $e, IcingaException::getConfidentialTraceAsString($e)); + + $success = false; + } + } + + return $success; + } + + private function runAppliesTo(Form $form) + { + try { + $appliesTo = $this->appliesTo($form); + } catch (\Exception $e) { + // Don't save exception to last errors because we do not want to disturb the user for messed up + // appliesTo checks + Logger::error("%s\n%s", $e, IcingaException::getConfidentialTraceAsString($e)); + + $appliesTo = false; + } + + return $appliesTo === true; + } +} diff --git a/library/Icinga/Application/Hook/GrapherHook.php b/library/Icinga/Application/Hook/GrapherHook.php new file mode 100644 index 0000000..dfb2135 --- /dev/null +++ b/library/Icinga/Application/Hook/GrapherHook.php @@ -0,0 +1,111 @@ +init(); + } + + /** + * Overwrite this function if you want to do some initialization stuff + * + * @return void + */ + protected function init() + { + } + + /** + * Whether this grapher provides previews + * + * @return bool + */ + public function hasPreviews() + { + return $this->hasPreviews; + } + + /** + * Whether this grapher provides tiny previews + * + * @return bool + */ + public function hasTinyPreviews() + { + return $this->hasTinyPreviews; + } + + /** + * Whether a graph for the monitoring object exist + * + * @param MonitoredObject $object + * + * @return bool + */ + abstract public function has(MonitoredObject $object); + + /** + * Get a preview for the given object + * + * This function must return an empty string if no graph exists. + * + * @param MonitoredObject $object + * + * @return string + * @throws ProgrammingError + * + */ + public function getPreviewHtml(MonitoredObject $object) + { + throw new ProgrammingError('This hook provide previews but it is not implemented'); + } + + + /** + * Get a tiny preview for the given object + * + * This function must return an empty string if no graph exists. + * + * @param MonitoredObject $object + * + * @return string + * @throws ProgrammingError + */ + public function getTinyPreviewHtml(MonitoredObject $object) + { + throw new ProgrammingError('This hook provide tiny previews but it is not implemented'); + } +} diff --git a/library/Icinga/Application/Hook/HealthHook.php b/library/Icinga/Application/Hook/HealthHook.php new file mode 100644 index 0000000..f6420b5 --- /dev/null +++ b/library/Icinga/Application/Hook/HealthHook.php @@ -0,0 +1,222 @@ +state; + } + + /** + * Set overall state + * + * @param int $state + * + * @return $this + */ + public function setState($state) + { + $this->state = $state; + + return $this; + } + + /** + * Get the message describing the overall state + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Set the message describing the overall state + * + * @param string $message + * + * @return $this + */ + public function setMessage($message) + { + $this->message = $message; + + return $this; + } + + /** + * Get available metrics + * + * @return array + */ + public function getMetrics() + { + return $this->metrics; + } + + /** + * Set available metrics + * + * @param array $metrics + * + * @return $this + */ + public function setMetrics(array $metrics) + { + $this->metrics = $metrics; + + return $this; + } + + /** + * Get the url to a graphical representation of the available metrics + * + * @return Url + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the url to a graphical representation of the available metrics + * + * @param Url $url + * + * @return $this + */ + public function setUrl(Url $url) + { + $this->url = $url; + + return $this; + } + + /** + * Collect available health data from hooks + * + * @return ArrayDatasource + */ + final public static function collectHealthData() + { + $checks = []; + foreach (Hook::all('health') as $hook) { + /** @var self $hook */ + + try { + $hook->checkHealth(); + $url = $hook->getUrl(); + $state = $hook->getState(); + $message = $hook->getMessage(); + $metrics = $hook->getMetrics(); + } catch (Exception $e) { + Logger::error('Failed to check health: %s', $e); + + $state = self::STATE_UNKNOWN; + $message = IcingaException::describe($e); + $metrics = null; + $url = null; + } + + $checks[] = (object) [ + 'module' => $hook->getModuleName(), + 'name' => $hook->getName(), + 'url' => $url ? $url->getAbsoluteUrl() : null, + 'state' => $state, + 'message' => $message, + 'metrics' => (object) $metrics + ]; + } + + return (new ArrayDatasource($checks)) + ->setKeyColumn('name'); + } + + /** + * Get the name of the hook + * + * Only used in API responses to differentiate it from other hooks of the same module. + * + * @return string + */ + public function getName() + { + $classPath = get_class($this); + $parts = explode('\\', $classPath); + $className = array_pop($parts); + + if (substr($className, -4) === 'Hook') { + $className = substr($className, 1, -4); + } + + return strtolower($className[0]) . substr($className, 1); + } + + /** + * Get the name of the module providing this hook + * + * @return string + * + * @throws LogicException + */ + public function getModuleName() + { + $classPath = get_class($this); + if (substr($classPath, 0, 14) !== 'Icinga\\Module\\') { + throw new LogicException('Not a module hook'); + } + + $withoutPrefix = substr($classPath, 14); + return strtolower(substr($withoutPrefix, 0, strpos($withoutPrefix, '\\'))); + } + + /** + * Check health + * + * Implement this method and set the overall state, message, url and metrics. + * + * @return void + */ + abstract public function checkHealth(); +} diff --git a/library/Icinga/Application/Hook/PdfexportHook.php b/library/Icinga/Application/Hook/PdfexportHook.php new file mode 100644 index 0000000..36e9f51 --- /dev/null +++ b/library/Icinga/Application/Hook/PdfexportHook.php @@ -0,0 +1,25 @@ +/public/css/themes + */ +abstract class ThemeLoaderHook +{ + /** + * Get the path for the given theme + * + * @param ?string $theme + * + * @return ?string The path or NULL if the theme is unknown + */ + abstract public function getThemeFile(?string $theme): ?string; +} diff --git a/library/Icinga/Application/Hook/Ticket/TicketPattern.php b/library/Icinga/Application/Hook/Ticket/TicketPattern.php new file mode 100644 index 0000000..e37fcc1 --- /dev/null +++ b/library/Icinga/Application/Hook/Ticket/TicketPattern.php @@ -0,0 +1,140 @@ +match[$offset]); + } + + public function offsetGet($offset): ?string + { + return array_key_exists($offset, $this->match) ? $this->match[$offset] : null; + } + + public function offsetSet($offset, $value): void + { + if ($offset === null) { + $this->match[] = $value; + } else { + $this->match[$offset] = $value; + } + } + + public function offsetUnset($offset): void + { + unset($this->match[$offset]); + } + + + /** + * Get the result of a performed ticket match + * + * @return array + */ + public function getMatch() + { + return $this->match; + } + + /** + * Set the result of a performed ticket match + * + * @param array $match + * + * @return $this + */ + public function setMatch(array $match) + { + $this->match = $match; + return $this; + } + + /** + * Get the name of the TTS integration + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the name of the TTS integration + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Get the ticket pattern + * + * @return string + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * Set the ticket pattern + * + * @param string $pattern + * + * @return $this + */ + public function setPattern($pattern) + { + $this->pattern = $pattern; + return $this; + } + + /** + * Whether the integration is properly configured, i.e. the pattern and the URL are not empty + * + * @return bool + */ + public function isValid() + { + return ! empty($this->pattern); + } +} diff --git a/library/Icinga/Application/Hook/TicketHook.php b/library/Icinga/Application/Hook/TicketHook.php new file mode 100644 index 0000000..ceb3738 --- /dev/null +++ b/library/Icinga/Application/Hook/TicketHook.php @@ -0,0 +1,210 @@ +init(); + } + + /** + * Overwrite this function for hook initialization, e.g. loading the hook's config + */ + protected function init() + { + } + + /** + * Create a link for each matched element in the subject text + * + * @param array|TicketPattern $match Matched element according to {@link getPattern()} + * + * @return string Replacement string + */ + abstract public function createLink($match); + + /** + * Get the pattern(s) to search for + * + * Return an array of TicketPattern instances here to support multiple TTS integrations. + * + * @return string|TicketPattern[] + */ + abstract public function getPattern(); + + /** + * Apply ticket patterns to the given text + * + * @param string $text + * @param TicketPattern[] $ticketPatterns + * + * @return string + */ + private function applyTicketPatterns($text, array $ticketPatterns) + { + $out = ''; + $start = 0; + + $iterator = new ArrayIterator($ticketPatterns); + $iterator->rewind(); + + while ($iterator->valid()) { + $ticketPattern = $iterator->current(); + + try { + preg_match($ticketPattern->getPattern(), $text, $match, PREG_OFFSET_CAPTURE, $start); + } catch (ErrorException $e) { + $this->fail('Can\'t create ticket links: Pattern is invalid: %s', IcingaException::describe($e)); + $iterator->next(); + continue; + } + + if (empty($match)) { + $iterator->next(); + continue; + } + + // Remove preg_offset from match for the ticket pattern + $carry = array(); + array_walk($match, function ($value, $key) use (&$carry) { + $carry[$key] = $value[0]; + }, $carry); + $ticketPattern->setMatch($carry); + + $offsetLeft = $match[0][1]; + $matchLength = strlen($match[0][0]); + + $out .= substr($text, $start, $offsetLeft - $start); + + try { + $out .= $this->createLink($ticketPattern); + } catch (Exception $e) { + $this->fail('Can\'t create ticket links: %s', IcingaException::describe($e)); + return $text; + } + + $start = $offsetLeft + $matchLength; + } + + $out .= substr($text, $start); + + return $out; + } + + /** + * Helper function to create a TicketPattern instance + * + * @param string $name Name of the TTS integration + * @param string $pattern Ticket pattern + * + * @return TicketPattern + */ + protected function createTicketPattern($name, $pattern) + { + $ticketPattern = new TicketPattern(); + $ticketPattern + ->setName($name) + ->setPattern($pattern); + return $ticketPattern; + } + + /** + * Set the hook as failed w/ the given message + * + * @param string $message Error message or error format string + * @param mixed ...$arg Format string argument + */ + private function fail($message) + { + $args = array_slice(func_get_args(), 1); + $lastError = vsprintf($message, $args); + Logger::debug($lastError); + $this->lastError = $lastError; + } + + /** + * Get the last error, if any + * + * @return string|null + */ + public function getLastError() + { + return $this->lastError; + } + + /** + * Create links w/ {@link createLink()} in the given text that matches to the subject from {@link getPattern()} + * + * In case of errors a debug message is recorded to the log and any subsequent call to {@link createLinks()} will + * be a no-op. + * + * @param string $text + * + * @return string + */ + final public function createLinks($text) + { + if ($this->lastError !== null) { + return $text; + } + + try { + $pattern = $this->getPattern(); + } catch (Exception $e) { + $this->fail('Can\'t create ticket links: Retrieving the pattern failed: %s', IcingaException::describe($e)); + return $text; + } + + if (empty($pattern)) { + $this->fail('Can\'t create ticket links: Pattern is empty'); + return $text; + } + + if (is_array($pattern)) { + $text = $this->applyTicketPatterns($text, $pattern); + } else { + try { + $text = preg_replace_callback( + $pattern, + array($this, 'createLink'), + $text + ); + } catch (ErrorException $e) { + $this->fail('Can\'t create ticket links: Pattern is invalid: %s', IcingaException::describe($e)); + return $text; + } catch (Exception $e) { + $this->fail('Can\'t create ticket links: %s', IcingaException::describe($e)); + return $text; + } + } + + return $text; + } +} diff --git a/library/Icinga/Application/Hook/WebBaseHook.php b/library/Icinga/Application/Hook/WebBaseHook.php new file mode 100644 index 0000000..09e8f4f --- /dev/null +++ b/library/Icinga/Application/Hook/WebBaseHook.php @@ -0,0 +1,54 @@ +view = $view; + + return $this; + } + + /** + * Get the view instance + * + * @return Zend_View + */ + public function getView() + { + if ($this->view === null) { + $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer'); + if ($viewRenderer->view === null) { + $viewRenderer->initView(); + } + $this->view = $viewRenderer->view; + } + + return $this->view; + } +} diff --git a/library/Icinga/Application/Icinga.php b/library/Icinga/Application/Icinga.php new file mode 100644 index 0000000..ba54015 --- /dev/null +++ b/library/Icinga/Application/Icinga.php @@ -0,0 +1,49 @@ +setupIcingaLegacyWrapper(); + } + + /** + * Get the Icinga-Web 1.x base path + * + * @throws Exception + * @return self + */ + public function getLecacyBasedir() + { + return $this->legacyBasedir; + } +} diff --git a/library/Icinga/Application/Libraries.php b/library/Icinga/Application/Libraries.php new file mode 100644 index 0000000..8e4a79d --- /dev/null +++ b/library/Icinga/Application/Libraries.php @@ -0,0 +1,91 @@ +libraries); + } + + /** + * Register a library from the given path + * + * @param string $path + * + * @return Library The registered library + */ + public function registerPath($path) + { + $library = new Library($path); + $this->libraries[] = $library; + + return $library; + } + + /** + * Check if a library with the given name has been registered + * + * Passing a version constraint also verifies that the library's version matches. + * + * @param string $name + * @param string $version + * + * @return bool + */ + public function has($name, $version = null) + { + $library = $this->get($name); + if ($library === null) { + return false; + } elseif ($version === null || $version === true) { + return true; + } + + $operator = '='; + if (preg_match('/^([<>=]{1,2})\s*v?((?:[\d.]+)(?:\D+)?)$/', $version, $match)) { + $operator = $match[1]; + $version = $match[2]; + } + + return version_compare($library->getVersion(), $version, $operator); + } + + /** + * Get a library by name + * + * @param string $name + * + * @return Library|null + */ + public function get($name) + { + $candidate = null; + foreach ($this->libraries as $library) { + $libraryName = $library->getName(); + if ($libraryName === $name) { + return $library; + } elseif (strpos($libraryName, '/') !== false && explode('/', $libraryName)[1] === $name) { + // Also return libs which only partially match + $candidate = $library; + } + } + + return $candidate; + } +} diff --git a/library/Icinga/Application/Libraries/Library.php b/library/Icinga/Application/Libraries/Library.php new file mode 100644 index 0000000..63e50b2 --- /dev/null +++ b/library/Icinga/Application/Libraries/Library.php @@ -0,0 +1,259 @@ +path = $path; + } + + /** + * Get this library's path + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Get path of this library's JS assets + * + * @return string + */ + public function getJsAssetPath() + { + $this->assets(); + return $this->jsAssetPath; + } + + /** + * Get path of this library's CSS assets + * + * @return string + */ + public function getCssAssetPath() + { + $this->assets(); + return $this->cssAssetPath; + } + + /** + * Get path of this library's static assets + * + * @return string + */ + public function getStaticAssetPath() + { + $this->assets(); + return $this->staticAssetPath; + } + + /** + * Get this library's name + * + * @return string + */ + public function getName() + { + return $this->metaData()['name']; + } + + /** + * Get this library's version + * + * @return string + */ + public function getVersion() + { + if ($this->version === null) { + if (isset($this->metaData()['version'])) { + $this->version = trim(ltrim($this->metaData()['version'], 'v')); + } else { + $versionFile = $this->path . DIRECTORY_SEPARATOR . 'VERSION'; + if (file_exists($versionFile)) { + $this->version = trim(ltrim(file_get_contents($versionFile), 'v')); + } else { + $this->version = ''; + } + } + } + + return $this->version; + } + + /** + * Check whether the given package is required + * + * @param string $vendor The vendor of the project + * @param string $project The project's name + * + * @return bool + */ + public function isRequired($vendor, $project) + { + // Ensure the parts are lowercase and separated by dashes, not capital letters + $project = strtolower(join('-', preg_split('/\w(?=[A-Z])/', $project))); + + return isset($this->metaData()['require'][strtolower($vendor) . '/' . $project]); + } + + /** + * Get this library's JS assets + * + * @return string[] Asset paths + */ + public function getJsAssets() + { + return $this->assets()['js']; + } + + /** + * Get this library's CSS assets + * + * @return string[] Asset paths + */ + public function getCssAssets() + { + return $this->assets()['css']; + } + + /** + * Get this library's static assets + * + * @return string[] Asset paths + */ + public function getStaticAssets() + { + return $this->assets()['static']; + } + + /** + * Register this library's autoloader + * + * @return void + */ + public function registerAutoloader() + { + $autoloaderPath = join(DIRECTORY_SEPARATOR, [$this->path, 'vendor', 'autoload.php']); + if (file_exists($autoloaderPath)) { + require_once $autoloaderPath; + } + } + + /** + * Parse and return this library's metadata + * + * @return array + * + * @throws ConfigurationError + * @throws JsonDecodeException + */ + protected function metaData() + { + if ($this->metaData === null) { + $metaData = @file_get_contents($this->path . DIRECTORY_SEPARATOR . 'composer.json'); + if ($metaData === false) { + throw new ConfigurationError('Library at "%s" is not a composerized project', $this->path); + } + + $this->metaData = Json::decode($metaData, true); + } + + return $this->metaData; + } + + /** + * Register and return this library's assets + * + * @return array + */ + protected function assets() + { + if ($this->assets !== null) { + return $this->assets; + } + + $listAssets = function ($type) { + $dir = join(DIRECTORY_SEPARATOR, [$this->path, 'asset', $type]); + if (! is_dir($dir)) { + return []; + } + + $this->{$type . 'AssetPath'} = $dir; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator( + $dir, + RecursiveDirectoryIterator::CURRENT_AS_FILEINFO | RecursiveDirectoryIterator::SKIP_DOTS + )); + if ($type === 'static') { + return $iterator; + } + + return new CallbackFilterIterator( + $iterator, + function ($path) use ($type) { + if ($type === 'js' && $path->getExtension() === 'js') { + return substr($path->getPathname(), -5 - strlen($type)) !== ".min.$type"; + } elseif ($type === 'css' + && ($path->getExtension() === 'css' || $path->getExtension() === 'less') + ) { + return substr($path->getPathname(), -5 - strlen($type)) !== ".min.$type"; + } + + return false; + } + ); + }; + + $this->assets = []; + + $jsAssets = $listAssets('js'); + $this->assets['js'] = is_array($jsAssets) ? $jsAssets : iterator_to_array($jsAssets); + + $cssAssets = $listAssets('css'); + $this->assets['css'] = is_array($cssAssets) ? $cssAssets : iterator_to_array($cssAssets); + + $staticAssets = $listAssets('static'); + $this->assets['static'] = is_array($staticAssets) ? $staticAssets : iterator_to_array($staticAssets); + + return $this->assets; + } +} diff --git a/library/Icinga/Application/Logger.php b/library/Icinga/Application/Logger.php new file mode 100644 index 0000000..347af44 --- /dev/null +++ b/library/Icinga/Application/Logger.php @@ -0,0 +1,349 @@ + 'DEBUG', + Logger::INFO => 'INFO', + Logger::WARNING => 'WARNING', + Logger::ERROR => 'ERROR' + ); + + /** + * This logger's instance + * + * @var static + */ + protected static $instance; + + /** + * Log writer + * + * @var \Icinga\Application\Logger\LogWriter + */ + protected $writer; + + /** + * Maximum level to emit + * + * @var int + */ + protected $level; + + /** + * Error messages to be displayed prior to any other log message + * + * @var array + */ + protected $configErrors = array(); + + /** + * Create a new logger object + * + * @param ConfigObject $config + * + * @throws ConfigurationError If the logging configuration directive 'log' is missing or if the logging level is + * not defined + */ + public function __construct(ConfigObject $config) + { + if ($config->log === null) { + throw new ConfigurationError('Required logging configuration directive \'log\' missing'); + } + + $this->setLevel($config->get('level', static::ERROR)); + + if (strtolower($config->get('log', 'syslog')) !== 'none') { + $this->writer = $this->createWriter($config); + } + } + + /** + * Set the logging level to use + * + * @param mixed $level + * + * @return $this + * + * @throws ConfigurationError In case the given level is invalid + */ + public function setLevel($level) + { + if (is_numeric($level)) { + $level = (int) $level; + if (! isset(static::$levels[$level])) { + throw new ConfigurationError( + 'Can\'t set logging level %d. Logging level is invalid. Use one of %s or one of the' + . ' Logger\'s constants.', + $level, + implode(', ', array_keys(static::$levels)) + ); + } + + $this->level = $level; + } else { + $level = strtoupper($level); + $levels = array_flip(static::$levels); + if (! isset($levels[$level])) { + throw new ConfigurationError( + 'Can\'t set logging level "%s". Logging level is invalid. Use one of %s.', + $level, + implode(', ', array_keys($levels)) + ); + } + + $this->level = $levels[$level]; + } + + return $this; + } + + /** + * Return the logging level being used + * + * @return int + */ + public function getLevel() + { + return $this->level; + } + + /** + * Register the given message as config error + * + * Config errors are logged every time a log message is being logged. + * + * @param mixed $arg,... A string, exception or format-string + substitutions + * + * @return $this + */ + public function registerConfigError() + { + if (func_num_args() > 0) { + $this->configErrors[] = static::formatMessage(func_get_args()); + } + + return $this; + } + + /** + * Create a new logger object + * + * @param ConfigObject $config + * + * @return static + */ + public static function create(ConfigObject $config) + { + static::$instance = new static($config); + return static::$instance; + } + + /** + * Create a log writer + * + * @param ConfigObject $config The configuration to initialize the writer with + * + * @return \Icinga\Application\Logger\LogWriter The requested log writer + * @throws ConfigurationError If the requested writer cannot be found + */ + protected function createWriter(ConfigObject $config) + { + $class = 'Icinga\\Application\\Logger\\Writer\\' . ucfirst(strtolower($config->log)) . 'Writer'; + if (! class_exists($class)) { + throw new ConfigurationError( + 'Cannot find log writer of type "%s"', + $config->log + ); + } + return new $class($config); + } + + /** + * Log a message + * + * @param int $level The logging level + * @param string $message The log message + */ + public function log($level, $message) + { + if ($this->writer !== null && $this->level <= $level) { + foreach ($this->configErrors as $error_message) { + $this->writer->log(static::ERROR, $error_message); + } + + $this->writer->log($level, $message); + } + } + + /** + * Return a string representation of the passed arguments + * + * This method provides three different processing techniques: + * - If the only passed argument is a string it is returned unchanged + * - If the only passed argument is an exception it is formatted as follows: + * in : with message: [ <- ...] + * - If multiple arguments are passed the first is interpreted as format-string + * that gets substituted with the remaining ones which can be of any type + * + * @param array $arguments The arguments to format + * + * @return string The formatted result + */ + protected static function formatMessage(array $arguments) + { + if (count($arguments) === 1) { + $message = $arguments[0]; + + if ($message instanceof Exception) { + $messages = array(); + $error = $message; + do { + $messages[] = IcingaException::describe($error); + } while ($error = $error->getPrevious()); + $message = implode(' <- ', $messages); + } + + return $message; + } + + return vsprintf( + array_shift($arguments), + array_map( + function ($a) { + return is_string($a) ? $a : ($a instanceof Exception + ? IcingaException::describe($a) + : Json::encode($a)); + }, + $arguments + ) + ); + } + + /** + * Log a message with severity ERROR + * + * @param mixed $arg,... A string, exception or format-string + substitutions + */ + public static function error() + { + if (static::$instance !== null && func_num_args() > 0) { + static::$instance->log(static::ERROR, static::formatMessage(func_get_args())); + } + } + + /** + * Log a message with severity WARNING + * + * @param mixed $arg,... A string, exception or format-string + substitutions + */ + public static function warning() + { + if (static::$instance !== null && func_num_args() > 0) { + static::$instance->log(static::WARNING, static::formatMessage(func_get_args())); + } + } + + /** + * Log a message with severity INFO + * + * @param mixed $arg,... A string, exception or format-string + substitutions + */ + public static function info() + { + if (static::$instance !== null && func_num_args() > 0) { + static::$instance->log(static::INFO, static::formatMessage(func_get_args())); + } + } + + /** + * Log a message with severity DEBUG + * + * @param mixed $arg,... A string, exception or format-string + substitutions + */ + public static function debug() + { + if (static::$instance !== null && func_num_args() > 0) { + static::$instance->log(static::DEBUG, static::formatMessage(func_get_args())); + } + } + + /** + * Get the log writer to use + * + * @return \Icinga\Application\Logger\LogWriter + */ + public function getWriter() + { + return $this->writer; + } + + /** + * Is the logger writing to Syslog? + * + * @return bool + */ + public static function writesToSyslog() + { + return static::$instance && static::$instance->getWriter() instanceof SyslogWriter; + } + + /** + * Is the logger writing to a file? + * + * @return bool + */ + public static function writesToFile() + { + return static::$instance && static::$instance->getWriter() instanceof FileWriter; + } + + /** + * Get this' instance + * + * @return static + */ + public static function getInstance() + { + return static::$instance; + } +} diff --git a/library/Icinga/Application/Logger/LogWriter.php b/library/Icinga/Application/Logger/LogWriter.php new file mode 100644 index 0000000..019bdad --- /dev/null +++ b/library/Icinga/Application/Logger/LogWriter.php @@ -0,0 +1,30 @@ +config = $config; + } + + /** + * Log a message with the given severity + */ + abstract public function log($severity, $message); +} diff --git a/library/Icinga/Application/Logger/Writer/FileWriter.php b/library/Icinga/Application/Logger/Writer/FileWriter.php new file mode 100644 index 0000000..6b4ed54 --- /dev/null +++ b/library/Icinga/Application/Logger/Writer/FileWriter.php @@ -0,0 +1,80 @@ +file === null) { + throw new ConfigurationError('Required logging configuration directive \'file\' missing'); + } + $this->file = $config->file; + + if (substr($this->file, 0, 6) !== 'php://' && ! file_exists(dirname($this->file))) { + throw new ConfigurationError( + 'Log path "%s" does not exist', + dirname($this->file) + ); + } + + try { + $this->write(''); // Avoid to handle such errors on every write access + } catch (Exception $e) { + throw new ConfigurationError( + 'Cannot write to log file "%s" (%s)', + $this->file, + $e->getMessage() + ); + } + } + + /** + * Log a message + * + * @param int $level The logging level + * @param string $message The log message + */ + public function log($level, $message) + { + $this->write(date('c') . ' - ' . Logger::$levels[$level] . ' - ' . $message . PHP_EOL); + } + + /** + * Write a message to the log + * + * @param string $message + */ + protected function write($message) + { + $file = new File($this->file, 'a'); + $file->fwrite($message); + $file->fflush(); + } +} diff --git a/library/Icinga/Application/Logger/Writer/PhpWriter.php b/library/Icinga/Application/Logger/Writer/PhpWriter.php new file mode 100644 index 0000000..dedb2bd --- /dev/null +++ b/library/Icinga/Application/Logger/Writer/PhpWriter.php @@ -0,0 +1,39 @@ +ident = $config->get('application', 'icingaweb2'); + } + + public function log($severity, $message) + { + if (ini_get('error_log') === 'syslog') { + $message = str_replace("\n", ' ', $message); + } + + error_log($this->ident . ': ' . Logger::$levels[$severity] . ' - ' . $message); + } +} diff --git a/library/Icinga/Application/Logger/Writer/StderrWriter.php b/library/Icinga/Application/Logger/Writer/StderrWriter.php new file mode 100644 index 0000000..2f35ff2 --- /dev/null +++ b/library/Icinga/Application/Logger/Writer/StderrWriter.php @@ -0,0 +1,61 @@ +screen === null) { + $this->screen = Screen::instance(STDERR); + } + + return $this->screen; + } + + /** + * Log a message with the given severity + * + * @param int $severity The severity to use + * @param string $message The message to log + */ + public function log($severity, $message) + { + switch ($severity) { + case Logger::ERROR: + $color = 'red'; + break; + case Logger::WARNING: + $color = 'yellow'; + break; + case Logger::INFO: + $color = 'green'; + break; + case Logger::DEBUG: + $color = 'blue'; + break; + } + + file_put_contents('php://stderr', $this->screen()->colorize($message, $color) . "\n"); + } +} diff --git a/library/Icinga/Application/Logger/Writer/StdoutWriter.php b/library/Icinga/Application/Logger/Writer/StdoutWriter.php new file mode 100644 index 0000000..a6f43e5 --- /dev/null +++ b/library/Icinga/Application/Logger/Writer/StdoutWriter.php @@ -0,0 +1,13 @@ + LOG_USER, + 'local0' => LOG_LOCAL0, + 'local1' => LOG_LOCAL1, + 'local2' => LOG_LOCAL2, + 'local3' => LOG_LOCAL3, + 'local4' => LOG_LOCAL4, + 'local5' => LOG_LOCAL5, + 'local6' => LOG_LOCAL6, + 'local7' => LOG_LOCAL7 + ); + + /** + * Log level to syslog severity map + * + * @var array + */ + public static $severityMap = array( + Logger::ERROR => LOG_ERR, + Logger::WARNING => LOG_WARNING, + Logger::INFO => LOG_INFO, + Logger::DEBUG => LOG_DEBUG + ); + + /** + * Create a new syslog log writer + * + * @param ConfigObject $config + */ + public function __construct(ConfigObject $config) + { + $this->ident = $config->get('application', 'icingaweb2'); + + $configuredFacility = $config->get('facility', 'user'); + if (! isset(static::$facilities[$configuredFacility])) { + throw new ConfigurationError( + 'Invalid logging facility: "%s" (expected one of: %s)', + $configuredFacility, + implode(', ', array_keys(static::$facilities)) + ); + } + $this->facility = static::$facilities[$configuredFacility]; + } + + /** + * Log a message + * + * @param int $level The logging level + * @param string $message The log message + */ + public function log($level, $message) + { + openlog($this->ident, LOG_PID, $this->facility); + syslog(static::$severityMap[$level], str_replace("\n", ' ', $message)); + } +} diff --git a/library/Icinga/Application/Modules/DashboardContainer.php b/library/Icinga/Application/Modules/DashboardContainer.php new file mode 100644 index 0000000..f3c8bc6 --- /dev/null +++ b/library/Icinga/Application/Modules/DashboardContainer.php @@ -0,0 +1,58 @@ +dashlets = $dashlets; + return $this; + } + + /** + * Return this dashboard's dashlets + * + * @return array + */ + public function getDashlets() + { + return $this->dashlets ?: array(); + } + + /** + * Add a new dashlet + * + * @param string $name + * @param string $url + * @param int $priority + * + * @return $this + */ + public function add($name, $url, $priority = null) + { + $this->dashlets[$name] = [ + 'url' => $url, + 'priority' => $priority + ]; + return $this; + } +} diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php new file mode 100644 index 0000000..55d074d --- /dev/null +++ b/library/Icinga/Application/Modules/Manager.php @@ -0,0 +1,698 @@ +app = $app; + $this->modulePaths = $availableDirs; + $this->enableDir = $enabledDir; + } + + /** + * Query interface for the module manager + * + * @return SimpleQuery + */ + public function select() + { + $source = new ArrayDatasource($this->getModuleInfo()); + return $source->select(); + } + + /** + * Check for enabled modules + * + * Update the internal $enabledDirs property with the enabled modules. + * + * @throws ConfigurationError If module dir does not exist, is not a directory or not readable + */ + private function detectEnabledModules() + { + if (! file_exists($parent = dirname($this->enableDir))) { + return; + } + if (! is_readable($parent)) { + throw new NotReadableError( + 'Cannot read enabled modules. Config directory "%s" is not readable', + $parent + ); + } + + if (! file_exists($this->enableDir)) { + return; + } + if (! is_dir($this->enableDir)) { + throw new NotReadableError( + 'Cannot read enabled modules. Module directory "%s" is not a directory', + $this->enableDir + ); + } + if (! is_readable($this->enableDir)) { + throw new NotReadableError( + 'Cannot read enabled modules. Module directory "%s" is not readable', + $this->enableDir + ); + } + if (($dh = opendir($this->enableDir)) !== false) { + $isPhar = substr($this->enableDir, 0, 8) === 'phar:///'; + $this->enabledDirs = array(); + while (($file = readdir($dh)) !== false) { + if ($file[0] === '.' || $file === 'README') { + continue; + } + + $link = $this->enableDir . DIRECTORY_SEPARATOR . $file; + if (! $isPhar && ! is_link($link)) { + Logger::warning( + 'Found invalid module in enabledModule directory "%s": "%s" is not a symlink', + $this->enableDir, + $link + ); + continue; + } + + $dir = $isPhar ? $link : realpath($link); + if ($dir !== false && is_dir($dir)) { + $this->enabledDirs[$file] = $dir; + } else { + $this->enabledDirs[$file] = null; + + Logger::warning( + 'Found invalid module in enabledModule directory "%s": "%s" points to non existing path "%s"', + $this->enableDir, + $link, + $dir + ); + } + + ksort($this->enabledDirs); + } + closedir($dh); + } + } + + /** + * Try to set all enabled modules in loaded sate + * + * @return $this + * @see Manager::loadModule() + */ + public function loadEnabledModules() + { + if (! $this->loadedAllEnabledModules) { + foreach ($this->listEnabledModules() as $name) { + $this->loadModule($name); + } + + $this->loadedAllEnabledModules = true; + } + + return $this; + } + + /** + * Whether we loaded all enabled modules + * + * @return bool + */ + public function loadedAllEnabledModules() + { + return $this->loadedAllEnabledModules; + } + + + /** + * Try to load the module and register it in the application + * + * @param string $name The name of the module to load + * @param mixed $basedir Optional module base directory + * + * @return $this + */ + public function loadModule($name, $basedir = null) + { + if ($this->hasLoaded($name)) { + return $this; + } + + $module = null; + if ($basedir === null) { + $module = new Module($this->app, $name, $this->getModuleDir($name)); + } else { + $module = new Module($this->app, $name, $basedir); + } + + if ($name !== 'ipl' && $name !== 'reactbundle') { + $module->register(); + } + + $this->loadedModules[$name] = $module; + return $this; + } + + /** + * Set the given module to the enabled state + * + * @param string $name The module to enable + * @param bool $force Whether to ignore unmet dependencies + * + * @return $this + * @throws ConfigurationError When trying to enable a module that is not installed + * @throws SystemPermissionException When insufficient permissions for the application exist + */ + public function enableModule($name, $force = false) + { + if (! $this->hasInstalled($name)) { + throw new ConfigurationError( + 'Cannot enable module "%s". Module is not installed.', + $name + ); + } + + if (strtolower(substr($name, 0, 18)) === 'icingaweb2-module-') { + throw new ConfigurationError( + 'Cannot enable module "%s": Directory name does not match the module\'s name.' + . ' Please rename the module to "%s" before enabling.', + $name, + substr($name, 18) + ); + } + + if ($this->hasUnmetDependencies($name)) { + if ($force) { + Logger::warning(t('Enabling module "%s" although it has unmet dependencies'), $name); + } else { + throw new ConfigurationError( + t('Module "%s" can\'t be enabled. Module has unmet dependencies'), + $name + ); + } + } + + clearstatcache(true); + $target = $this->installedBaseDirs[$name]; + $link = $this->enableDir . DIRECTORY_SEPARATOR . $name; + + if (! is_dir($this->enableDir)) { + if (!@mkdir($this->enableDir, 0777, true)) { + $error = error_get_last(); + throw new SystemPermissionException( + 'Failed to create enabledModules directory "%s" (%s)', + $this->enableDir, + $error['message'] + ); + } + + chmod($this->enableDir, 02770); + } elseif (! is_writable($this->enableDir)) { + throw new SystemPermissionException( + 'Cannot enable module "%s". Check the permissions for the enabledModules directory: %s', + $name, + $this->enableDir + ); + } + + $this->loadedAllEnabledModules = false; + + if (file_exists($link) && is_link($link)) { + return $this; + } + + if (! @symlink($target, $link)) { + $error = error_get_last(); + if (strstr($error["message"], "File exists") === false) { + throw new SystemPermissionException( + 'Cannot enable module "%s" at %s due to file system errors. ' + . 'Please check path and mounting points because this is not a permission error. ' + . 'Primary error was: %s', + $name, + $this->enableDir, + $error['message'] + ); + } + } + + $this->enabledDirs[$name] = $link; + $this->loadModule($name); + return $this; + } + + /** + * Disable the given module and remove its enabled state + * + * @param string $name The name of the module to disable + * + * @return $this + * + * @throws ConfigurationError When the module is not installed or it's not a symlink + * @throws SystemPermissionException When insufficient permissions for the application exist + */ + public function disableModule($name) + { + if (! $this->hasEnabled($name)) { + throw new ConfigurationError( + 'Cannot disable module "%s". Module is not installed.', + $name + ); + } + + if (! is_writable($this->enableDir)) { + throw new SystemPermissionException( + 'Cannot disable module "%s". Check the permissions for the enabledModules directory: %s', + $name, + $this->enableDir + ); + } + + $link = $this->enableDir . DIRECTORY_SEPARATOR . $name; + if (! is_link($link)) { + throw new ConfigurationError( + 'Cannot disable module %s at %s. ' + . 'It looks like you have installed this module manually and moved it to your module folder. ' + . 'In order to dynamically enable and disable modules, you have to create a symlink to ' + . 'the enabledModules folder.', + $name, + $this->enableDir + ); + } + + if (is_link($link)) { + if (! @unlink($link)) { + $error = error_get_last(); + throw new SystemPermissionException( + 'Cannot enable module "%s" at %s due to file system errors. ' + . 'Please check path and mounting points because this is not a permission error. ' + . 'Primary error was: %s', + $name, + $this->enableDir, + $error['message'] + ); + } + } + + unset($this->enabledDirs[$name]); + return $this; + } + + /** + * Return the directory of the given module as a string, optionally with a given sub directoy + * + * @param string $name The module name to return the module directory of + * @param string $subdir The sub directory to append to the path + * + * @return string + * + * @throws ProgrammingError When the module is not installed or existing + */ + public function getModuleDir($name, $subdir = '') + { + if ($this->hasLoaded($name)) { + return $this->getModule($name)->getBaseDir() . $subdir; + } + + if ($this->hasEnabled($name)) { + return $this->enabledDirs[$name]. $subdir; + } + + if ($this->hasInstalled($name)) { + return $this->installedBaseDirs[$name] . $subdir; + } + + throw new ProgrammingError( + 'Trying to access uninstalled module dir: %s', + $name + ); + } + + /** + * Return true when the module with the given name is installed, otherwise false + * + * @param string $name The module to check for being installed + * + * @return bool + */ + public function hasInstalled($name) + { + if (!count($this->installedBaseDirs)) { + $this->detectInstalledModules(); + } + return array_key_exists($name, $this->installedBaseDirs); + } + + /** + * Return true when the given module is in enabled state, otherwise false + * + * @param string $name The module to check for being enabled + * + * @return bool + */ + public function hasEnabled($name) + { + return array_key_exists($name, $this->enabledDirs); + } + + /** + * Return true when the module is in loaded state, otherwise false + * + * @param string $name The module to check for being loaded + * + * @return bool + */ + public function hasLoaded($name) + { + return array_key_exists($name, $this->loadedModules); + } + + /** + * Check if a module with the given name is enabled + * + * Passing a version constraint also verifies that the module's version matches. + * + * @param string $name + * @param string $version + * + * @return bool + */ + public function has($name, $version = null) + { + if (! $this->hasEnabled($name)) { + return false; + } elseif ($version === null || $version === true) { + return true; + } + + $operator = '='; + if (preg_match('/^([<>=]{1,2})\s*v?((?:[\d.]+)(?:.+)?)$/', $version, $match)) { + $operator = $match[1]; + $version = $match[2]; + } + + $modVersion = ltrim($this->getModule($name)->getVersion(), 'v'); + return version_compare($modVersion, $version, $operator); + } + + /** + * Get the currently loaded modules + * + * @return Module[] + */ + public function getLoadedModules() + { + return $this->loadedModules; + } + + /** + * Get a module + * + * @param string $name Name of the module + * @param bool $assertLoaded Whether or not to throw an exception if the module hasn't been loaded + * + * @return Module + * @throws ProgrammingError If the module hasn't been loaded + */ + public function getModule($name, $assertLoaded = true) + { + if ($this->hasLoaded($name)) { + return $this->loadedModules[$name]; + } elseif (! (bool) $assertLoaded) { + return new Module($this->app, $name, $this->getModuleDir($name)); + } + throw new ProgrammingError( + 'Can\'t access module %s because it hasn\'t been loaded', + $name + ); + } + + /** + * Return an array containing information objects for each available module + * + * Each entry has the following fields + * * name, name of the module as a string + * * path, path where the module is located as a string + * * installed, whether the module is installed or not as a boolean + * * enabled, whether the module is enabled or not as a boolean + * * loaded, whether the module is loaded or not as a boolean + * + * @return array + */ + public function getModuleInfo() + { + $info = array(); + + $installed = $this->listInstalledModules(); + foreach ($installed as $name) { + $info[$name] = (object) array( + 'name' => $name, + 'path' => $this->installedBaseDirs[$name], + 'installed' => true, + 'enabled' => $this->hasEnabled($name), + 'loaded' => $this->hasLoaded($name) + ); + } + + $enabled = $this->listEnabledModules(); + foreach ($enabled as $name) { + $info[$name] = (object) array( + 'name' => $name, + 'path' => $this->enabledDirs[$name], + 'installed' => $this->enabledDirs[$name] !== null, + 'enabled' => true, + 'loaded' => $this->hasLoaded($name) + ); + } + + return $info; + } + + /** + * Check if the given module has unmet dependencies + * + * @param string $name + * + * @return bool + */ + public function hasUnmetDependencies($name) + { + $module = $this->getModule($name, false); + + $requiredMods = $module->getRequiredModules(); + + if (isset($requiredMods['monitoring'], $requiredMods['icingadb'])) { + if (! $this->has('monitoring', $requiredMods['monitoring']) + && ! $this->has('icingadb', $requiredMods['icingadb']) + ) { + return true; + } + + unset($requiredMods['monitoring'], $requiredMods['icingadb']); + } + + foreach ($requiredMods as $moduleName => $moduleVersion) { + if (! $this->has($moduleName, $moduleVersion)) { + return true; + } + } + + $libraries = Icinga::app()->getLibraries(); + + $requiredLibs = $module->getRequiredLibraries(); + foreach ($requiredLibs as $libraryName => $libraryVersion) { + if (! $libraries->has($libraryName, $libraryVersion)) { + return true; + } + } + + return false; + } + + /** + * Return an array containing all enabled module names as strings + * + * @return array + */ + public function listEnabledModules() + { + if (count($this->enabledDirs) === 0) { + $this->detectEnabledModules(); + } + + return array_keys($this->enabledDirs); + } + + /** + * Return an array containing all loaded module names as strings + * + * @return array + */ + public function listLoadedModules() + { + return array_keys($this->loadedModules); + } + + /** + * Return an array of module names from installed modules + * + * Calls detectInstalledModules() if no module discovery has been performed yet + * + * @return array + * + * @see detectInstalledModules() + */ + public function listInstalledModules() + { + if (!count($this->installedBaseDirs)) { + $this->detectInstalledModules(); + } + + if (count($this->installedBaseDirs)) { + return array_keys($this->installedBaseDirs); + } + + return array(); + } + + /** + * Detect installed modules from every path provided in modulePaths + * + * @param array $availableDirs Installed modules location + * + * @return $this + */ + public function detectInstalledModules(array $availableDirs = null) + { + $modulePaths = $availableDirs !== null ? $availableDirs : $this->modulePaths; + foreach ($modulePaths as $basedir) { + $canonical = realpath($basedir); + if ($canonical === false) { + Logger::warning('Module path "%s" does not exist', $basedir); + continue; + } + if (!is_dir($canonical)) { + Logger::error('Module path "%s" is not a directory', $canonical); + continue; + } + if (!is_readable($canonical)) { + Logger::error('Module path "%s" is not readable', $canonical); + continue; + } + if (($dh = opendir($canonical)) !== false) { + while (($file = readdir($dh)) !== false) { + if ($file[0] === '.') { + continue; + } + if (is_dir($canonical . '/' . $file)) { + if (! array_key_exists($file, $this->installedBaseDirs)) { + $this->installedBaseDirs[$file] = $canonical . '/' . $file; + } else { + Logger::debug( + 'Module "%s" already exists in installation path "%s" and is ignored.', + $canonical . '/' . $file, + $this->installedBaseDirs[$file] + ); + } + } + } + closedir($dh); + } + } + ksort($this->installedBaseDirs); + return $this; + } + + /** + * Get the directories where to look for installed modules + * + * @return array + */ + public function getModuleDirs() + { + return $this->modulePaths; + } +} diff --git a/library/Icinga/Application/Modules/MenuItemContainer.php b/library/Icinga/Application/Modules/MenuItemContainer.php new file mode 100644 index 0000000..88599e6 --- /dev/null +++ b/library/Icinga/Application/Modules/MenuItemContainer.php @@ -0,0 +1,55 @@ +children = $children; + return $this; + } + + /** + * Return this menu item's children + * + * @return array + */ + public function getChildren() + { + return $this->children ?: array(); + } + + /** + * Add a new sub menu + * + * @param string $name + * @param array $properties + * + * @return MenuItemContainer The newly added sub menu + */ + public function add($name, array $properties = array()) + { + $child = new MenuItemContainer($name, $properties); + $this->children[] = $child; + return $child; + } +} diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php new file mode 100644 index 0000000..00a3383 --- /dev/null +++ b/library/Icinga/Application/Modules/Module.php @@ -0,0 +1,1444 @@ +app = $app; + $this->name = $name; + $this->basedir = $basedir; + $this->cssdir = $basedir . '/public/css'; + $this->jsdir = $basedir . '/public/js'; + $this->libdir = $basedir . '/library'; + $this->configdir = $app->getConfigDir('modules/' . $name); + $this->appdir = $basedir . '/application'; + $this->localedir = $basedir . '/application/locale'; + $this->formdir = $basedir . '/application/forms'; + $this->controllerdir = $basedir . '/application/controllers'; + $this->runScript = $basedir . '/run.php'; + $this->configScript = $basedir . '/configuration.php'; + $this->metadataFile = $basedir . '/module.info'; + + $this->translationDomain = $name; + } + + /** + * Provide a search URL + * + * @param string $title + * @param string $url + * @param int $priority + * + * @return $this + */ + public function provideSearchUrl($title, $url, $priority = 0) + { + $this->searchUrls[] = (object) array( + 'title' => (string) $title, + 'url' => (string) $url, + 'priority' => (int) $priority + ); + + return $this; + } + + /** + * Get this module's search urls + * + * @return array + */ + public function getSearchUrls() + { + $this->launchConfigScript(); + return $this->searchUrls; + } + + /** + * Return this module's dashboard + * + * @return Navigation + */ + public function getDashboard() + { + $this->launchConfigScript(); + return $this->createDashboard($this->paneItems); + } + + /** + * Create and return a new navigation for the given dashboard panes + * + * @param DashboardContainer[] $panes + * + * @return Navigation + */ + public function createDashboard(array $panes) + { + $navigation = new Navigation(); + foreach ($panes as $pane) { + /** @var DashboardContainer $pane */ + $dashlets = []; + foreach ($pane->getDashlets() as $dashletName => $dashletConfig) { + $dashlets[$dashletName] = [ + 'label' => $this->translate($dashletName), + 'url' => $dashletConfig['url'], + 'priority' => $dashletConfig['priority'] + ]; + } + + $navigation->addItem( + $pane->getName(), + array_merge( + $pane->getProperties(), + array( + 'label' => $this->translate($pane->getName()), + 'type' => 'dashboard-pane', + 'children' => $dashlets + ) + ) + ); + } + + return $navigation; + } + + /** + * Add or get a dashboard pane + * + * @param string $name + * @param array $properties + * + * @return DashboardContainer + */ + protected function dashboard($name, array $properties = array()) + { + if (array_key_exists($name, $this->paneItems)) { + $this->paneItems[$name]->setProperties($properties); + } else { + $this->paneItems[$name] = new DashboardContainer($name, $properties); + } + + return $this->paneItems[$name]; + } + + /** + * Return this module's menu + * + * @return Navigation + */ + public function getMenu() + { + $this->launchConfigScript(); + return Navigation::fromArray($this->createMenu($this->menuItems)); + } + + /** + * Create and return an array structure for the given menu items + * + * @param MenuItemContainer[] $items + * + * @return array + */ + private function createMenu(array $items) + { + $navigation = array(); + foreach ($items as $item) { + /** @var MenuItemContainer $item */ + $properties = $item->getProperties(); + $properties['children'] = $this->createMenu($item->getChildren()); + if (! isset($properties['label'])) { + $properties['label'] = $this->translate($item->getName()); + } + + $navigation[$item->getName()] = $properties; + } + + return $navigation; + } + + /** + * Add or get a menu section + * + * @param string $name + * @param array $properties + * + * @return MenuItemContainer + */ + protected function menuSection($name, array $properties = array()) + { + if (array_key_exists($name, $this->menuItems)) { + $this->menuItems[$name]->setProperties($properties); + } else { + $this->menuItems[$name] = new MenuItemContainer($name, $properties); + } + + return $this->menuItems[$name]; + } + + /** + * Register module + * + * @return bool + */ + public function register() + { + if ($this->registered) { + return true; + } + + $this->registerAutoloader(); + try { + $this->launchRunScript(); + } catch (Exception $e) { + Logger::warning( + 'Launching the run script %s for module %s failed with the following exception: %s', + $this->runScript, + $this->name, + $e->getMessage() + ); + return false; + } + $this->registerWebIntegration(); + $this->registered = true; + + return true; + } + + /** + * Get whether this module has been registered + * + * @return bool + */ + public function isRegistered() + { + return $this->registered; + } + + /** + * Test for an enabled module by name + * + * @param string $name + * + * @return bool + */ + public static function exists($name) + { + return Icinga::app()->getModuleManager()->hasEnabled($name); + } + + /** + * Get a module by name + * + * @param string $name + * @param bool $autoload + * + * @return self + * + * @throws ProgrammingError When the module is not yet loaded + */ + public static function get($name, $autoload = false) + { + $manager = Icinga::app()->getModuleManager(); + if (!$manager->hasLoaded($name)) { + if ($autoload === true && $manager->hasEnabled($name)) { + $manager->loadModule($name); + } + } + // Throws ProgrammingError when the module is not yet loaded + return $manager->getModule($name); + } + + /** + * Provide an additional CSS/LESS file + * + * @param string $path The path to the file, relative to self::$cssdir + * + * @return $this + */ + protected function provideCssFile($path) + { + $this->cssFiles[] = $this->cssdir . DIRECTORY_SEPARATOR . $path; + return $this; + } + + /** + * Test if module provides css + * + * @return bool + */ + public function hasCss() + { + if (file_exists($this->getCssFilename())) { + return true; + } + + $this->launchConfigScript(); + return !empty($this->cssFiles); + } + + /** + * Returns the complete less file name + * + * @return string + */ + public function getCssFilename() + { + return $this->cssdir . '/module.less'; + } + + /** + * Return the CSS/LESS files this module provides + * + * @return array + */ + public function getCssFiles() + { + $this->launchConfigScript(); + $files = $this->cssFiles; + if (file_exists($this->getCssFilename())) { + $files[] = $this->getCssFilename(); + } + return $files; + } + + /** + * Provide an additional Javascript file + * + * @param string $path The path to the file, relative to self::$jsdir + * + * @return $this + */ + protected function provideJsFile($path) + { + $this->jsFiles[] = $this->jsdir . DIRECTORY_SEPARATOR . $path; + return $this; + } + + /** + * Test if module provides js + * + * @return bool + */ + public function hasJs() + { + if (file_exists($this->getJsFilename())) { + return true; + } + + $this->launchConfigScript(); + return !empty($this->jsFiles); + } + + /** + * Returns the complete js file name + * + * @return string + */ + public function getJsFilename() + { + return $this->jsdir . '/module.js'; + } + + /** + * Return the Javascript files this module provides + * + * @return array + */ + public function getJsFiles() + { + $this->launchConfigScript(); + $files = $this->jsFiles; + $files[] = $this->getJsFilename(); + return $files; + } + + /** + * Get the module name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the module namespace + * + * @return string + */ + public function getNamespace() + { + return 'Icinga\\Module\\' . ucfirst($this->getName()); + } + + /** + * Get the module version + * + * @return string + */ + public function getVersion() + { + return $this->metadata()->version; + } + + /** + * Get the module description + * + * @return string + */ + public function getDescription() + { + return $this->metadata()->description; + } + + /** + * Get the module title (short description) + * + * @return string + */ + public function getTitle() + { + return $this->metadata()->title; + } + + /** + * Get the module dependencies + * + * @return array + * @deprecated Use method getRequiredModules() instead + */ + public function getDependencies() + { + return $this->metadata()->depends; + } + + /** + * Get required libraries + * + * @return array + */ + public function getRequiredLibraries() + { + $requiredLibraries = $this->metadata()->libraries; + + // Register module requirements for ipl and reactbundle as library requirements + $requiredModules = $this->metadata()->modules ?: $this->metadata()->depends; + if (isset($requiredModules['ipl']) && ! isset($requiredLibraries['icinga-php-library'])) { + $requiredLibraries['icinga-php-library'] = $requiredModules['ipl']; + } + + if (isset($requiredModules['reactbundle']) && ! isset($requiredLibraries['icinga-php-thirdparty'])) { + $requiredLibraries['icinga-php-thirdparty'] = $requiredModules['reactbundle']; + } + + return $requiredLibraries; + } + + /** + * Get required modules + * + * @return array + */ + public function getRequiredModules() + { + $requiredModules = $this->metadata()->modules ?: $this->metadata()->depends; + + $hasIcingadb = isset($requiredModules['icingadb']); + if (isset($requiredModules['monitoring']) && ($this->isSupportingIcingadb() || $hasIcingadb)) { + $requiredMods = []; + $icingadbVersion = true; + if ($hasIcingadb) { + $icingadbVersion = isset($requiredModules['icingadb']) ? $requiredModules['icingadb'] : true; + unset($requiredModules['icingadb']); + } + + foreach ($requiredModules as $name => $version) { + $requiredMods[$name] = $version; + if ($name === 'monitoring') { + $requiredMods['icingadb'] = $icingadbVersion; + } + } + + $requiredModules = $requiredMods; + } + + // Both modules are deprecated and their successors are now dependencies of web itself + unset($requiredModules['ipl'], $requiredModules['reactbundle']); + + return $requiredModules; + } + + /** + * Check whether module supports icingadb + * + * @return bool + */ + protected function isSupportingIcingadb() + { + $icingadbSupportingModules = [ + 'cube' => '1.2.0', + 'jira' => '1.2.0', + 'graphite' => '1.2.0', + 'director' => '1.9.0', + 'toplevelview' => '0.4.0', + 'businessprocess' => '2.4.0' + ]; + + return array_key_exists($this->getName(), $icingadbSupportingModules) + && version_compare($this->getVersion(), $icingadbSupportingModules[$this->getName()], '>='); + } + + /** + * Fetch module metadata + * + * @return object + */ + protected function metadata() + { + if ($this->metadata === null) { + $metadata = (object) [ + 'name' => $this->getName(), + 'version' => '0.0.0', + 'title' => null, + 'description' => '', + 'depends' => [], + 'libraries' => [], + 'modules' => [] + ]; + + if (file_exists($this->metadataFile)) { + $key = null; + $simpleRequires = false; + $file = new File($this->metadataFile, 'r'); + foreach ($file as $lineno => $line) { + $line = rtrim($line); + + if ($key === 'description') { + if (empty($line)) { + $metadata->description .= "\n"; + continue; + } elseif ($line[0] === ' ') { + $metadata->description .= $line; + continue; + } + } elseif (empty($line)) { + continue; + } + + if (strpos($line, ':') === false) { + Logger::debug( + "Can't process line %d in %s: Line does not specify a key:value pair" + . " nor is it part of the description (indented with a single space)", + $lineno, + $this->metadataFile + ); + + break; + } + + $parts = preg_split('/:\s+/', $line, 2); + if (count($parts) === 1) { + $parts[] = ''; + } + + list($key, $val) = $parts; + + $key = strtolower($key); + switch ($key) { + case 'requires': + if ($val) { + $simpleRequires = true; + $key = 'libraries'; + } else { + break; + } + + // Shares the syntax with `Depends` + case ' libraries': + case ' modules': + if ($simpleRequires && $key[0] === ' ') { + Logger::debug( + 'Can\'t process line %d in %s: Requirements already registered by a previous line', + $lineno, + $this->metadataFile + ); + break; + } + + $key = ltrim($key); + // Shares the syntax with `Depends` + case 'depends': + if (strpos($val, ' ') === false) { + $metadata->{$key}[$val] = true; + continue 2; + } + + $parts = preg_split('/,\s+/', $val); + foreach ($parts as $part) { + if (preg_match('/^([\w\-\/]+)\s+\((.+)\)$/', $part, $m)) { + $metadata->{$key}[$m[1]] = $m[2]; + } else { + $metadata->{$key}[$part] = true; + } + } + + break; + case 'description': + if ($metadata->title === null) { + $metadata->title = $val; + } else { + $metadata->description = $val; + } + break; + + default: + $metadata->{$key} = $val; + } + } + } + + if ($metadata->title === null) { + $metadata->title = $this->getName(); + } + + if ($metadata->description === '') { + $metadata->description = t( + 'This module has no description' + ); + } + + $this->metadata = $metadata; + } + return $this->metadata; + } + + /** + * Get the module's CSS directory + * + * @return string + */ + public function getCssDir() + { + return $this->cssdir; + } + + /** + * Get the module's JS directory + * + * @return string + */ + public function getJsDir() + { + return $this->jsdir; + } + + /** + * Get the module's controller directory + * + * @return string + */ + public function getControllerDir() + { + return $this->controllerdir; + } + + /** + * Get the module's base directory + * + * @return string + */ + public function getBaseDir() + { + return $this->basedir; + } + + /** + * Get the module's application directory + * + * @return string + */ + public function getApplicationDir() + { + return $this->appdir; + } + + /** + * Get the module's library directory + * + * @return string + */ + public function getLibDir() + { + return $this->libdir; + } + + /** + * Get the module's configuration directory + * + * @return string + */ + public function getConfigDir() + { + return $this->configdir; + } + + /** + * Get the module's form directory + * + * @return string + */ + public function getFormDir() + { + return $this->formdir; + } + + /** + * Get the module config + * + * @param string $file + * + * @return Config + */ + public function getConfig($file = 'config') + { + return $this->app->getConfig()->module($this->name, $file); + } + + /** + * Get provided permissions + * + * @return array + */ + public function getProvidedPermissions() + { + $this->launchConfigScript(); + return $this->permissionList; + } + + /** + * Get provided restrictions + * + * @return array + */ + public function getProvidedRestrictions() + { + $this->launchConfigScript(); + return $this->restrictionList; + } + + /** + * Whether the module provides the given restriction + * + * @param string $name Restriction name + * + * @return bool + */ + public function providesRestriction($name) + { + $this->launchConfigScript(); + return array_key_exists($name, $this->restrictionList); + } + + /** + * Whether the module provides the given permission + * + * @param string $name Permission name + * + * @return bool + */ + public function providesPermission($name) + { + $this->launchConfigScript(); + return array_key_exists($name, $this->permissionList); + } + + /** + * Get the module configuration tabs + * + * @return \Icinga\Web\Widget\Tabs + */ + public function getConfigTabs() + { + $this->launchConfigScript(); + $tabs = Widget::create('tabs'); + /** @var \Icinga\Web\Widget\Tabs $tabs */ + $tabs->add('info', array( + 'url' => 'config/module', + 'urlParams' => array('name' => $this->getName()), + 'label' => 'Module: ' . $this->getName() + )); + + if ($this->app->getModuleManager()->hasEnabled($this->name)) { + foreach ($this->configTabs as $name => $config) { + $tabs->add($name, $config); + } + } + + return $tabs; + } + + /** + * Whether the module provides a setup wizard + * + * @return bool + */ + public function providesSetupWizard() + { + $this->launchConfigScript(); + if ($this->setupWizard && class_exists($this->setupWizard)) { + $wizard = new $this->setupWizard; + return $wizard instanceof SetupWizard; + } + + return false; + } + + /** + * Get the module's setup wizard + * + * @return SetupWizard + */ + public function getSetupWizard() + { + return new $this->setupWizard; + } + + /** + * Get the module's user backends + * + * @return array + */ + public function getUserBackends() + { + $this->launchConfigScript(); + return $this->userBackends; + } + + /** + * Get the module's user group backends + * + * @return array + */ + public function getUserGroupBackends() + { + $this->launchConfigScript(); + return $this->userGroupBackends; + } + + /** + * Return this module's configurable navigation items + * + * @return array + */ + public function getNavigationItems() + { + $this->launchConfigScript(); + return $this->navigationItems; + } + + /** + * Provide a named permission + * + * @param string $name Unique permission name + * @param string $description Permission description + * + * @throws IcingaException If the permission is already provided + */ + protected function providePermission($name, $description) + { + if ($this->providesPermission($name)) { + throw new IcingaException( + 'Cannot provide permission "%s" twice', + $name + ); + } + $this->permissionList[$name] = (object) array( + 'name' => $name, + 'description' => $description + ); + } + + /** + * Provide a named restriction + * + * @param string $name Unique restriction name + * @param string $description Restriction description + * + * @throws IcingaException If the restriction is already provided + */ + protected function provideRestriction($name, $description) + { + if ($this->providesRestriction($name)) { + throw new IcingaException( + 'Cannot provide restriction "%s" twice', + $name + ); + } + $this->restrictionList[$name] = (object) array( + 'name' => $name, + 'description' => $description + ); + } + + /** + * Provide a module config tab + * + * @param string $name Unique tab name + * @param array $config Tab config + * + * @return $this + * @throws ProgrammingError If $config lacks the key 'url' + */ + protected function provideConfigTab($name, $config = array()) + { + if (! array_key_exists('url', $config)) { + throw new ProgrammingError('A module config tab MUST provide a "url"'); + } + $config['url'] = $this->getName() . '/' . ltrim($config['url'], '/'); + $this->configTabs[$name] = $config; + return $this; + } + + /** + * Provide a setup wizard + * + * @param string $className The name of the class + * + * @return $this + */ + protected function provideSetupWizard($className) + { + $this->setupWizard = $className; + return $this; + } + + /** + * Provide a user backend capable of authenticating users + * + * @param string $identifier The identifier of the new backend type + * @param string $className The name of the class + * + * @return $this + */ + protected function provideUserBackend($identifier, $className) + { + $this->userBackends[strtolower($identifier)] = $className; + return $this; + } + + /** + * Provide a user group backend + * + * @param string $identifier The identifier of the new backend type + * @param string $className The name of the class + * + * @return $this + */ + protected function provideUserGroupBackend($identifier, $className) + { + $this->userGroupBackends[strtolower($identifier)] = $className; + return $this; + } + + /** + * Provide a new type of configurable navigation item with a optional label and config filename + * + * @param string $type + * @param string $label + * @param string $config + * + * @return $this + */ + protected function provideNavigationItem($type, $label = null, $config = null) + { + $this->navigationItems[$type] = array( + 'label' => $label, + 'config' => $config + ); + + return $this; + } + + /** + * Register module namespaces on our class loader + * + * @return $this + */ + protected function registerAutoloader() + { + if ($this->registeredAutoloader) { + return $this; + } + + $moduleName = ucfirst($this->getName()); + + $this->app->getLoader()->registerNamespace( + 'Icinga\\Module\\' . $moduleName, + $this->getLibDir() . '/'. $moduleName, + $this->getApplicationDir() + ); + + $this->registeredAutoloader = true; + + return $this; + } + + /** + * Bind text domain for i18n + * + * @return $this + */ + protected function registerLocales() + { + if ($this->hasLocales() && StaticTranslator::$instance instanceof GettextTranslator) { + StaticTranslator::$instance->addTranslationDirectory($this->localedir, $this->name); + } + + return $this; + } + + /** + * Get whether the module has translations + */ + public function hasLocales() + { + return file_exists($this->localedir) && is_dir($this->localedir); + } + + /** + * List all available locales + * + * @return array Locale list + */ + public function listLocales() + { + $locales = array(); + if (! $this->hasLocales()) { + return $locales; + } + + $dh = opendir($this->localedir); + while (false !== ($file = readdir($dh))) { + $filename = $this->localedir . DIRECTORY_SEPARATOR . $file; + if (preg_match('/^[a-z]{2}_[A-Z]{2}$/', $file) && is_dir($filename)) { + $locales[] = $file; + } + } + closedir($dh); + sort($locales); + return $locales; + } + + /** + * Register web integration + * + * Add controller directory to mvc + * + * @return $this + */ + protected function registerWebIntegration() + { + if (! $this->app->isWeb()) { + return $this; + } + + return $this + ->registerLocales() + ->registerRoutes(); + } + + /** + * Add routes for static content and any route added via {@link addRoute()} to the route chain + * + * @return $this + */ + protected function registerRoutes() + { + $router = $this->app->getFrontController()->getRouter(); + + // TODO: We should not be required to do this. Please check dispatch() + $this->app->getfrontController()->addControllerDirectory( + $this->getControllerDir(), + $this->getName() + ); + + /** @var \Zend_Controller_Router_Rewrite $router */ + foreach ($this->routes as $name => $route) { + $router->addRoute($name, $route); + } + $router->addRoute( + $this->name . '_jsprovider', + new Zend_Controller_Router_Route( + 'js/' . $this->name . '/:file', + array( + 'action' => 'javascript', + 'controller' => 'static', + 'module' => 'default', + 'module_name' => $this->name + ) + ) + ); + $router->addRoute( + $this->name . '_img', + new Zend_Controller_Router_Route_Regex( + 'img/' . $this->name . '/(.+)', + array( + 'action' => 'img', + 'controller' => 'static', + 'module' => 'default', + 'module_name' => $this->name + ), + array( + 1 => 'file' + ) + ) + ); + return $this; + } + + /** + * Run module bootstrap script + * + * @return $this + */ + protected function launchRunScript() + { + return $this->includeScript($this->runScript); + } + + /** + * Include a php script if it is readable + * + * @param string $file File to include + * + * @return $this + */ + protected function includeScript($file) + { + if (file_exists($file) && is_readable($file)) { + include $file; + } + + return $this; + } + + /** + * Run module config script + * + * @return $this + */ + protected function launchConfigScript() + { + if ($this->triedToLaunchConfigScript) { + return $this; + } + $this->triedToLaunchConfigScript = true; + $this->registerAutoloader(); + return $this->includeScript($this->configScript); + } + + protected function slashesToNamespace($class) + { + $list = explode('/', $class); + foreach ($list as &$part) { + $part = ucfirst($part); + } + + return implode('\\', $list); + } + + /** + * Provide a hook implementation + * + * @param string $name Name of the hook for which to provide an implementation + * @param string $implementation Fully qualified name of the class providing the hook implementation. + * Defaults to the module's ProvidedHook namespace plus the hook's name for the + * class name + * @param bool $alwaysRun To run the hook always (e.g. without permission check) + * + * @return $this + */ + protected function provideHook($name, $implementation = null, $alwaysRun = false) + { + if ($implementation === null) { + $implementation = $name; + } + + if (strpos($implementation, '\\') === false) { + $class = $this->getNamespace() + . '\\ProvidedHook\\' + . $this->slashesToNamespace($implementation); + } else { + $class = $implementation; + } + + Hook::register($name, $class, $class, $alwaysRun); + return $this; + } + + /** + * Add a route which will be added to the route chain + * + * @param string $name Name of the route + * @param Zend_Controller_Router_Route_Abstract $route Instance of the route + * + * @return $this + * @see registerRoutes() + */ + protected function addRoute($name, Zend_Controller_Router_Route_Abstract $route) + { + $this->routes[$name] = $route; + return $this; + } +} diff --git a/library/Icinga/Application/Modules/NavigationItemContainer.php b/library/Icinga/Application/Modules/NavigationItemContainer.php new file mode 100644 index 0000000..c906ccb --- /dev/null +++ b/library/Icinga/Application/Modules/NavigationItemContainer.php @@ -0,0 +1,117 @@ +name = $name; + $this->properties = $properties; + } + + /** + * Set this menu item's name + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Return this menu item's name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set this menu item's properties + * + * @param array $properties + * + * @return $this + */ + public function setProperties(array $properties) + { + $this->properties = $properties; + return $this; + } + + /** + * Return this menu item's properties + * + * @return array + */ + public function getProperties() + { + return $this->properties ?: array(); + } + + /** + * Allow dynamic setters and getters for properties + * + * @param string $name + * @param array $arguments + * + * @return mixed + * + * @throws ProgrammingError In case the called method is not supported + */ + public function __call($name, $arguments) + { + if (method_exists($this, $name)) { + return call_user_func(array($this, $name), $this, $arguments); + } + + $type = substr($name, 0, 3); + if ($type !== 'set' && $type !== 'get') { + throw new ProgrammingError( + 'Dynamic method %s is not supported. Only getters (get*) and setters (set*) are.', + $name + ); + } + + $propertyName = strtolower(join('_', preg_split('~(?=[A-Z])~', lcfirst(substr($name, 3))))); + if ($type === 'set') { + $this->properties[$propertyName] = $arguments[0]; + return $this; + } else { // $type === 'get' + return array_key_exists($propertyName, $this->properties) ? $this->properties[$propertyName] : null; + } + } +} diff --git a/library/Icinga/Application/Platform.php b/library/Icinga/Application/Platform.php new file mode 100644 index 0000000..b1b8d90 --- /dev/null +++ b/library/Icinga/Application/Platform.php @@ -0,0 +1,435 @@ + 2) { + return 'linux'; + } + + foreach (array( + 'fedora' => '/etc/fedora-release', + 'centos' => '/etc/centos-release' + ) as $distro => $releaseFile) { + if (! (false === ( + $release = @file_get_contents($releaseFile) + ) || false === strpos(strtolower($release), $distro))) { + return $distro; + } + } + + if (false !== ($release = @file_get_contents('/etc/redhat-release'))) { + $release = strtolower($release); + if (false !== strpos($release, 'red hat enterprise linux')) { + return 'rhel'; + } + foreach (array('fedora', 'centos') as $distro) { + if (false !== strpos($release, $distro)) { + return $distro; + } + } + return $reliable < 2 ? 'redhat' : 'linux'; + } + + if (false !== ($release = @file_get_contents('/etc/SuSE-release'))) { + $release = strtolower($release); + foreach (array( + 'opensuse' => 'opensuse', + 'sles' => 'suse linux enterprise server', + 'sled' => 'suse linux enterprise desktop' + ) as $distro => $name) { + if (false !== strpos($release, $name)) { + return $distro; + } + } + return $reliable < 2 ? 'suse' : 'linux'; + } + + if ($reliable < 1) { + if (false === ($procVersion = @file_get_contents('/proc/version'))) { + return false; + } + $procVersion = strtolower($procVersion); + foreach (array( + 'redhat' => 'red hat', + 'suse' => 'suse linux', + 'ubuntu' => 'ubuntu', + 'debian' => 'debian' + ) as $distro => $name) { + if (false !== strpos($procVersion, $name)) { + return $distro; + } + } + } + + return 'linux'; + } + + /** + * Test of CLI environment + * + * @return bool + */ + public static function isCli() + { + if (PHP_SAPI == 'cli') { + return true; + } elseif ((PHP_SAPI == 'cgi' || PHP_SAPI == 'cgi-fcgi') + && empty($_SERVER['SERVER_NAME'])) { + return true; + } + return false; + } + + /** + * Get the hostname + * + * @return string + */ + public static function getHostname() + { + if (self::$hostname === null) { + self::discoverHostname(); + } + return self::$hostname; + } + + /** + * Get the domain name + * + * @return string + */ + public static function getDomain() + { + if (self::$domain === null) { + self::discoverHostname(); + } + return self::$domain; + } + + /** + * Get the fully qualified domain name + * + * @return string + */ + public static function getFqdn() + { + if (self::$fqdn === null) { + self::discoverHostname(); + } + return self::$fqdn; + } + + /** + * Initialize domain and host strings + */ + protected static function discoverHostname() + { + self::$hostname = gethostname(); + self::$fqdn = gethostbyaddr(gethostbyname(self::$hostname)); + + if (substr(self::$fqdn, 0, strlen(self::$hostname)) === self::$hostname) { + self::$domain = substr(self::$fqdn, strlen(self::$hostname) + 1); + } else { + $parts = preg_split('~\.~', self::$hostname, 2); + self::$domain = array_shift($parts); + } + } + + /** + * Return the version of PHP + * + * @return string + */ + public static function getPhpVersion() + { + return phpversion(); + } + + /** + * Return the username PHP is running as + * + * @return string + */ + public static function getPhpUser() + { + if (static::isWindows()) { + return get_current_user(); // http://php.net/manual/en/function.get-current-user.php#75059 + } + + if (function_exists('posix_geteuid')) { + $userInfo = posix_getpwuid(posix_geteuid()); + return $userInfo['name']; + } + } + + /** + * Test for php extension + * + * @param string $extensionName E.g. mysql, ldap + * + * @return bool + */ + public static function extensionLoaded($extensionName) + { + return extension_loaded($extensionName); + } + + /** + * Return the value for the given PHP configuration option + * + * @param string $option The option name for which to return the value + * + * @return string|false + */ + public static function getPhpConfig($option) + { + return ini_get($option); + } + + /** + * Return whether the given class exists + * + * @param string $name The name of the class to check + * + * @return bool + */ + public static function classExists($name) + { + if (@class_exists($name)) { + return true; + } + + if (strpos($name, '_') !== false) { + // Assume it's a Zend-Framework class + return (@include str_replace('_', '/', $name) . '.php') !== false; + } + + return false; + } + + /** + * Return whether it's possible to connect to a LDAP server + * + * Checks whether the ldap extension is loaded + * + * @return bool + */ + public static function hasLdapSupport() + { + return static::extensionLoaded('ldap'); + } + + /** + * Return whether it's possible to connect to any of the supported database servers + * + * @return bool + */ + public static function hasDatabaseSupport() + { + return static::hasMssqlSupport() || static::hasMysqlSupport() || static::hasOciSupport() + || static::hasOracleSupport() || static::hasPostgresqlSupport(); + } + + /** + * Return whether it's possible to connect to a MSSQL database + * + * Checks whether the mssql/dblib pdo or sqlsrv extension has + * been loaded and Zend framework adapter for MSSQL is available + * + * @return bool + */ + public static function hasMssqlSupport() + { + if ((static::extensionLoaded('mssql') || static::extensionLoaded('pdo_dblib')) + && static::classExists('Zend_Db_Adapter_Pdo_Mssql') + ) { + return true; + } + + return static::extensionLoaded('sqlsrv') && static::classExists('Zend_Db_Adapter_Sqlsrv'); + } + + /** + * Return whether it's possible to connect to a MySQL database + * + * Checks whether the mysql pdo extension has been loaded and the Zend framework adapter for MySQL is available + * + * @return bool + */ + public static function hasMysqlSupport() + { + return static::extensionLoaded('pdo_mysql') && static::classExists('Zend_Db_Adapter_Pdo_Mysql'); + } + + /** + * Return whether it's possible to connect to a IBM DB2 database + * + * Checks whether the ibm pdo extension has been loaded and the Zend framework adapter for IBM is available + * + * @return bool + */ + public static function hasIbmSupport() + { + return static::extensionLoaded('pdo_ibm') && static::classExists('Zend_Db_Adapter_Pdo_Ibm'); + } + + /** + * Return whether it's possible to connect to a Oracle database using OCI8 + * + * Checks whether the OCI8 extension has been loaded and the Zend framework adapter for Oracle is available + * + * @return bool + */ + public static function hasOciSupport() + { + return static::extensionLoaded('oci8') && static::classExists('Zend_Db_Adapter_Oracle'); + } + + /** + * Return whether it's possible to connect to a Oracle database using PDO_OCI + * + * Checks whether the OCI PDO extension has been loaded and the Zend framework adapter for Oci is available + * + * @return bool + */ + public static function hasOracleSupport() + { + return static::extensionLoaded('pdo_oci') && static::classExists('Zend_Db_Adapter_Pdo_Mysql'); + } + + /** + * Return whether it's possible to connect to a PostgreSQL database + * + * Checks whether the pgsql pdo extension has been loaded and the Zend framework adapter for PostgreSQL is available + * + * @return bool + */ + public static function hasPostgresqlSupport() + { + return static::extensionLoaded('pdo_pgsql') && static::classExists('Zend_Db_Adapter_Pdo_Pgsql'); + } + + /** + * Return whether it's possible to connect to a SQLite database + * + * Checks whether the sqlite pdo extension has been loaded and the Zend framework adapter for SQLite is available + * + * @return bool + */ + public static function hasSqliteSupport() + { + return static::extensionLoaded('pdo_sqlite') && static::classExists('Zend_Db_Adapter_Pdo_Sqlite'); + } +} diff --git a/library/Icinga/Application/StaticWeb.php b/library/Icinga/Application/StaticWeb.php new file mode 100644 index 0000000..5c64dcb --- /dev/null +++ b/library/Icinga/Application/StaticWeb.php @@ -0,0 +1,21 @@ +setupErrorHandling() + ->loadLibraries() + ->loadConfig() + ->setupLogging() + ->setupLogger() + ->setupRequest() + ->setupResponse(); + } +} diff --git a/library/Icinga/Application/Version.php b/library/Icinga/Application/Version.php new file mode 100644 index 0000000..3045e8c --- /dev/null +++ b/library/Icinga/Application/Version.php @@ -0,0 +1,65 @@ + self::VERSION); + preg_match('/2.(\d+)\./', self::VERSION, $matches); + $version['docVersion'] = isset($matches[1]) ? '2.' . $matches[1] : null; + + if (false !== ($appVersion = @file_get_contents(Icinga::app()->getApplicationDir('VERSION')))) { + $matches = array(); + if (@preg_match('/^(?P\w+) (?P\S+)/', $appVersion, $matches)) { + return array_merge($version, $matches); + } + } + + $gitCommitId = static::getGitHead(Icinga::app()->getBaseDir()); + if ($gitCommitId !== false) { + $version['gitCommitID'] = $gitCommitId; + } + + return $version; + } + + /** + * Get the current commit of the Git repository in the given path + * + * @param string $repo Path to the Git repository + * @param bool $bare Whether the Git repository is bare + * + * @return string|bool False if not available + */ + public static function getGitHead($repo, $bare = false) + { + if (! $bare) { + $repo .= '/.git'; + } + + $head = @file_get_contents($repo . '/HEAD'); + + if ($head !== false) { + if (preg_match('/^ref: (.+)/', $head, $matches)) { + return @file_get_contents($repo . '/' . $matches[1]); + } + + return $head; + } + + return false; + } +} diff --git a/library/Icinga/Application/Web.php b/library/Icinga/Application/Web.php new file mode 100644 index 0000000..4ce9b45 --- /dev/null +++ b/library/Icinga/Application/Web.php @@ -0,0 +1,505 @@ + + * use Icinga\Application\Web; + * Web::start(); + * + */ +class Web extends EmbeddedWeb +{ + /** + * View object + * + * @var View + */ + private $viewRenderer; + + /** + * Zend front controller instance + * + * @var Zend_Controller_Front + */ + private $frontController; + + /** + * Session object + * + * @var BaseSession + */ + private $session; + + /** + * User object + * + * @var User + */ + private $user; + + /** + * Identify web bootstrap + * + * @var bool + */ + protected $isWeb = true; + + /** + * Initialize all together + * + * @return $this + */ + protected function bootstrap() + { + return $this + ->setupLogging() + ->setupErrorHandling() + ->loadLibraries() + ->loadConfig() + ->setupLogger() + ->setupRequest() + ->setupSession() + ->setupNotifications() + ->setupResponse() + ->setupZendMvc() + ->prepareInternationalization() + ->setupModuleManager() + ->loadSetupModuleIfNecessary() + ->loadEnabledModules() + ->setupRoute() + ->setupPagination() + ->setupUserBackendFactory() + ->setupUser() + ->setupTimezone() + ->setupInternationalization() + ->setupFatalErrorHandling(); + } + + /** + * Get themes provided by Web 2 and all enabled modules + * + * @return string[] Array of theme names as keys and values + */ + public function getThemes() + { + $themes = array(StyleSheet::DEFAULT_THEME); + $applicationThemePath = $this->getBaseDir('public/css/themes'); + if (DirectoryIterator::isReadable($applicationThemePath)) { + foreach (new DirectoryIterator($applicationThemePath, 'less') as $name => $theme) { + $themes[] = substr($name, 0, -5); + } + } + $mm = $this->getModuleManager(); + foreach ($mm->listEnabledModules() as $moduleName) { + $moduleThemePath = $mm->getModule($moduleName)->getCssDir() . '/themes'; + if (! DirectoryIterator::isReadable($moduleThemePath)) { + continue; + } + foreach (new DirectoryIterator($moduleThemePath, 'less') as $name => $theme) { + $themes[] = $moduleName . '/' . substr($name, 0, -5); + } + } + return array_combine($themes, $themes); + } + + /** + * Prepare routing + * + * @return $this + */ + private function setupRoute() + { + $this->frontController->getRouter()->addRoute( + 'module_javascript', + new Zend_Controller_Router_Route( + 'js/components/:module_name/:file', + array( + 'controller' => 'static', + 'action' => 'javascript' + ) + ) + ); + + return $this; + } + + /** + * Getter for frontController + * + * @return Zend_Controller_Front + */ + public function getFrontController() + { + return $this->frontController; + } + + /** + * Getter for view + * + * @return View + */ + public function getViewRenderer() + { + return $this->viewRenderer; + } + + private function hasAccessToSharedNavigationItem(&$config, Config $navConfig) + { + // TODO: Provide a more sophisticated solution + + if (isset($config['owner']) && strtolower($config['owner']) === strtolower($this->user->getUsername())) { + unset($config['owner']); + unset($config['users']); + unset($config['groups']); + return true; + } + + if (isset($config['parent']) && $navConfig->hasSection($config['parent'])) { + unset($config['owner']); + if (isset($this->accessibleMenuItems[$config['parent']])) { + return $this->accessibleMenuItems[$config['parent']]; + } + + $parentConfig = $navConfig->getSection($config['parent']); + $this->accessibleMenuItems[$config['parent']] = $this->hasAccessToSharedNavigationItem( + $parentConfig, + $navConfig + ); + return $this->accessibleMenuItems[$config['parent']]; + } + + if (isset($config['users'])) { + $users = array_map('trim', explode(',', strtolower($config['users']))); + if (in_array('*', $users, true) || in_array(strtolower($this->user->getUsername()), $users, true)) { + unset($config['owner']); + unset($config['users']); + unset($config['groups']); + return true; + } + } + + if (isset($config['groups'])) { + $groups = array_map('trim', explode(',', strtolower($config['groups']))); + if (in_array('*', $groups, true)) { + unset($config['owner']); + unset($config['users']); + unset($config['groups']); + return true; + } + + $userGroups = array_map('strtolower', $this->user->getGroups()); + $matches = array_intersect($userGroups, $groups); + if (! empty($matches)) { + unset($config['owner']); + unset($config['users']); + unset($config['groups']); + return true; + } + } + + return false; + } + + /** + * Load and return the shared navigation of the given type + * + * @param string $type + * + * @return Navigation + */ + public function getSharedNavigation($type) + { + $config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type); + + if ($type === 'dashboard-pane') { + $panes = array(); + foreach ($config as $dashletName => $dashletConfig) { + if ($this->hasAccessToSharedNavigationItem($dashletConfig)) { + // TODO: Throw ConfigurationError if pane or url is missing + $panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url; + } + } + + $navigation = new Navigation(); + foreach ($panes as $paneName => $dashlets) { + $navigation->addItem( + $paneName, + array( + 'type' => 'dashboard-pane', + 'dashlets' => $dashlets + ) + ); + } + } else { + $items = array(); + foreach ($config as $name => $typeConfig) { + if (isset($this->accessibleMenuItems[$name])) { + if ($this->accessibleMenuItems[$name]) { + $items[$name] = $typeConfig; + } + } else { + if ($this->hasAccessToSharedNavigationItem($typeConfig, $config)) { + $this->accessibleMenuItems[$name] = true; + $items[$name] = $typeConfig; + } else { + $this->accessibleMenuItems[$name] = false; + } + } + } + + $navigation = Navigation::fromConfig($items); + } + + return $navigation; + } + + /** + * Dispatch public interface + */ + public function dispatch() + { + $this->frontController->dispatch($this->getRequest(), $this->getResponse()); + } + + /** + * Prepare Zend MVC Base + * + * @return $this + */ + private function setupZendMvc() + { + Zend_Layout::startMvc( + array( + 'layout' => 'layout', + 'layoutPath' => $this->getApplicationDir('/layouts/scripts') + ) + ); + + $this->setupFrontController(); + $this->setupViewRenderer(); + return $this; + } + + /** + * Create user object + * + * @return $this + */ + private function setupUser() + { + $auth = Auth::getInstance(); + if (! $this->request->isXmlHttpRequest() && $this->request->isApiRequest() && ! $auth->isAuthenticated()) { + $auth->authHttp(); + } + if ($auth->isAuthenticated()) { + $user = $auth->getUser(); + $this->getRequest()->setUser($user); + $this->user = $user; + + if ($user->can('user/application/stacktraces')) { + $displayExceptions = $this->user->getPreferences()->getValue( + 'icingaweb', + 'show_stacktraces' + ); + + if ($displayExceptions !== null) { + $this->frontController->setParams( + array( + 'displayExceptions' => $displayExceptions + ) + ); + } + } + } + return $this; + } + + /** + * Initialize a session provider + * + * @return $this + */ + private function setupSession() + { + $this->session = Session::create(); + return $this; + } + + /** + * Initialize notifications to remove them immediately from session + * + * @return $this + */ + private function setupNotifications() + { + Notification::getInstance(); + return $this; + } + + /** + * Instantiate front controller + * + * @return $this + */ + private function setupFrontController() + { + $this->frontController = Zend_Controller_Front::getInstance(); + $this->frontController->setDispatcher(new Dispatcher()); + $this->frontController->setRequest($this->getRequest()); + $this->frontController->setControllerDirectory($this->getApplicationDir('/controllers')); + + $displayExceptions = $this->config->get('global', 'show_stacktraces', true); + + $this->frontController->setParams( + array( + 'displayExceptions' => $displayExceptions + ) + ); + return $this; + } + + /** + * Register helper paths and views for renderer + * + * @return $this + */ + private function setupViewRenderer() + { + $view = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer'); + /** @var \Zend_Controller_Action_Helper_ViewRenderer $view */ + $view->setView(new View()); + $view->view->addHelperPath($this->getApplicationDir('/views/helpers')); + $view->view->setEncoding('UTF-8'); + $view->view->headTitle()->prepend($this->config->get('global', 'project', 'Icinga')); + $view->view->headTitle()->setSeparator(' :: '); + $this->viewRenderer = $view; + return $this; + } + + /** + * Configure pagination settings + * + * @return $this + */ + private function setupPagination() + { + // TODO: document what we need for whatever reason?! + Zend_Paginator::addScrollingStylePrefixPath( + 'Icinga_Web_Paginator_ScrollingStyle_', + $this->getLibraryDir('Icinga/Web/Paginator/ScrollingStyle') + ); + + Zend_Paginator::addScrollingStylePrefixPath( + 'Icinga_Web_Paginator_ScrollingStyle', + 'Icinga/Web/Paginator/ScrollingStyle' + ); + + Zend_Paginator::setDefaultScrollingStyle('SlidingWithBorder'); + Zend_View_Helper_PaginationControl::setDefaultViewPartial( + array('mixedPagination.phtml', 'default') + ); + return $this; + } + + /** + * Fatal error handling configuration + * + * @return $this + */ + protected function setupFatalErrorHandling() + { + register_shutdown_function(function () { + $error = error_get_last(); + + if ($error !== null && $error['type'] === E_ERROR) { + $frontController = Icinga::app()->getFrontController(); + $response = $frontController->getResponse(); + + $response->setException(new ErrorException( + $error['message'], + 0, + $error['type'], + $error['file'], + $error['line'] + )); + + // Clean PHP's fatal error stack trace and replace it with ours + ob_end_clean(); + $frontController->dispatch($frontController->getRequest(), $response); + } + }); + + return $this; + } + + /** + * (non-PHPDoc) + * @see ApplicationBootstrap::detectTimezone() For the method documentation. + */ + protected function detectTimezone() + { + $auth = Auth::getInstance(); + if (! $auth->isAuthenticated() + || ($timezone = $auth->getUser()->getPreferences()->getValue('icingaweb', 'timezone')) === null + ) { + $detect = new TimezoneDetect(); + $timezone = $detect->getTimezoneName(); + } + return $timezone; + } + + /** + * Setup internationalization using gettext + * + * Uses the preferred user language or the browser suggested language or our default. + * + * @return string Detected locale code + */ + protected function detectLocale() + { + $auth = Auth::getInstance(); + if ($auth->isAuthenticated() + && ($locale = $auth->getUser()->getPreferences()->getValue('icingaweb', 'language')) !== null + ) { + return $locale; + } + + /** @var GettextTranslator $translator */ + $translator = StaticTranslator::$instance; + + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + return (new Locale())->getPreferred($_SERVER['HTTP_ACCEPT_LANGUAGE'], $translator->listLocales()); + } + + return $translator->getDefaultLocale(); + } +} diff --git a/library/Icinga/Application/functions.php b/library/Icinga/Application/functions.php new file mode 100644 index 0000000..12736fb --- /dev/null +++ b/library/Icinga/Application/functions.php @@ -0,0 +1,110 @@ +translate($messageId, $context); + } + + /** + * @see Translator::translateInDomain() For the function documentation. + */ + function mt(string $domain, string $messageId, ?string $context = null): string + { + return StaticTranslator::$instance->translateInDomain($domain, $messageId, $context); + } + + /** + * @see Translator::translatePlural() For the function documentation. + */ + function tp(string $messageId, string $messageId2, ?int $number, ?string $context = null): string + { + return StaticTranslator::$instance->translatePlural($messageId, $messageId2, $number ?? 0, $context); + } + + /** + * @see Translator::translatePluralInDomain() For the function documentation. + */ + function mtp(string $domain, string $messageId, string $messageId2, ?int $number, ?string $context = null): string + { + return StaticTranslator::$instance->translatePluralInDomain( + $domain, + $messageId, + $messageId2, + $number ?? 0, + $context + ); + } + +} else { + + /** + * @see Translator::translate() For the function documentation. + */ + function t(string $messageId, ?string $context = null): string + { + return $messageId; + } + + /** + * @see Translator::translate() For the function documentation. + */ + function mt(string $domain, string $messageId, ?string $context = null): string + { + return $messageId; + } + + /** + * @see Translator::translatePlural() For the function documentation. + */ + function tp(string $messageId, string $messageId2, ?int $number, ?string $context = null): string + { + if ((int) $number !== 1) { + return $messageId2; + } + + return $messageId; + } + + /** + * @see Translator::translatePlural() For the function documentation. + */ + function mtp(string $domain, string $messageId, string $messageId2, ?int $number, ?string $context = null): string + { + if ((int) $number !== 1) { + return $messageId2; + } + + return $messageId; + } + +} diff --git a/library/Icinga/Application/webrouter.php b/library/Icinga/Application/webrouter.php new file mode 100644 index 0000000..d9ab30b --- /dev/null +++ b/library/Icinga/Application/webrouter.php @@ -0,0 +1,106 @@ +initFromRequest(); + $pie->toSvg(); +} elseif ($path === 'png/chart.php') { + if (!array_key_exists('data', $_GET)) { + return false; + } + include __DIR__ . '/EmbeddedWeb.php'; + EmbeddedWeb::start(); + header('Content-Type: image/png'); + $pie = new PieChart(); + $pie->initFromRequest(); + $pie->toPng(); +} elseif (substr($path, 0, 4) === 'lib/') { + include_once __DIR__ . '/StaticWeb.php'; + $app = StaticWeb::start(); + (new StaticController())->handle($app->getRequest()); + $app->getResponse()->sendResponse(); +} elseif (file_exists($baseDir . '/' . $path) && is_file($baseDir . '/' . $path)) { + return false; +} else { + include __DIR__ . '/Web.php'; + Web::start()->dispatch(); +} diff --git a/library/Icinga/Authentication/AdmissionLoader.php b/library/Icinga/Authentication/AdmissionLoader.php new file mode 100644 index 0000000..0c3fd3f --- /dev/null +++ b/library/Icinga/Authentication/AdmissionLoader.php @@ -0,0 +1,249 @@ + 'application/announcements', + 'application/stacktraces' => 'user/application/stacktraces', + 'application/share/navigation' => 'user/share/navigation', + // Migrating config/application/* would include config/modules, so that's skipped + //'config/application/*' => 'config/*', + 'config/application/general' => 'config/general', + 'config/application/resources' => 'config/resources', + 'config/application/navigation' => 'config/navigation', + 'config/application/userbackend' => 'config/access-control/users', + 'config/application/usergroupbackend' => 'config/access-control/groups', + 'config/authentication/*' => 'config/access-control/*', + 'config/authentication/users/*' => 'config/access-control/users', + 'config/authentication/users/show' => 'config/access-control/users', + 'config/authentication/users/add' => 'config/access-control/users', + 'config/authentication/users/edit' => 'config/access-control/users', + 'config/authentication/users/remove' => 'config/access-control/users', + 'config/authentication/groups/*' => 'config/access-control/groups', + 'config/authentication/groups/show' => 'config/access-control/groups', + 'config/authentication/groups/edit' => 'config/access-control/groups', + 'config/authentication/groups/add' => 'config/access-control/groups', + 'config/authentication/groups/remove' => 'config/access-control/groups', + 'config/authentication/roles/*' => 'config/access-control/roles', + 'config/authentication/roles/show' => 'config/access-control/roles', + 'config/authentication/roles/add' => 'config/access-control/roles', + 'config/authentication/roles/edit' => 'config/access-control/roles', + 'config/authentication/roles/remove' => 'config/access-control/roles' + ]; + + /** @var Role[] */ + protected $roles; + + /** @var ConfigObject */ + protected $roleConfig; + + public function __construct() + { + try { + $this->roleConfig = Config::app('roles'); + } catch (NotReadableError $e) { + Logger::error('Can\'t access roles configuration. An exception was thrown:', $e); + } + } + + /** + * Whether the user or groups are a member of the role + * + * @param string $username + * @param array $userGroups + * @param ConfigObject $section + * + * @return bool + */ + protected function match($username, $userGroups, ConfigObject $section) + { + $username = strtolower($username); + if (! empty($section->users)) { + $users = array_map('strtolower', StringHelper::trimSplit($section->users)); + if (in_array('*', $users)) { + return true; + } + + if (in_array($username, $users)) { + return true; + } + } + + if (! empty($section->groups)) { + $groups = array_map('strtolower', StringHelper::trimSplit($section->groups)); + foreach ($userGroups as $userGroup) { + if (in_array(strtolower($userGroup), $groups)) { + return true; + } + } + } + + return false; + } + + /** + * Process role configuration and yield resulting roles + * + * This will also resolve any parent-child relationships. + * + * @param string $name + * @param ConfigObject $section + * + * @return Generator + * @throws ConfigurationError + */ + protected function loadRole($name, ConfigObject $section) + { + if (! isset($this->roles[$name])) { + $permissions = $section->permissions ? StringHelper::trimSplit($section->permissions) : []; + $refusals = $section->refusals ? StringHelper::trimSplit($section->refusals) : []; + + list($permissions, $newRefusals) = self::migrateLegacyPermissions($permissions); + if (! empty($newRefusals)) { + array_push($refusals, ...$newRefusals); + } + + $restrictions = $section->toArray(); + unset($restrictions['users'], $restrictions['groups']); + unset($restrictions['parent'], $restrictions['unrestricted']); + unset($restrictions['refusals'], $restrictions['permissions']); + + $role = new Role(); + $this->roles[$name] = $role + ->setName($name) + ->setRefusals($refusals) + ->setPermissions($permissions) + ->setRestrictions($restrictions) + ->setIsUnrestricted($section->get('unrestricted', false)); + + if (isset($section->parent)) { + $parentName = $section->parent; + if (! $this->roleConfig->hasSection($parentName)) { + Logger::error( + 'Failed to parse authentication configuration: Missing parent role "%s" (required by "%s")', + $parentName, + $name + ); + throw new ConfigurationError( + t('Unable to parse authentication configuration. Check the log for more details.') + ); + } + + foreach ($this->loadRole($parentName, $this->roleConfig->getSection($parentName)) as $parent) { + if ($parent->getName() === $parentName) { + $role->setParent($parent); + $parent->addChild($role); + + // Only yield main role once fully assembled + yield $role; + } + + yield $parent; + } + } else { + yield $role; + } + } else { + yield $this->roles[$name]; + } + } + + /** + * Apply permissions, restrictions and roles to the given user + * + * @param User $user + */ + public function applyRoles(User $user) + { + if ($this->roleConfig === null) { + return; + } + + $username = $user->getUsername(); + $userGroups = $user->getGroups(); + + $roles = []; + $permissions = []; + $restrictions = []; + $assignedRoles = []; + $isUnrestricted = false; + foreach ($this->roleConfig as $roleName => $roleConfig) { + $assigned = $this->match($username, $userGroups, $roleConfig); + if ($assigned) { + $assignedRoles[] = $roleName; + } + + if (! isset($roles[$roleName]) && $assigned) { + foreach ($this->loadRole($roleName, $roleConfig) as $role) { + /** @var Role $role */ + if (isset($roles[$role->getName()])) { + continue; + } + + $roles[$role->getName()] = $role; + + $permissions = array_merge( + $permissions, + array_diff($role->getPermissions(), $permissions) + ); + + $roleRestrictions = $role->getRestrictions(); + foreach ($roleRestrictions as $name => & $restriction) { + $restriction = str_replace( + '$user.local_name$', + $user->getLocalUsername(), + $restriction + ); + $restrictions[$name][] = $restriction; + } + + $role->setRestrictions($roleRestrictions); + + if (! $isUnrestricted) { + $isUnrestricted = $role->isUnrestricted(); + } + } + } + } + + $user->setAdditional('assigned_roles', $assignedRoles); + + $user->setIsUnrestricted($isUnrestricted); + $user->setRestrictions($isUnrestricted ? [] : $restrictions); + $user->setPermissions($permissions); + $user->setRoles(array_values($roles)); + } + + public static function migrateLegacyPermissions(array $permissions) + { + $migratedGrants = []; + $refusals = []; + + foreach ($permissions as $permission) { + if (array_key_exists($permission, self::LEGACY_PERMISSIONS)) { + $migratedGrants[] = self::LEGACY_PERMISSIONS[$permission]; + } elseif ($permission === 'no-user/password-change') { + $refusals[] = 'user/password-change'; + } else { + $migratedGrants[] = $permission; + } + } + + return [$migratedGrants, $refusals]; + } +} diff --git a/library/Icinga/Authentication/Auth.php b/library/Icinga/Authentication/Auth.php new file mode 100644 index 0000000..f358eac --- /dev/null +++ b/library/Icinga/Authentication/Auth.php @@ -0,0 +1,453 @@ +user !== null) { + return true; + } + $this->authenticateFromSession(); + if ($this->user === null && ! $this->authExternal()) { + return false; + } + return true; + } + + public function setAuthenticated(User $user, $persist = true) + { + $this->setupUser($user); + + // Reload CSS if the theme changed + $themingConfig = Icinga::app()->getConfig()->getSection('themes'); + $userTheme = $user->getPreferences()->getValue('icingaweb', 'theme'); + if (! (bool) $themingConfig->get('disabled', false) && $userTheme !== null) { + $defaultTheme = $themingConfig->get('default', StyleSheet::DEFAULT_THEME); + if ($userTheme !== $defaultTheme) { + $this->getResponse()->setReloadCss(true); + } + } + + // Also reload CSS if the theme mode changed + $themeMode = $user->getPreferences()->getValue('icingaweb', 'theme_mode'); + if ($themeMode && $themeMode !== StyleSheet::DEFAULT_MODE) { + $this->getResponse()->setReloadCss(true); + } + + // Reload entire layout if the locale changed + if (($locale = $user->getPreferences()->getValue('icingaweb', 'language')) !== null) { + if (setlocale(LC_ALL, 0) !== $locale && $this->getRequest()->isXmlHttpRequest()) { + $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes'); + } + } + + $this->user = $user; + if ($persist) { + $this->persistCurrentUser(); + } + + AuditHook::logActivity('login', 'User logged in'); + } + + /** + * Getter for groups belonged to authenticated user + * + * @return array + * @see User::getGroups + */ + public function getGroups() + { + return $this->user->getGroups(); + } + + /** + * Get the request + * + * @return \Icinga\Web\Request + */ + public function getRequest() + { + if ($this->request === null) { + $this->request = Icinga::app()->getRequest(); + } + return $this->request; + } + + /** + * Get the response + * + * @return \Icinga\Web\Response + */ + public function getResponse() + { + if ($this->response === null) { + $this->response = Icinga::app()->getResponse(); + } + return $this->response; + } + + /** + * Get applied restrictions matching a given restriction name + * + * Returns a list of applied restrictions, empty if no user is + * authenticated + * + * @param string $restriction Restriction name + * @return array + */ + public function getRestrictions($restriction) + { + if (! $this->isAuthenticated()) { + return array(); + } + return $this->user->getRestrictions($restriction); + } + + /** + * Returns the current user or null if no user is authenticated + * + * @return User|null + */ + public function getUser() + { + return $this->user; + } + + /** + * Set the authenticated user + * + * Note that this method just sets the authenticated user and thus bypasses our default authentication process in + * {@link setAuthenticated()}. + * + * @param User $user + * + * @return $this + */ + public function setUser(User $user) + { + $this->user = $user; + + return $this; + } + + /** + * Try to authenticate the user with the current session + * + * Authentication for externally-authenticated users will be revoked if the username changed or external + * authentication is no longer in effect + */ + public function authenticateFromSession() + { + $this->user = Session::getSession()->get('user'); + if ($this->user !== null && $this->user->isExternalUser()) { + list($originUsername, $field) = $this->user->getExternalUserInformation(); + $username = ExternalBackend::getRemoteUser($field); + if ($username === null || $username !== $originUsername) { + $this->removeAuthorization(); + } + } + } + + /** + * Attempt to authenticate a user from external user backends + * + * @return bool + */ + protected function authExternal() + { + $user = new User(''); + foreach ($this->getAuthChain() as $userBackend) { + if ($userBackend instanceof ExternalBackend) { + if ($userBackend->authenticate($user)) { + if (! $user->hasDomain()) { + $user->setDomain(Config::app()->get('authentication', 'default_domain')); + } + $this->setAuthenticated($user); + return true; + } + } + } + return false; + } + + /** + * Attempt to authenticate a user using HTTP authentication on API requests only + * + * Supports only the Basic HTTP authentication scheme. XHR will be ignored. + * + * @return bool + */ + public function authHttp() + { + $request = $this->getRequest(); + $header = $request->getHeader('Authorization'); + if (empty($header)) { + return false; + } + list($scheme) = explode(' ', $header, 2); + if ($scheme !== 'Basic') { + return false; + } + $authorization = substr($header, strlen('Basic ')); + $credentials = base64_decode($authorization); + $credentials = array_filter(explode(':', $credentials, 2)); + if (count($credentials) !== 2) { + // Deny empty username and/or password + return false; + } + $user = new User($credentials[0]); + if (! $user->hasDomain()) { + $user->setDomain(Config::app()->get('authentication', 'default_domain')); + } + $password = $credentials[1]; + if ($this->getAuthChain()->setSkipExternalBackends(true)->authenticate($user, $password)) { + $this->setAuthenticated($user, false); + $user->setIsHttpUser(true); + return true; + } else { + return false; + } + } + + /** + * Challenge client immediately for HTTP authentication + * + * Sends the response w/ the 401 Unauthorized status code and WWW-Authenticate header. + */ + public function challengeHttp() + { + $response = $this->getResponse(); + $response->setHttpResponseCode(401); + $response->setHeader('WWW-Authenticate', 'Basic realm="Icinga Web 2"'); + $response->sendHeaders(); + exit(); + } + + /** + * Whether an authenticated user has a given permission + * + * @param string $permission Permission name + * + * @return bool True if the user owns the given permission, false if not or if not authenticated + */ + public function hasPermission($permission) + { + if (! $this->isAuthenticated()) { + return false; + } + return $this->user->can($permission); + } + + /** + * Writes the current user to the session + */ + public function persistCurrentUser() + { + // @TODO(el): https://dev.icinga.com/issues/10646 + $params = session_get_cookie_params(); + setcookie( + 'icingaweb2-session', + time(), + 0, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + Session::getSession()->set('user', $this->user)->refreshId(); + } + + /** + * Purges the current authorization information and session + */ + public function removeAuthorization() + { + AuditHook::logActivity('logout', 'User logged out'); + $this->user = null; + Session::getSession()->purge(); + } + + /** + * Setup the given user + * + * This loads preferences, groups and roles. + * + * @param User $user + * + * @return void + */ + public function setupUser(User $user) + { + // Load the user's preferences + + try { + $config = Config::app(); + } catch (NotReadableError $e) { + Logger::error( + new IcingaException( + 'Cannot load preferences for user "%s". An exception was thrown: %s', + $user->getUsername(), + $e + ) + ); + $config = new Config(); + } + + $preferencesConfig = new ConfigObject([ + 'resource' => $config->get('global', 'config_resource') + ]); + + try { + $preferencesStore = PreferencesStore::create($preferencesConfig, $user); + $preferences = new Preferences($preferencesStore->load()); + } catch (Exception $e) { + Logger::error( + new IcingaException( + 'Cannot load preferences for user "%s". An exception was thrown: %s', + $user->getUsername(), + $e + ) + ); + $preferences = new Preferences(); + } + + $user->setPreferences($preferences); + + // Load the user's groups + $groups = $user->getGroups(); + $userBackendName = $user->getAdditional('backend_name'); + foreach (Config::app('groups') as $name => $config) { + $groupsUserBackend = $config->user_backend; + if ($groupsUserBackend + && $groupsUserBackend !== 'none' + && $userBackendName !== null + && $groupsUserBackend !== $userBackendName + ) { + // Do not ask for Group membership if a specific User Backend + // has been assigned to that Group Backend, and the user has + // been authenticated by another User Backend + continue; + } + + try { + $groupBackend = UserGroupBackend::create($name, $config); + $groupsFromBackend = $groupBackend->getMemberships($user); + } catch (Exception $e) { + Logger::error( + 'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown: %s', + $user->getUsername(), + $name, + $e + ); + continue; + } + + if (empty($groupsFromBackend)) { + Logger::debug( + 'No groups found in backend "%s" which the user "%s" is a member of.', + $name, + $user->getUsername() + ); + continue; + } + + $groupsFromBackend = array_values($groupsFromBackend); + Logger::debug( + 'Groups found in backend "%s" for user "%s": %s', + $name, + $user->getUsername(), + join(', ', $groupsFromBackend) + ); + $groups = array_merge($groups, array_combine($groupsFromBackend, $groupsFromBackend)); + } + + $user->setGroups($groups); + + // Load the user's roles + $admissionLoader = new AdmissionLoader(); + $admissionLoader->applyRoles($user); + } +} diff --git a/library/Icinga/Authentication/AuthChain.php b/library/Icinga/Authentication/AuthChain.php new file mode 100644 index 0000000..39468e3 --- /dev/null +++ b/library/Icinga/Authentication/AuthChain.php @@ -0,0 +1,269 @@ +config = Config::app(static::AUTHENTICATION_CONFIG); + } catch (NotReadableError $e) { + $this->config = new Config(); + $this->error = static::EPERM; + } + } else { + $this->config = $config; + } + } + + /** + * {@inheritdoc} + */ + public function authenticate(User $user, $password) + { + $this->error = null; + $backendsTried = 0; + $backendsWithError = 0; + foreach ($this as $backend) { + ++$backendsTried; + try { + $authenticated = $backend->authenticate($user, $password); + } catch (AuthenticationException $e) { + Logger::error($e); + ++$backendsWithError; + continue; + } + if ($authenticated) { + $user->setAdditional('backend_name', $backend->getName()); + $user->setAdditional('backend_type', $this->config->current()->get('backend')); + return true; + } + } + + if ($backendsTried === 0) { + $this->error = static::EEMPTY; + } elseif ($backendsTried === $backendsWithError) { + $this->error = static::EFAIL; + } elseif ($backendsWithError) { + $this->error = static::ENOTALL; + } else { + AuditHook::logActivity('login-failed', 'User failed to authenticate', null, $user->getUsername()); + } + + return false; + } + + /** + * Get the last error code + * + * @return int|null + */ + public function getError() + { + return $this->error; + } + + /** + * Whether authentication had errors + * + * @return bool + */ + public function hasError() + { + return $this->error !== null; + } + + /** + * Get whether to skip external user backends on iteration + * + * @return bool + */ + public function getSkipExternalBackends() + { + return $this->skipExternalBackends; + } + + /** + * Set whether to skip external user backends on iteration + * + * @param bool $skipExternalBackends + * + * @return $this + */ + public function setSkipExternalBackends($skipExternalBackends = true) + { + $this->skipExternalBackends = (bool) $skipExternalBackends; + return $this; + } + + /** + * Rewind the chain + * + * @return void + */ + public function rewind(): void + { + $this->currentBackend = null; + $this->config->rewind(); + } + + /** + * Get the current user backend + * + * @return UserBackendInterface + */ + public function current(): UserBackendInterface + { + return $this->currentBackend; + } + + /** + * Get the key of the current user backend config + * + * @return string + */ + public function key(): string + { + return $this->config->key(); + } + + /** + * Move forward to the next user backend config + * + * @return void + */ + public function next(): void + { + $this->config->next(); + } + + /** + * Check whether the current user backend is valid, i.e. it's enabled, not an external user backend and whether its + * config is valid + * + * @return bool + */ + public function valid(): bool + { + if (! $this->config->valid()) { + // Stop when there are no more backends to check + return false; + } + + $backendConfig = $this->config->current(); + if ((bool) $backendConfig->get('disabled', false)) { + $this->next(); + return $this->valid(); + } + + $name = $this->key(); + try { + $backend = UserBackend::create($name, $backendConfig); + } catch (ConfigurationError $e) { + Logger::error( + new ConfigurationError( + 'Can\'t create authentication backend "%s". An exception was thrown:', + $name, + $e + ) + ); + $this->next(); + return $this->valid(); + } + + if ($this->getSkipExternalBackends() + && $backend instanceof ExternalBackend + ) { + $this->next(); + return $this->valid(); + } + + $this->currentBackend = $backend; + return true; + } +} diff --git a/library/Icinga/Authentication/Authenticatable.php b/library/Icinga/Authentication/Authenticatable.php new file mode 100644 index 0000000..c10d6d3 --- /dev/null +++ b/library/Icinga/Authentication/Authenticatable.php @@ -0,0 +1,21 @@ +name; + } + + /** + * Set the name of the role + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get the role from which privileges are inherited + * + * @return Role + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set the role from which to inherit privileges + * + * @param Role $parent + * + * @return $this + */ + public function setParent(Role $parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * Get the roles to which privileges are inherited + * + * @return Role[] + */ + public function getChildren() + { + return $this->children; + } + + /** + * Set the roles to which inherit privileges + * + * @param Role[] $children + * + * @return $this + */ + public function setChildren(array $children) + { + $this->children = $children; + + return $this; + } + + /** + * Add a role to which inherit privileges + * + * @param Role $role + * + * @return $this + */ + public function addChild(Role $role) + { + $this->children[] = $role; + + return $this; + } + + /** + * Get whether restrictions should not apply to owners of the role + * + * @return bool + */ + public function isUnrestricted() + { + return $this->unrestricted; + } + + /** + * Set whether restrictions should not apply to owners of the role + * + * @param bool $state + * + * @return $this + */ + public function setIsUnrestricted($state) + { + $this->unrestricted = (bool) $state; + + return $this; + } + + /** + * Get the permissions of the role + * + * @return string[] + */ + public function getPermissions() + { + return $this->permissions; + } + + /** + * Set the permissions of the role + * + * @param string[] $permissions + * + * @return $this + */ + public function setPermissions(array $permissions) + { + $this->permissions = $permissions; + + return $this; + } + + /** + * Get the refusals of the role + * + * @return string[] + */ + public function getRefusals() + { + return $this->refusals; + } + + /** + * Set the refusals of the role + * + * @param array $refusals + * + * @return $this + */ + public function setRefusals(array $refusals) + { + $this->refusals = $refusals; + + return $this; + } + + /** + * Get the restrictions of the role + * + * @param string $name Optional name of the restriction + * + * @return string[]|null + */ + public function getRestrictions($name = null) + { + $restrictions = $this->restrictions; + + if ($name === null) { + return $restrictions; + } + + if (isset($restrictions[$name])) { + return $restrictions[$name]; + } + + return null; + } + + /** + * Set the restrictions of the role + * + * @param string[] $restrictions + * + * @return $this + */ + public function setRestrictions(array $restrictions) + { + $this->restrictions = $restrictions; + + return $this; + } + + /** + * Whether this role grants the given permission + * + * @param string $permission + * @param bool $ignoreParent Only evaluate the role's own permissions + * @param bool $cascadeUpwards `false` if `foo/bar/*` and `foo/bar/raboof` should not match `foo/*` + * + * @return bool + */ + public function grants($permission, $ignoreParent = false, $cascadeUpwards = true) + { + foreach ($this->permissions as $grantedPermission) { + if ($this->match($grantedPermission, $permission, $cascadeUpwards)) { + return true; + } + } + + if (! $ignoreParent && $this->getParent() !== null) { + return $this->getParent()->grants($permission, false, $cascadeUpwards); + } + + return false; + } + + /** + * Whether this role denies the given permission + * + * @param string $permission + * @param bool $ignoreParent Only evaluate the role's own refusals + * + * @return bool + */ + public function denies($permission, $ignoreParent = false) + { + foreach ($this->refusals as $refusedPermission) { + if ($this->match($refusedPermission, $permission, false)) { + return true; + } + } + + if (! $ignoreParent && $this->getParent() !== null) { + return $this->getParent()->denies($permission); + } + + return false; + } + + /** + * Get whether the role expression matches the required permission + * + * @param string $roleExpression + * @param string $requiredPermission + * @param bool $cascadeUpwards `false` if `foo/bar/*` and `foo/bar/raboof` should not match `foo/*` + * + * @return bool + */ + protected function match($roleExpression, $requiredPermission, $cascadeUpwards = true) + { + if ($roleExpression === '*' || $roleExpression === $requiredPermission) { + return true; + } + + $requiredWildcard = strpos($requiredPermission, '*'); + if ($requiredWildcard !== false) { + if (($grantedWildcard = strpos($roleExpression, '*')) !== false) { + $wildcard = $cascadeUpwards ? min($requiredWildcard, $grantedWildcard) : $grantedWildcard; + } else { + $wildcard = $cascadeUpwards ? $requiredWildcard : false; + } + } else { + $wildcard = strpos($roleExpression, '*'); + } + + if ($wildcard !== false && $wildcard > 0) { + if (substr($requiredPermission, 0, $wildcard) === substr($roleExpression, 0, $wildcard)) { + return true; + } + } elseif ($requiredPermission === $roleExpression) { + return true; + } + + return false; + } +} diff --git a/library/Icinga/Authentication/RolesConfig.php b/library/Icinga/Authentication/RolesConfig.php new file mode 100644 index 0000000..ac5695f --- /dev/null +++ b/library/Icinga/Authentication/RolesConfig.php @@ -0,0 +1,43 @@ + [ + 'name' => 'roles', + 'keyColumn' => 'name' + ] + ]; + + protected function initializeQueryColumns() + { + $columns = [ + 'roles' => [ + 'parent', + 'name', + 'users', + 'groups', + 'refusals', + 'permissions', + 'unrestricted', + 'application/share/users', + 'application/share/groups' + ] + ]; + + $moduleManager = Icinga::app()->getModuleManager(); + foreach ($moduleManager->listInstalledModules() as $moduleName) { + foreach ($moduleManager->getModule($moduleName, false)->getProvidedRestrictions() as $restriction) { + $columns['roles'][] = $restriction->name; + } + } + + return $columns; + } +} diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php new file mode 100644 index 0000000..0e8cc6a --- /dev/null +++ b/library/Icinga/Authentication/User/DbUserBackend.php @@ -0,0 +1,256 @@ + array( + 'user' => 'name COLLATE utf8mb4_general_ci', + 'user_name' => 'name', + 'is_active' => 'active', + 'created_at' => 'UNIX_TIMESTAMP(ctime)', + 'last_modified' => 'UNIX_TIMESTAMP(mtime)' + ) + ); + + /** + * The statement columns being provided + * + * @var array + */ + protected $statementColumns = array( + 'user' => array( + 'password' => 'password_hash', + 'created_at' => 'ctime', + 'last_modified' => 'mtime' + ) + ); + + /** + * The columns which are not permitted to be queried + * + * @var array + */ + protected $blacklistedQueryColumns = array('user'); + + /** + * The search columns being provided + * + * @var array + */ + protected $searchColumns = array('user'); + + /** + * The default sort rules to be applied on a query + * + * @var array + */ + protected $sortRules = array( + 'user_name' => array( + 'columns' => array( + 'is_active desc', + 'user_name' + ) + ) + ); + + /** + * The value conversion rules to apply on a query or statement + * + * @var array + */ + protected $conversionRules = array( + 'user' => array( + 'password' + ) + ); + + /** + * Initialize this database user backend + */ + protected function init() + { + if (! $this->ds->getTablePrefix()) { + $this->ds->setTablePrefix('icingaweb_'); + } + } + + /** + * Initialize this repository's filter columns + * + * @return array + */ + protected function initializeFilterColumns() + { + $userLabel = t('Username') . ' ' . t('(Case insensitive)'); + return array( + $userLabel => 'user', + t('Username') => 'user_name', + t('Active') => 'is_active', + t('Created at') => 'created_at', + t('Last modified') => 'last_modified' + ); + } + + /** + * Insert a table row with the given data + * + * @param string $table + * @param array $bind + * + * @return void + */ + public function insert($table, array $bind, array $types = array()) + { + $this->requireTable($table); + $bind['created_at'] = date('Y-m-d H:i:s'); + $this->ds->insert( + $this->prependTablePrefix($table), + $this->requireStatementColumns($table, $bind), + array( + 'active' => PDO::PARAM_INT, + 'password_hash' => PDO::PARAM_LOB + ) + ); + } + + /** + * Update table rows with the given data, optionally limited by using a filter + * + * @param string $table + * @param array $bind + * @param Filter $filter + */ + public function update($table, array $bind, Filter $filter = null, array $types = array()) + { + $this->requireTable($table); + $bind['last_modified'] = date('Y-m-d H:i:s'); + if ($filter) { + $filter = $this->requireFilter($table, $filter); + } + + $this->ds->update( + $this->prependTablePrefix($table), + $this->requireStatementColumns($table, $bind), + $filter, + array( + 'active' => PDO::PARAM_INT, + 'password_hash' => PDO::PARAM_LOB + ) + ); + } + + /** + * Hash and return the given password + * + * @param string $value + * + * @return string + */ + protected function persistPassword($value) + { + return password_hash($value, PASSWORD_DEFAULT); + } + + /** + * Fetch the hashed password for the given user + * + * @param string $username The name of the user + * + * @return string + */ + protected function getPasswordHash($username) + { + if ($this->ds->getDbType() === 'pgsql') { + // Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape' + $columns = array('password_hash' => 'ENCODE(password_hash, \'escape\')'); + } else { + $columns = array('password_hash'); + } + + $nameColumn = 'name'; + if ($this->ds->getDbType() === 'mysql') { + $username = strtolower($username); + $nameColumn = 'BINARY LOWER(name)'; + } + + $query = $this->ds->select() + ->from($this->prependTablePrefix('user'), $columns) + ->where($nameColumn, $username) + ->where('active', true); + + $statement = $this->ds->getDbAdapter()->prepare($query->getSelectQuery()); + $statement->execute(); + $statement->bindColumn(1, $lob, PDO::PARAM_LOB); + $statement->fetch(PDO::FETCH_BOUND); + if (is_resource($lob)) { + $lob = stream_get_contents($lob); + } + + if ($lob === null) { + return ''; + } + + return $this->ds->getDbType() === 'pgsql' ? pg_unescape_bytea($lob) : $lob; + } + + /** + * Authenticate the given user + * + * @param User $user + * @param string $password + * + * @return bool True on success, false on failure + * + * @throws AuthenticationException In case authentication is not possible due to an error + */ + public function authenticate(User $user, $password) + { + try { + return password_verify( + $password, + $this->getPasswordHash($user->getUsername()) + ); + } catch (Exception $e) { + throw new AuthenticationException( + 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', + $user->getUsername(), + $this->getName(), + $e + ); + } + } + + /** + * Inspect this object to gain extended information about its health + * + * @return Inspection The inspection result + */ + public function inspect() + { + $insp = new Inspection('Db User Backend'); + $insp->write($this->ds->inspect()); + try { + $insp->write(sprintf('%s active users', $this->select()->where('is_active', true)->count())); + } catch (Exception $e) { + $insp->error(sprintf('Query failed: %s', $e->getMessage())); + } + return $insp; + } +} diff --git a/library/Icinga/Authentication/User/DomainAwareInterface.php b/library/Icinga/Authentication/User/DomainAwareInterface.php new file mode 100644 index 0000000..3ff9c31 --- /dev/null +++ b/library/Icinga/Authentication/User/DomainAwareInterface.php @@ -0,0 +1,17 @@ +stripUsernameRegexp = $config->get('strip_username_regexp'); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Get the remote user from environment or $_SERVER, if any + * + * @param string $variable The name of the variable where to read the user from + * + * @return string|null + */ + public static function getRemoteUser($variable = 'REMOTE_USER') + { + $username = getenv($variable); + if (! empty($username)) { + return $username; + } + + if (array_key_exists($variable, $_SERVER) && ! empty($_SERVER[$variable])) { + return $_SERVER[$variable]; + } + } + + /** + * Get the remote user information from environment or $_SERVER, if any + * + * @return array Contains always two entries, the username and origin which may both set to null. + */ + public static function getRemoteUserInformation() + { + foreach (static::$remoteUserEnvvars as $envVar) { + $username = static::getRemoteUser($envVar); + if ($username !== null) { + return array($username, $envVar); + } + } + + return array(null, null); + } + + /** + * {@inheritdoc} + */ + public function authenticate(User $user, $password = null) + { + list($username, $field) = static::getRemoteUserInformation(); + if ($username !== null) { + $user->setExternalUserInformation($username, $field); + + if ($this->stripUsernameRegexp) { + $stripped = @preg_replace($this->stripUsernameRegexp, '', $username); + if ($stripped === false) { + Logger::error('Failed to strip external username. The configured regular expression is invalid.'); + return false; + } + + $username = $stripped; + } + + $user->setUsername($username); + return true; + } + + return false; + } +} diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php new file mode 100644 index 0000000..8c8a230 --- /dev/null +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -0,0 +1,477 @@ + array( + 'columns' => array( + 'is_active desc', + 'user_name' + ) + ) + ); + + /** + * Set the base DN to use for a query + * + * @param string $baseDn + * + * @return $this + */ + public function setBaseDn($baseDn) + { + if ($baseDn && ($baseDn = trim($baseDn))) { + $this->baseDn = $baseDn; + } + + return $this; + } + + /** + * Return the base DN to use for a query + * + * @return string + */ + public function getBaseDn() + { + return $this->baseDn; + } + + /** + * Set the objectClass where to look for users + * + * @param string $userClass + * + * @return $this + */ + public function setUserClass($userClass) + { + $this->userClass = $this->getNormedAttribute($userClass); + return $this; + } + + /** + * Return the objectClass where to look for users + * + * @return string + */ + public function getUserClass() + { + return $this->userClass; + } + + /** + * Set the attribute name where to find a user's name + * + * @param string $userNameAttribute + * + * @return $this + */ + public function setUserNameAttribute($userNameAttribute) + { + $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute); + return $this; + } + + /** + * Return the attribute name where to find a user's name + * + * @return string + */ + public function getUserNameAttribute() + { + return $this->userNameAttribute; + } + + /** + * Set the custom LDAP filter to apply on search queries + * + * @param string $filter + * + * @return $this + */ + public function setFilter($filter) + { + if ($filter && ($filter = trim($filter))) { + if ($filter[0] === '(') { + $filter = substr($filter, 1, -1); + } + + $this->filter = $filter; + } + + return $this; + } + + /** + * Return the custom LDAP filter to apply on search queries + * + * @return string + */ + public function getFilter() + { + return $this->filter; + } + + public function getDomain() + { + return $this->domain; + } + + /** + * Set the domain the backend is responsible for + * + * @param string $domain + * + * @return $this + */ + public function setDomain($domain) + { + if ($domain && ($domain = trim($domain))) { + $this->domain = $domain; + } + + return $this; + } + + /** + * Initialize this repository's virtual tables + * + * @return array + * + * @throws ProgrammingError In case $this->userClass has not been set yet + */ + protected function initializeVirtualTables() + { + if ($this->userClass === null) { + throw new ProgrammingError('It is required to set the object class where to find users first'); + } + + return array( + 'user' => $this->userClass + ); + } + + /** + * Initialize this repository's query columns + * + * @return array + * + * @throws ProgrammingError In case $this->userNameAttribute has not been set yet + */ + protected function initializeQueryColumns() + { + if ($this->userNameAttribute === null) { + throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first'); + } + + if ($this->ds->getCapabilities()->isActiveDirectory()) { + $isActiveAttribute = 'userAccountControl'; + $createdAtAttribute = 'whenCreated'; + $lastModifiedAttribute = 'whenChanged'; + } else { + // TODO(jom): Elaborate whether it is possible to add dynamic support for the ppolicy + $isActiveAttribute = 'shadowExpire'; + + $createdAtAttribute = 'createTimestamp'; + $lastModifiedAttribute = 'modifyTimestamp'; + } + + return array( + 'user' => array( + 'user' => $this->userNameAttribute, + 'user_name' => $this->userNameAttribute, + 'is_active' => $isActiveAttribute, + 'created_at' => $createdAtAttribute, + 'last_modified' => $lastModifiedAttribute + ) + ); + } + + /** + * Initialize this repository's filter columns + * + * @return array + */ + protected function initializeFilterColumns() + { + return array( + t('Username') => 'user_name', + t('Active') => 'is_active', + t('Created At') => 'created_at', + t('Last modified') => 'last_modified' + ); + } + + /** + * Initialize this repository's conversion rules + * + * @return array + */ + protected function initializeConversionRules() + { + if ($this->ds->getCapabilities()->isActiveDirectory()) { + $stateConverter = 'user_account_control'; + } else { + $stateConverter = 'shadow_expire'; + } + + return array( + 'user' => array( + 'is_active' => $stateConverter, + 'created_at' => 'generalized_time', + 'last_modified' => 'generalized_time' + ) + ); + } + + /** + * Return whether the given userAccountControl value defines that a user is permitted to login + * + * @param string|null $value + * + * @return bool + */ + protected function retrieveUserAccountControl($value) + { + if ($value === null) { + return $value; + } + + $ADS_UF_ACCOUNTDISABLE = 2; + return ((int) $value & $ADS_UF_ACCOUNTDISABLE) === 0; + } + + /** + * Return whether the given shadowExpire value defines that a user is permitted to login + * + * @param string|null $value + * + * @return bool + */ + protected function retrieveShadowExpire($value) + { + if ($value === null) { + return $value; + } + + $now = new DateTime(); + $bigBang = clone $now; + $bigBang->setTimestamp(0); + return ((int) $value) >= $bigBang->diff($now)->days; + } + + /** + * Validate that the requested table exists + * + * @param string $table The table to validate + * @param RepositoryQuery $query An optional query to pass as context + * + * @return string + * + * @throws ProgrammingError In case the given table does not exist + */ + public function requireTable($table, RepositoryQuery $query = null) + { + if ($query !== null) { + $query->getQuery()->setBase($this->baseDn); + if ($this->filter) { + $query->getQuery()->setNativeFilter($this->filter); + } + } + + return parent::requireTable($table, $query); + } + + /** + * Validate that the given column is a valid query target and return it or the actual name if it's an alias + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid query column + */ + public function requireQueryColumn($table, $name, RepositoryQuery $query = null) + { + $column = parent::requireQueryColumn($table, $name, $query); + if ($name === 'user_name' && $query !== null) { + $query->getQuery()->setUnfoldAttribute('user_name'); + } + + return $column; + } + + /** + * Authenticate the given user + * + * @param User $user + * @param string $password + * + * @return bool True on success, false on failure + * + * @throws AuthenticationException In case authentication is not possible due to an error + */ + public function authenticate(User $user, $password) + { + if ($this->domain !== null) { + if (! $user->hasDomain() || strtolower($user->getDomain()) !== strtolower($this->domain)) { + return false; + } + + $username = $user->getLocalUsername(); + } else { + $username = $user->getUsername(); + } + + try { + $userDn = $this + ->select() + ->where('user_name', str_replace('*', '', $username)) + ->getQuery() + ->setUsePagedResults(false) + ->fetchDn(); + if ($userDn === null) { + return false; + } + + $validCredentials = $this->ds->testCredentials($userDn, $password); + if ($validCredentials) { + $user->setAdditional('ldap_dn', $userDn); + } + + return $validCredentials; + } catch (LdapException $e) { + throw new AuthenticationException( + 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', + $username, + $this->getName(), + $e + ); + } + } + + /** + * Inspect if this LDAP User Backend is working as expected by probing the backend + * and testing if thea uthentication is possible + * + * Try to bind to the backend and fetch a single user to check if: + *
    + *
  • Connection credentials are correct and the bind is possible
  • + *
  • At least one user exists
  • + *
  • The specified userClass has the property specified by userNameAttribute
  • + *
+ * + * @return Inspection Inspection result + */ + public function inspect() + { + $result = new Inspection('Ldap User Backend'); + + // inspect the used connection to get more diagnostic info in case the connection is not working + $result->write($this->ds->inspect()); + try { + try { + $res = $this->select()->fetchRow(); + } catch (LdapException $e) { + throw new AuthenticationException('Connection not possible', $e); + } + $result->write('Searching for: ' . sprintf( + 'objectClass "%s" in DN "%s" (Filter: %s)', + $this->userClass, + $this->baseDn ?: $this->ds->getDn(), + $this->filter ?: 'None' + )); + if ($res === false) { + throw new AuthenticationException('Error, no users found in backend'); + } + $result->write(sprintf('%d users found in backend', $this->select()->count())); + if (! isset($res->user_name)) { + throw new AuthenticationException( + 'UserNameAttribute "%s" not existing in objectClass "%s"', + $this->userNameAttribute, + $this->userClass + ); + } + } catch (AuthenticationException $e) { + if (($previous = $e->getPrevious()) !== null) { + $result->error($previous->getMessage()); + } else { + $result->error($e->getMessage()); + } + } catch (Exception $e) { + $result->error(sprintf('Unable to validate authentication: %s', $e->getMessage())); + } + return $result; + } +} diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php new file mode 100644 index 0000000..f2059ed --- /dev/null +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -0,0 +1,257 @@ +getModuleManager()->getLoadedModules() as $module) { + foreach ($module->getUserBackends() as $identifier => $className) { + if (array_key_exists($identifier, $providedBy)) { + Logger::warning( + 'Cannot register user backend of type "%s" provided by module "%s".' + . ' The type is already provided by module "%s"', + $identifier, + $module->getName(), + $providedBy[$identifier] + ); + } elseif (in_array($identifier, static::$defaultBackends)) { + Logger::warning( + 'Cannot register user backend of type "%s" provided by module "%s".' + . ' The type is a default type provided by Icinga Web 2', + $identifier, + $module->getName() + ); + } else { + $providedBy[$identifier] = $module->getName(); + static::$customBackends[$identifier] = $className; + } + } + } + } + + /** + * Get config forms of all custom user backends + */ + public static function getCustomBackendConfigForms() + { + $customBackendConfigForms = []; + static::registerCustomUserBackends(); + foreach (self::$customBackends as $customBackendType => $customBackendClass) { + if (method_exists($customBackendClass, 'getConfigurationFormClass')) { + $customBackendConfigForms[$customBackendType] = $customBackendClass::getConfigurationFormClass(); + } + } + + return $customBackendConfigForms; + } + + /** + * Return the class for the given custom user backend + * + * @param string $identifier The identifier of the custom user backend + * + * @return string|null The name of the class or null in case there was no + * backend found with the given identifier + * + * @throws ConfigurationError In case the class associated to the given identifier does not exist + */ + protected static function getCustomUserBackend($identifier) + { + static::registerCustomUserBackends(); + if (array_key_exists($identifier, static::$customBackends)) { + $className = static::$customBackends[$identifier]; + if (! class_exists($className)) { + throw new ConfigurationError( + 'Cannot utilize user backend of type "%s". Class "%s" does not exist', + $identifier, + $className + ); + } + + return $className; + } + } + + /** + * Create and return a user backend with the given name and given configuration applied to it + * + * @param string $name + * @param ConfigObject $backendConfig + * + * @return UserBackendInterface + * + * @throws ConfigurationError + */ + public static function create($name, ConfigObject $backendConfig = null) + { + if ($backendConfig === null) { + self::assertBackendsExist(); + if (self::$backends->hasSection($name)) { + $backendConfig = self::$backends->getSection($name); + } else { + throw new ConfigurationError('User backend "%s" does not exist', $name); + } + } + + if ($backendConfig->name !== null) { + $name = $backendConfig->name; + } + + if (! ($backendType = strtolower($backendConfig->backend))) { + throw new ConfigurationError( + 'Authentication configuration for user backend "%s" is missing the \'backend\' directive', + $name + ); + } + if ($backendType === 'external') { + $backend = new ExternalBackend($backendConfig); + $backend->setName($name); + return $backend; + } + if (in_array($backendType, static::$defaultBackends)) { + // The default backend check is the first one because of performance reasons: + // Do not attempt to load a custom user backend unless it's actually required + } elseif (($customClass = static::getCustomUserBackend($backendType)) !== null) { + $backend = new $customClass($backendConfig); + if (! is_a($backend, 'Icinga\Authentication\User\UserBackendInterface')) { + throw new ConfigurationError( + 'Cannot utilize user backend of type "%s". Class "%s" does not implement UserBackendInterface', + $backendType, + $customClass + ); + } + + $backend->setName($name); + return $backend; + } else { + throw new ConfigurationError( + 'Authentication configuration for user backend "%s" defines an invalid backend type.' + . ' Backend type "%s" is not supported', + $name, + $backendType + ); + } + + if ($backendConfig->resource === null) { + throw new ConfigurationError( + 'Authentication configuration for user backend "%s" is missing the \'resource\' directive', + $name + ); + } + + $resourceConfig = ResourceFactory::getResourceConfig($backendConfig->resource); + if ($backendType === 'db' && $resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + $resource = ResourceFactory::createResource($resourceConfig); + switch ($backendType) { + case 'db': + $backend = new DbUserBackend($resource); + break; + case 'msldap': + $backend = new LdapUserBackend($resource); + $backend->setBaseDn($backendConfig->base_dn); + $backend->setUserClass($backendConfig->get('user_class', 'user')); + $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'sAMAccountName')); + $backend->setFilter($backendConfig->filter); + $backend->setDomain($backendConfig->domain); + break; + case 'ldap': + $backend = new LdapUserBackend($resource); + $backend->setBaseDn($backendConfig->base_dn); + $backend->setUserClass($backendConfig->get('user_class', 'inetOrgPerson')); + $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'uid')); + $backend->setFilter($backendConfig->filter); + $backend->setDomain($backendConfig->domain); + break; + } + + $backend->setName($name); + return $backend; + } +} diff --git a/library/Icinga/Authentication/User/UserBackendInterface.php b/library/Icinga/Authentication/User/UserBackendInterface.php new file mode 100644 index 0000000..4660eb0 --- /dev/null +++ b/library/Icinga/Authentication/User/UserBackendInterface.php @@ -0,0 +1,39 @@ + array( + 'group_id' => 'g.id', + 'group' => 'g.name COLLATE utf8mb4_general_ci', + 'group_name' => 'g.name', + 'parent' => 'g.parent', + 'created_at' => 'UNIX_TIMESTAMP(g.ctime)', + 'last_modified' => 'UNIX_TIMESTAMP(g.mtime)' + ), + 'group_membership' => array( + 'group_id' => 'gm.group_id', + 'user' => 'gm.username COLLATE utf8mb4_general_ci', + 'user_name' => 'gm.username', + 'created_at' => 'UNIX_TIMESTAMP(gm.ctime)', + 'last_modified' => 'UNIX_TIMESTAMP(gm.mtime)' + ) + ); + + /** + * The table aliases being applied + * + * @var array + */ + protected $tableAliases = array( + 'group' => 'g', + 'group_membership' => 'gm' + ); + + /** + * The statement columns being provided + * + * @var array + */ + protected $statementColumns = array( + 'group' => array( + 'group_id' => 'id', + 'group_name' => 'name', + 'parent' => 'parent', + 'created_at' => 'ctime', + 'last_modified' => 'mtime' + ), + 'group_membership' => array( + 'group_id' => 'group_id', + 'group_name' => 'group_id', + 'user_name' => 'username', + 'created_at' => 'ctime', + 'last_modified' => 'mtime' + ) + ); + + /** + * The columns which are not permitted to be queried + * + * @var array + */ + protected $blacklistedQueryColumns = array('group', 'user'); + + /** + * The search columns being provided + * + * @var array + */ + protected $searchColumns = array('group', 'user'); + + /** + * The value conversion rules to apply on a query or statement + * + * @var array + */ + protected $conversionRules = array( + 'group' => array( + 'parent' => 'group_id' + ), + 'group_membership' => array( + 'group_name' => 'group_id' + ) + ); + + /** + * Initialize this database user group backend + */ + protected function init() + { + if (! $this->ds->getTablePrefix()) { + $this->ds->setTablePrefix('icingaweb_'); + } + } + + /** + * Initialize this repository's filter columns + * + * @return array + */ + protected function initializeFilterColumns() + { + $userLabel = t('Username') . ' ' . t('(Case insensitive)'); + $groupLabel = t('User Group') . ' ' . t('(Case insensitive)'); + return array( + $userLabel => 'user', + t('Username') => 'user_name', + $groupLabel => 'group', + t('User Group') => 'group_name', + t('Parent') => 'parent', + t('Created At') => 'created_at', + t('Last modified') => 'last_modified' + ); + } + + /** + * Insert a table row with the given data + * + * @param string $table + * @param array $bind + */ + public function insert($table, array $bind, array $types = array()) + { + $bind['created_at'] = date('Y-m-d H:i:s'); + parent::insert($table, $bind); + } + + /** + * Update table rows with the given data, optionally limited by using a filter + * + * @param string $table + * @param array $bind + * @param Filter $filter + */ + public function update($table, array $bind, Filter $filter = null, array $types = array()) + { + $bind['last_modified'] = date('Y-m-d H:i:s'); + parent::update($table, $bind, $filter); + } + + /** + * Delete table rows, optionally limited by using a filter + * + * @param string $table + * @param Filter $filter + */ + public function delete($table, Filter $filter = null) + { + if ($table === 'group') { + parent::delete('group_membership', $filter); + $idQuery = $this->select(array('group_id')); + if ($filter !== null) { + $idQuery->applyFilter($filter); + } + + $this->update('group', array('parent' => null), Filter::where('parent', $idQuery->fetchColumn())); + } + + parent::delete($table, $filter); + } + + /** + * Return the groups the given user is a member of + * + * @param User $user + * + * @return array + */ + public function getMemberships(User $user) + { + $groupQuery = $this->ds + ->select() + ->from( + array('g' => $this->prependTablePrefix('group')), + array( + 'group_name' => 'g.name', + 'parent_name' => 'gg.name' + ) + )->joinLeft( + array('gg' => $this->prependTablePrefix('group')), + 'g.parent = gg.id', + array() + ); + + $groups = array(); + foreach ($groupQuery as $group) { + $groups[$group->group_name] = $group->parent_name; + } + + $membershipQuery = $this + ->select() + ->from('group_membership', array('group_name')) + ->where('user_name', $user->getUsername()); + + $memberships = array(); + foreach ($membershipQuery as $membership) { + $memberships[] = $membership->group_name; + $parent = $groups[$membership->group_name]; + while ($parent !== null) { + $memberships[] = $parent; + // Usually a parent is an existing group, but since we do not have a constraint on our table.. + $parent = isset($groups[$parent]) ? $groups[$parent] : null; + } + } + + return $memberships; + } + + /** + * Return the name of the backend that is providing the given user + * + * @param string $username Currently unused + * + * @return null|string The name of the backend or null in case this information is not available + */ + public function getUserBackendName($username) + { + return null; // TODO(10373): Store this to the database when inserting and fetch it here + } + + /** + * Join group into group_membership + * + * @param RepositoryQuery $query + */ + protected function joinGroup(RepositoryQuery $query) + { + $query->getQuery()->join( + $this->requireTable('group'), + 'gm.group_id = g.id', + array() + ); + } + + /** + * Join group_membership into group + * + * @param RepositoryQuery $query + */ + protected function joinGroupMembership(RepositoryQuery $query) + { + $query->getQuery()->joinLeft( + $this->requireTable('group_membership'), + 'g.id = gm.group_id', + array() + )->group('g.id'); + } + + /** + * Fetch and return the corresponding id for the given group's name + * + * @param string|array $groupName + * + * @return int + * + * @throws NotFoundError + */ + protected function persistGroupId($groupName) + { + if (! $groupName || empty($groupName) || is_numeric($groupName)) { + return $groupName; + } + + if (is_array($groupName)) { + if (is_numeric($groupName[0])) { + return $groupName; // In case the array contains mixed types... + } + + $groupIds = $this->ds + ->select() + ->from($this->prependTablePrefix('group'), array('id')) + ->where('name', $groupName) + ->fetchColumn(); + if (empty($groupIds)) { + throw new NotFoundError('No groups found matching one of: %s', implode(', ', $groupName)); + } + + return $groupIds; + } + + $groupId = $this->ds + ->select() + ->from($this->prependTablePrefix('group'), array('id')) + ->where('name', $groupName) + ->fetchOne(); + if ($groupId === false) { + throw new NotFoundError('Group "%s" does not exist', $groupName); + } + + return $groupId; + } + + /** + * Inspect this object to gain extended information about its health + * + * @return Inspection The inspection result + */ + public function inspect() + { + $insp = new Inspection('Db User Group Backend'); + $insp->write($this->ds->inspect()); + + try { + $insp->write(sprintf('%s group(s)', $this->select()->count())); + } catch (Exception $e) { + $insp->error(sprintf('Query failed: %s', $e->getMessage())); + } + + return $insp; + } +} diff --git a/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php new file mode 100644 index 0000000..54ccaa9 --- /dev/null +++ b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php @@ -0,0 +1,944 @@ + array( + 'order' => 'asc' + ) + ); + + /** + * Set the user backend to be associated with this user group backend + * + * @param LdapUserBackend $backend + * + * @return $this + */ + public function setUserBackend(LdapUserBackend $backend) + { + $this->userBackend = $backend; + return $this; + } + + /** + * Return the user backend being associated with this user group backend + * + * @return LdapUserBackend + */ + public function getUserBackend() + { + return $this->userBackend; + } + + /** + * Set the base DN to use for a user query + * + * @param string $baseDn + * + * @return $this + */ + public function setUserBaseDn($baseDn) + { + if ($baseDn && ($baseDn = trim($baseDn))) { + $this->userBaseDn = $baseDn; + } + + return $this; + } + + /** + * Return the base DN to use for a user query + * + * @return string + */ + public function getUserBaseDn() + { + return $this->userBaseDn; + } + + /** + * Set the base DN to use for a group query + * + * @param string $baseDn + * + * @return $this + */ + public function setGroupBaseDn($baseDn) + { + if ($baseDn && ($baseDn = trim($baseDn))) { + $this->groupBaseDn = $baseDn; + } + + return $this; + } + + /** + * Return the base DN to use for a group query + * + * @return string + */ + public function getGroupBaseDn() + { + return $this->groupBaseDn; + } + + /** + * Set the objectClass where to look for users + * + * @param string $userClass + * + * @return $this + */ + public function setUserClass($userClass) + { + $this->userClass = $this->getNormedAttribute($userClass); + return $this; + } + + /** + * Return the objectClass where to look for users + * + * @return string + */ + public function getUserClass() + { + return $this->userClass; + } + + /** + * Set the objectClass where to look for groups + * + * @param string $groupClass + * + * @return $this + */ + public function setGroupClass($groupClass) + { + $this->groupClass = $this->getNormedAttribute($groupClass); + return $this; + } + + /** + * Return the objectClass where to look for groups + * + * @return string + */ + public function getGroupClass() + { + return $this->groupClass; + } + + /** + * Set the attribute name where to find a user's name + * + * @param string $userNameAttribute + * + * @return $this + */ + public function setUserNameAttribute($userNameAttribute) + { + $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute); + return $this; + } + + /** + * Return the attribute name where to find a user's name + * + * @return string + */ + public function getUserNameAttribute() + { + return $this->userNameAttribute; + } + + /** + * Set the attribute name where to find a group's name + * + * @param string $groupNameAttribute + * + * @return $this + */ + public function setGroupNameAttribute($groupNameAttribute) + { + $this->groupNameAttribute = $this->getNormedAttribute($groupNameAttribute); + return $this; + } + + /** + * Return the attribute name where to find a group's name + * + * @return string + */ + public function getGroupNameAttribute() + { + return $this->groupNameAttribute; + } + + /** + * Set the attribute name where to find a group's member + * + * @param string $groupMemberAttribute + * + * @return $this + */ + public function setGroupMemberAttribute($groupMemberAttribute) + { + $this->groupMemberAttribute = $this->getNormedAttribute($groupMemberAttribute); + return $this; + } + + /** + * Return the attribute name where to find a group's member + * + * @return string + */ + public function getGroupMemberAttribute() + { + return $this->groupMemberAttribute; + } + + /** + * Set the custom LDAP filter to apply on a user query + * + * @param string $filter + * + * @return $this + */ + public function setUserFilter($filter) + { + if ($filter && ($filter = trim($filter))) { + if ($filter[0] === '(') { + $filter = substr($filter, 1, -1); + } + + $this->userFilter = $filter; + } + + return $this; + } + + /** + * Return the custom LDAP filter to apply on a user query + * + * @return string + */ + public function getUserFilter() + { + return $this->userFilter; + } + + /** + * Set the custom LDAP filter to apply on a group query + * + * @param string $filter + * + * @return $this + */ + public function setGroupFilter($filter) + { + if ($filter && ($filter = trim($filter))) { + $this->groupFilter = $filter; + } + + return $this; + } + + /** + * Return the custom LDAP filter to apply on a group query + * + * @return string + */ + public function getGroupFilter() + { + return $this->groupFilter; + } + + /** + * Set nestedGroupSearch for the group query + * + * @param bool $enable + * + * @return $this + */ + public function setNestedGroupSearch($enable = true) + { + $this->nestedGroupSearch = $enable; + return $this; + } + + /** + * Get nestedGroupSearch for the group query + * + * @return bool + */ + public function getNestedGroupSearch() + { + return $this->nestedGroupSearch; + } + + /** + * Get the domain the backend is responsible for + * + * If the LDAP group backend is linked with a LDAP user backend, + * the domain of the user backend will be returned. + * + * @return string + */ + public function getDomain() + { + return $this->userBackend !== null ? $this->userBackend->getDomain() : $this->domain; + } + + /** + * Set the domain the backend is responsible for + * + * If the LDAP group backend is linked with a LDAP user backend, + * the domain of the user backend will be used nonetheless. + * + * @param string $domain + * + * @return $this + */ + public function setDomain($domain) + { + if ($domain && ($domain = trim($domain))) { + $this->domain = $domain; + } + + return $this; + } + + /** + * Return whether the attribute name where to find a group's member holds ambiguous values + * + * This tries to detect if the member attribute of groups contain: + * + * full DN -> distinguished name of another object + * other -> ambiguous field referencing the member by userNameAttribute + * + * @return bool + * + * @throws ProgrammingError In case either $this->groupClass or $this->groupMemberAttribute + * has not been set yet + */ + protected function isMemberAttributeAmbiguous() + { + if ($this->ambiguousMemberAttribute === null) { + if ($this->groupClass === null) { + throw new ProgrammingError( + 'It is required to set the objectClass where to look for groups first' + ); + } elseif ($this->groupMemberAttribute === null) { + throw new ProgrammingError( + 'It is required to set a attribute name where to find a group\'s members first' + ); + } + + $sampleValues = $this->ds + ->select() + ->from($this->groupClass, array($this->groupMemberAttribute)) + ->where($this->groupMemberAttribute, '*') + ->limit(Logger::getInstance()->getLevel() === Logger::DEBUG ? 3 : 1) + ->setUnfoldAttribute($this->groupMemberAttribute) + ->setBase($this->groupBaseDn) + ->fetchAll(); + + Logger::debug('Ambiguity query returned %d results', count($sampleValues)); + + $i = 0; + $sampleValue = null; + foreach ($sampleValues as $key => $value) { + if ($sampleValue === null) { + $sampleValue = $value; + } + + Logger::debug('Result %d: %s (%s)', ++$i, $value, $key); + } + + if (is_object($sampleValue) && isset($sampleValue->{$this->groupMemberAttribute})) { + $this->ambiguousMemberAttribute = ! LdapUtils::isDn($sampleValue->{$this->groupMemberAttribute}); + + Logger::debug( + 'Ambiguity check came to the conclusion that the member attribute %s ambiguous. Tested sample: %s', + $this->ambiguousMemberAttribute ? 'is' : 'is not', + $sampleValue->{$this->groupMemberAttribute} + ); + } else { + Logger::warning( + 'Ambiguity query returned zero or invalid results. Sample value is `%s`', + print_r($sampleValue, true) + ); + } + } + + return $this->ambiguousMemberAttribute; + } + + /** + * Initialize this repository's virtual tables + * + * @return array + * + * @throws ProgrammingError In case $this->groupClass has not been set yet + */ + protected function initializeVirtualTables() + { + if ($this->groupClass === null) { + throw new ProgrammingError('It is required to set the object class where to find groups first'); + } + + return array( + 'group' => $this->groupClass, + 'group_membership' => $this->groupClass + ); + } + + /** + * Initialize this repository's query columns + * + * @return array + * + * @throws ProgrammingError In case either $this->groupNameAttribute or + * $this->groupMemberAttribute has not been set yet + */ + protected function initializeQueryColumns() + { + if ($this->groupNameAttribute === null) { + throw new ProgrammingError('It is required to set a attribute name where to find a group\'s name first'); + } + if ($this->groupMemberAttribute === null) { + throw new ProgrammingError('It is required to set a attribute name where to find a group\'s members first'); + } + + if ($this->ds->getCapabilities()->isActiveDirectory()) { + $createdAtAttribute = 'whenCreated'; + $lastModifiedAttribute = 'whenChanged'; + } else { + $createdAtAttribute = 'createTimestamp'; + $lastModifiedAttribute = 'modifyTimestamp'; + } + + $columns = array( + 'group' => $this->groupNameAttribute, + 'group_name' => $this->groupNameAttribute, + 'user' => $this->groupMemberAttribute, + 'user_name' => $this->groupMemberAttribute, + 'created_at' => $createdAtAttribute, + 'last_modified' => $lastModifiedAttribute + ); + return array('group' => $columns, 'group_membership' => $columns); + } + + /** + * Initialize this repository's filter columns + * + * @return array + */ + protected function initializeFilterColumns() + { + return array( + t('Username') => 'user_name', + t('User Group') => 'group_name', + t('Created At') => 'created_at', + t('Last modified') => 'last_modified' + ); + } + + /** + * Initialize this repository's conversion rules + * + * @return array + */ + protected function initializeConversionRules() + { + $rules = array( + 'group' => array( + 'created_at' => 'generalized_time', + 'last_modified' => 'generalized_time' + ), + 'group_membership' => array( + 'created_at' => 'generalized_time', + 'last_modified' => 'generalized_time' + ) + ); + if (! $this->isMemberAttributeAmbiguous()) { + $rules['group_membership']['user_name'] = 'user_name'; + $rules['group_membership']['user'] = 'user_name'; + $rules['group']['user_name'] = 'user_name'; + $rules['group']['user'] = 'user_name'; + } + + return $rules; + } + + /** + * Return the distinguished name for the given uid or gid + * + * @param string $name + * + * @return string + */ + protected function persistUserName($name) + { + try { + $userDn = $this->ds + ->select() + ->from($this->userClass, array()) + ->where($this->userNameAttribute, $name) + ->setBase($this->userBaseDn) + ->setUsePagedResults(false) + ->fetchDn(); + if ($userDn) { + return $userDn; + } + + $groupDn = $this->ds + ->select() + ->from($this->groupClass, array()) + ->where($this->groupNameAttribute, $name) + ->setBase($this->groupBaseDn) + ->setUsePagedResults(false) + ->fetchDn(); + if ($groupDn) { + return $groupDn; + } + } catch (LdapException $_) { + // pass + } + + Logger::debug('Unable to persist uid or gid "%s" in repository "%s". No DN found.', $name, $this->getName()); + return $name; + } + + /** + * Return the uid for the given distinguished name + * + * @param string $username + * + * @param string + */ + protected function retrieveUserName($dn) + { + return $this->ds + ->select() + ->from('*', array($this->userNameAttribute)) + ->setUnfoldAttribute($this->userNameAttribute) + ->setBase($dn) + ->fetchOne(); + } + + /** + * Validate that the requested table exists + * + * @param string $table The table to validate + * @param RepositoryQuery $query An optional query to pass as context + * + * @return string + * + * @throws ProgrammingError In case the given table does not exist + */ + public function requireTable($table, RepositoryQuery $query = null) + { + if ($query !== null) { + $query->getQuery()->setBase($this->groupBaseDn); + if ($table === 'group' && $this->groupFilter) { + $query->getQuery()->setNativeFilter($this->groupFilter); + } + } + + return parent::requireTable($table, $query); + } + + /** + * Validate that the given column is a valid query target and return it or the actual name if it's an alias + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid query column + */ + public function requireQueryColumn($table, $name, RepositoryQuery $query = null) + { + $column = parent::requireQueryColumn($table, $name, $query); + if (($name === 'user_name' || $name === 'group_name') && $query !== null) { + $query->getQuery()->setUnfoldAttribute($name); + } + + return $column; + } + + /** + * Return the groups the given user is a member of + * + * @param User $user + * + * @return array + */ + public function getMemberships(User $user) + { + $domain = $this->getDomain(); + + if ($domain !== null) { + if (! $user->hasDomain() || strtolower($user->getDomain()) !== strtolower($domain)) { + return array(); + } + + $username = $user->getLocalUsername(); + } else { + $username = $user->getUsername(); + } + + if ($this->isMemberAttributeAmbiguous()) { + $queryValue = $username; + } elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) { + $userQuery = $this->ds + ->select() + ->from($this->userClass) + ->where($this->userNameAttribute, $username) + ->setBase($this->userBaseDn) + ->setUsePagedResults(false); + if ($this->userFilter) { + $userQuery->setNativeFilter($this->userFilter); + } + + if (($queryValue = $userQuery->fetchDn()) === null) { + return array(); + } + } + + if ($this->nestedGroupSearch) { + $groupMemberAttribute = $this->groupMemberAttribute . ':1.2.840.113556.1.4.1941:'; + } else { + $groupMemberAttribute = $this->groupMemberAttribute; + } + + $groupQuery = $this->ds + ->select() + ->from($this->groupClass, array($this->groupNameAttribute)) + ->setUnfoldAttribute($this->groupNameAttribute) + ->where($groupMemberAttribute, $queryValue) + ->setBase($this->groupBaseDn); + if ($this->groupFilter) { + $groupQuery->setNativeFilter($this->groupFilter); + } + + $groups = array(); + foreach ($groupQuery as $row) { + $groups[] = $row->{$this->groupNameAttribute}; + if ($domain !== null) { + $groups[] = $row->{$this->groupNameAttribute} . "@$domain"; + } + } + + return $groups; + } + + /** + * Return the name of the backend that is providing the given user + * + * @param string $username Unused + * + * @return null|string The name of the backend or null in case this information is not available + */ + public function getUserBackendName($username) + { + $userBackend = $this->getUserBackend(); + if ($userBackend !== null) { + return $userBackend->getName(); + } + } + + /** + * Apply the given configuration on this backend + * + * @param ConfigObject $config + * + * @return $this + * + * @throws ConfigurationError In case a linked user backend does not exist or is invalid + */ + public function setConfig(ConfigObject $config) + { + if ($config->backend === 'ldap') { + $defaults = $this->getOpenLdapDefaults(); + } elseif ($config->backend === 'msldap') { + $defaults = $this->getActiveDirectoryDefaults(); + } else { + $defaults = new ConfigObject(); + } + + if ($config->user_backend && $config->user_backend !== 'none') { + $userBackend = UserBackend::create($config->user_backend); + if (! $userBackend instanceof LdapUserBackend) { + throw new ConfigurationError('User backend "%s" is not of type LDAP', $config->user_backend); + } + + if ($this->ds->getHostname() !== $userBackend->getDataSource()->getHostname() + || $this->ds->getPort() !== $userBackend->getDataSource()->getPort() + ) { + // TODO(jom): Elaborate whether it makes sense to link directories on different hosts + throw new ConfigurationError( + 'It is required that a linked user backend refers to the ' + . 'same directory as it\'s user group backend counterpart' + ); + } + + $this->setUserBackend($userBackend); + $defaults->merge(array( + 'user_base_dn' => $userBackend->getBaseDn(), + 'user_class' => $userBackend->getUserClass(), + 'user_name_attribute' => $userBackend->getUserNameAttribute(), + 'user_filter' => $userBackend->getFilter(), + 'domain' => $userBackend->getDomain() + )); + } + + return $this + ->setGroupBaseDn($config->base_dn) + ->setUserBaseDn($config->get('user_base_dn', $defaults->get('user_base_dn', $this->getGroupBaseDn()))) + ->setGroupClass($config->get('group_class', $defaults->group_class)) + ->setUserClass($config->get('user_class', $defaults->user_class)) + ->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute)) + ->setUserNameAttribute($config->get('user_name_attribute', $defaults->user_name_attribute)) + ->setGroupMemberAttribute($config->get('group_member_attribute', $defaults->group_member_attribute)) + ->setGroupFilter($config->group_filter) + ->setUserFilter($config->user_filter) + ->setNestedGroupSearch((bool) $config->get('nested_group_search', $defaults->nested_group_search)) + ->setDomain($defaults->get('domain', $config->domain)); + } + + /** + * Return the configuration defaults for an OpenLDAP environment + * + * @return ConfigObject + */ + public function getOpenLdapDefaults() + { + return new ConfigObject(array( + 'group_class' => 'group', + 'user_class' => 'inetOrgPerson', + 'group_name_attribute' => 'gid', + 'user_name_attribute' => 'uid', + 'group_member_attribute' => 'member', + 'nested_group_search' => '0' + )); + } + + /** + * Return the configuration defaults for an ActiveDirectory environment + * + * @return ConfigObject + */ + public function getActiveDirectoryDefaults() + { + return new ConfigObject(array( + 'group_class' => 'group', + 'user_class' => 'user', + 'group_name_attribute' => 'sAMAccountName', + 'user_name_attribute' => 'sAMAccountName', + 'group_member_attribute' => 'member', + 'nested_group_search' => '0' + )); + } + + /** + * Inspect if this LDAP User Group Backend is working as expected by probing the backend + * + * Try to bind to the backend and fetch a single group to check if: + *
    + *
  • Connection credentials are correct and the bind is possible
  • + *
  • At least one group exists
  • + *
  • The specified groupClass has the property specified by groupNameAttribute
  • + *
+ * + * @return Inspection Inspection result + */ + public function inspect() + { + $result = new Inspection('Ldap User Group Backend'); + + // inspect the used connection to get more diagnostic info in case the connection is not working + $result->write($this->ds->inspect()); + + try { + try { + $groupQuery = $this->ds + ->select() + ->from($this->groupClass, array($this->groupNameAttribute)) + ->setBase($this->groupBaseDn); + + if ($this->groupFilter) { + $groupQuery->setNativeFilter($this->groupFilter); + } + + $res = $groupQuery->fetchRow(); + } catch (LdapException $e) { + throw new AuthenticationException('Connection not possible', $e); + } + + $result->write('Searching for: ' . sprintf( + 'objectClass "%s" in DN "%s" (Filter: %s)', + $this->groupClass, + $this->groupBaseDn ?: $this->ds->getDn(), + $this->groupFilter ?: 'None' + )); + + if ($res === false) { + throw new AuthenticationException('Error, no groups found in backend'); + } + + $result->write(sprintf('%d groups found in backend', $groupQuery->count())); + + if (! isset($res->{$this->groupNameAttribute})) { + throw new AuthenticationException( + 'GroupNameAttribute "%s" not existing in objectClass "%s"', + $this->groupNameAttribute, + $this->groupClass + ); + } + } catch (AuthenticationException $e) { + if (($previous = $e->getPrevious()) !== null) { + $result->error($previous->getMessage()); + } else { + $result->error($e->getMessage()); + } + } catch (Exception $e) { + $result->error(sprintf('Unable to validate backend: %s', $e->getMessage())); + } + + return $result; + } +} diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php new file mode 100644 index 0000000..76fa2d0 --- /dev/null +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php @@ -0,0 +1,188 @@ +getModuleManager()->getLoadedModules() as $module) { + foreach ($module->getUserGroupBackends() as $identifier => $className) { + if (array_key_exists($identifier, $providedBy)) { + Logger::warning( + 'Cannot register user group backend of type "%s" provided by module "%s".' + . ' The type is already provided by module "%s"', + $identifier, + $module->getName(), + $providedBy[$identifier] + ); + } elseif (in_array($identifier, static::$defaultBackends)) { + Logger::warning( + 'Cannot register user group backend of type "%s" provided by module "%s".' + . ' The type is a default type provided by Icinga Web 2', + $identifier, + $module->getName() + ); + } else { + $providedBy[$identifier] = $module->getName(); + static::$customBackends[$identifier] = $className; + } + } + } + } + + /** + * Get config forms of all custom user group backends + */ + public static function getCustomBackendConfigForms() + { + $customBackendConfigForms = []; + static::registerCustomUserGroupBackends(); + foreach (self::$customBackends as $customBackendType => $customBackendClass) { + if (method_exists($customBackendClass, 'getConfigurationFormClass')) { + $customBackendConfigForms[$customBackendType] = $customBackendClass::getConfigurationFormClass(); + } + } + + return $customBackendConfigForms; + } + + /** + * Return the class for the given custom user group backend + * + * @param string $identifier The identifier of the custom user group backend + * + * @return string|null The name of the class or null in case there was no + * backend found with the given identifier + * + * @throws ConfigurationError In case the class associated to the given identifier does not exist + */ + protected static function getCustomUserGroupBackend($identifier) + { + static::registerCustomUserGroupBackends(); + if (array_key_exists($identifier, static::$customBackends)) { + $className = static::$customBackends[$identifier]; + if (! class_exists($className)) { + throw new ConfigurationError( + 'Cannot utilize user group backend of type "%s". Class "%s" does not exist', + $identifier, + $className + ); + } + + return $className; + } + } + + /** + * Create and return a user group backend with the given name and given configuration applied to it + * + * @param string $name + * @param ConfigObject $backendConfig + * + * @return UserGroupBackendInterface + * + * @throws ConfigurationError + */ + public static function create($name, ConfigObject $backendConfig) + { + if ($backendConfig->name !== null) { + $name = $backendConfig->name; + } + + if (! ($backendType = strtolower($backendConfig->backend))) { + throw new ConfigurationError( + 'Configuration for user group backend "%s" is missing the \'backend\' directive', + $name + ); + } + if (in_array($backendType, static::$defaultBackends)) { + // The default backend check is the first one because of performance reasons: + // Do not attempt to load a custom user group backend unless it's actually required + } elseif (($customClass = static::getCustomUserGroupBackend($backendType)) !== null) { + $backend = new $customClass($backendConfig); + if (! is_a($backend, 'Icinga\Authentication\UserGroup\UserGroupBackendInterface')) { + throw new ConfigurationError( + 'Cannot utilize user group backend of type "%s".' + . ' Class "%s" does not implement UserGroupBackendInterface', + $backendType, + $customClass + ); + } + + $backend->setName($name); + return $backend; + } else { + throw new ConfigurationError( + 'Configuration for user group backend "%s" defines an invalid backend type.' + . ' Backend type "%s" is not supported', + $name, + $backendType + ); + } + + if ($backendConfig->resource === null) { + throw new ConfigurationError( + 'Configuration for user group backend "%s" is missing the \'resource\' directive', + $name + ); + } + + $resourceConfig = ResourceFactory::getResourceConfig($backendConfig->resource); + if ($backendType === 'db' && $resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + $resource = ResourceFactory::createResource($resourceConfig); + switch ($backendType) { + case 'db': + $backend = new DbUserGroupBackend($resource); + break; + case 'ldap': + case 'msldap': + $backend = new LdapUserGroupBackend($resource); + $backend->setConfig($backendConfig); + break; + } + + $backend->setName($name); + return $backend; + } +} diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php b/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php new file mode 100644 index 0000000..cc9438f --- /dev/null +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php @@ -0,0 +1,56 @@ +xUnit->addValues($dataset, 0); + $this->yUnit->addValues($dataset, 1); + } + + /** + * Set the AxisUnit implementation to use for generating the x axis + * + * @param AxisUnit $unit The AxisUnit implementation to use for the x axis + * + * @return $this This Axis Object + * @see Axis::CalendarUnit + * @see Axis::LinearUnit + */ + public function setUnitForXAxis(AxisUnit $unit) + { + $this->xUnit = $unit; + return $this; + } + + /** + * Set the AxisUnit implementation to use for generating the y axis + * + * @param AxisUnit $unit The AxisUnit implementation to use for the y axis + * + * @return $this This Axis Object + * @see Axis::CalendarUnit + * @see Axis::LinearUnit + */ + public function setUnitForYAxis(AxisUnit $unit) + { + $this->yUnit = $unit; + return $this; + } + + /** + * Return the padding this axis requires + * + * @return array An array containing the padding for all sides + */ + public function getRequiredPadding() + { + return array(10, 5, 15, 10); + } + + /** + * Render the horizontal axis + * + * @param RenderContext $ctx The context to use for rendering + * @param DOMElement $group The DOMElement this axis will be added to + */ + private function renderHorizontalAxis(RenderContext $ctx, DOMElement $group) + { + $steps = $this->ticksPerX($this->xUnit->getTicks(), $ctx->getNrOfUnitsX(), $this->minUnitsPerStep); + $ticks = $this->ticksPerX($this->xUnit->getTicks(), $ctx->getNrOfUnitsX(), $this->minUnitsPerTick); + + // Steps should always be ticks + if ($ticks !== $steps) { + $steps = $ticks * 5; + } + + // Check whether there is enough room for regular labels + $labelRotationStyle = $this->labelRotationStyle; + if ($this->labelsOversized($this->xUnit, 6)) { + $labelRotationStyle = self::LABEL_ROTATE_DIAGONAL; + } + + /* + $line = new Line(0, 100, 100, 100); + $line->setStrokeWidth(2); + $group->appendChild($line->toSvg($ctx)); + */ + + // contains the approximate end position of the last label + $lastLabelEnd = -1; + $shift = 0; + + $i = 0; + foreach ($this->xUnit as $label => $pos) { + if ($i % $ticks === 0) { + /* + $tick = new Line($pos, 100, $pos, 101); + $group->appendChild($tick->toSvg($ctx)); + */ + } + + if ($i % $steps === 0) { + if ($labelRotationStyle === self::LABEL_ROTATE_HORIZONTAL) { + // If the last label would overlap this label we shift the y axis a bit + if ($lastLabelEnd > $pos) { + $shift = ($shift + 5) % 10; + } else { + $shift = 0; + } + } + + $labelField = new Text($pos + 0.5, ($this->xLabel ? 107 : 105) + $shift, $label); + if ($labelRotationStyle === self::LABEL_ROTATE_HORIZONTAL) { + $labelField->setAlignment(Text::ALIGN_MIDDLE) + ->setFontSize('2.5em'); + } else { + $labelField->setFontSize('2.5em'); + } + + if ($labelRotationStyle === self::LABEL_ROTATE_DIAGONAL) { + $labelField = new Rotator($labelField, 45); + } + $labelField = $labelField->toSvg($ctx); + + $group->appendChild($labelField); + + if ($this->drawYGrid) { + $bgLine = new Line($pos, 0, $pos, 100); + $bgLine->setStrokeWidth(0.5) + ->setStrokeColor('#BFBFBF'); + $group->appendChild($bgLine->toSvg($ctx)); + } + $lastLabelEnd = $pos + strlen($label) * 1.2; + } + $i++; + } + } + + /** + * Render the vertical axis + * + * @param RenderContext $ctx The context to use for rendering + * @param DOMElement $group The DOMElement this axis will be added to + */ + private function renderVerticalAxis(RenderContext $ctx, DOMElement $group) + { + $steps = $this->ticksPerX($this->yUnit->getTicks(), $ctx->getNrOfUnitsY(), $this->minUnitsPerStep); + $ticks = $this->ticksPerX($this->yUnit->getTicks(), $ctx->getNrOfUnitsY(), $this->minUnitsPerTick); + + // Steps should always be ticks + if ($ticks !== $steps) { + $steps = $ticks * 5; + } + /* + $line = new Line(0, 0, 0, 100); + $line->setStrokeWidth(2); + $group->appendChild($line->toSvg($ctx)); + */ + + $i = 0; + foreach ($this->yUnit as $label => $pos) { + $pos = 100 - $pos; + + if ($i % $ticks === 0) { + // draw a tick + //$tick = new Line(0, $pos, -1, $pos); + //$group->appendChild($tick->toSvg($ctx)); + } + + if ($i % $steps === 0) { + // draw a step + $labelField = new Text(-0.5, $pos + 0.5, $label); + $labelField->setFontSize('2.5em') + ->setAlignment(Text::ALIGN_END); + + $group->appendChild($labelField->toSvg($ctx)); + if ($this->drawXGrid) { + $bgLine = new Line(0, $pos, 100, $pos); + $bgLine->setStrokeWidth(0.5) + ->setStrokeColor('#BFBFBF'); + $group->appendChild($bgLine->toSvg($ctx)); + } + } + $i++; + } + + if ($this->yLabel || $this->xLabel) { + if ($this->yLabel && $this->xLabel) { + $txt = $this->yLabel . ' / ' . $this->xLabel; + } elseif ($this->xLabel) { + $txt = $this->xLabel; + } else { + $txt = $this->yLabel; + } + + $axisLabel = new Text(50, -3, $txt); + $axisLabel->setFontSize('2em') + ->setFontWeight('bold') + ->setAlignment(Text::ALIGN_MIDDLE); + + $group->appendChild($axisLabel->toSvg($ctx)); + } + } + + /** + * Factory method, create an Axis instance using Linear ticks as the unit + * + * @return Axis The axis that has been created + * @see LinearUnit + */ + public static function createLinearAxis() + { + $axis = new Axis(); + $axis->setUnitForXAxis(self::linearUnit()); + $axis->setUnitForYAxis(self::linearUnit()); + return $axis; + } + + /** + * Set the label for the x axis + * + * An empty string means 'no label'. + * + * @param string $label The label to use for the x axis + * + * @return $this Fluid interface + */ + public function setXLabel($label) + { + $this->xLabel = $label; + return $this; + } + + /** + * Set the label for the y axis + * + * An empty string means 'no label'. + * + * @param string $label The label to use for the y axis + * + * @return $this Fluid interface + */ + public function setYLabel($label) + { + $this->yLabel = $label; + return $this; + } + + /** + * Set the labels minimum value for the x axis + * + * Setting the value to null let's the axis unit decide which value to use for the minimum + * + * @param int $xMin The minimum value to use for the x axis + * + * @return $this Fluid interface + */ + public function setXMin($xMin) + { + $this->xUnit->setMin($xMin); + return $this; + } + + /** + * Set the labels minimum value for the y axis + * + * Setting the value to null let's the axis unit decide which value to use for the minimum + * + * @param int $yMin The minimum value to use for the x axis + * + * @return $this Fluid interface + */ + public function setYMin($yMin) + { + $this->yUnit->setMin($yMin); + return $this; + } + + /** + * Set the labels maximum value for the x axis + * + * Setting the value to null let's the axis unit decide which value to use for the maximum + * + * @param int $xMax The minimum value to use for the x axis + * + * @return $this Fluid interface + */ + public function setXMax($xMax) + { + $this->xUnit->setMax($xMax); + return $this; + } + + /** + * Set the labels maximum value for the y axis + * + * Setting the value to null let's the axis unit decide which value to use for the maximum + * + * @param int $yMax The minimum value to use for the y axis + * + * @return $this Fluid interface + */ + public function setYMax($yMax) + { + $this->yUnit->setMax($yMax); + return $this; + } + + /** + * Transform all coordinates of the given dataset to coordinates that fit the graph's coordinate system + * + * @param array $dataSet The absolute coordinates as provided in the draw call + * + * @return array A graph relative representation of the given coordinates + */ + public function transform(array &$dataSet) + { + $result = array(); + foreach ($dataSet as &$points) { + $result[] = array( + $this->xUnit->transform($points[0]), + 100 - $this->yUnit->transform($points[1]) + ); + } + return $result; + } + + /** + * Create an AxisUnit that can be used in the axis to represent timestamps + * + * @return CalendarUnit + */ + public static function calendarUnit() + { + return new CalendarUnit(); + } + + /** + * Create an AxisUnit that can be used in the axis to represent a dataset as equally distributed + * ticks + * + * @param int $ticks + * @return LinearUnit + */ + public static function linearUnit($ticks = 10) + { + return new LinearUnit($ticks); + } + + /** + * Return the SVG representation of this object + * + * @param RenderContext $ctx The context to use for calculations + * + * @return DOMElement + * @see Drawable::toSvg + */ + public function toSvg(RenderContext $ctx) + { + $group = $ctx->getDocument()->createElement('g'); + $this->renderHorizontalAxis($ctx, $group); + $this->renderVerticalAxis($ctx, $group); + return $group; + } + + protected function ticksPerX($ticks, $units, $min) + { + $per = 1; + while ($per * $units / $ticks < $min) { + $per++; + } + return $per; + } + + /** + * Returns whether at least one label of the given Axis + * is bigger than the given maxLength + * + * @param AxisUnit $axis The axis that contains the labels that will be checked + * + * @return boolean Whether at least one label is bigger than maxLength + */ + private function labelsOversized(AxisUnit $axis, $maxLength = 5) + { + $oversized = false; + foreach ($axis as $label => $pos) { + if (strlen($label) > $maxLength) { + $oversized = true; + } + } + return $oversized; + } +} diff --git a/library/Icinga/Chart/Chart.php b/library/Icinga/Chart/Chart.php new file mode 100644 index 0000000..eaf69d1 --- /dev/null +++ b/library/Icinga/Chart/Chart.php @@ -0,0 +1,162 @@ +legend = new Legend(); + $this->palette = new Palette(); + $this->init(); + } + + /** + * Extension point for subclasses, called on __construct + */ + protected function init() + { + } + + /** + * Extension point for implementing rendering logic + * + * This method is called after data validation, but before toSvg is called + */ + protected function build() + { + } + + /** + * Check if the current dataset has the proper structure for this chart. + * + * Needs to be overwritten by extending classes. The default implementation returns false. + * + * @return bool True when the dataset is valid, otherwise false + */ + abstract public function isValidDataFormat(); + + + /** + * Disable the legend for this chart + */ + public function disableLegend() + { + $this->legend = null; + } + + /** + * Render this graph and return the created SVG + * + * @return string The SVG created by the SvgRenderer + * + * @throws IcingaException Thrown wen the dataset is not valid for this graph + * @see SVGRenderer::render + */ + public function render() + { + if (!$this->isValidDataFormat()) { + throw new IcingaException('Dataset for graph doesn\'t have the proper structure'); + } + $this->build(); + if ($this->align) { + $this->renderer->preserveAspectRatio(); + $this->renderer->setXAspectRatioAlignment(SVGRenderer::X_ASPECT_RATIO_MIN); + $this->renderer->setYAspectRatioAlignment(SVGRenderer::Y_ASPECT_RATIO_MIN); + } + + $this->renderer->setAriaDescription($this->description); + $this->renderer->setAriaTitle($this->title); + $this->renderer->getCanvas()->setAriaRole('presentation'); + + $this->renderer->getCanvas()->addElement($this); + return $this->renderer->render(); + } + + /** + * Return this graph rendered as PNG + * + * @param int $width The width of the PNG in pixel + * @param int $height The height of the PNG in pixel + * + * @return string A PNG binary string + * + * @throws IcingaException In case ImageMagick is not available + */ + public function toPng($width, $height) + { + if (! class_exists('Imagick')) { + throw new IcingaException('Cannot render PNGs without ImageMagick'); + } + + $image = new Imagick(); + $image->readImageBlob($this->render()); + $image->setImageFormat('png24'); + $image->resizeImage($width, $height, imagick::FILTER_LANCZOS, 1); + return $image; + } + + /** + * Align the chart to the top left corner instead of centering it + * + * @param bool $align + */ + public function alignTopLeft($align = true) + { + $this->align = $align; + } +} diff --git a/library/Icinga/Chart/Donut.php b/library/Icinga/Chart/Donut.php new file mode 100644 index 0000000..9d2a2a8 --- /dev/null +++ b/library/Icinga/Chart/Donut.php @@ -0,0 +1,465 @@ + 'slice-state-ok']) + * + * @return $this + */ + public function addSlice($data, $attributes = array()) + { + $this->slices[] = array($data, $attributes); + + $this->count += $data; + + return $this; + } + + /** + * Set the thickness for this Donut + * + * @param integer $thickness + * + * @return $this + */ + public function setThickness($thickness) + { + $this->thickness = $thickness; + + return $this; + } + /** + * Get the thickness for this Donut + * + * @return integer + */ + public function getThickness() + { + return $this->thickness; + } + + /** + * Set the center color for this Donut + * + * @param string $centerColor + * + * @return $this + */ + public function setCenterColor($centerColor) + { + $this->centerColor = $centerColor; + + return $this; + } + + /** + * Get the center color for this Donut + * + * @return string + */ + public function getCenterColor() + { + return $this->centerColor; + } + + /** + * Set the text of the big label + * + * @param string $labelBig + * + * @return $this + */ + public function setLabelBig($labelBig) + { + $this->labelBig = $labelBig; + + return $this; + } + + /** + * Get the text of the big label + * + * @return string + */ + public function getLabelBig() + { + return $this->labelBig; + } + + /** + * Set the url behind the big label + * + * @param Url $labelBigUrl + * + * @return $this + */ + public function setLabelBigUrl($labelBigUrl) + { + $this->labelBigUrl = $labelBigUrl; + + return $this; + } + + /** + * Get the url behind the big label + * + * @return Url + */ + public function getLabelBigUrl() + { + return $this->labelBigUrl; + } + + /** + * Get whether the big label shall be eye-catching + * + * @return bool + */ + public function getLabelBigEyeCatching() + { + return $this->labelBigState !== null; + } + + /** + * Set whether the big label shall be eye-catching + * + * @param bool $labelBigEyeCatching + * + * @return $this + */ + public function setLabelBigEyeCatching($labelBigEyeCatching = true) + { + $this->labelBigState = $labelBigEyeCatching ? 'critical' : null; + + return $this; + } + + /** + * Get the state the big label shall indicate + * + * @return string|null + */ + public function getLabelBigState() + { + return $this->labelBigState; + } + + /** + * Set the state the big label shall indicate + * + * @param string|null $labelBigState + * + * @return $this + */ + public function setLabelBigState($labelBigState) + { + $this->labelBigState = $labelBigState; + + return $this; + } + + /** + * Set the text of the small label + * + * @param string $labelSmall + * + * @return $this + */ + public function setLabelSmall($labelSmall) + { + $this->labelSmall = $labelSmall; + + return $this; + } + + /** + * Get the text of the small label + * + * @return string + */ + public function getLabelSmall() + { + return $this->labelSmall; + } + + /** + * Put together all slices of this Donut + * + * @return array $svg + */ + protected function assemble() + { + // svg tag containing the ring + $svg = array( + 'tag' => 'svg', + 'attributes' => array( + 'xmlns' => 'http://www.w3.org/2000/svg', + 'viewbox' => '0 0 40 40', + 'class' => 'donut-graph' + ), + 'content' => array() + ); + + // Donut hole + $svg['content'][] = array( + 'tag' => 'circle', + 'attributes' => array( + 'cx' => 20, + 'cy' => 20, + 'r' => sprintf('%F', $this->radius), + 'fill' => $this->getCenterColor() + ) + ); + + // When there is no data show gray circle + $svg['content'][] = array( + 'tag' => 'circle', + 'attributes' => array( + 'aria-hidden' => true, + 'cx' => 20, + 'cy' => 20, + 'r' => sprintf('%F', $this->radius), + 'fill' => $this->getCenterColor(), + 'stroke-width' => $this->getThickness(), + 'class' => 'slice-state-not-checked' + ) + ); + + $slices = $this->slices; + + if ($this->count !== 0) { + array_walk($slices, function (&$slice) { + $slice[0] = round(100 / $this->count * $slice[0], 2); + }); + } + + // on 0 the donut would start at "3 o'clock" and the offset shifts counterclockwise + $offset = 25; + + foreach ($slices as $slice) { + $svg['content'][] = array( + 'tag' => 'circle', + 'attributes' => $slice[1] + array( + 'cx' => 20, + 'cy' => 20, + 'r' => sprintf('%F', $this->radius), + 'fill' => 'transparent', + 'stroke-width' => $this->getThickness(), + 'stroke-dasharray' => sprintf('%F', $slice[0]) + . ' ' + . sprintf('%F', (99.9 - $slice[0])), // 99.9 prevents gaps (slight overlap) + 'stroke-dashoffset' => sprintf('%F', $offset) + ) + ); + // negative values shift in the clockwise direction + $offset -= $slice[0]; + } + + $result = array( + 'tag' => 'div', + 'content' => array($svg) + ); + + $labelBig = (string) $this->getLabelBig(); + $labelSmall = (string) $this->getLabelSmall(); + + if ($labelBig !== '' || $labelSmall !== '') { + $labels = array( + 'tag' => 'div', + 'attributes' => array( + 'class' => 'donut-label' + ), + 'content' => array() + ); + + if ($labelBig !== '') { + $labels['content'][] = + array( + 'tag' => 'a', + 'attributes' => array( + 'aria-label' => $labelBig . ' ' . $labelSmall, + 'href' => $this->getLabelBigUrl() ? $this->getLabelBigUrl()->getAbsoluteUrl() : null, + 'class' => $this->labelBigState === null + ? 'donut-label-big' + : 'donut-label-big state-' . $this->labelBigState + ), + 'content' => $this->shortenLabel($labelBig) + ); + } + + if ($labelSmall !== '') { + $labels['content'][] = array( + 'tag' => 'p', + 'attributes' => array( + 'class' => 'donut-label-small', + 'x' => '50%', + 'y' => '50%' + ), + 'content' => $labelSmall + ); + } + + $result['content'][] = $labels; + } + + return $result; + } + + /** + * Shorten the label to 3 digits if it is numeric + * + * 10 => 10 ... 1111 => ~1k ... 1888 => ~2k + * + * @param int|string $label + * + * @return string + */ + protected function shortenLabel($label) + { + if (is_numeric($label) && strlen($label) > 3) { + return round($label, -3)/1000 . 'k'; + } + + return $label; + } + + protected function encode($content) + { + return htmlspecialchars($content, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true); + } + + protected function renderAttributes(array $attributes) + { + $html = array(); + + foreach ($attributes as $name => $value) { + if ($value === null) { + continue; + } + + if (is_bool($value) && $value) { + $html[] = $name; + continue; + } + + if (is_array($value)) { + $value = implode(' ', $value); + } + + $html[] = "$name=\"" . $this->encode($value) . '"'; + } + + return implode(' ', $html); + } + + protected function renderContent(array $element) + { + $tag = $element['tag']; + $attributes = isset($element['attributes']) ? $element['attributes'] : array(); + $content = isset($element['content']) ? $element['content'] : null; + + $html = array( + // rtrim because attributes may be empty + rtrim("<$tag " . $this->renderAttributes($attributes)) + . ">" + ); + + if ($content !== null) { + if (is_array($content)) { + foreach ($content as $child) { + $html[] = is_array($child) ? $this->renderContent($child) : $this->encode($child); + } + } else { + $html[] = $this->encode($content); + } + } + + $html[] = ""; + + return implode("\n", $html); + } + + public function render() + { + $svg = $this->assemble(); + + return $this->renderContent($svg); + } +} diff --git a/library/Icinga/Chart/Format.php b/library/Icinga/Chart/Format.php new file mode 100644 index 0000000..9e6c4db --- /dev/null +++ b/library/Icinga/Chart/Format.php @@ -0,0 +1,21 @@ +order = $order; + $this->dataSet = $dataSet; + + $this->tooltips = $tooltips; + foreach ($this->tooltips as $value) { + $ts[] = $value; + } + $this->tooltips = $ts; + + $this->graphs = $graphs; + } + + /** + * Apply configuration styles from the $cfg + * + * @param array $cfg The configuration as given in the drawBars call + */ + public function setStyleFromConfig(array $cfg) + { + foreach ($cfg as $elem => $value) { + if ($elem === 'color') { + $this->setFill($value); + } elseif ($elem === 'width') { + $this->setStrokeWidth($value); + } + } + } + + /** + * Draw a single rectangle + * + * @param array $point The + * @param string $fill The fill color to use + * @param $strokeWidth + * @param null $index + * + * @return Rect + */ + private function drawSingleBar($point, $fill, $strokeWidth, $index = null) + { + $rect = new Rect($point[0] - ($this->barWidth / 2), $point[1], $this->barWidth, 100 - $point[1]); + $rect->setFill($fill); + $rect->setStrokeWidth($strokeWidth); + $rect->setStrokeColor('black'); + if (isset($index)) { + $rect->setAttribute('data-icinga-graph-index', $index); + } + $rect->setAttribute('data-icinga-graph-type', 'bar'); + $rect->setAdditionalStyle('clip-path: url(#clip);'); + return $rect; + } + + /** + * Render this BarChart + * + * @param RenderContext $ctx The rendering context to use for drawing + * + * @return DOMElement $dom Element + */ + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + $group = $doc->createElement('g'); + $idx = 0; + + if (count($this->dataSet) > 15) { + $this->barWidth = 2; + } + if (count($this->dataSet) > 25) { + $this->barWidth = 1; + } + + foreach ($this->dataSet as $x => $point) { + // add white background bar, to prevent other bars from altering transparency effects + $bar = $this->drawSingleBar($point, 'white', $this->strokeWidth, $idx++)->toSvg($ctx); + $group->appendChild($bar); + + // draw actual bar + $bar = $this->drawSingleBar($point, $this->fill, $this->strokeWidth)->toSvg($ctx); + if (isset($this->tooltips[$x])) { + $data = array( + 'label' => isset($this->graphs[$this->order]['label']) ? + strtolower($this->graphs[$this->order]['label']) : '', + 'color' => isset($this->graphs[$this->order]['color']) ? + strtolower($this->graphs[$this->order]['color']) : '#fff' + ); + $format = isset($this->graphs[$this->order]['tooltip']) + ? $this->graphs[$this->order]['tooltip'] : null; + $title = $ctx->getDocument()->createElement('title'); + $title->textContent = $this->tooltips[$x]->renderNoHtml($this->order, $data, $format); + $bar->appendChild($title); + } + $group->appendChild($bar); + } + return $group; + } +} diff --git a/library/Icinga/Chart/Graph/LineGraph.php b/library/Icinga/Chart/Graph/LineGraph.php new file mode 100644 index 0000000..6954c59 --- /dev/null +++ b/library/Icinga/Chart/Graph/LineGraph.php @@ -0,0 +1,195 @@ +dataset = $dataset; + $this->graphs = $graphs; + + $this->tooltips = $tooltips; + foreach ($this->tooltips as $value) { + $ts[] = $value; + } + $this->tooltips = $ts; + $this->order = $order; + } + + /** + * Set datapoints to be emphased via dots + * + * @param bool $bool True to enable datapoints, otherwise false + */ + public function setShowDataPoints($bool) + { + $this->showDataPoints = $bool; + } + + /** + * Sort the daset by the xaxis + * + * @param array $v1 + * @param array $v2 + * @return int + */ + private function sortByX(array $v1, array $v2) + { + if ($v1[0] === $v2[0]) { + return 0; + } + return ($v1[0] < $v2[0]) ? -1 : 1; + } + + /** + * Configure this style + * + * @param array $cfg The configuration as given in the drawLine call + */ + public function setStyleFromConfig(array $cfg) + { + $fill = false; + foreach ($cfg as $elem => $value) { + if ($elem === 'color') { + $this->setStrokeColor($value); + } elseif ($elem === 'width') { + $this->setStrokeWidth($value); + } elseif ($elem === 'showPoints') { + $this->setShowDataPoints($value); + } elseif ($elem === 'fill') { + $fill = $value; + } elseif ($elem === 'discrete') { + $this->isDiscrete = true; + } + } + if ($fill) { + $this->setFill($this->strokeColor); + $this->setStrokeColor('black'); + } + } + + /** + * Render this BarChart + * + * @param RenderContext $ctx The rendering context to use for drawing + * + * @return DOMElement $dom Element + */ + public function toSvg(RenderContext $ctx) + { + $path = new Path($this->dataset); + if ($this->isDiscrete) { + $path->setDiscrete(true); + } + $path->setStrokeColor($this->strokeColor); + $path->setStrokeWidth($this->strokeWidth); + + $path->setAttribute('data-icinga-graph-type', 'line'); + if ($this->fill !== 'none') { + $firstX = $this->dataset[0][0]; + $lastX = $this->dataset[count($this->dataset)-1][0]; + $path->prepend(array($firstX, 100)) + ->append(array($lastX, 100)); + $path->setFill($this->fill); + } + + $path->setAdditionalStyle('clip-path: url(#clip);'); + $path->setId($this->id); + $group = $path->toSvg($ctx); + + foreach ($this->dataset as $x => $point) { + if ($this->showDataPoints === true) { + $dot = new Circle($point[0], $point[1], $this->dotWith); + $dot->setFill($this->strokeColor); + $group->appendChild($dot->toSvg($ctx)); + } + + // Draw invisible circle for tooltip hovering + if (isset($this->tooltips[$x])) { + $invisible = new Circle($point[0], $point[1], 20); + $invisible->setFill($this->strokeColor); + $invisible->setAdditionalStyle('opacity: 0.0;'); + $data = array( + 'label' => isset($this->graphs[$this->order]['label']) ? + strtolower($this->graphs[$this->order]['label']) : '', + 'color' => isset($this->graphs[$this->order]['color']) ? + strtolower($this->graphs[$this->order]['color']) : '#fff' + ); + $format = isset($this->graphs[$this->order]['tooltip']) + ? $this->graphs[$this->order]['tooltip'] : null; + $title = $ctx->getDocument()->createElement('title'); + $title->textContent = $this->tooltips[$x]->renderNoHtml($this->order, $data, $format); + $invisibleRendered = $invisible->toSvg($ctx); + $invisibleRendered->appendChild($title); + $group->appendChild($invisibleRendered); + } + } + + return $group; + } +} diff --git a/library/Icinga/Chart/Graph/StackedGraph.php b/library/Icinga/Chart/Graph/StackedGraph.php new file mode 100644 index 0000000..49801a9 --- /dev/null +++ b/library/Icinga/Chart/Graph/StackedGraph.php @@ -0,0 +1,88 @@ +points[$x])) { + $this->points[$x] = 0; + } + // store old y-value for displaying the actual (non-aggregated) + // value in the tooltip + $point[2] = $point[1]; + + $this->points[$x] += $point[1]; + $point[1] = $this->points[$x]; + } + } + + /** + * Add a graph to the stack + * + * @param $graph + */ + public function addToStack($graph) + { + $this->stack[] = $graph; + } + + /** + * Empty the stack + * + * @return bool + */ + public function stackEmpty() + { + return empty($this->stack); + } + + /** + * Render this stack in the correct order + * + * @param RenderContext $ctx The context to use for rendering + * + * @return DOMElement The SVG representation of this graph + */ + public function toSvg(RenderContext $ctx) + { + $group = $ctx->getDocument()->createElement('g'); + $renderOrder = array_reverse($this->stack); + foreach ($renderOrder as $stackElem) { + $group->appendChild($stackElem->toSvg($ctx)); + } + return $group; + } +} diff --git a/library/Icinga/Chart/Graph/Tooltip.php b/library/Icinga/Chart/Graph/Tooltip.php new file mode 100644 index 0000000..7236685 --- /dev/null +++ b/library/Icinga/Chart/Graph/Tooltip.php @@ -0,0 +1,143 @@ + + *
  • Global properties
  • : Key-value pairs that stay the same every time render is called, and are + * passed to an instance in the constructor. + *
  • Aggregated properties
  • : Global properties that are created automatically from + * all attached data points. + *
  • Local properties
  • : Key-value pairs that only apply to a single data point and + * are passed to the render-function. + * + */ +class Tooltip +{ + /** + * The default format string used + * when no other format is specified + * + * @var string + */ + private $defaultFormat; + + /** + * All aggregated points + * + * @var array + */ + private $points = array(); + + /** + * Contains all static replacements + * + * @var array + */ + private $data = array( + 'sum' => 0 + ); + + /** + * Used to format the displayed tooltip. + * + * @var string + */ + protected $tooltipFormat; + + /** + * Create a new tooltip with the specified default format string + * + * Allows you to set the global data for this tooltip, that is displayed every + * time render is called. + * + * @param array $data Map of global properties + * @param string $format The default format string + */ + public function __construct( + $data = array(), + $format = '{title}: {value} {label}' + ) { + $this->data = array_merge($this->data, $data); + $this->defaultFormat = $format; + } + + /** + * Add a single data point to update the aggregated properties for this tooltip + * + * @param $point array Contains the (x,y) values of the data set + */ + public function addDataPoint($point) + { + // set x-value + if (!isset($this->data['title'])) { + $this->data['title'] = $point[0]; + } + + // aggregate y-values + $y = (int)$point[1]; + if (isset($point[2])) { + // load original value in case value already aggregated + $y = (int)$point[2]; + } + + if (!isset($this->data['min']) || $this->data['min'] > $y) { + $this->data['min'] = $y; + } + if (!isset($this->data['max']) || $this->data['max'] < $y) { + $this->data['max'] = $y; + } + $this->data['sum'] += $y; + $this->points[] = $y; + } + + /** + * Format the tooltip for a certain data point + * + * @param array $order Which data set to render + * @param array $data The local data for this tooltip + * @param string $format Use a custom format string for this data set + * + * @return mixed|string The tooltip value + */ + public function render($order, $data = array(), $format = null) + { + if (isset($format)) { + $str = $format; + } else { + $str = $this->defaultFormat; + } + $data['value'] = $this->points[$order]; + foreach (array_merge($this->data, $data) as $key => $value) { + $str = str_replace('{' . $key . '}', $value, $str); + } + return $str; + } + + /** + * Format the tooltip for a certain data point but remove all + * occurring html tags + * + * This is useful for rendering clean tooltips on client without JavaScript + * + * @param array $order Which data set to render + * @param array $data The local data for this tooltip + * @param string $format Use a custom format string for this data set + * + * @return mixed|string The tooltip value, without any HTML tags + */ + public function renderNoHtml($order, $data, $format) + { + return strip_tags($this->render($order, $data, $format)); + } +} diff --git a/library/Icinga/Chart/GridChart.php b/library/Icinga/Chart/GridChart.php new file mode 100644 index 0000000..a8cfca6 --- /dev/null +++ b/library/Icinga/Chart/GridChart.php @@ -0,0 +1,446 @@ + + * + * $this->chart = new GridChart(); + * $this->chart->setAxisLabel("X axis label", "Y axis label"); + * $this->chart->setXAxis(Axis::CalendarUnit()); + * $this->chart->drawLines( + * array( + * 'data' => array( + * array(time()-7200, 10),array(time()-3620, 30), array(time()-1800, 15), array(time(), 92)) + * ) + * ); + * + * + */ +class GridChart extends Chart +{ + /** + * Internal identifier for Line Chart elements + */ + const TYPE_LINE = "LINE"; + + /** + * Internal identifier fo Bar Chart elements + */ + const TYPE_BAR = "BAR"; + + /** + * Internal array containing all elements to be drawn in the order they are drawn + * + * @var array + */ + private $graphs = array(); + + /** + * An associative array containing all axis of this Chart in the "name" => Axis() form. + * + * Currently only the 'default' axis is really supported + * + * @var array + */ + private $axis = array(); + + /** + * An associative array containing all StackedGraph objects used for cumulative graphs + * + * The array key is the 'stack' value given in the graph definitions + * + * @var array + */ + private $stacks = array(); + + /** + * An associative array containing all Tooltips used to render the titles + * + * Each tooltip represents the summary for all y-values of a certain x-value + * in the grid chart + * + * @var Tooltip + */ + private $tooltips = array(); + + public function __construct() + { + $this->title = t('Grid Chart'); + $this->description = t('Contains data in a bar or line chart.'); + parent::__construct(); + } + + /** + * Check if the current dataset has the proper structure for this chart. + * + * Needs to be overwritten by extending classes. The default implementation returns false. + * + * @return bool True when the dataset is valid, otherwise false + */ + public function isValidDataFormat() + { + foreach ($this->graphs as $values) { + foreach ($values as $value) { + if (!isset($value['data']) || !is_array($value['data'])) { + return false; + } + } + } + return true; + } + + /** + * Calls Axis::addDataset for every graph added to this GridChart + * + * @see Axis::addDataset + */ + private function configureAxisFromDatasets() + { + foreach ($this->graphs as $axis => &$graphs) { + $axisObj = $this->axis[$axis]; + foreach ($graphs as &$graph) { + $axisObj->addDataset($graph); + } + } + } + + /** + * Add an arbitrary number of lines to be drawn + * + * Refer to the graphs.md for a detailed list of allowed attributes + * + * @param array $axis,... The line definitions to draw + * + * @return $this Fluid interface + */ + public function drawLines(array $axis) + { + $this->draw(self::TYPE_LINE, func_get_args()); + return $this; + } + + /** + * Add arbitrary number of bars to be drawn + * + * Refer to the graphs.md for a detailed list of allowed attributes + * + * @param array $axis + * @return $this + */ + public function drawBars(array $axis) + { + $this->draw(self::TYPE_BAR, func_get_args()); + return $this; + } + + /** + * Generic method for adding elements to the drawing stack + * + * @param string $type The type of the element to draw (see TYPE_ constants in this class) + * @param array $data The data given to the draw call + */ + private function draw($type, $data) + { + $axisName = 'default'; + if (is_string($data[0])) { + $axisName = $data[0]; + array_shift($data); + } + foreach ($data as &$graph) { + $graph['graphType'] = $type; + if (isset($graph['stack'])) { + if (!isset($this->stacks[$graph['stack']])) { + $this->stacks[$graph['stack']] = new StackedGraph(); + } + $this->stacks[$graph['stack']]->addGraph($graph); + $graph['stack'] = $this->stacks[$graph['stack']]; + } + + if (!isset($graph['color'])) { + $colorType = isset($graph['palette']) ? $graph['palette'] : Palette::NEUTRAL; + $graph['color'] = $this->palette->getNext($colorType); + } + $this->graphs[$axisName][] = $graph; + if ($this->legend) { + $this->legend->addDataset($graph); + } + } + $this->initTooltips($data); + } + + + private function initTooltips($data) + { + foreach ($data as &$graph) { + foreach ($graph['data'] as $x => $point) { + if (!array_key_exists($x, $this->tooltips)) { + $this->tooltips[$x] = new Tooltip( + array( + 'color' => $graph['color'], + + ) + ); + } + $this->tooltips[$x]->addDataPoint($point); + } + } + } + + /** + * Set the label for the x and y axis + * + * @param string $xAxisLabel The label to use for the x axis + * @param string $yAxisLabel The label to use for the y axis + * @param string $axisName The name of the axis, for now 'default' + * + * @return $this Fluid interface + */ + public function setAxisLabel($xAxisLabel, $yAxisLabel, $axisName = 'default') + { + $this->axis[$axisName]->setXLabel($xAxisLabel)->setYLabel($yAxisLabel); + return $this; + } + + /** + * Set the AxisUnit to use for calculating the values of the x axis + * + * @param AxisUnit $unit The unit for the x axis + * @param string $axisName The name of the axis to set the label for, currently only 'default' + * + * @return $this Fluid interface + */ + public function setXAxis(AxisUnit $unit, $axisName = 'default') + { + $this->axis[$axisName]->setUnitForXAxis($unit); + return $this; + } + + /** + * Set the AxisUnit to use for calculating the values of the y axis + * + * @param AxisUnit $unit The unit for the y axis + * @param string $axisName The name of the axis to set the label for, currently only 'default' + * + * @return $this Fluid interface + */ + public function setYAxis(AxisUnit $unit, $axisName = 'default') + { + $this->axis[$axisName]->setUnitForYAxis($unit); + return $this; + } + + /** + * Pre-render setup of the axis + * + * @see Chart::build + */ + protected function build() + { + $this->configureAxisFromDatasets(); + } + + /** + * Initialize the renderer and overwrite it with an 2:1 ration renderer + */ + protected function init() + { + $this->renderer = new SVGRenderer(100, 100); + $this->setAxis(Axis::createLinearAxis()); + } + + /** + * Overwrite the axis to use + * + * @param Axis $axis The new axis to use + * @param string $name The name of the axis, currently only 'default' + * + * @return $this Fluid interface + */ + public function setAxis(Axis $axis, $name = 'default') + { + $this->axis = array($name => $axis); + return $this; + } + + /** + * Add an axis to this graph (not really supported right now) + * + * @param Axis $axis The axis object to add + * @param string $name The name of the axis + * + * @return $this Fluid interface + */ + public function addAxis(Axis $axis, $name) + { + $this->axis[$name] = $axis; + return $this; + } + + /** + * Set minimum values for the x and y axis. + * + * Setting null to an axis means this will use a value determined by the dataset + * + * @param int $xMin The minimum value for the x axis or null to use a dynamic value + * @param int $yMin The minimum value for the y axis or null to use a dynamic value + * @param string $axisName The name of the axis to set the minimum, currently only 'default' + * + * @return $this Fluid interface + */ + public function setAxisMin($xMin = null, $yMin = null, $axisName = 'default') + { + $this->axis[$axisName]->setXMin($xMin)->setYMin($yMin); + return $this; + } + + /** + * Set maximum values for the x and y axis. + * + * Setting null to an axis means this will use a value determined by the dataset + * + * @param int $xMax The maximum value for the x axis or null to use a dynamic value + * @param int $yMax The maximum value for the y axis or null to use a dynamic value + * @param string $axisName The name of the axis to set the maximum, currently only 'default' + * + * @return $this Fluid interface + */ + public function setAxisMax($xMax = null, $yMax = null, $axisName = 'default') + { + $this->axis[$axisName]->setXMax($xMax)->setYMax($yMax); + return $this; + } + + /** + * Render this GridChart to SVG + * + * @param RenderContext $ctx The context to use for rendering + * + * @return DOMElement + */ + public function toSvg(RenderContext $ctx) + { + $outerBox = new Canvas('outerGraph', new LayoutBox(0, 0, 100, 100)); + $innerBox = new Canvas('graph', new LayoutBox(0, 0, 95, 90)); + + $maxPadding = array(0,0,0,0); + foreach ($this->axis as $axis) { + $padding = $axis->getRequiredPadding(); + for ($i=0; $i < count($padding); $i++) { + $maxPadding[$i] = max($maxPadding[$i], $padding[$i]); + } + $innerBox->addElement($axis); + } + $this->renderGraphContent($innerBox); + + $innerBox->getLayout()->setPadding($maxPadding[0], $maxPadding[1], $maxPadding[2], $maxPadding[3]); + $this->createContentClipBox($innerBox); + + $outerBox->addElement($innerBox); + if ($this->legend) { + $outerBox->addElement($this->legend); + } + return $outerBox->toSvg($ctx); + } + + /** + * Create a clip box that defines which area of the graph is drawable and adds it to the graph. + * + * The clipbox has the id '#clip' and can be used in the clip-mask element + * + * @param Canvas $innerBox The inner canvas of the graph to add the clip box to + */ + private function createContentClipBox(Canvas $innerBox) + { + $clipBox = new Canvas('clip', new LayoutBox(0, 0, 100, 100)); + $clipBox->toClipPath(); + $innerBox->addElement($clipBox); + $rect = new Rect(0.1, 0, 100, 99.9); + $clipBox->addElement($rect); + } + + /** + * Render the content of the graph, i.e. the draw stack + * + * @param Canvas $innerBox The inner canvas of the graph to add the content to + */ + private function renderGraphContent(Canvas $innerBox) + { + foreach ($this->graphs as $axisName => $graphs) { + $axis = $this->axis[$axisName]; + $graphObj = null; + foreach ($graphs as $dataset => $graph) { + // determine the type and create a graph object for it + switch ($graph['graphType']) { + case self::TYPE_BAR: + $graphObj = new BarGraph( + $axis->transform($graph['data']), + $graphs, + $dataset, + $this->tooltips + ); + break; + case self::TYPE_LINE: + $graphObj = new LineGraph( + $axis->transform($graph['data']), + $graphs, + $dataset, + $this->tooltips + ); + break; + default: + continue 2; + } + $el = $this->setupGraph($graphObj, $graph); + if ($el) { + $innerBox->addElement($el); + } + } + } + } + + /** + * Setup the provided Graph type + * + * @param mixed $graphObject The graph class, needs the setStyleFromConfig method + * @param array $graphConfig The configration array of the graph + * + * @return mixed Either the graph to be added or null if the graph is not directly added + * to the document (e.g. stacked graphs are added by + * the StackedGraph Composite object) + */ + private function setupGraph($graphObject, array $graphConfig) + { + $graphObject->setStyleFromConfig($graphConfig); + // When in a stack return the StackedGraph object instead of the graphObject + if (isset($graphConfig['stack'])) { + $graphConfig['stack']->addToStack($graphObject); + if (!$graphConfig['stack']->stackEmpty()) { + return $graphConfig['stack']; + } + // return no object when the graph should not be rendered + return null; + } + return $graphObject; + } +} diff --git a/library/Icinga/Chart/Inline/Inline.php b/library/Icinga/Chart/Inline/Inline.php new file mode 100644 index 0000000..3acbd73 --- /dev/null +++ b/library/Icinga/Chart/Inline/Inline.php @@ -0,0 +1,96 @@ + $value) { + $sanitized[$key] = htmlspecialchars($value); + } + return $sanitized; + } + + /** + * Populate the properties from the current request. + */ + public function initFromRequest() + { + $this->data = explode(',', $_GET['data']); + foreach ($this->data as $key => $value) { + $this->data[$key] = (int)$value; + } + for ($i = 0; $i < count($this->data); $i++) { + $this->labels[] = ''; + } + + if (array_key_exists('colors', $_GET)) { + $this->colors = $this->sanitizeStringArray(explode(',', $_GET['colors'])); + } + while (count($this->colors) < count($this->data)) { + $this->colors[] = '#FEFEFE'; + } + + if (array_key_exists('width', $_GET)) { + $this->width = (int)$_GET['width']; + } + if (array_key_exists('height', $_GET)) { + $this->height = (int)$_GET['height']; + } + } +} diff --git a/library/Icinga/Chart/Inline/PieChart.php b/library/Icinga/Chart/Inline/PieChart.php new file mode 100644 index 0000000..de68213 --- /dev/null +++ b/library/Icinga/Chart/Inline/PieChart.php @@ -0,0 +1,41 @@ +alignTopLeft(); + $pie->disableLegend(); + $pie->drawPie(array( + 'data' => $this->data, 'colors' => $this->colors, 'labels' => $this->labels + )); + return $pie; + } + + public function toSvg($output = true) + { + if ($output) { + echo $this->getChart()->render(); + } else { + return $this->getChart()->render(); + } + } + + public function toPng($output = true) + { + if ($output) { + echo $this->getChart()->toPng($this->width, $this->height); + } else { + return $this->getChart()->toPng($this->width, $this->height); + } + } +} diff --git a/library/Icinga/Chart/Legend.php b/library/Icinga/Chart/Legend.php new file mode 100644 index 0000000..ab1c9e0 --- /dev/null +++ b/library/Icinga/Chart/Legend.php @@ -0,0 +1,102 @@ +internalCtr); + } + if (!isset($dataset['color'])) { + return; + } + $this->dataset[$dataset['color']] = $dataset['label']; + } + + /** + * Render the legend to an SVG object + * + * @param RenderContext $ctx The context to use for rendering this legend + * + * @return DOMElement The SVG representation of this legend + */ + public function toSvg(RenderContext $ctx) + { + $outer = new Canvas('legend', new LayoutBox(0, 0, 100, 100)); + $outer->getLayout()->setPadding(2, 2, 2, 2); + $nrOfColumns = 4; + + $topstep = 10 / $nrOfColumns + 2; + + $top = 0; + $left = 0; + $lastLabelEndPos = -1; + foreach ($this->dataset as $color => $text) { + $leftstep = 100 / $nrOfColumns + strlen($text); + + // Make sure labels don't overlap each other + while ($lastLabelEndPos >= $left) { + $left += $leftstep; + } + // When a label is longer than the available space, use the next line + if ($left + strlen($text) > 100) { + $top += $topstep; + $left = 0; + } + + $colorBox = new Rect($left, $top, 2, 2); + $colorBox->setFill($color)->setStrokeWidth(2); + $colorBox->keepRatio(); + $outer->addElement($colorBox); + + $textBox = new Text($left+5, $top+2, $text); + $textBox->setFontSize('2em'); + $outer->addElement($textBox); + + // readjust layout + $lastLabelEndPos = $left + strlen($text); + $left += $leftstep; + } + $svg = $outer->toSvg($ctx); + return $svg; + } +} diff --git a/library/Icinga/Chart/Palette.php b/library/Icinga/Chart/Palette.php new file mode 100644 index 0000000..90ad74b --- /dev/null +++ b/library/Icinga/Chart/Palette.php @@ -0,0 +1,65 @@ + array('#00FF00'), + self::PROBLEM => array('#FF0000'), + self::WARNING => array('#FFFF00'), + self::NEUTRAL => array('#f3f3f3') + ); + + /** + * Return the next available color as an hex string for the given type + * + * @param string $type The type to receive a color from + * + * @return string The color in hex format + */ + public function getNext($type = self::NEUTRAL) + { + if (!isset($this->colorSets[$type])) { + $type = self::NEUTRAL; + } + + $color = current($this->colorSets[$type]); + if ($color === false) { + reset($this->colorSets[$type]); + + $color = current($this->colorSets[$type]); + } + next($this->colorSets[$type]); + return $color; + } +} diff --git a/library/Icinga/Chart/PieChart.php b/library/Icinga/Chart/PieChart.php new file mode 100644 index 0000000..1bcf380 --- /dev/null +++ b/library/Icinga/Chart/PieChart.php @@ -0,0 +1,306 @@ +title = t('Pie Chart'); + $this->description = t('Contains data in a pie chart.'); + parent::__construct(); + } + + /** + * Test if the given pies have the correct format + * + * @return bool True when the given pies are correct, otherwise false + */ + public function isValidDataFormat() + { + foreach ($this->pies as $pie) { + if (!isset($pie['data']) || !is_array($pie['data'])) { + return false; + } + } + return true; + } + + /** + * Create renderer and normalize the dataset to represent percentage information + */ + protected function build() + { + $this->renderer = new SVGRenderer(($this->type === self::STACKED) ? 1 : count($this->pies), 1); + foreach ($this->pies as &$pie) { + $this->normalizeDataSet($pie); + } + } + + /** + * Normalize the given dataset to represent percentage information instead of absolute valuess + * + * @param array $pie The pie definition given in the drawPie call + */ + private function normalizeDataSet(&$pie) + { + $total = array_sum($pie['data']); + if ($total === 100) { + return; + } + if ($total == 0) { + return; + } + foreach ($pie['data'] as &$slice) { + $slice = $slice/$total * 100; + } + } + + /** + * Draw an arbitrary number of pies in this chart + * + * @param array $dataSet,... The pie definition, see graphs.md for further details concerning the format + * + * @return $this Fluent interface + */ + public function drawPie(array $dataSet) + { + $dataSets = func_get_args(); + $this->pies += $dataSets; + foreach ($dataSets as $dataSet) { + $this->legend->addDataset($dataSet); + } + return $this; + } + + /** + * Return the SVG representation of this graph + * + * @param RenderContext $ctx The context to use for drawings + * + * @return DOMElement The SVG representation of this graph + */ + public function toSvg(RenderContext $ctx) + { + $labelBox = $ctx->getDocument()->createElement('g'); + if (!$this->noCaption) { + // Scale SVG to make room for captions + $outerBox = new Canvas('outerGraph', new LayoutBox(33, -5, 40, 40)); + $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100)); + $innerBox->getLayout()->setPadding(10, 10, 10, 10); + } else { + $outerBox = new Canvas('outerGraph', new LayoutBox(1.5, -10, 124, 124)); + $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100)); + $innerBox->getLayout()->setPadding(0, 0, 0, 0); + } + $this->createContentClipBox($innerBox); + $this->renderPies($innerBox, $labelBox); + $innerBox->addElement(new RawElement($labelBox)); + $outerBox->addElement($innerBox); + + return $outerBox->toSvg($ctx); + } + + /** + * Render the pies in the draw stack using the selected algorithm for composition + * + * @param Canvas $innerBox The canvas to use for inserting the pies + * @param DOMElement $labelBox The DOM element to add the labels to (so they can't be overlapped by pie elements) + */ + private function renderPies(Canvas $innerBox, DOMElement $labelBox) + { + if ($this->type === self::STACKED) { + $this->renderStackedPie($innerBox, $labelBox); + } else { + $this->renderPieRow($innerBox, $labelBox); + } + } + + /** + * Return the color to be used for the given pie slice + * + * @param array $pie The pie configuration as provided in the drawPie call + * @param int $dataIdx The index of the pie slice in the pie configuration + * + * @return string The hex color string to use for the pie slice + */ + private function getColorForPieSlice(array $pie, $dataIdx) + { + if (isset($pie['colors']) && is_array($pie['colors']) && isset($pie['colors'][$dataIdx])) { + return $pie['colors'][$dataIdx]; + } + $type = Palette::NEUTRAL; + if (isset($pie['palette']) && is_array($pie['palette']) && isset($pie['palette'][$dataIdx])) { + $type = $pie['palette'][$dataIdx]; + } + return $this->palette->getNext($type); + } + + /** + * Render a row of pies + * + * @param Canvas $innerBox The canvas to insert the pies to + * @param DOMElement $labelBox The DOMElement to use for adding label elements + */ + private function renderPieRow(Canvas $innerBox, DOMElement $labelBox) + { + $radius = 50 / count($this->pies); + $x = $radius; + foreach ($this->pies as $pie) { + $labelPos = 0; + $lastRadius = 0; + + foreach ($pie['data'] as $idx => $dataset) { + $slice = new PieSlice($radius, $dataset, $lastRadius); + $slice->setX($x) + ->setStrokeColor('#000') + ->setStrokeWidth(1) + ->setY(50) + ->setFill($this->getColorForPieSlice($pie, $idx)); + $innerBox->addElement($slice); + // add caption if not disabled + if (!$this->noCaption && isset($pie['labels'])) { + $slice->setCaption($pie['labels'][$labelPos++]) + ->setLabelGroup($labelBox); + } + $lastRadius += $dataset; + } + // shift right for next pie + $x += $radius*2; + } + } + + /** + * Render pies in a stacked way so one pie is nested in the previous pie + * + * @param Canvas $innerBox The canvas to insert the pie to + * @param DOMElement $labelBox The DOMElement to use for adding label elements + */ + private function renderStackedPie(Canvas $innerBox, DOMElement $labelBox) + { + $radius = 40; + $minRadius = 20; + if (count($this->pies) == 0) { + return; + } + $shrinkStep = ($radius - $minRadius) / count($this->pies); + $x = $radius; + + for ($i = 0; $i < count($this->pies); $i++) { + $pie = $this->pies[$i]; + // the offset for the caption path, outer caption indicator shouldn't point + // to the middle of the slice as there will be another pie + $offset = isset($this->pies[$i+1]) ? $radius - $shrinkStep : 0; + $labelPos = 0; + $lastRadius = 0; + foreach ($pie['data'] as $idx => $dataset) { + $color = $this->getColorForPieSlice($pie, $idx); + if ($dataset == 0) { + $labelPos++; + continue; + } + $slice = new PieSlice($radius, $dataset, $lastRadius); + $slice->setY(50) + ->setX($x) + ->setStrokeColor('#000') + ->setStrokeWidth(1) + ->setFill($color) + ->setLabelGroup($labelBox); + + if (!$this->noCaption && isset($pie['labels'])) { + $slice->setCaption($pie['labels'][$labelPos++]) + ->setCaptionOffset($offset) + ->setOuterCaptionBound(50); + } + $innerBox->addElement($slice); + $lastRadius += $dataset; + } + // shrinken the next pie + $radius -= $shrinkStep; + } + } + + /** + * Set the composition type of this PieChart + * + * @param string $type Either self::STACKED or self::ROW + * + * @return $this Fluent interface + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Hide the caption from this PieChart + * + * @return $this Fluent interface + */ + public function disableLegend() + { + $this->noCaption = true; + return $this; + } + + /** + * Create the content for this PieChart + * + * @param Canvas $innerBox The innerbox to add the clip mask to + */ + private function createContentClipBox(Canvas $innerBox) + { + $clipBox = new Canvas('clip', new LayoutBox(0, 0, 100, 100)); + $clipBox->toClipPath(); + $innerBox->addElement($clipBox); + $rect = new Rect(0.1, 0, 100, 99.9); + $clipBox->addElement($rect); + } +} diff --git a/library/Icinga/Chart/Primitive/Animatable.php b/library/Icinga/Chart/Primitive/Animatable.php new file mode 100644 index 0000000..69ba0e1 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Animatable.php @@ -0,0 +1,43 @@ +animation = $anim; + } + + /** + * Append the animation to the given element + * + * @param DOMElement $dom The element to append the animation to + * @param RenderContext $ctx The context to use for rendering the animation object + */ + protected function appendAnimation(DOMElement $dom, RenderContext $ctx) + { + if ($this->animation) { + $dom->appendChild($this->animation->toSvg($ctx)); + } + } +} diff --git a/library/Icinga/Chart/Primitive/Animation.php b/library/Icinga/Chart/Primitive/Animation.php new file mode 100644 index 0000000..4e55d0e --- /dev/null +++ b/library/Icinga/Chart/Primitive/Animation.php @@ -0,0 +1,87 @@ +attribute = $attribute; + $this->from = $from; + $this->to = $to; + $this->duration = $duration; + $this->begin = $begin; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + + $animate = $ctx->getDocument()->createElement('animate'); + $animate->setAttribute('attributeName', $this->attribute); + $animate->setAttribute('attributeType', 'XML'); + $animate->setAttribute('from', $this->from); + $animate->setAttribute('to', $this->to); + $animate->setAttribute('begin', $this->begin . 's'); + $animate->setAttribute('dur', $this->duration . 's'); + $animate->setAttributE('fill', "freeze"); + + return $animate; + } +} diff --git a/library/Icinga/Chart/Primitive/Canvas.php b/library/Icinga/Chart/Primitive/Canvas.php new file mode 100644 index 0000000..32f06bf --- /dev/null +++ b/library/Icinga/Chart/Primitive/Canvas.php @@ -0,0 +1,140 @@ +rect = $rect; + $this->name = $name; + } + + /** + * Convert this canvas to a clipPath element + */ + public function toClipPath() + { + $this->isClipPath = true; + } + + /** + * Return the layout of this canvas + * + * @return LayoutBox + */ + public function getLayout() + { + return $this->rect; + } + + /** + * Add an element to this canvas + * + * @param Drawable $child + */ + public function addElement(Drawable $child) + { + $this->children[] = $child; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + if ($this->isClipPath) { + $outer = $doc->createElement('defs'); + $innerContainer = $element = $doc->createElement('clipPath'); + $outer->appendChild($element); + } else { + $outer = $element = $doc->createElement('g'); + $innerContainer = $doc->createElement('g'); + $innerContainer->setAttribute('x', 0); + $innerContainer->setAttribute('y', 0); + $innerContainer->setAttribute('id', $this->name . '_inner'); + $innerContainer->setAttribute('transform', $this->rect->getInnerTransform($ctx)); + $element->appendChild($innerContainer); + } + + $element->setAttribute('id', $this->name); + foreach ($this->children as $child) { + $innerContainer->appendChild($child->toSvg($ctx)); + } + + if (isset($this->ariaRole)) { + $outer->setAttribute('role', $this->ariaRole); + } + return $outer; + } + + /** + * Set the aria role used to determine the meaning of this canvas in the accessibility tree + * + * The role 'presentation' will indicate that the purpose of this canvas is entirely decorative, while the role + * 'img' will indicate that the canvas contains an image, with a possible title or a description. For other + * possible roles, see http://www.w3.org/TR/wai-aria/roles + * + * @param $role string The aria role to set + */ + public function setAriaRole($role) + { + $this->ariaRole = $role; + } +} diff --git a/library/Icinga/Chart/Primitive/Circle.php b/library/Icinga/Chart/Primitive/Circle.php new file mode 100644 index 0000000..2cb2b72 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Circle.php @@ -0,0 +1,68 @@ +x = $x; + $this->y = $y; + $this->radius = $radius; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + $coords = $ctx->toAbsolute($this->x, $this->y); + $circle = $ctx->getDocument()->createElement('circle'); + $circle->setAttribute('cx', Format::formatSVGNumber($coords[0])); + $circle->setAttribute('cy', Format::formatSVGNumber($coords[1])); + $circle->setAttribute('r', $this->radius); + $circle->setAttribute('style', $this->getStyle()); + $this->applyAttributes($circle); + return $circle; + } +} diff --git a/library/Icinga/Chart/Primitive/Drawable.php b/library/Icinga/Chart/Primitive/Drawable.php new file mode 100644 index 0000000..5b4355c --- /dev/null +++ b/library/Icinga/Chart/Primitive/Drawable.php @@ -0,0 +1,22 @@ +xStart = $x1; + $this->xEnd = $x2; + $this->yStart = $y1; + $this->yEnd = $y2; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + list($x1, $y1) = $ctx->toAbsolute($this->xStart, $this->yStart); + list($x2, $y2) = $ctx->toAbsolute($this->xEnd, $this->yEnd); + $line = $doc->createElement('line'); + $line->setAttribute('x1', Format::formatSVGNumber($x1)); + $line->setAttribute('x2', Format::formatSVGNumber($x2)); + $line->setAttribute('y1', Format::formatSVGNumber($y1)); + $line->setAttribute('y2', Format::formatSVGNumber($y2)); + $line->setAttribute('style', $this->getStyle()); + $this->applyAttributes($line); + return $line; + } +} diff --git a/library/Icinga/Chart/Primitive/Path.php b/library/Icinga/Chart/Primitive/Path.php new file mode 100644 index 0000000..2fa793a --- /dev/null +++ b/library/Icinga/Chart/Primitive/Path.php @@ -0,0 +1,174 @@ +append($points); + } + + /** + * Append a single point or an array of points to this path + * + * @param array $points Either a single [x, y] point or an array of x, y points + * + * @return $this Fluid interface + */ + public function append(array $points) + { + if (count($points) === 0) { + return $this; + } + if (!is_array($points[0])) { + $points = array($points); + } + $this->points = array_merge($this->points, $points); + return $this; + } + + /** + * Prepend a single point or an array of points to this path + * + * @param array $points Either a single [x, y] point or an array of x, y points + * + * @return $this Fluid interface + */ + public function prepend(array $points) + { + if (count($points) === 0) { + return $this; + } + if (!is_array($points[0])) { + $points = array($points); + } + $this->points = array_merge($points, $this->points); + return $this; + } + + /** + * Set this path to be discrete + * + * @param boolean $bool True to draw discrete or false to draw straight lines between points + * + * @return $this Fluid interface + */ + public function setDiscrete($bool) + { + $this->discrete = $bool; + return $this; + } + + /** + * Mark this path as containing absolute coordinates + * + * @return $this Fluid interface + */ + public function toAbsolute() + { + $this->isAbsolute = true; + return $this; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + $group = $doc->createElement('g'); + + $pathDescription = ''; + $tpl = self::TPL_MOVE; + $lastPoint = null; + foreach ($this->points as $point) { + if (!$this->isAbsolute) { + $point = $ctx->toAbsolute($point[0], $point[1]); + } + $point[0] = Format::formatSVGNumber($point[0]); + $point[1] = Format::formatSVGNumber($point[1]); + if ($lastPoint && $this->discrete) { + $pathDescription .= sprintf($tpl, $point[0], $lastPoint[1]); + } + $pathDescription .= vsprintf($tpl, $point); + $lastPoint = $point; + $tpl = self::TPL_STRAIGHT; + } + + $path = $doc->createElement('path'); + if ($this->id) { + $path->setAttribute('id', $this->id); + } + $path->setAttribute('d', $pathDescription); + $path->setAttribute('style', $this->getStyle()); + $this->applyAttributes($path); + $group->appendChild($path); + return $group; + } +} diff --git a/library/Icinga/Chart/Primitive/PieSlice.php b/library/Icinga/Chart/Primitive/PieSlice.php new file mode 100644 index 0000000..f5a0ce9 --- /dev/null +++ b/library/Icinga/Chart/Primitive/PieSlice.php @@ -0,0 +1,293 @@ +x = $this->y = $this->radius = $radius; + + $this->startRadian = M_PI * $percentStart/50; + $this->endRadian = M_PI * ($percent + $percentStart)/50; + } + + /** + * Create the path for the pie slice + * + * @param int $x The x position of the pie slice + * @param int $y The y position of the pie slice + * @param int $r The absolute radius of the pie slice + * + * @return string A SVG path string + */ + private function getPieSlicePath($x, $y, $r) + { + // The coordinate system is mirrored on the Y axis, so we have to flip cos and sin + $xStart = $x + ($r * sin($this->startRadian)); + $yStart = $y - ($r * cos($this->startRadian)); + + if ($this->endRadian - $this->startRadian == 2*M_PI) { + // To draw a full circle, adjust arc endpoint by a small (unvisible) value + $this->endRadian -= 0.001; + $pathString = 'M ' . Format::formatSVGNumber($xStart) . ' ' . Format::formatSVGNumber($yStart); + } else { + // Start at the center of the pieslice + $pathString = 'M ' . $x . ' ' . $y; + // Draw a straight line to the upper part of the arc + $pathString .= ' L ' . Format::formatSVGNumber($xStart) . ' ' . Format::formatSVGNumber($yStart); + } + + // Instead of directly connecting the upper part of the arc (leaving a triangle), draw a bow with the radius + $pathString .= ' A ' . Format::formatSVGNumber($r) . ' ' . Format::formatSVGNumber($r); + // These are the flags for the bow, see the SVG path documentation for details + // http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands + $pathString .= ' 0 ' . (($this->endRadian - $this->startRadian > M_PI) ? '1' : '0 ') . ' 1'; + + // xEnd and yEnd are the lower point of the arc + $xEnd = $x + ($r * sin($this->endRadian)); + $yEnd = $y - ($r * cos($this->endRadian)); + $pathString .= ' ' . Format::formatSVGNumber($xEnd) . ' ' . Format::formatSVGNumber($yEnd); + + return $pathString; + } + + /** + * Draw the label handler and the text for this pie slice + * + * @param RenderContext $ctx The rendering context to use for coordinate translation + * @param int $r The radius of the pie in absolute coordinates + * + * @return DOMElement The group DOMElement containing the handle and label + */ + private function drawDescriptionLabel(RenderContext $ctx, $r) + { + $group = $ctx->getDocument()->createElement('g'); + $rOuter = ($ctx->xToAbsolute($this->outerCaptionBound) + $ctx->yToAbsolute($this->outerCaptionBound)) / 2; + $addOffset = $rOuter - $r ; + if ($addOffset < 0) { + $addOffset = 0; + } + list($x, $y) = $ctx->toAbsolute($this->x, $this->y); + $midRadius = $this->startRadian + ($this->endRadian - $this->startRadian) / 2; + list($offsetX, $offsetY) = $ctx->toAbsolute($this->captionOffset, $this->captionOffset); + + $midX = $x + intval(($offsetX + $r)/2 * sin($midRadius)); + $midY = $y - intval(($offsetY + $r)/2 * cos($midRadius)); + + // Draw the handle + $path = new Path(array($midX, $midY)); + + $midX += ($addOffset + $r/3) * ($midRadius > M_PI ? -1 : 1); + $path->append(array($midX, $midY))->toAbsolute(); + + $midX += intval($r/2 * sin(M_PI/9)) * ($midRadius > M_PI ? -1 : 1); + $midY -= intval($r/2 * cos(M_PI/3)) * ($midRadius < M_PI*1.4 && $midRadius > M_PI/3 ? -1 : 1); + + if ($ctx->ytoRelative($midY) > 100) { + $midY = $ctx->yToAbsolute(100); + } elseif ($ctx->ytoRelative($midY) < 0) { + $midY = $ctx->yToAbsolute($ctx->ytoRelative(100+$midY)); + } + + $path->append(array($midX , $midY)); + $rel = $ctx->toRelative($midX, $midY); + + // Draw the text box + $text = new Text($rel[0]+1.5, $rel[1], $this->caption); + $text->setFontSize('5em'); + $text->setAlignment(($midRadius > M_PI ? Text::ALIGN_END : Text::ALIGN_START)); + + $group->appendChild($path->toSvg($ctx)); + $group->appendChild($text->toSvg($ctx)); + + return $group; + } + + /** + * Set the x position of the pie slice + * + * @param int $x The new x position + * + * @return $this Fluid interface + */ + public function setX($x) + { + $this->x = $x; + return $this; + } + + /** + * Set the y position of the pie slice + * + * @param int $y The new y position + * + * @return $this Fluid interface + */ + public function setY($y) + { + $this->y = $y; + return $this; + } + + /** + * Set a root element to be used for drawing labels + * + * @param DOMElement $group The label group + * + * @return $this Fluid interface + */ + public function setLabelGroup(DOMElement $group) + { + $this->labelGroup = $group; + return $this; + } + + /** + * Set the caption for this label + * + * @param string $caption The caption for this element + * + * @return $this Fluid interface + */ + public function setCaption($caption) + { + $this->caption = $caption; + return $this; + } + + /** + * Set the internal offset of the caption handle + * + * @param int $offset The offset for the caption handle + * + * @return $this Fluid interface + */ + public function setCaptionOffset($offset) + { + $this->captionOffset = $offset; + return $this; + } + + /** + * Set the minimum radius to be used for drawing labels + * + * @param int $bound The offset for the caption text + * + * @return $this Fluid interface + */ + public function setOuterCaptionBound($bound) + { + $this->outerCaptionBound = $bound; + return $this; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + $group = $doc->createElement('g'); + $r = ($ctx->xToAbsolute($this->radius) + $ctx->yToAbsolute($this->radius)) / 2; + list($x, $y) = $ctx->toAbsolute($this->x, $this->y); + + $slicePath = $doc->createElement('path'); + + $slicePath->setAttribute('d', $this->getPieSlicePath($x, $y, $r)); + $slicePath->setAttribute('style', $this->getStyle()); + $slicePath->setAttribute('data-icinga-graph-type', 'pieslice'); + + $this->applyAttributes($slicePath); + $group->appendChild($slicePath); + if ($this->caption != "") { + $lblGroup = ($this->labelGroup ? $this->labelGroup : $group); + $lblGroup->appendChild($this->drawDescriptionLabel($ctx, $r)); + } + return $group; + } +} diff --git a/library/Icinga/Chart/Primitive/RawElement.php b/library/Icinga/Chart/Primitive/RawElement.php new file mode 100644 index 0000000..721b6e0 --- /dev/null +++ b/library/Icinga/Chart/Primitive/RawElement.php @@ -0,0 +1,43 @@ +domEl = $el; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + return $this->domEl; + } +} diff --git a/library/Icinga/Chart/Primitive/Rect.php b/library/Icinga/Chart/Primitive/Rect.php new file mode 100644 index 0000000..0145e07 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Rect.php @@ -0,0 +1,105 @@ +x = $x; + $this->y = $y; + $this->width = $width; + $this->height = $height; + } + + /** + * Call to let the rectangle keep the ratio + */ + public function keepRatio() + { + $this->keepRatio = true; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + $doc = $ctx->getDocument(); + $rect = $doc->createElement('rect'); + + list($x, $y) = $ctx->toAbsolute($this->x, $this->y); + if ($this->keepRatio) { + $ctx->keepRatio(); + } + list($width, $height) = $ctx->toAbsolute($this->width, $this->height); + if ($this->keepRatio) { + $ctx->ignoreRatio(); + } + $rect->setAttribute('x', Format::formatSVGNumber($x)); + $rect->setAttribute('y', Format::formatSVGNumber($y)); + $rect->setAttribute('width', Format::formatSVGNumber($width)); + $rect->setAttribute('height', Format::formatSVGNumber($height)); + $rect->setAttribute('style', $this->getStyle()); + + $this->applyAttributes($rect); + $this->appendAnimation($rect, $ctx); + + return $rect; + } +} diff --git a/library/Icinga/Chart/Primitive/Styleable.php b/library/Icinga/Chart/Primitive/Styleable.php new file mode 100644 index 0000000..678b940 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Styleable.php @@ -0,0 +1,154 @@ +strokeWidth = $width; + return $this; + } + + /** + * Set the color for the stroke or none for no stroke + * + * @param string $color The color to set for the stroke + * + * @return $this Fluid interface + */ + public function setStrokeColor($color) + { + $this->strokeColor = $color ? $color : 'none'; + return $this; + } + + /** + * Set additional styles for this drawable + * + * @param string $styles The styles to set additionally + * + * @return $this Fluid interface + */ + public function setAdditionalStyle($styles) + { + $this->additionalStyle = $styles; + return $this; + } + + /** + * Set the fill for this styleable + * + * @param string $color The color to use for filling or null to use no fill + * + * @return $this Fluid interface + */ + public function setFill($color = null) + { + $this->fill = $color ? $color : 'none'; + return $this; + } + + /** + * Set the id for this element + * + * @param string $id The id to set for this element + * + * @return $this Fluid interface + */ + public function setId($id) + { + $this->id = $id; + + return $this; + } + + /** + * Return the content of the style attribute as a string + * + * @return string A string containing styles + */ + public function getStyle() + { + $base = sprintf("fill: %s; stroke: %s;stroke-width: %s;", $this->fill, $this->strokeColor, $this->strokeWidth); + $base .= ';' . $this->additionalStyle . ';'; + return $base; + } + + /** + * Add an additional attribute to this element + */ + public function setAttribute($key, $value) + { + $this->attributes[$key] = $value; + } + + /** + * Apply attribute to a DOMElement + * + * @param DOMElement $el Element to apply attributes + */ + protected function applyAttributes(DOMElement $el) + { + foreach ($this->attributes as $name => $value) { + $el->setAttribute($name, $value); + } + } +} diff --git a/library/Icinga/Chart/Primitive/Text.php b/library/Icinga/Chart/Primitive/Text.php new file mode 100644 index 0000000..e647f40 --- /dev/null +++ b/library/Icinga/Chart/Primitive/Text.php @@ -0,0 +1,168 @@ +x = $x; + $this->y = $y; + $this->text = $text; + $this->fontSize = $fontSize; + } + + /** + * Set the font size of the svg text element + * + * @param string $size The font size including a unit + * + * @return $this Fluid interface + */ + public function setFontSize($size) + { + $this->fontSize = $size; + return $this; + } + + /** + * Set the text alignment with one of the ALIGN_* constants + * + * @param String $align Value how to align + * + * @return $this Fluid interface + */ + public function setAlignment($align) + { + $this->alignment = $align; + return $this; + } + + /** + * Set the weight of the current font + * + * @param string $weight The weight of the string + * + * @return $this Fluid interface + */ + public function setFontWeight($weight) + { + $this->fontWeight = $weight; + return $this; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + list($x, $y) = $ctx->toAbsolute($this->x, $this->y); + $text = $ctx->getDocument()->createElement('text'); + $text->setAttribute('x', Format::formatSVGNumber($x - 15)); + $text->setAttribute( + 'style', + $this->getStyle() + . ';font-size:' . $this->fontSize + . '; font-family: Ubuntu, Calibri, Trebuchet MS, Helvetica, Verdana, sans-serif' + . ';font-weight: ' . $this->fontWeight + . ';font-stretch: ' . $this->fontStretch + . '; font-style: normal;' + . 'text-anchor: ' . $this->alignment + ); + + $text->setAttribute('y', Format::formatSVGNumber($y)); + $text->appendChild(new DOMText($this->text)); + return $text; + } +} diff --git a/library/Icinga/Chart/Render/LayoutBox.php b/library/Icinga/Chart/Render/LayoutBox.php new file mode 100644 index 0000000..fa49461 --- /dev/null +++ b/library/Icinga/Chart/Render/LayoutBox.php @@ -0,0 +1,200 @@ +height = $height ? $height : 100; + $this->width = $width ? $width : 100; + $this->x = $x; + $this->y = $y; + } + + /** + * Set a padding to all four sides uniformly + * + * @param int $padding The padding to set for all four sides + */ + public function setUniformPadding($padding) + { + $this->padding = array($padding, $padding, $padding, $padding); + } + + /** + * Set the padding for this LayoutBox + * + * @param int $top The top side padding + * @param int $right The right side padding + * @param int $bottom The bottom side padding + * @param int $left The left side padding + */ + public function setPadding($top, $right, $bottom, $left) + { + $this->padding = array($top, $right, $bottom, $left); + } + + /** + * Return a string containing the SVG transform attribute values for the padding + * + * @param RenderContext $ctx The context to determine the translation coordinates + * + * @return string The transformation string + */ + public function getInnerTransform(RenderContext $ctx) + { + list($translateX, $translateY) = $ctx->toAbsolute( + $this->padding[self::PADDING_LEFT] + $this->getX(), + $this->padding[self::PADDING_TOP] + $this->getY() + ); + list($scaleX, $scaleY) = $ctx->paddingToScaleFactor($this->padding); + + $scaleX *= $this->getWidth()/100; + $scaleY *= $this->getHeight()/100; + return sprintf( + 'translate(%s, %s) scale(%s, %s)', + Format::formatSVGNumber($translateX), + Format::formatSVGNumber($translateY), + Format::formatSVGNumber($scaleX), + Format::formatSVGNumber($scaleY) + ); + } + + /** + * String representation for this Layout, for debug purposes + * + * @return string A string containing the bounds of this LayoutBox + */ + public function __toString() + { + return sprintf( + 'Rectangle: x: %s y: %s, height: %s, width: %s', + $this->x, + $this->y, + $this->height, + $this->width + ); + } + + /** + * Return a four element array with the padding + * + * @return array The padding of this LayoutBox + */ + public function getPadding() + { + return $this->padding; + } + + /** + * Return the height of this LayoutBox + * + * @return int The height of this box + */ + public function getHeight() + { + return $this->height; + } + + /** + * Return the width of this LayoutBox + * + * @return int The width of this box + */ + public function getWidth() + { + return $this->width; + } + + /** + * Return the x position of this LayoutBox + * + * @return int The x position of this box + */ + public function getX() + { + return $this->x; + } + + /** + * Return the y position of this LayoutBox + * + * @return int The y position of this box + */ + public function getY() + { + return $this->y; + } +} diff --git a/library/Icinga/Chart/Render/RenderContext.php b/library/Icinga/Chart/Render/RenderContext.php new file mode 100644 index 0000000..457fbf3 --- /dev/null +++ b/library/Icinga/Chart/Render/RenderContext.php @@ -0,0 +1,225 @@ +document = $document; + if ($width > $height) { + $this->xratio = $width / $height; + } elseif ($height > $width) { + $this->yratio = $height / $width; + } + } + + /** + * Return the document represented by this Rendering context + * + * @return DOMDocument The DOMDocument for creating files + */ + public function getDocument() + { + return $this->document; + } + + /** + * Let successive toAbsolute operations ignore ratio correction + * + * This can be called to avoid distortion on certain elements like rectangles. + */ + public function keepRatio() + { + $this->respectRatio = true; + } + + /** + * Let successive toAbsolute operations perform ratio correction + * + * This will cause distortion on certain elements like rectangles. + */ + public function ignoreRatio() + { + $this->respectRatio = false; + } + + /** + * Return how many unit s are available in the Y axis + * + * @return int The number of units available on the y axis + */ + public function getNrOfUnitsY() + { + return intval($this->viewBoxSize[1] * $this->yratio); + } + + /** + * Return how many unit s are available in the X axis + * + * @return int The number of units available on the x axis + */ + public function getNrOfUnitsX() + { + return intval($this->viewBoxSize[0] * $this->xratio); + } + + /** + * Transforms the x,y coordinate from relative coordinates to absolute world coordinates + * + * (50, 50) would be a point in the middle of the document and map to 500, 1000 on a + * 1000 x 1000 viewbox with a 1:2 ratio. + * + * @param int $x The relative x coordinate + * @param int $y The relative y coordinate + * + * @return array An x,y tuple containing absolute coordinates + * @see RenderContext::toRelative + */ + public function toAbsolute($x, $y) + { + return array($this->xToAbsolute($x), $this->yToAbsolute($y)); + } + + /** + * Transforms the x,y coordinate from absolute coordinates to relative world coordinates + * + * This is the inverse function of toAbsolute + * + * @param int $x The absolute x coordinate + * @param int $y The absolute y coordinate + * + * @return array An x,y tupel containing absolute coordinates + * @see RenderContext::toAbsolute + */ + public function toRelative($x, $y) + { + return array($this->xToRelative($x), $this->yToRelative($y)); + } + + /** + * Calculates the scale transformation required to apply the padding on an Canvas + * + * @param array $padding A 4 element array containing top, right, bottom and left padding + * + * @return array An array containing the x and y scale + */ + public function paddingToScaleFactor(array $padding) + { + list($horizontalPadding, $verticalPadding) = $this->toAbsolute( + $padding[LayoutBox::PADDING_RIGHT] + $padding[LayoutBox::PADDING_LEFT], + $padding[LayoutBox::PADDING_TOP] + $padding[LayoutBox::PADDING_BOTTOM] + ); + + return array( + ($this->getNrOfUnitsX() - $horizontalPadding) / $this->getNrOfUnitsX(), + ($this->getNrOfUnitsY() - $verticalPadding) / $this->getNrOfUnitsY() + ); + } + + /** + * Transform a relative x coordinate to an absolute one + * + * @param int $x A relative x coordinate + * + * @return int An absolute x coordinate + **/ + public function xToAbsolute($x) + { + return $this->getNrOfUnitsX() / 100 * $x / ($this->respectRatio ? $this->xratio : 1); + } + + /** + * Transform a relative y coordinate to an absolute one + * + * @param int $y A relative y coordinate + * + * @return int An absolute y coordinate + */ + public function yToAbsolute($y) + { + return $this->getNrOfUnitsY() / 100 * $y / ($this->respectRatio ? $this->yratio : 1); + } + + /** + * Transform a absolute x coordinate to an relative one + * + * @param int $x An absolute x coordinate + * + * @return int A relative x coordinate + */ + public function xToRelative($x) + { + return $x / $this->getNrOfUnitsX() * 100 * ($this->respectRatio ? $this->xratio : 1); + } + + /** + * Transform a absolute y coordinate to an relative one + * + * @param int $y An absolute x coordinate + * + * @return int A relative x coordinate + */ + public function yToRelative($y) + { + return $y / $this->getNrOfUnitsY() * 100 * ($this->respectRatio ? $this->yratio : 1); + } +} diff --git a/library/Icinga/Chart/Render/Rotator.php b/library/Icinga/Chart/Render/Rotator.php new file mode 100644 index 0000000..3e7071c --- /dev/null +++ b/library/Icinga/Chart/Render/Rotator.php @@ -0,0 +1,80 @@ +element = $element; + $this->degrees = $degrees; + } + + /** + * Rotate the given element. + * + * @param RenderContext $ctx The rendering context + * @param DOMElement $el The element to rotate + * @param $degrees The amount of degrees + * + * @return DOMElement The rotated DOMElement + */ + private function rotate(RenderContext $ctx, DOMElement $el, $degrees) + { + // Create a box containing the rotated element relative to the original element position + $container = $ctx->getDocument()->createElement('g'); + $x = $el->getAttribute('x'); + $y = $el->getAttribute('y'); + $container->setAttribute('transform', 'translate(' . $x . ',' . $y . ')'); + $el->removeAttribute('x'); + $el->removeAttribute('y'); + + // Put the element into a rotated group + //$rotate = $ctx->getDocument()->createElement('g'); + $el->setAttribute('transform', 'rotate(' . $degrees . ')'); + //$rotate->appendChild($el); + + $container->appendChild($el); + return $container; + } + + /** + * Create the SVG representation from this Drawable + * + * @param RenderContext $ctx The context to use for rendering + * + * @return DOMElement The SVG Element + */ + public function toSvg(RenderContext $ctx) + { + $el = $this->element->toSvg($ctx); + return $this->rotate($ctx, $el, $this->degrees); + } +} diff --git a/library/Icinga/Chart/SVGRenderer.php b/library/Icinga/Chart/SVGRenderer.php new file mode 100644 index 0000000..d3891f2 --- /dev/null +++ b/library/Icinga/Chart/SVGRenderer.php @@ -0,0 +1,331 @@ +createDocumentType( + 'svg', + '-//W3C//DTD SVG 1.1//EN', + 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' + ); + + $this->document = $implementation->createDocument(null, '', $docType); + $this->svg = $this->createOuterBox(); + $this->document->appendChild($this->svg); + } + + /** + * Create the outer SVG box containing the root svg element and namespace and return it + * + * @return DOMElement The SVG root node + */ + private function createOuterBox() + { + $ctx = $this->createRenderContext(); + $svg = $this->document->createElement('svg'); + $svg->setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + $svg->setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); + $svg->setAttribute('role', $this->ariaRole); + $svg->setAttribute('width', '100%'); + $svg->setAttribute('height', '100%'); + $svg->setAttribute( + 'viewBox', + sprintf( + '0 0 %s %s', + $ctx->getNrOfUnitsX(), + $ctx->getNrOfUnitsY() + ) + ); + if ($this->preserveAspectRatio) { + $svg->setAttribute( + 'preserveAspectRatio', + sprintf( + '%s%s %s', + $this->xAspectRatio, + $this->yAspectRatio, + $this->xFillMode + ) + ); + } + return $svg; + } + + /** + * Add aria title and description + * + * Adds an aria title and desc element to the given SVG node, which are used to describe this SVG by accessibility + * tools such as screen readers. + * + * @param DOMNode $svg The SVG DOMNode to which the aria attributes should be attached + * @param $title The title text + * @param $description The description text + */ + private function addAriaDescription(DOMNode $svg, $titleText, $descriptionText) + { + $doc = $svg->ownerDocument; + + $titleId = $descId = ''; + if (isset($this->ariaTitle)) { + $titleId = 'aria-title-' . $this->stripNonAlphanumeric($titleText); + $title = $doc->createElement('title'); + $title->setAttribute('id', $titleId); + + $title->appendChild($doc->createTextNode($titleText)); + $svg->appendChild($title); + } + + if (isset($this->ariaDescription)) { + $descId = 'aria-desc-' . $this->stripNonAlphanumeric($descriptionText); + $desc = $doc->createElement('desc'); + $desc->setAttribute('id', $descId); + + $desc->appendChild($doc->createTextNode($descriptionText)); + $svg->appendChild($desc); + } + + $svg->setAttribute('aria-labelledby', join(' ', array($titleId, $descId))); + } + + /** + * Initialises the XML-document, SVG-element and this figure's root canvas + * + * @param int $width The width ratio + * @param int $height The height ratio + */ + public function __construct($width, $height) + { + $this->width = $width; + $this->height = $height; + $this->rootCanvas = new Canvas('root', new LayoutBox(0, 0)); + } + + /** + * Render the SVG-document + * + * @return string The resulting XML structure + */ + public function render() + { + $this->createRootDocument(); + $ctx = $this->createRenderContext(); + $this->addAriaDescription($this->svg, $this->ariaTitle, $this->ariaDescription); + $this->svg->appendChild($this->rootCanvas->toSvg($ctx)); + $this->document->formatOutput = true; + $this->document->encoding = 'UTF-8'; + return $this->document->saveXML(); + } + + /** + * Create a render context that will be used for rendering elements + * + * @return RenderContext The created RenderContext instance + */ + public function createRenderContext() + { + return new RenderContext($this->document, $this->width, $this->height); + } + + /** + * Return the root canvas of this rendered + * + * @return Canvas The canvas that will be the uppermost element in this figure + */ + public function getCanvas() + { + return $this->rootCanvas; + } + + /** + * Preserve the aspect ratio of the rendered object + * + * Do not deform the content of the SVG when the aspect ratio of the viewBox + * differs from the aspect ratio of the SVG element, but add padding or cutoff + * instead + * + * @param bool $preserve Whether the aspect ratio should be preserved + */ + public function preserveAspectRatio($preserve = true) + { + $this->preserveAspectRatio = $preserve; + } + + /** + * Change the horizontal alignment of the SVG element + * + * Change the horizontal alignment of the svg, when preserveAspectRatio is used and + * padding is present. Defaults to + */ + public function setXAspectRatioAlignment($alignment) + { + $this->xAspectRatio = $alignment; + } + + /** + * Change the vertical alignment of the SVG element + * + * Change the vertical alignment of the svg, when preserveAspectRatio is used and + * padding is present. + */ + public function setYAspectRatioAlignment($alignment) + { + $this->yAspectRatio = $alignment; + } + + /** + * Set the aria description, that is used as a title for this SVG in screen readers + * + * @param $text + */ + public function setAriaTitle($text) + { + $this->ariaTitle = $text; + } + + /** + * Set the aria description, that is used to describe this SVG in screen readers + * + * @param $text + */ + public function setAriaDescription($text) + { + $this->ariaDescription = $text; + } + + /** + * Set the aria role, that is used to describe the purpose of this SVG in screen readers + * + * @param $text + */ + public function setAriaRole($text) + { + $this->ariaRole = $text; + } + + + private function stripNonAlphanumeric($str) + { + return preg_replace('/[^A-Za-z]+/', '', $str); + } +} diff --git a/library/Icinga/Chart/Unit/AxisUnit.php b/library/Icinga/Chart/Unit/AxisUnit.php new file mode 100644 index 0000000..251787f --- /dev/null +++ b/library/Icinga/Chart/Unit/AxisUnit.php @@ -0,0 +1,56 @@ +labels = array(); + $duration = $this->getMax() - $this->getMin(); + + if ($duration <= self::HOUR) { + $unit = self::MINUTE; + } elseif ($duration <= self::DAY) { + $unit = self::HOUR; + } elseif ($duration <= self::MONTH) { + $unit = self::DAY; + } else { + $unit = self::MONTH; + } + $this->calculateLabels($unit); + } + + /** + * Calculate the labels for this dataset + * + * @param integer $unit The unit to use as the basis for calculation + */ + private function calculateLabels($unit) + { + $fac = new DateTime(); + + $duration = $this->getMax() - $this->getMin(); + + // Calculate number of ticks, but not more than 30 + $tickCount = ($duration/$unit * 10); + if ($tickCount > 30) { + $tickCount = 30; + } + + $step = $duration / $tickCount; + $format = $this->timeFormat; + if ($unit === self::DAY) { + $format = $this->dateFormat; + } elseif ($unit === self::MONTH) { + $format = $this->dateFormat; + } + + for ($i = 0; $i <= $duration; $i += $step) { + $this->labels[] = $fac->setTimestamp($this->getMin() + $i)->format($format); + } + } + + /** + * Add a dataset to this CalendarUnit and update labels + * + * @param array $dataset The dataset to update + * @param int $idx The index to use for determining the data + * + * @return $this Fluid interface + */ + public function addValues(array $dataset, $idx = 0) + { + parent::addValues($dataset, $idx); + $this->createLabels(); + return $this; + } + + /** + * Return the current axis relative position + * + * @return int The position of the next tick (between 0 and 100) + */ + public function current() + { + return 100 * (key($this->labels) / count($this->labels)); + } + + /** + * Move to next tick + */ + public function next() + { + next($this->labels); + } + + /** + * Return the current tick caption + * + * @return string + */ + public function key() + { + return current($this->labels); + } + + /** + * Return true when the iterator is in a valid range + * + * @return bool + */ + public function valid() + { + return current($this->labels) !== false; + } + + /** + * Rewind the internal array + */ + public function rewind() + { + reset($this->labels); + } +} diff --git a/library/Icinga/Chart/Unit/LinearUnit.php b/library/Icinga/Chart/Unit/LinearUnit.php new file mode 100644 index 0000000..ea4792b --- /dev/null +++ b/library/Icinga/Chart/Unit/LinearUnit.php @@ -0,0 +1,227 @@ +min = PHP_INT_MAX; + $this->max = ~PHP_INT_MAX; + $this->nrOfTicks = $nrOfTicks; + } + + /** + * Add a dataset and calculate the minimum and maximum value for this AxisUnit + * + * @param array $dataset The dataset to add + * @param int $idx The idx (0 for x, 1 for y) + * + * @return $this Fluent interface + */ + public function addValues(array $dataset, $idx = 0) + { + $datapoints = array(); + + foreach ($dataset['data'] as $points) { + $datapoints[] = $points[$idx]; + } + if (empty($datapoints)) { + return $this; + } + sort($datapoints); + if (!$this->staticMax) { + $this->max = max($this->max, $datapoints[count($datapoints) - 1]); + } + if (!$this->staticMin) { + $this->min = min($this->min, $datapoints[0]); + } + $this->currentTick = 0; + $this->currentValue = $this->min; + if ($this->max === $this->min) { + $this->max = $this->min + 10; + } + $this->nrOfTicks = $this->max - $this->min; + return $this; + } + + /** + * Transform the absolute value to an axis relative value + * + * @param int $value The absolute coordinate from the dataset + * @return float|int The axis relative coordinate (between 0 and 100) + */ + public function transform($value) + { + if ($value < $this->min) { + return 0; + } elseif ($value > $this->max) { + return 100; + } else { + return 100 * ($value - $this->min) / $this->nrOfTicks; + } + } + + /** + * Return the position of the current tick + * + * @return int + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->currentTick; + } + + /** + * Calculate the next tick and tick value + */ + public function next(): void + { + $this->currentTick += (100 / $this->nrOfTicks); + $this->currentValue += (($this->max - $this->min) / $this->nrOfTicks); + } + + /** + * Return the label for the current tick + * + * @return string The label for the current tick + */ + #[\ReturnTypeWillChange] + public function key() + { + return (string) intval($this->currentValue); + } + + /** + * True when we're at a valid tick (iterator interface) + * + * @return bool + */ + public function valid(): bool + { + return $this->currentTick >= 0 && $this->currentTick <= 100; + } + + /** + * Reset the current tick and label value + */ + public function rewind(): void + { + $this->currentTick = 0; + $this->currentValue = $this->min; + } + + /** + * Set the axis maximum value to a fixed value + * + * @param int $max The new maximum value + */ + public function setMax($max) + { + if ($max !== null) { + $this->max = $max; + $this->staticMax = true; + } + } + + /** + * Set the axis minimum value to a fixed value + * + * @param int $min The new minimum value + */ + public function setMin($min) + { + if ($min !== null) { + $this->min = $min; + $this->staticMin = true; + } + } + + /** + * Return the current minimum value of the axis + * + * @return int The minimum set for this axis + */ + public function getMin() + { + return $this->min; + } + + /** + * Return the current maximum value of the axis + * + * @return int The maximum set for this axis + */ + public function getMax() + { + return $this->max; + } + + /** + * Get the amount of ticks necessary to display this AxisUnit + * + * @return int + */ + public function getTicks() + { + return $this->nrOfTicks; + } +} diff --git a/library/Icinga/Chart/Unit/LogarithmicUnit.php b/library/Icinga/Chart/Unit/LogarithmicUnit.php new file mode 100644 index 0000000..90cad57 --- /dev/null +++ b/library/Icinga/Chart/Unit/LogarithmicUnit.php @@ -0,0 +1,263 @@ + + * this article for a more detailed description. + */ +class LogarithmicUnit implements AxisUnit +{ + /** + * @var int + */ + protected $base; + + /** + * @var + */ + protected $currentTick; + + /** + * @var + */ + protected $minExp; + + /** + * @var + */ + protected $maxExp; + + /** + * True when the minimum value is static and isn't affected by the data set + * + * @var bool + */ + protected $staticMin = false; + + /** + * True when the maximum value is static and isn't affected by the data set + * + * @var bool + */ + protected $staticMax = false; + + /** + * Create and initialize this AxisUnit + * + * @param int $nrOfTicks The number of ticks to use + */ + public function __construct($base = 10) + { + $this->base = $base; + $this->minExp = PHP_INT_MAX; + $this->maxExp = ~PHP_INT_MAX; + } + + /** + * Add a dataset and calculate the minimum and maximum value for this AxisUnit + * + * @param array $dataset The dataset to add + * @param int $idx The idx (0 for x, 1 for y) + * + * @return $this Fluent interface + */ + public function addValues(array $dataset, $idx = 0) + { + $datapoints = array(); + + foreach ($dataset['data'] as $points) { + $datapoints[] = $points[$idx]; + } + if (empty($datapoints)) { + return $this; + } + sort($datapoints); + if (!$this->staticMax) { + $this->maxExp = max($this->maxExp, $this->logCeil($datapoints[count($datapoints) - 1])); + } + if (!$this->staticMin) { + $this->minExp = min($this->minExp, $this->logFloor($datapoints[0])); + } + $this->currentTick = 0; + + return $this; + } + + /** + * Transform the absolute value to an axis relative value + * + * @param int $value The absolute coordinate from the data set + * @return float|int The axis relative coordinate (between 0 and 100) + */ + public function transform($value) + { + if ($value < $this->pow($this->minExp)) { + return 0; + } elseif ($value > $this->pow($this->maxExp)) { + return 100; + } else { + return 100 * ($this->log($value) - $this->minExp) / $this->getTicks(); + } + } + + /** + * Return the position of the current tick + * + * @return int + */ + public function current() + { + return $this->currentTick * (100 / $this->getTicks()); + } + + /** + * Calculate the next tick and tick value + */ + public function next() + { + ++ $this->currentTick; + } + + /** + * Return the label for the current tick + * + * @return string The label for the current tick + */ + public function key() + { + $currentBase = $this->currentTick + $this->minExp; + if (abs($currentBase) > 4) { + return $this->base . 'E' . $currentBase; + } + return (string) intval($this->pow($currentBase)); + } + + /** + * True when we're at a valid tick (iterator interface) + * + * @return bool + */ + public function valid() + { + return $this->currentTick >= 0 && $this->currentTick < $this->getTicks(); + } + + /** + * Reset the current tick and label value + */ + public function rewind() + { + $this->currentTick = 0; + } + + /** + * Perform a log-modulo transformation + * + * @param $value The value to transform + * + * @return double The transformed value + */ + protected function log($value) + { + $sign = $value > 0 ? 1 : -1; + return $sign * log1p($sign * $value) / log($this->base); + } + + /** + * Calculate the biggest exponent necessary to display the given data point + * + * @param $value + * + * @return float + */ + protected function logCeil($value) + { + return ceil($this->log($value)) + 1; + } + + /** + * Calculate the smallest exponent necessary to display the given data point + * + * @param $value + * + * @return float + */ + protected function logFloor($value) + { + return floor($this->log($value)); + } + + /** + * Inverse function to the log-modulo transformation + * + * @param $value + * + * @return double + */ + protected function pow($value) + { + if ($value == 0) { + return 0; + } + $sign = $value > 0 ? 1 : -1; + return $sign * (pow($this->base, $sign * $value)); + } + + /** + * Set the axis minimum value to a fixed value + * + * @param int $min The new minimum value + */ + public function setMin($min) + { + $this->minExp = $this->logFloor($min); + $this->staticMin = true; + } + + /** + * Set the axis maximum value to a fixed value + * + * @param int $max The new maximum value + */ + public function setMax($max) + { + $this->maxExp = $this->logCeil($max); + $this->staticMax = true; + } + + /** + * Return the current minimum value of the axis + * + * @return int The minimum set for this axis + */ + public function getMin() + { + return $this->pow($this->minExp); + } + + /** + * Return the current maximum value of the axis + * + * @return int The maximum set for this axis + */ + public function getMax() + { + return $this->pow($this->maxExp); + } + + /** + * Get the amount of ticks necessary to display this AxisUnit + * + * @return int + */ + public function getTicks() + { + return $this->maxExp - $this->minExp; + } +} diff --git a/library/Icinga/Chart/Unit/StaticAxis.php b/library/Icinga/Chart/Unit/StaticAxis.php new file mode 100644 index 0000000..d563091 --- /dev/null +++ b/library/Icinga/Chart/Unit/StaticAxis.php @@ -0,0 +1,129 @@ +items[] = $points[$idx]; + } + $this->items = array_unique($this->items); + + return $this; + } + + /** + * Transform the given absolute value in an axis relative value + * + * @param int $value The absolute, dataset dependent value + * + * @return int An axis relative value + */ + public function transform($value) + { + $flipped = array_flip($this->items); + if (!isset($flipped[$value])) { + return 0; + } + $pos = $flipped[$value]; + return 1 + (99 / count($this->items) * $pos); + } + /** + * Set the axis minimum value to a fixed value + * + * @param int $min The new minimum value + */ + public function setMin($min) + { + } + + /** + * Set the axis maximum value to a fixed value + * + * @param int $max The new maximum value + */ + public function setMax($max) + { + } + + /** + * (PHP 5 >= 5.0.0)
    + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + */ + public function current() + { + return 1 + (99 / count($this->items) * key($this->items)); + } + + /** + * (PHP 5 >= 5.0.0)
    + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + */ + public function next() + { + return next($this->items); + } + + /** + * (PHP 5 >= 5.0.0)
    + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + */ + public function key() + { + return current($this->items); + } + + /** + * (PHP 5 >= 5.0.0)
    + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + */ + public function valid() + { + return current($this->items) !== false; + } + + /** + * (PHP 5 >= 5.0.0)
    + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + */ + public function rewind() + { + return reset($this->items); + } + + /** + * Get the amount of ticks of this axis + * + * @return int + */ + public function getTicks() + { + return count($this->items); + } +} diff --git a/library/Icinga/Cli/AnsiScreen.php b/library/Icinga/Cli/AnsiScreen.php new file mode 100644 index 0000000..2780f08 --- /dev/null +++ b/library/Icinga/Cli/AnsiScreen.php @@ -0,0 +1,122 @@ + '30', + 'darkgray' => '1;30', + 'red' => '31', + 'lightred' => '1;31', + 'green' => '32', + 'lightgreen' => '1;32', + 'brown' => '33', + 'yellow' => '1;33', + 'blue' => '34', + 'lightblue' => '1;34', + 'purple' => '35', + 'lightpurple' => '1;35', + 'cyan' => '36', + 'lightcyan' => '1;36', + 'lightgray' => '37', + 'white' => '1;37', + ); + + protected $bgColors = array( + 'black' => '40', + 'red' => '41', + 'green' => '42', + 'brown' => '43', + 'blue' => '44', + 'purple' => '45', + 'cyan' => '46', + 'lightgray' => '47', + ); + + public function strlen($string) + { + return strlen($this->stripAnsiCodes($string)); + } + + public function stripAnsiCodes($string) + { + return preg_replace('/\e\[?.*?[\@-~]/', '', $string); + } + + public function clear() + { + return "\033[2J" // Clear the whole screen + . "\033[1;1H" // Move the cursor to row 1, column 1 + . "\033[1S"; // Scroll whole page up by 1 line (why?) + } + + public function underline($text) + { + return "\033[4m" + . $text + . "\033[0m"; // Reset color codes + } + + public function colorize($text, $fgColor = null, $bgColor = null) + { + return $this->startColor($fgColor, $bgColor) + . $text + . "\033[0m"; // Reset color codes + } + + protected function fgColor($color) + { + if (! array_key_exists($color, $this->fgColors)) { + throw new IcingaException( + 'There is no such foreground color: %s', + $color + ); + } + return $this->fgColors[$color]; + } + + protected function bgColor($color) + { + if (! array_key_exists($color, $this->bgColors)) { + throw new IcingaException( + 'There is no such background color: %s', + $color + ); + } + return $this->bgColors[$color]; + } + + protected function startColor($fgColor = null, $bgColor = null) + { + $escape = "ESC["; + $parts = array(); + if ($fgColor !== null + && $bgColor !== null + && ! array_key_exists($bgColor, $this->bgColors) + && array_key_exists($bgColor, $this->fgColors) + && array_key_exists($fgColor, $this->bgColors) + ) { + $parts[] = '7'; // reverse video, negative image + $parts[] = $this->bgColor($fgColor); + $parts[] = $this->fgColor($bgColor); + } else { + if ($fgColor !== null) { + $parts[] = $this->fgColor($fgColor); + } + if ($bgColor !== null) { + $parts[] = $this->bgColor($bgColor); + } + } + if (empty($parts)) { + return ''; + } + return "\033[" . implode(';', $parts) . 'm'; + } +} diff --git a/library/Icinga/Cli/Command.php b/library/Icinga/Cli/Command.php new file mode 100644 index 0000000..d1a1986 --- /dev/null +++ b/library/Icinga/Cli/Command.php @@ -0,0 +1,208 @@ +app = $app; + $this->moduleName = $moduleName; + $this->commandName = $commandName; + $this->actionName = $actionName; + $this->params = $app->getParams(); + $this->screen = Screen::instance(); + $this->trace = $this->params->shift('trace', false); + $this->isVerbose = $this->params->shift('verbose', false); + $this->isDebugging = $this->params->shift('debug', false); + $this->configs = []; + + $this->translationDomain = $moduleName ?: 'icinga'; + + if ($this->loadEnabledModules) { + try { + $app->getModuleManager()->loadEnabledModules(); + } catch (NotReadableError $e) { + Logger::error(new IcingaException('Cannot load enabled modules. An exception was thrown:', $e)); + } + } + + if ($initialize) { + $this->init(); + } + } + + public function Config($file = null) + { + if ($this->isModule()) { + return $this->getModuleConfig($file); + } else { + return $this->getMainConfig($file); + } + } + + private function getModuleConfig($file = null) + { + if ($file === null) { + if ($this->config === null) { + $this->config = Config::module($this->moduleName); + } + return $this->config; + } else { + if (! array_key_exists($file, $this->configs)) { + $this->configs[$file] = Config::module($this->moduleName, $file); + } + return $this->configs[$file]; + } + } + + private function getMainConfig($file = null) + { + if ($file === null) { + if ($this->config === null) { + $this->config = Config::app(); + } + return $this->config; + } else { + if (! array_key_exists($file, $this->configs)) { + $this->configs[$file] = Config::app($file); + } + return $this->configs[$file]; + } + return $this->config; + } + + public function isModule() + { + return substr(get_class($this), 0, 14) === 'Icinga\\Module\\'; + } + + public function setParams(Params $params) + { + $this->params = $params; + } + + public function hasRemainingParams() + { + return $this->params->count() > 0; + } + + public function showTrace() + { + return $this->trace; + } + + public function fail($msg) + { + throw new IcingaException('%s', $msg); + } + + public function getDefaultActionName() + { + return $this->defaultActionName; + } + + /** + * Get {@link moduleName} + * + * @return string + */ + public function getModuleName() + { + return $this->moduleName; + } + + public function hasDefaultActionName() + { + return $this->hasActionName($this->defaultActionName); + } + + public function hasActionName($name) + { + $actions = $this->listActions(); + return in_array($name, $actions); + } + + public function listActions() + { + $actions = array(); + foreach (get_class_methods($this) as $method) { + if (preg_match('~^([A-Za-z0-9]+)Action$~', $method, $m)) { + $actions[] = $m[1]; + } + } + sort($actions); + return $actions; + } + + public function docs() + { + if ($this->docs === null) { + $this->docs = new Documentation($this->app); + } + return $this->docs; + } + + public function showUsage($action = null) + { + if ($action === null) { + $action = $this->actionName; + } + echo $this->docs()->usage( + $this->moduleName, + $this->commandName, + $action + ); + return false; + } + + public function init() + { + } +} diff --git a/library/Icinga/Cli/Documentation.php b/library/Icinga/Cli/Documentation.php new file mode 100644 index 0000000..111745e --- /dev/null +++ b/library/Icinga/Cli/Documentation.php @@ -0,0 +1,162 @@ +app = $app; + $this->loader = $app->cliLoader(); + } + + public function usage($module = null, $command = null, $action = null) + { + if ($module) { + $module = $this->loader->resolveModuleName($module); + return $this->moduleUsage($module, $command, $action); + } + if ($command) { + $command = $this->loader->resolveCommandName($command); + return $this->commandUsage($command, $action); + } + return $this->globalUsage(); + } + + public function globalUsage() + { + $d = "USAGE: icingacli [module] [action] [options]\n\n" + . "Available commands:\n\n"; + foreach ($this->loader->listCommands() as $command) { + if ($command !== 'autocomplete') { + $obj = $this->loader->getCommandInstance($command); + $d .= sprintf( + " %-14s %s\n", + $command, + $this->getClassTitle($obj) + ); + } + } + $d .= "\nAvailable modules:\n\n"; + foreach ($this->loader->listModules() as $module) { + $d .= ' ' . $module . "\n"; + } + $d .= "\nGlobal options:\n\n" + . " --log [t] Log to , either stderr, file or syslog (default: stderr)\n" + . " --log-path Which file to log into in case of --log file\n" + . " --verbose Be verbose\n" + . " --debug Show debug output\n" + . " --help Show help\n" + . " --benchmark Show benchmark summary\n" + . " --watch [s] Refresh output every seconds (default: 5)\n" + . " --version Shows version of Icinga Web 2, loaded modules and PHP\n" + ; + $d .= "\nShow help on a specific command : icingacli help " + . "\nShow help on a specific module : icingacli help " + . "\n"; + return $d; + } + + public function moduleUsage($module, $command = null, $action = null) + { + $commands = $this->loader->listModuleCommands($module); + + if (empty($commands)) { + return "The '$module' module does not provide any CLI commands\n"; + } + $d = ''; + if ($command) { + $obj = $this->loader->getModuleCommandInstance($module, $command); + } + if ($command === null) { + $d = "USAGE: icingacli $module [] [options]\n\n" + . "Available commands:\n\n"; + foreach ($commands as $command) { + $d .= ' ' . $command . "\n"; + } + $d .= "\nShow help on a specific command: icingacli help $module \n"; + } elseif ($action === null) { + $d .= $this->showCommandActions($obj, $command); + } else { + $action = $this->loader->resolveObjectActionName($obj, $action); + $d .= $this->getMethodDocumentation($obj, $action); + } + return $d; + } + + /** + * @param Command $command + * @param string $name + * + * @return string + */ + protected function showCommandActions($command, $name) + { + $actions = $command->listActions(); + $d = $this->getClassDocumentation($command) + . "Available actions:\n\n"; + foreach ($actions as $action) { + $d .= sprintf( + " %-14s %s\n", + $action, + $this->getMethodTitle($command, $action) + ); + } + $d .= "\nShow help on a specific action: icingacli help "; + if ($command->isModule()) { + $d .= $command->getModuleName() . ' '; + } + $d .= "$name \n"; + return $d; + } + + public function commandUsage($command, $action = null) + { + $obj = $this->loader->getCommandInstance($command); + $action = $this->loader->resolveObjectActionName($obj, $action); + + $d = "\n"; + if ($action) { + $d .= $this->getMethodDocumentation($obj, $action); + } else { + $d .= $this->showCommandActions($obj, $command); + } + return $d; + } + + protected function getClassTitle($class) + { + $ref = new ReflectionClass($class); + $comment = new CommentParser($ref->getDocComment()); + return $comment->getTitle(); + } + + protected function getClassDocumentation($class) + { + $ref = new ReflectionClass($class); + $comment = new CommentParser($ref->getDocComment()); + return $comment->dump(); + } + + protected function getMethodTitle($class, $method) + { + $ref = new ReflectionMethod($class, $method . 'Action'); + $comment = new CommentParser($ref->getDocComment()); + return $comment->getTitle(); + } + + protected function getMethodDocumentation($class, $method) + { + $ref = new ReflectionMethod($class, $method . 'Action'); + $comment = new CommentParser($ref->getDocComment()); + return $comment->dump(); + } +} diff --git a/library/Icinga/Cli/Documentation/CommentParser.php b/library/Icinga/Cli/Documentation/CommentParser.php new file mode 100644 index 0000000..4104848 --- /dev/null +++ b/library/Icinga/Cli/Documentation/CommentParser.php @@ -0,0 +1,85 @@ +raw = $raw; + if ($raw) { + $this->parse(); + } + } + + public function getTitle() + { + return $this->title; + } + + protected function parse() + { + $plain = $this->raw; + + // Strip comment start /** + $plain = preg_replace('~^/\s*\*\*\n~s', '', $plain); + + // Strip comment end */ + $plain = preg_replace('~\n\s*\*/\s*~s', "\n", $plain); + $p = null; + foreach (preg_split('~\n~', $plain) as $line) { + // Strip * at line start + $line = preg_replace('~^\s*\*\s?~', '', $line); + $line = rtrim($line); + if ($this->title === null) { + $this->title = $line; + continue; + } + if ($p === null && empty($this->paragraphs)) { + $p = & $this->paragraphs[]; + } + + if ($line === '') { + if ($p !== null) { + $p = & $this->paragraphs[]; + } + continue; + } + if ($p === null) { + $p = $line; + } else { + if (substr($line, 0, 2) === ' ') { + $p .= "\n" . $line; + } else { + $p .= ' ' . $line; + } + } + } + if ($p === null) { + array_pop($this->paragraphs); + } + } + + public function dump() + { + if ($this->title) { + $res = $this->title . "\n" . str_repeat('=', strlen($this->title)) . "\n\n"; + } else { + $res = ''; + } + + foreach ($this->paragraphs as $p) { + $res .= wordwrap($p, Screen::instance()->getColumns()) . "\n\n"; + } + + return $res; + } +} diff --git a/library/Icinga/Cli/Loader.php b/library/Icinga/Cli/Loader.php new file mode 100644 index 0000000..732c3bc --- /dev/null +++ b/library/Icinga/Cli/Loader.php @@ -0,0 +1,499 @@ +app = $app; + $this->coreAppDir = $app->getApplicationDir('clicommands'); + } + + /** + * Screen shortcut + * + * @return Screen + */ + protected function screen() + { + if ($this->screen === null) { + $this->screen = Screen::instance(STDERR); + } + + return $this->screen; + } + + /** + * Documentation shortcut + * + * @return Documentation + */ + protected function docs() + { + if ($this->docs === null) { + $this->docs = new Documentation($this->app); + } + return $this->docs; + } + + /** + * Show given message and exit + * + * @param string $msg message to show + */ + public function fail($msg) + { + fprintf(STDERR, "%s: %s\n", $this->screen()->colorize('ERROR', 'red'), $msg); + exit(1); + } + + public function getModuleName() + { + return $this->moduleName; + } + + public function setModuleName($name) + { + $this->moduleName = $name; + return $this; + } + + public function getCommandName() + { + return $this->commandName; + } + + public function getActionName() + { + return $this->actionName; + } + + public function getCommandInstance($command) + { + if (! array_key_exists($command, $this->commandInstances)) { + $this->assertCommandExists($command); + require_once $this->commandFileMap[$command]; + $className = $this->commandClassMap[$command]; + $this->commandInstances[$command] = new $className( + $this->app, + null, + $command, + null, + false + ); + } + return $this->commandInstances[$command]; + } + + public function getModuleCommandInstance($module, $command) + { + if (! array_key_exists($command, $this->moduleInstances[$module])) { + $this->assertModuleCommandExists($module, $command); + require_once $this->moduleFileMap[$module][$command]; + $className = $this->moduleClassMap[$module][$command]; + $this->moduleInstances[$module][$command] = new $className( + $this->app, + $module, + $command, + null, + false + ); + } + return $this->moduleInstances[$module][$command]; + } + + public function getLastSuggestions() + { + return $this->lastSuggestions; + } + + public function showLastSuggestions() + { + if (! empty($this->lastSuggestions)) { + foreach ($this->lastSuggestions as & $s) { + $s = $this->screen()->colorize($s, 'lightblue'); + } + fprintf( + STDERR, + "Did you mean %s?\n", + implode(" or ", $this->lastSuggestions) + ); + } + } + + public function parseParams(Params $params = null) + { + if ($params === null) { + $params = $this->app->getParams(); + } + + if ($this->moduleName === null) { + $first = $params->shift(); + if (! $first) { + return; + } + $found = $this->resolveName($first); + } else { + $found = $this->moduleName; + } + if (! $found) { + $msg = "There is no such module or command: '$first'"; + fprintf(STDERR, "%s: %s\n", $this->screen()->colorize('ERROR', 'red'), $msg); + $this->showLastSuggestions(); + fwrite(STDERR, "\n"); + } + + $obj = null; + if ($this->hasCommand($found)) { + $this->commandName = $found; + $obj = $this->getCommandInstance($this->commandName); + } elseif ($this->hasModule($found)) { + $this->moduleName = $found; + $command = $this->resolveModuleCommandName($found, $params->shift()); + if ($command) { + $this->commandName = $command; + $obj = $this->getModuleCommandInstance( + $this->moduleName, + $this->commandName + ); + } + } + if ($obj !== null) { + $action = $this->resolveObjectActionName( + $obj, + $params->getStandalone() + ); + if ($obj->hasActionName($action)) { + $this->actionName = $action; + $params->shift(); + } elseif ($obj->hasDefaultActionName()) { + $this->actionName = $obj->getDefaultActionName(); + } + } + return $this; + } + + public function handleParams(Params $params = null) + { + $this->parseParams($params); + $this->dispatch(); + } + + public function dispatch(Params $overrideParams = null) + { + if ($this->commandName === null) { + fwrite(STDERR, $this->docs()->usage($this->moduleName)); + return false; + } elseif ($this->actionName === null) { + fwrite(STDERR, $this->docs()->usage($this->moduleName, $this->commandName)); + return false; + } + + try { + if ($this->moduleName) { + $this->app->getModuleManager()->loadModule($this->moduleName); + $obj = $this->getModuleCommandInstance( + $this->moduleName, + $this->commandName + ); + } else { + $obj = $this->getCommandInstance($this->commandName); + } + if ($overrideParams !== null) { + $obj->setParams($overrideParams); + } + $obj->init(); + return $obj->{$this->actionName . 'Action'}(); + } catch (Exception $e) { + if ($obj && $obj instanceof Command && $obj->showTrace()) { + fwrite(STDERR, $this->formatTrace($e->getTrace())); + } + + $this->fail(IcingaException::describe($e)); + } + } + + protected function searchMatch($needle, $haystack) + { + if ($needle === null) { + $needle = ''; + } + + $this->lastSuggestions = preg_grep(sprintf('/^%s.*$/', preg_quote($needle, '/')), $haystack); + $match = array_search($needle, $haystack, true); + if (false !== $match) { + return $haystack[$match]; + } + if (count($this->lastSuggestions) === 1) { + $lastSuggestions = array_values($this->lastSuggestions); + return $lastSuggestions[0]; + } + return false; + } + + public function resolveName($name) + { + return $this->searchMatch( + $name, + array_merge($this->listCommands(), $this->listModules()) + ); + } + + public function resolveCommandName($name) + { + return $this->searchMatch($name, $this->listCommands()); + } + + public function resolveModuleName($name) + { + return $this->searchMatch($name, $this->listModules()); + } + + public function resolveModuleCommandName($module, $name) + { + return $this->searchMatch($name, $this->listModuleCommands($module)); + } + + public function resolveObjectActionName($obj, $name) + { + return $this->searchMatch($name, $obj->listActions()); + } + + protected function assertModuleExists($module) + { + if (! $this->hasModule($module)) { + throw new ProgrammingError( + 'There is no such module: %s', + $module + ); + } + } + + protected function assertCommandExists($command) + { + if (! $this->hasCommand($command)) { + throw new ProgrammingError( + 'There is no such command: %s', + $command + ); + } + } + + protected function assertModuleCommandExists($module, $command) + { + $this->assertModuleExists($module); + if (! $this->hasModuleCommand($module, $command)) { + throw new ProgrammingError( + 'The module \'%s\' has no such command: %s', + $module, + $command + ); + } + } + + protected function formatTrace($trace) + { + $output = array(); + foreach ($trace as $i => $step) { + $object = ''; + if (isset($step['object']) && is_object($step['object'])) { + $object = sprintf('[%s]', get_class($step['object'])) . $step['type']; + } elseif (! empty($step['object'])) { + $object = (string) $step['object'] . $step['type']; + } + if (isset($step['args']) && is_array($step['args'])) { + foreach ($step['args'] as & $arg) { + if (is_object($arg)) { + $arg = sprintf('[%s]', get_class($arg)); + } + if (is_string($arg)) { + $arg = preg_replace('~\n~', '\n', $arg); + if (strlen($arg) > 50) { + $arg = substr($arg, 0, 47) . '...'; + } + $arg = "'" . $arg . "'"; + } + if ($arg === null) { + $arg = 'NULL'; + } + if (is_bool($arg)) { + $arg = $arg ? 'TRUE' : 'FALSE'; + } + } + } else { + $step['args'] = array(); + } + $args = $step['args']; + foreach ($args as & $v) { + if (is_array($v)) { + $v = var_export($v, 1); + } else { + $v = (string) $v; + } + } + $output[$i] = sprintf( + '#%d %s:%d %s%s(%s)', + $i, + isset($step['file']) ? preg_replace( + '~.+/library/~', + 'library/', + $step['file'] + ) : '[unknown file]', + isset($step['line']) ? $step['line'] : '0', + $object, + $step['function'], + implode(', ', $args) + ); + } + return implode(PHP_EOL, $output) . PHP_EOL; + } + + public function hasCommand($name) + { + return in_array($name, $this->listCommands()); + } + + public function hasModule($name) + { + return in_array($name, $this->listModules()); + } + + public function hasModuleCommand($module, $name) + { + return in_array($name, $this->listModuleCommands($module)); + } + + public function listModules() + { + if ($this->modules === null) { + $this->modules = array(); + try { + $this->modules = array_unique(array_merge( + $this->app->getModuleManager()->listEnabledModules(), + $this->app->getModuleManager()->listLoadedModules() + )); + } catch (NotReadableError $e) { + $this->fail($e->getMessage()); + } + } + return $this->modules; + } + + protected function retrieveCommandsFromDir($dirname) + { + $commands = array(); + if (! @file_exists($dirname) || ! is_readable($dirname)) { + return $commands; + } + + $base = opendir($dirname); + if ($base === false) { + return $commands; + } + while (false !== ($dir = readdir($base))) { + if ($dir[0] === '.') { + continue; + } + if (preg_match('~^([A-Za-z0-9]+)Command\.php$~', $dir, $m)) { + $cmd = strtolower($m[1]); + $commands[] = $cmd; + } + } + closedir($base); + sort($commands); + return $commands; + } + + public function listCommands() + { + if ($this->commands === null) { + $this->commands = array(); + $ns = 'Icinga\\Clicommands\\'; + $this->commands = $this->retrieveCommandsFromDir($this->coreAppDir); + foreach ($this->commands as $cmd) { + $this->commandClassMap[$cmd] = $ns . ucfirst($cmd) . 'Command'; + $this->commandFileMap[$cmd] = $this->coreAppDir . '/' . ucfirst($cmd) . 'Command.php'; + } + } + return $this->commands; + } + + public function listModuleCommands($module) + { + if (! array_key_exists($module, $this->moduleCommands)) { + $ns = 'Icinga\\Module\\' . ucfirst($module) . '\\Clicommands\\'; + $this->assertModuleExists($module); + $manager = $this->app->getModuleManager(); + $manager->loadModule($module); + $dir = $manager->getModuleDir($module) . '/application/clicommands'; + $this->moduleCommands[$module] = $this->retrieveCommandsFromDir($dir); + $this->moduleInstances[$module] = array(); + foreach ($this->moduleCommands[$module] as $cmd) { + $this->moduleClassMap[$module][$cmd] = $ns . ucfirst($cmd) . 'Command'; + $this->moduleFileMap[$module][$cmd] = $dir . '/' . ucfirst($cmd) . 'Command.php'; + } + } + return $this->moduleCommands[$module]; + } +} diff --git a/library/Icinga/Cli/Params.php b/library/Icinga/Cli/Params.php new file mode 100644 index 0000000..463d4ae --- /dev/null +++ b/library/Icinga/Cli/Params.php @@ -0,0 +1,320 @@ +program = array_shift($argv); + for ($i = 0; $i < count($argv); $i++) { + if ($argv[$i] === '--') { + $noOptionFlag = true; + } elseif (!$noOptionFlag && substr($argv[$i], 0, 2) === '--') { + $key = substr($argv[$i], 2); + $matches = array(); + if (1 === preg_match( + '/(?params[$matches[1]] = $matches[2]; + } elseif (! isset($argv[$i + 1]) || substr($argv[$i + 1], 0, 2) === '--') { + $this->params[$key] = true; + } elseif (array_key_exists($key, $this->params)) { + if (!is_array($this->params[$key])) { + $this->params[$key] = array($this->params[$key]); + } + $this->params[$key][] = $argv[++$i]; + } else { + $this->params[$key] = $argv[++$i]; + } + } else { + $this->standalone[] = $argv[$i]; + } + } + } + + /** + * Return the value for an argument by position + * + * @param int $pos The position of the argument + * @param mixed $default The default value to return + * + * @return mixed + */ + public function getStandalone($pos = 0, $default = null) + { + if (isset($this->standalone[$pos])) { + return $this->standalone[$pos]; + } + return $default; + } + + /** + * Count and return the number of arguments and options + * + * @return int + */ + public function count() + { + return count($this->standalone) + count($this->params); + } + + /** + * Return the options + * + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * Return the arguments + * + * @return array + */ + public function getAllStandalone() + { + return $this->standalone; + } + + /** + * Support isset() and empty() checks on options + * + * @param $name + * + * @return bool + */ + public function __isset($name) + { + return isset($this->params[$name]); + } + + /** + * @see Params::get() + */ + public function __get($key) + { + return $this->get($key); + } + + /** + * Return whether the given option exists + * + * @param string $key The option name to check + * + * @return bool + */ + public function has($key) + { + return array_key_exists($key, $this->params); + } + + /** + * Return the value of the given option + * + * @param string $key The option name + * @param mixed $default The default value to return + * + * @return mixed + */ + public function get($key, $default = null) + { + if ($this->has($key)) { + return $this->params[$key]; + } + return $default; + } + + /** + * Require a parameter + * + * @param string $name Name of the parameter + * @param bool $strict Whether the parameter's value must not be the empty string + * + * @return mixed + * + * @throws MissingParameterException If the parameter was not given + */ + public function getRequired($name, $strict = true) + { + if ($this->has($name)) { + $value = $this->get($name); + if (! $strict || strlen($value) > 0) { + return $value; + } + } + $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name); + $e->setParameter($name); + throw $e; + } + + /** + * Set a value for the given option + * + * @param string $key The option name + * @param mixed $value The value to set + * + * @return $this + */ + public function set($key, $value) + { + $this->params[$key] = $value; + return $this; + } + + /** + * Remove a single option or multiple options + * + * @param string|array $keys The option or options to remove + * + * @return $this + */ + public function remove($keys = array()) + { + if (! is_array($keys)) { + $keys = array($keys); + } + foreach ($keys as $key) { + if (array_key_exists($key, $this->params)) { + unset($this->params[$key]); + } + } + return $this; + } + + /** + * Return a copy of this object with the given options being removed + * + * @param string|array $keys The option or options to remove + * + * @return Params + */ + public function without($keys = array()) + { + $params = clone($this); + return $params->remove($keys); + } + + /** + * Remove and return the value of the given option + * + * Called multiple times for an option with multiple values returns + * them one by one in case the default is not an array. + * + * @param string $key The option name + * @param mixed $default The default value to return + * + * @return mixed + */ + public function shift($key = null, $default = null) + { + if ($key === null) { + if (count($this->standalone) > 0) { + return array_shift($this->standalone); + } + return $default; + } + $result = $this->get($key, $default); + if (is_array($result) && !is_array($default)) { + $result = array_shift($result) || $default; + if ($result === $default) { + $this->remove($key); + } + } else { + $this->remove($key); + } + return $result; + } + + /** + * Require and remove a parameter + * + * @param string $name Name of the parameter + * @param bool $strict Whether the parameter's value must not be the empty string + * + * @return mixed + * + * @throws MissingParameterException If the parameter was not given + */ + public function shiftRequired($name, $strict = true) + { + if ($this->has($name)) { + $value = $this->get($name); + if (! $strict || strlen($value) > 0) { + $this->shift($name); + return $value; + } + } + $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name); + $e->setParameter($name); + throw $e; + } + + /** + * Put the given value onto the argument stack + * + * @param mixed $key The argument + * + * @return $this + */ + public function unshift($key) + { + array_unshift($this->standalone, $key); + return $this; + } + + /** + * Parse the given commandline + * + * @param array $argv The commandline to parse + * + * @return Params + */ + public static function parse($argv = null) + { + if ($argv === null) { + $argv = $GLOBALS['argv']; + } + $params = new self($argv); + return $params; + } +} diff --git a/library/Icinga/Cli/Screen.php b/library/Icinga/Cli/Screen.php new file mode 100644 index 0000000..4ffad72 --- /dev/null +++ b/library/Icinga/Cli/Screen.php @@ -0,0 +1,106 @@ +strlen($txt); + $width = floor(($this->getColumns() + $len) / 2) - $len; + return str_repeat(' ', $width) . $txt; + } + + public function hasUtf8() + { + if ($this->isUtf8 === null) { + // null should equal 0 here, however seems to equal '' on some systems: + $current = setlocale(LC_ALL, 0); + + $parts = preg_split('/;/', $current); + $lc_parts = array(); + foreach ($parts as $part) { + if (strpos($part, '=') === false) { + continue; + } + list($key, $val) = preg_split('/=/', $part, 2); + $lc_parts[$key] = $val; + } + + $this->isUtf8 = array_key_exists('LC_CTYPE', $lc_parts) + && preg_match('~\.UTF-8$~i', $lc_parts['LC_CTYPE']); + } + return $this->isUtf8; + } + + public function clear() + { + return "\n"; + } + + public function underline($text) + { + return $text; + } + + public function colorize($text, $fgColor = null, $bgColor = null) + { + return $text; + } + + public static function instance($output = STDOUT) + { + if (! isset(self::$instances[(int) $output])) { + if (function_exists('posix_isatty') && posix_isatty($output)) { + self::$instances[(int) $output] = new AnsiScreen(); + } else { + self::$instances[(int) $output] = new Screen(); + } + } + + return self::$instances[(int) $output]; + } +} diff --git a/library/Icinga/Common/Database.php b/library/Icinga/Common/Database.php new file mode 100644 index 0000000..4c97765 --- /dev/null +++ b/library/Icinga/Common/Database.php @@ -0,0 +1,56 @@ +hasDb()) { + throw new LogicException('Please check if a db instance exists at all'); + } + + $config = new SqlConfig(ResourceFactory::getResourceConfig( + IcingaConfig::app()->get('global', 'config_resource') + )); + if ($config->db === 'mysql') { + $config->charset = 'utf8mb4'; + } + + $config->options = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ]; + if ($config->db === 'mysql') { + $config->options[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET SESSION SQL_MODE='STRICT_TRANS_TABLES" + . ",NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"; + } + + return new Connection($config); + } + + /** + * Check if db exists + * + * @return bool true if a database was found otherwise false + */ + protected function hasDb() + { + return (bool) IcingaConfig::app()->get('global', 'config_resource'); + } +} diff --git a/library/Icinga/Common/PdfExport.php b/library/Icinga/Common/PdfExport.php new file mode 100644 index 0000000..c3f10d4 --- /dev/null +++ b/library/Icinga/Common/PdfExport.php @@ -0,0 +1,105 @@ +getModuleManager()->has('pdfexport')) { + throw new ConfigurationError('The pdfexport module is required for exports to PDF'); + } + + putenv('ICINGAWEB_EXPORT_FORMAT=pdf'); + Environment::raiseMemoryLimit('512M'); + Environment::raiseExecutionTime(300); + + $time = DateFormatter::formatDateTime(time()); + $iconPath = is_readable($this->pdfHeaderImage) + ? $this->pdfHeaderImage + : Icinga::app()->getBootstrapDirectory() . '/' . $this->pdfHeaderImage; + $encodedIcon = is_readable($iconPath) ? base64_encode(file_get_contents($iconPath)) : null; + $html = $this instanceof CompatController && ! empty($this->content) + ? $this->content + : $this->renderControllerAction(); + + $doc = (new PrintableHtmlDocument()) + ->setTitle($this->view->title) + ->setHeader(Html::wantHtml([ + Html::tag('span', ['class' => 'title']), + $encodedIcon + ? Html::tag('img', ['height' => 13, 'src' => 'data:image/png;base64,' . $encodedIcon]) + : null, + Html::tag('time', null, $time) + ])) + ->setFooter(Html::wantHtml([ + Html::tag('span', null, [ + t('Page') . ' ', + Html::tag('span', ['class' => 'pageNumber']), + ' / ', + Html::tag('span', ['class' => 'totalPages']) + ]), + Html::tag('p', null, rawurldecode(Url::fromRequest()->setParams($this->params))) + ])) + ->addHtml($html); + + if (($moduleName = $this->getRequest()->getModuleName()) !== 'default') { + $doc->getAttributes()->add('class', 'icinga-module module-' . $moduleName); + } + + \Icinga\Module\Pdfexport\ProvidedHook\Pdfexport::first()->streamPdfFromHtml($doc, sprintf( + '%s-%s', + $this->view->title ?: $this->getRequest()->getActionName(), + $time + )); + } + + /** + * Render the requested action + * + * @return ValidHtml + */ + protected function renderControllerAction() + { + /** @var Controller $this */ + $this->view->compact = true; + + $viewRenderer = $this->getHelper('viewRenderer'); + $viewRenderer->postDispatch(); + + $layoutHelper = $this->getHelper('layout'); + $oldLayout = $layoutHelper->getLayout(); + $layout = $layoutHelper->setLayout('inline'); + + $layout->content = $this->getResponse(); + $html = $layout->render(); + + // Restore previous layout and reset content, to properly show errors + $this->getResponse()->clearBody($viewRenderer->getResponseSegment()); + $layoutHelper->setLayout($oldLayout); + + return HtmlString::create($html); + } +} diff --git a/library/Icinga/Crypt/AesCrypt.php b/library/Icinga/Crypt/AesCrypt.php new file mode 100644 index 0000000..8e9d453 --- /dev/null +++ b/library/Icinga/Crypt/AesCrypt.php @@ -0,0 +1,337 @@ +encrypt($data); // Accepts a string + * + * + * // Encrypt and encode to Base64 + * $encryptedData = (new AesCrypt())->encryptToBase64($data); // Accepts a string + * + * + * // Decryption + * $aesCrypt = (new AesCrypt()) + * ->setTag($tag) // if exists + * ->setIV($iv) + * ->setKey($key); + * + * $decryptedData = $aesCrypt->decrypt($data); + * + * // Decode from Base64 and decrypt + * $aesCrypt = (new AesCrypt()) + * ->setTag($tag) + * ->setIV($iv) + * ->setKey($key); + * + * $decryptedData = $aesCrypt->decryptFromBase64($data); + * ``` + * + */ +class AesCrypt +{ + /** @var array The list of cipher methods */ + const METHODS = [ + 'aes-256-gcm', + 'aes-256-cbc', + 'aes-256-ctr' + ]; + + /** @var string The encryption key */ + private $key; + + /** @var int The length of the key */ + private $keyLength; + + /** @var string The initialization vector which is not NULL */ + private $iv; + + /** @var string The authentication tag which is passed by reference when using AEAD cipher mode */ + private $tag; + + /** @var string The cipher method */ + private $method; + + public function __construct($keyLength = 128) + { + $this->keyLength = $keyLength; + } + + /** + * Set the method + * + * @return $this + */ + public function setMethod($method) + { + $this->method = $method; + + return $this; + } + + /** + * Get the method + * + * @return string + */ + public function getMethod() + { + if ($this->method === null) { + $this->method = $this->getSupportedMethod(); + } + + return $this->method; + } + + /** + * Get supported method + * + * @return string + * + * @throws RuntimeException If none of the methods listed in the METHODS array is available + */ + protected function getSupportedMethod() + { + $availableMethods = openssl_get_cipher_methods(); + $methods = self::METHODS; + + if (! $this->isAuthenticatedEncryptionSupported()) { + unset($methods[0]); + } + + foreach ($methods as $method) { + if (in_array($method, $availableMethods)) { + return $method; + } + } + + throw new RuntimeException('No supported method found'); + } + + /** + * Set the key + * + * @return $this + */ + public function setKey($key) + { + $this->key = $key; + + return $this; + } + + /** + * Get the key + * + * @return string + * + */ + public function getKey() + { + if (empty($this->key)) { + $this->key = random_bytes($this->keyLength); + } + + return $this->key; + } + + /** + * Set the IV + * + * @return $this + */ + public function setIV($iv) + { + $this->iv = $iv; + + return $this; + } + + /** + * Get the IV + * + * @return string + * + */ + public function getIV() + { + if (empty($this->iv)) { + $len = openssl_cipher_iv_length($this->getMethod()); + $this->iv = random_bytes($len); + } + + return $this->iv; + } + + /** + * Set the Tag + * + * @return $this + * + * @throws RuntimeException If a tag is available but authenticated encryption (AE) is not supported. + * + * @throws UnexpectedValueException If tag length is less then 16 + */ + public function setTag($tag) + { + if (! $this->isAuthenticatedEncryptionSupported()) { + throw new RuntimeException(sprintf( + "The given decryption method is not supported in php version '%s'", + PHP_VERSION + )); + } + + if (strlen($tag) !== 16) { + throw new UnexpectedValueException(sprintf( + 'expects tag length to be 16, got instead %s', + strlen($tag) + )); + } + + $this->tag = $tag; + + return $this; + } + + /** + * Get the Tag + * + * @return string + * + * @throws RuntimeException If the Tag is not set + */ + public function getTag() + { + if (empty($this->tag)) { + throw new RuntimeException('No tag set'); + } + + return $this->tag; + } + + /** + * Decrypt the given string + * + * @param string $data + * + * @return string + * + * @throws RuntimeException If decryption fails + */ + public function decrypt($data) + { + if (! $this->isAuthenticatedEncryptionRequired()) { + return $this->nonAEDecrypt($data); + } + + $decrypt = openssl_decrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV(), $this->getTag()); + + if ($decrypt === false) { + throw new RuntimeException('Decryption failed'); + } + + return $decrypt; + } + + /** + * Encrypt the given string + * + * @param string $data + * + * @return string encrypted string + * + * @throws RuntimeException If decryption fails + */ + public function encrypt($data) + { + if (! $this->isAuthenticatedEncryptionRequired()) { + return $this->nonAEEncrypt($data); + } + + $encrypt = openssl_encrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV(), $this->tag); + + if ($encrypt === false) { + throw new RuntimeException('Encryption failed'); + } + + return $encrypt; + } + + /** + * Decrypt the given string with non Authenticated encryption (AE) cipher method + * + * @param string $data + * + * @return string decrypted string + * + * @throws RuntimeException If decryption fails + */ + private function nonAEDecrypt($data) + { + $c = base64_decode($data); + $hmac = substr($c, 0, 32); + $data = substr($c, 32); + + $decrypt = openssl_decrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV()); + $calcHmac = hash_hmac('sha256', $this->getIV() . $data, $this->getKey(), true); + + if ($decrypt === false || ! hash_equals($hmac, $calcHmac)) { + throw new RuntimeException('Decryption failed'); + } + + return $decrypt; + } + + /** + * Encrypt the given string with non Authenticated encryption (AE) cipher method + * + * @param string $data + * + * @return string encrypted string + * + * @throws RuntimeException If encryption fails + */ + private function nonAEEncrypt($data) + { + $encrypt = openssl_encrypt($data, $this->getMethod(), $this->getKey(), 0, $this->getIV()); + + if ($encrypt === false) { + throw new RuntimeException('Encryption failed'); + } + + $hmac = hash_hmac('sha256', $this->getIV() . $encrypt, $this->getKey(), true); + + return base64_encode($hmac . $encrypt); + } + + /** + * Whether the Authenticated encryption (a tag) is required + * + * @return bool True if required false otherwise + */ + public function isAuthenticatedEncryptionRequired() + { + return $this->getMethod() === 'aes-256-gcm'; + } + + /** + * Whether the php version supports Authenticated encryption (AE) or not + * + * @return bool True if supported false otherwise + */ + public function isAuthenticatedEncryptionSupported() + { + return PHP_VERSION_ID >= 70100; + } +} diff --git a/library/Icinga/Data/ConfigObject.php b/library/Icinga/Data/ConfigObject.php new file mode 100644 index 0000000..c9a3134 --- /dev/null +++ b/library/Icinga/Data/ConfigObject.php @@ -0,0 +1,289 @@ +data as $key => $value) { + if ($value instanceof self) { + $array[$key] = clone $value; + } else { + $array[$key] = $value; + } + } + + $this->data = $array; + } + + /** + * Reset the current position of $this->data + * + * @return void + */ + public function rewind(): void + { + reset($this->data); + } + + /** + * Return the section's or property's value of the current iteration + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return current($this->data); + } + + /** + * Return whether the position of the current iteration is valid + * + * @return bool + */ + public function valid(): bool + { + return key($this->data) !== null; + } + + /** + * Return the section's or property's name of the current iteration + * + * @return string + */ + public function key(): string + { + return key($this->data); + } + + /** + * Advance the position of the current iteration and return the new section's or property's value + * + * @return void + */ + public function next(): void + { + next($this->data); + } + + /** + * Return whether the given section or property is set + * + * @param string $key The name of the section or property + * + * @return bool + */ + public function __isset($key) + { + return isset($this->data[$key]); + } + + /** + * Return the value for the given property or the config for the given section + * + * @param string $key The name of the property or section + * + * @return mixed|NULL The value or NULL in case $key does not exist + */ + public function __get($key) + { + return $this->get($key); + } + + /** + * Add a new property or section + * + * @param string $key The name of the new property or section + * @param mixed $value The value to set for the new property or section + */ + public function __set($key, $value) + { + if (is_array($value)) { + $this->data[$key] = new static($value); + } else { + $this->data[$key] = $value; + } + } + + /** + * Remove the given property or section + * + * @param string $key The property or section to remove + */ + public function __unset($key) + { + unset($this->data[$key]); + } + + /** + * Return whether the given section or property is set + * + * @param string $key The name of the section or property + * + * @return bool + */ + public function offsetExists($key): bool + { + return isset($this->$key); + } + + /** + * Return the value for the given property or the config for the given section + * + * @param string $key The name of the property or section + * + * @return ?mixed The value or NULL in case $key does not exist + */ + #[\ReturnTypeWillChange] + public function offsetGet($key) + { + return $this->get($key); + } + + /** + * Add a new property or section + * + * @param string $key The name of the new property or section + * @param mixed $value The value to set for the new property or section + * + * @throws ProgrammingError If the key is null + */ + public function offsetSet($key, $value): void + { + if ($key === null) { + throw new ProgrammingError('Appending values without an explicit key is not supported'); + } + + $this->$key = $value; + } + + /** + * Remove the given property or section + * + * @param string $key The property or section to remove + */ + public function offsetUnset($key): void + { + unset($this->$key); + } + + /** + * Return whether this config has any data + * + * @return bool + */ + public function isEmpty() + { + return empty($this->data); + } + + /** + * Return the value for the given property or the config for the given section + * + * @param string $key The name of the property or section + * @param mixed $default The value to return in case the property or section is missing + * + * @return mixed + */ + public function get($key, $default = null) + { + if (array_key_exists($key, $this->data)) { + return $this->data[$key]; + } + + return $default; + } + + /** + * Return all section and property names + * + * @return array + */ + public function keys() + { + return array_keys($this->data); + } + + /** + * Return this config's data as associative array + * + * @return array + */ + public function toArray() + { + $array = array(); + foreach ($this->data as $key => $value) { + if ($value instanceof self) { + $array[$key] = $value->toArray(); + } else { + $array[$key] = $value; + } + } + + return $array; + } + + /** + * Merge the given data with this config + * + * @param array|ConfigObject $data An array or a config + * + * @return $this + */ + public function merge($data) + { + if ($data instanceof self) { + $data = $data->toArray(); + } + + foreach ($data as $key => $value) { + if (array_key_exists($key, $this->data)) { + if (is_array($value)) { + if ($this->data[$key] instanceof self) { + $this->data[$key]->merge($value); + } else { + $this->data[$key] = new static($value); + } + } else { + $this->data[$key] = $value; + } + } else { + $this->data[$key] = is_array($value) ? new static($value) : $value; + } + } + + return $this; + } +} diff --git a/library/Icinga/Data/ConnectionInterface.php b/library/Icinga/Data/ConnectionInterface.php new file mode 100644 index 0000000..bd7d026 --- /dev/null +++ b/library/Icinga/Data/ConnectionInterface.php @@ -0,0 +1,8 @@ +data = $data; + } + + /** + * Set the name of the column to map array keys on + * + * @param string $name + * + * @return $this + */ + public function setKeyColumn($name) + { + $this->keyColumn = $name; + return $this; + } + + /** + * Return the name of the column to map array keys on + * + * @return string + */ + public function getKeyColumn() + { + return $this->keyColumn; + } + + /** + * Provide a query for this data source + * + * @return SimpleQuery + */ + public function select() + { + return new SimpleQuery(clone $this); + } + + /** + * Fetch and return all rows of the given query's result set using an iterator + * + * @param SimpleQuery $query + * + * @return ArrayIterator + */ + public function query(SimpleQuery $query) + { + return new ArrayIterator($this->fetchAll($query)); + } + + /** + * Fetch and return a column of all rows of the result set as an array + * + * @param SimpleQuery $query + * + * @return array + */ + public function fetchColumn(SimpleQuery $query) + { + $result = array(); + foreach ($this->getResult($query) as $row) { + $arr = (array) $row; + $result[] = array_shift($arr); + } + + return $result; + } + + /** + * Fetch and return all rows of the given query's result as a flattened key/value based array + * + * @param SimpleQuery $query + * + * @return array + */ + public function fetchPairs(SimpleQuery $query) + { + $result = array(); + $keys = null; + foreach ($this->getResult($query) as $row) { + if ($keys === null) { + $keys = array_keys((array) $row); + if (count($keys) < 2) { + $keys[1] = $keys[0]; + } + } + + $result[$row->{$keys[0]}] = $row->{$keys[1]}; + } + + return $result; + } + + /** + * Fetch and return the first row of the given query's result + * + * @param SimpleQuery $query + * + * @return object|false The row or false in case the result is empty + */ + public function fetchRow(SimpleQuery $query) + { + $result = $this->getResult($query); + if (empty($result)) { + return false; + } + + return array_shift($result); + } + + /** + * Fetch and return all rows of the given query's result as an array + * + * @param SimpleQuery $query + * + * @return array + */ + public function fetchAll(SimpleQuery $query) + { + return $this->getResult($query); + } + + /** + * Count all rows of the given query's result + * + * @param SimpleQuery $query + * + * @return int + */ + public function count(SimpleQuery $query) + { + if ($this->count === null) { + $this->count = count($this->createResult($query)); + } + + return $this->count; + } + + /** + * Create and return the result for the given query + * + * @param SimpleQuery $query + * + * @return array + */ + protected function createResult(SimpleQuery $query) + { + $columns = $query->getColumns(); + $filter = $query->getFilter(); + $offset = $query->hasOffset() ? $query->getOffset() : 0; + $limit = $query->hasLimit() ? $query->getLimit() : 0; + $data = $this->data; + + if ($query->hasOrder()) { + uasort($data, [$query, 'compare']); + } + + $foundStringKey = false; + $result = []; + $skipped = 0; + foreach ($data as $key => $row) { + if ($this->keyColumn !== null && !isset($row->{$this->keyColumn})) { + $row = clone $row; // Make sure that this won't affect the actual data + $row->{$this->keyColumn} = $key; + } + + if (! $filter->matches($row)) { + continue; + } elseif ($skipped < $offset) { + $skipped++; + continue; + } + + // Get only desired columns if asked so + if (! empty($columns)) { + $filteredRow = (object) array(); + foreach ($columns as $alias => $name) { + if (! is_string($alias)) { + $alias = $name; + } + + if (isset($row->$name)) { + $filteredRow->$alias = $row->$name; + } else { + $filteredRow->$alias = null; + } + } + } else { + $filteredRow = $row; + } + + $foundStringKey |= is_string($key); + $result[$key] = $filteredRow; + + if (count($result) === $limit) { + break; + } + } + + if (! $foundStringKey) { + $result = array_values($result); + } + + return $result; + } + + /** + * Return whether a query result exists + * + * @return bool + */ + protected function hasResult() + { + return $this->result !== null; + } + + /** + * Set the current result + * + * @param array $result + * + * @return $this + */ + protected function setResult(array $result) + { + $this->result = $result; + return $this; + } + + /** + * Return the result for the given query + * + * @param SimpleQuery $query + * + * @return array + */ + protected function getResult(SimpleQuery $query) + { + if (! $this->hasResult()) { + $this->setResult($this->createResult($query)); + } + + return $this->result; + } +} diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php new file mode 100644 index 0000000..fc6814d --- /dev/null +++ b/library/Icinga/Data/Db/DbConnection.php @@ -0,0 +1,655 @@ + false, + Zend_Db::CASE_FOLDING => Zend_Db::CASE_LOWER + ); + + private static $driverOptions = array( + PDO::ATTR_TIMEOUT => 10, + PDO::ATTR_CASE => PDO::CASE_LOWER, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + + /** + * Create a new connection object + * + * @param ConfigObject $config + */ + public function __construct(ConfigObject $config = null) + { + $this->config = $config; + $this->connect(); + } + + /** + * Provide a query on this connection + * + * @return DbQuery + */ + public function select() + { + return new DbQuery($this); + } + + /** + * Fetch and return all rows of the given query's result set using an iterator + * + * @param DbQuery $query + * + * @return Iterator + */ + public function query(DbQuery $query) + { + return $query->getSelectQuery()->query(); + } + + /** + * Get the connection configuration + * + * @return ConfigObject + */ + public function getConfig() + { + return $this->config; + } + + /** + * Getter for database type + * + * @return string + */ + public function getDbType() + { + return $this->dbType; + } + + /** + * Getter for the Zend_Db_Adapter + * + * @return \Zend_Db_Adapter_Abstract + */ + public function getDbAdapter() + { + return $this->dbAdapter; + } + + /** + * Create a new connection + */ + private function connect() + { + $genericAdapterOptions = self::$genericAdapterOptions; + $driverOptions = self::$driverOptions; + $adapterParamaters = array( + 'host' => $this->config->host, + 'username' => $this->config->username, + 'password' => $this->config->password, + 'dbname' => $this->config->dbname, + 'charset' => $this->config->charset ?: null, + 'options' => & $genericAdapterOptions, + 'driver_options' => & $driverOptions + ); + $this->dbType = strtolower($this->config->get('db', 'mysql')); + switch ($this->dbType) { + case 'mssql': + $adapter = 'Pdo_Mssql'; + $pdoType = $this->config->get('pdoType'); + if (empty($pdoType)) { + if (extension_loaded('sqlsrv')) { + $adapter = 'Sqlsrv'; + } else { + $pdoType = 'dblib'; + } + } + if ($pdoType === 'dblib') { + // Driver does not support setting attributes + unset($adapterParamaters['options']); + unset($adapterParamaters['driver_options']); + } + if (! empty($pdoType)) { + $adapterParamaters['pdoType'] = $pdoType; + } + $defaultPort = 1433; + break; + case 'mysql': + $adapter = 'Pdo_Mysql'; + if ($this->config->use_ssl) { + # The presence of these keys as empty strings or null cause non-ssl connections to fail + if ($this->config->ssl_key) { + $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_KEY] = $this->config->ssl_key; + } + if ($this->config->ssl_cert) { + $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CERT] = $this->config->ssl_cert; + } + if ($this->config->ssl_ca) { + $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CA] = $this->config->ssl_ca; + } + if ($this->config->ssl_capath) { + $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CAPATH] = $this->config->ssl_capath; + } + if ($this->config->ssl_cipher) { + $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_CIPHER] = $this->config->ssl_cipher; + } + if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') + && $this->config->ssl_do_not_verify_server_cert + ) { + $adapterParamaters['driver_options'][PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = false; + } + } + /* + * Set MySQL server SQL modes to behave as closely as possible to Oracle and PostgreSQL. Note that the + * ONLY_FULL_GROUP_BY mode is left on purpose because MySQL requires you to specify all non-aggregate + * columns in the group by list even if the query is grouped by the master table's primary key which is + * valid ANSI SQL though. Further in that case the query plan would suffer if you add more columns to + * the group by list. + */ + $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] = + 'SET SESSION SQL_MODE=\'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,' + . 'ANSI_QUOTES,PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION\''; + if (isset($adapterParamaters['charset'])) { + $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', NAMES ' . $adapterParamaters['charset']; + if (trim($adapterParamaters['charset']) === 'latin1') { + // Required for MySQL 8+ because we need PIPES_AS_CONCAT and + // have several columns with explicit COLLATE instructions + $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ' COLLATE latin1_general_ci'; + } + + unset($adapterParamaters['charset']); + } + + $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .= ", time_zone='" . $this->defaultTimezoneOffset() . "'"; + $driverOptions[PDO::MYSQL_ATTR_INIT_COMMAND] .=';'; + $defaultPort = 3306; + break; + case 'oci': + $adapter = 'Oracle'; + unset($adapterParamaters['options']); + unset($adapterParamaters['driver_options']); + $adapterParamaters['driver_options'] = array( + 'lob_as_string' => true + ); + $defaultPort = 1521; + break; + case 'oracle': + $adapter = 'Pdo_Oci'; + $defaultPort = 1521; + + // remove host parameter when not configured + if (empty($this->config->host)) { + unset($adapterParamaters['host']); + } + break; + case 'pgsql': + $adapter = 'Pdo_Pgsql'; + $defaultPort = 5432; + break; + case 'ibm': + $adapter = 'Pdo_Ibm'; + $defaultPort = 50000; + break; + case 'sqlite': + $adapter = 'Pdo_Sqlite'; + $defaultPort = 0; // Dummy port because a value is required + break; + default: + throw new ConfigurationError( + 'Backend "%s" is not supported', + $this->dbType + ); + } + $adapterParamaters['port'] = $this->config->get('port', $defaultPort); + $this->dbAdapter = Zend_Db::factory($adapter, $adapterParamaters); + $this->dbAdapter->setFetchMode(Zend_Db::FETCH_OBJ); + // TODO(el/tg): The profiler is disabled per default, why do we disable the profiler explicitly? + $this->dbAdapter->getProfiler()->setEnabled(false); + } + + public static function fromResourceName($name) + { + return new static(ResourceFactory::getResourceConfig($name)); + } + + /** + * Getter for the table prefix + * + * @return string + */ + public function getTablePrefix() + { + return $this->tablePrefix; + } + + /** + * Setter for the table prefix + * + * @param string $prefix + * + * @return $this + */ + public function setTablePrefix($prefix) + { + $this->tablePrefix = $prefix; + return $this; + } + + /** + * Get offset from the current default timezone to GMT + * + * @return string + */ + protected function defaultTimezoneOffset() + { + $tz = new DateTimeZone(date_default_timezone_get()); + $offset = $tz->getOffset(new DateTime()); + $prefix = $offset >= 0 ? '+' : '-'; + $offset = abs($offset); + $hours = (int) floor($offset / 3600); + $minutes = (int) floor(($offset % 3600) / 60); + return sprintf('%s%d:%02d', $prefix, $hours, $minutes); + } + + /** + * Count all rows of the result set + * + * @param DbQuery $query + * + * @return int + */ + public function count(DbQuery $query) + { + return (int) $this->dbAdapter->fetchOne($query->getCountQuery()); + } + + /** + * Retrieve an array containing all rows of the result set + * + * @param DbQuery $query + * + * @return array + */ + public function fetchAll(DbQuery $query) + { + return $this->dbAdapter->fetchAll($query->getSelectQuery()); + } + + /** + * Fetch the first row of the result set + * + * @param DbQuery $query + * + * @return mixed + */ + public function fetchRow(DbQuery $query) + { + return $this->dbAdapter->fetchRow($query->getSelectQuery()); + } + + /** + * Fetch the first column of all rows of the result set as an array + * + * @param DbQuery $query + * + * @return array + */ + public function fetchColumn(DbQuery $query) + { + return $this->dbAdapter->fetchCol($query->getSelectQuery()); + } + + /** + * Fetch the first column of the first row of the result set + * + * @param DbQuery $query + * + * @return string + */ + public function fetchOne(DbQuery $query) + { + return $this->dbAdapter->fetchOne($query->getSelectQuery()); + } + + /** + * Fetch all rows of the result set as an array of key-value pairs + * + * The first column is the key, the second column is the value. + * + * @param DbQuery $query + * + * @return array + */ + public function fetchPairs(DbQuery $query) + { + return $this->dbAdapter->fetchPairs($query->getSelectQuery()); + } + + /** + * Insert a table row with the given data + * + * Note that the base implementation does not perform any quoting on the $table argument. + * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value + * as third parameter $types to define a different type than string for a particular column. + * + * @param string $table + * @param array $bind + * @param array $types + * + * @return int The number of affected rows + */ + public function insert($table, array $bind, array $types = array()) + { + $columns = $values = array(); + foreach ($bind as $column => $value) { + $columns[] = $column; + if ($value instanceof Zend_Db_Expr) { + $values[] = (string) $value; + unset($bind[$column]); + } else { + $values[] = ':' . $column; + } + } + + $sql = 'INSERT INTO ' . $table + . ' (' . join(', ', $columns) . ') ' + . 'VALUES (' . join(', ', $values) . ')'; + $statement = $this->dbAdapter->prepare($sql); + + foreach ($bind as $column => $value) { + $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR; + $statement->bindValue(':' . $column, $value, $type); + } + + $statement->execute(); + return $statement->rowCount(); + } + + /** + * Update table rows with the given data, optionally limited by using a filter + * + * Note that the base implementation does not perform any quoting on the $table argument. + * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value + * as fourth parameter $types to define a different type than string for a particular column. + * + * @param string $table + * @param array $bind + * @param Filter $filter + * @param array $types + * + * @return int The number of affected rows + */ + public function update($table, array $bind, Filter $filter = null, array $types = array()) + { + $set = array(); + foreach ($bind as $column => $value) { + if ($value instanceof Zend_Db_Expr) { + $set[] = $column . ' = ' . $value; + unset($bind[$column]); + } else { + $set[] = $column . ' = :' . $column; + } + } + + $sql = 'UPDATE ' . $table + . ' SET ' . join(', ', $set) + . ($filter ? ' WHERE ' . $this->renderFilter($filter) : ''); + $statement = $this->dbAdapter->prepare($sql); + + foreach ($bind as $column => $value) { + $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR; + $statement->bindValue(':' . $column, $value, $type); + } + + $statement->execute(); + return $statement->rowCount(); + } + + /** + * Delete table rows, optionally limited by using a filter + * + * @param string $table + * @param Filter $filter + * + * @return int The number of affected rows + */ + public function delete($table, Filter $filter = null) + { + return $this->dbAdapter->delete($table, $filter ? $this->renderFilter($filter) : ''); + } + + /** + * Render and return the given filter as SQL-WHERE clause + * + * @param Filter $filter + * + * @return string + */ + public function renderFilter(Filter $filter, $level = 0) + { + // TODO: This is supposed to supersede DbQuery::renderFilter() + $where = ''; + if ($filter->isChain()) { + if ($filter instanceof FilterAnd) { + $operator = ' AND '; + } elseif ($filter instanceof FilterOr) { + $operator = ' OR '; + } elseif ($filter instanceof FilterNot) { + $operator = ' AND '; + $where .= ' NOT '; + } else { + throw new ProgrammingError('Cannot render filter: %s', get_class($filter)); + } + + if (! $filter->isEmpty()) { + $parts = array(); + foreach ($filter->filters() as $filterPart) { + $part = $this->renderFilter($filterPart, $level + 1); + if ($part) { + $parts[] = $part; + } + } + + if (! empty($parts)) { + if ($level > 0) { + $where .= ' (' . implode($operator, $parts) . ') '; + } else { + $where .= implode($operator, $parts); + } + } + } else { + return ''; // Explicitly return the empty string due to the FilterNot case + } + } else { + $where .= $this->renderFilterExpression($filter); + } + + return $where; + } + + /** + * Render and return the given filter expression + * + * @param Filter $filter + * + * @return string + */ + protected function renderFilterExpression(Filter $filter) + { + $column = $filter->getColumn(); + $sign = $filter->getSign(); + $value = $filter->getExpression(); + + if (is_array($value)) { + $comp = []; + $pattern = []; + foreach ($value as $val) { + if (strpos($val, '*') === false) { + $comp[] = $val; + } else { + $pattern[] = $this->renderFilterExpression(Filter::expression($column, $sign, $val)); + } + } + + $sql = $pattern; + if ($sign === '=') { + if (! empty($comp)) { + $sql[] = $column . ' IN (' . $this->dbAdapter->quote($comp) . ')'; + } + + $operator = 'OR'; + } elseif ($sign === '!=') { + if (! empty($comp)) { + $sql[] = sprintf( + '(%1$s NOT IN (%2$s) OR %1$s IS NULL)', + $column, + $this->dbAdapter->quote($comp) + ); + } + + $operator = 'AND'; + } else { + throw new ProgrammingError( + 'Unable to render array expressions with operators other than equal or not equal' + ); + } + + return count($sql) === 1 ? $sql[0] : '(' . implode(" $operator ", $sql) . ')'; + } elseif ($sign === '=' + && ! $filter instanceof FilterEqual + && $value !== null + && strpos($value, '*') !== false + ) { + if ($value === '*') { + return $column . ' IS NOT NULL'; + } + + return $column . ' LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value)); + } elseif ($sign === '!=' + && ! $filter instanceof FilterNotEqual + && $value !== null + && strpos($value, '*') !== false + ) { + if ($value === '*') { + return $column . ' IS NULL'; + } + + return sprintf( + '(%1$s NOT LIKE %2$s OR %1$s IS NULL)', + $column, + $this->dbAdapter->quote(preg_replace('~\*~', '%', $value)) + ); + } elseif ($sign === '!=') { + return sprintf('(%1$s != %2$s OR %1$s IS NULL)', $column, $this->dbAdapter->quote($value)); + } else { + return sprintf('%s %s %s', $column, $sign, $this->dbAdapter->quote($value)); + } + } + + public function inspect() + { + $insp = new Inspection('Db Connection'); + try { + $this->getDbAdapter()->getConnection(); + $config = $this->dbAdapter->getConfig(); + $insp->write(sprintf( + 'Connection to %s as %s on %s:%s successful', + $config['dbname'], + $config['username'], + array_key_exists('host', $config) ? $config['host'] : '(none)', + $config['port'] + )); + switch ($this->dbType) { + case 'mysql': + $rows = $this->dbAdapter->query( + 'SHOW VARIABLES WHERE variable_name ' . + 'IN (\'version\', \'protocol_version\', \'version_compile_os\', \'have_ssl\');' + )->fetchAll(); + $sqlinsp = new Inspection('MySQL'); + $hasSsl = false; + foreach ($rows as $row) { + $sqlinsp->write($row->variable_name . ': ' . $row->value); + if ($row->variable_name === 'have_ssl' && $row->value === 'YES') { + $hasSsl = true; + } + } + if ($hasSsl) { + $ssl_rows = $this->dbAdapter->query( + 'SHOW STATUS WHERE variable_name ' . + 'IN (\'Ssl_Cipher\');' + )->fetchAll(); + foreach ($ssl_rows as $ssl_row) { + $sqlinsp->write($ssl_row->variable_name . ': ' . $ssl_row->value); + } + } + $insp->write($sqlinsp); + break; + case 'pgsql': + $row = $this->dbAdapter->query('SELECT version();')->fetchAll(); + $sqlinsp = new Inspection('PostgreSQL'); + $sqlinsp->write($row[0]->version); + $insp->write($sqlinsp); + break; + } + } catch (Exception $e) { + return $insp->error(sprintf('Connection failed %s', $e->getMessage())); + } + return $insp; + } +} diff --git a/library/Icinga/Data/Db/DbQuery.php b/library/Icinga/Data/Db/DbQuery.php new file mode 100644 index 0000000..30816a7 --- /dev/null +++ b/library/Icinga/Data/Db/DbQuery.php @@ -0,0 +1,564 @@ +db = $this->ds->getDbAdapter(); + $this->select = $this->db->select(); + parent::init(); + } + + /** + * Get whether or not the query is a sub query + */ + public function getIsSubQuery() + { + return $this->isSubQuery; + } + + /** + * Set whether or not the query is a sub query + * + * @param bool $isSubQuery + * + * @return $this + */ + public function setIsSubQuery($isSubQuery = true) + { + $this->isSubQuery = (bool) $isSubQuery; + return $this; + } + + public function setUseSubqueryCount($useSubqueryCount = true) + { + $this->useSubqueryCount = $useSubqueryCount; + return $this; + } + + public function from($target, array $fields = null) + { + parent::from($target, $fields); + $this->select->from($this->target, array()); + return $this; + } + + public function where($condition, $value = null) + { + // $this->count = $this->select = null; + return parent::where($condition, $value); + } + + public function addFilter(Filter $filter) + { + $this->expressionsToTimestamp($filter); + return parent::addFilter($filter); + } + + private function expressionsToTimestamp(Filter $filter) + { + if ($filter->isChain()) { + foreach ($filter->filters() as $child) { + $this->expressionsToTimestamp($child); + } + } elseif ($this->isTimestamp($filter->getColumn())) { + $filter->setExpression($this->valueToTimestamp($filter->getExpression())); + } + } + + protected function dbSelect() + { + return clone $this->select; + } + + /** + * Return the underlying select + * + * @return Zend_Db_Select + */ + public function select() + { + return $this->select; + } + + /** + * Get the select query + * + * Applies order and limit if any + * + * @return Zend_Db_Select + */ + public function getSelectQuery() + { + $select = $this->dbSelect(); + // Add order fields to select for postgres distinct queries (#6351) + if ($this->hasOrder() + && $this->getDatasource()->getDbType() === 'pgsql' + && $select->getPart(Zend_Db_Select::DISTINCT) === true) { + foreach ($this->getOrder() as $fieldAndDirection) { + if (array_search($fieldAndDirection[0], $this->columns, true) === false) { + $this->columns[] = $fieldAndDirection[0]; + } + } + } + + $group = $this->getGroup(); + if ($group) { + $select->group($group); + } + + if (! empty($this->columns)) { + $select->columns($this->columns); + } + + $this->applyFilterSql($select); + + if ($this->hasLimit() || $this->hasOffset()) { + $select->limit($this->getLimit(), $this->getOffset()); + } + if ($this->hasOrder()) { + foreach ($this->getOrder() as $fieldAndDirection) { + $select->order( + $fieldAndDirection[0] . ' ' . $fieldAndDirection[1] + ); + } + } + + return $select; + } + + protected function applyFilterSql($select) + { + $where = $this->getDatasource()->renderFilter($this->filter); + if ($where !== '') { + $select->where($where); + } + } + + protected function escapeForSql($value) + { + // bindParam? bindValue? + if (is_array($value)) { + $ret = array(); + foreach ($value as $val) { + $ret[] = $this->escapeForSql($val); + } + return implode(', ', $ret); + } else { + //if (preg_match('/^\d+$/', $value)) { + // return $value; + //} else { + return $this->db->quote($value); + //} + } + } + + protected function escapeWildcards($value) + { + return preg_replace('/\*/', '%', $value); + } + + protected function valueToTimestamp($value) + { + if (is_string($value)) { + if (ctype_digit($value)) { + $value = (int) $value; + } else { + $value = strtotime($value); + } + } elseif (! is_int($value)) { + $value = (int) $value; + } + + return $value; + } + + /** + * Render the given timestamp based on the local timezone + * + * Since {@see DbConnection::defaultTimezoneOffset()} tells the database the timezone with just an offset, + * this will prepare the rendered value in a way that it plays fine with daylight savings. + * + * @param int $value + * @return string + */ + protected function timestampForSql($value) + { + if ($this->getDatasource()->getDbType() === 'pgsql') { + // We don't tell PostgreSQL the user's timezone + $dateTime = (new DateTime()) + ->setTimezone(new DateTimeZone('UTC')) + ->setTimestamp($value); + } else { + $dateTime = new DateTime(); + // Get "current" offset the database will use + $offsetToUTC = $dateTime->getOffset(); + // Set timezone to UTC and initialize it with the timestamp + $dateTime->setTimezone(new DateTimeZone('UTC'))->setTimestamp($value); + // Normalize every datetime based on the only offset the database knows about + if ($offsetToUTC >= 0) { + $dateTime->add(new DateInterval("PT{$offsetToUTC}S")); + } else { + $offsetToUTC = abs($offsetToUTC); + $dateTime->sub(new DateInterval("PT{$offsetToUTC}S")); + } + } + + return $dateTime->format('Y-m-d H:i:s'); + } + + /** + * Check for timestamp fields + * + * TODO: This is not here to do automagic timestamp stuff. One may + * override this function for custom voodoo, IdoQuery right now + * does. IMO we need to split whereToSql functionality, however + * I'd prefer to wait with this unless we understood how other + * backends will work. We probably should also rename this + * function to isTimestampColumn(). + * + * @param string $field Field Field name to checked + * @return bool Whether this field expects timestamps + */ + public function isTimestamp($field) + { + return false; + } + + /** + * Get the count query + * + * @return Zend_Db_Select + */ + public function getCountQuery() + { + // TODO: there may be situations where we should clone the "select" + $count = $this->dbSelect(); + $this->applyFilterSql($count); + $group = $this->getGroup(); + if ($this->useSubqueryCount || $group) { + if (! empty($this->columns)) { + $count->columns($this->columns); + } + if ($group) { + $count->group($group); + } + $columns = array('cnt' => 'COUNT(*)'); + return $this->db->select()->from($count, $columns); + } + + $count->columns(array('cnt' => 'COUNT(*)')); + return $count; + } + + /** + * Count all rows of the result set + * + * @return int + */ + public function count(): int + { + if ($this->count === null) { + $this->count = parent::count(); + } + + return $this->count; + } + + /** + * Return the select and count query as a textual representation + * + * @return string A string containing the select and count query, using unix style newlines as linebreaks + */ + public function dump() + { + return "QUERY\n=====\n" + . $this->getSelectQuery() + . "\n\nCOUNT\n=====\n" + . $this->getCountQuery() + . "\n\n"; + } + + public function __clone() + { + parent::__clone(); + $this->select = clone $this->select; + } + + /** + * @return string + */ + public function __toString() + { + try { + $select = (string) $this->getSelectQuery(); + return $this->getIsSubQuery() ? ('(' . $select . ')') : $select; + } catch (Exception $e) { + Logger::debug('Failed to render DbQuery. An error occured: %s', $e); + return ''; + } + } + + /** + * Add a GROUP BY clause + * + * @param string|array $group + * + * @return $this + */ + public function group($group) + { + $this->group = $group; + return $this; + } + + /** + * Return the GROUP BY clause + * + * @return string|array + */ + public function getGroup() + { + return $this->group; + } + + /** + * Return whether the given table has been joined + * + * @param string $table + * + * @return bool + */ + public function hasJoinedTable($table) + { + $fromPart = $this->select->getPart(Zend_Db_Select::FROM); + if (isset($fromPart[$table])) { + return true; + } + + foreach ($fromPart as $options) { + if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) { + return true; + } + } + + return false; + } + + /** + * Return the alias used for joining the given table + * + * @param string $table + * + * @return string|null null in case no alias is being used + * + * @throws ProgrammingError In case the given table has not been joined + */ + public function getJoinedTableAlias($table) + { + $fromPart = $this->select->getPart(Zend_Db_Select::FROM); + if (isset($fromPart[$table])) { + if ($fromPart[$table]['joinType'] === Zend_Db_Select::FROM) { + throw new ProgrammingError('Table "%s" has not been joined', $table); + } + + return; // No alias in use + } + + foreach ($fromPart as $alias => $options) { + if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) { + return $alias; + } + } + + throw new ProgrammingError('Table "%s" has not been joined', $table); + } + + /** + * Add an INNER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function join($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinInner($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add an INNER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinInner($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinInner($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add a LEFT OUTER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinLeft($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinLeft($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add a RIGHT OUTER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinRight($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinRight($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add a FULL OUTER JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param string $cond Join on this condition + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinFull($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinFull($name, $cond, $cols, $schema); + return $this; + } + + /** + * Add a CROSS JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinCross($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinCross($name, $cols, $schema); + return $this; + } + + /** + * Add a NATURAL JOIN table and colums to the query + * + * @param array|string|Zend_Db_Expr $name The table name + * @param array|string $cols The columns to select from the joined table + * @param string $schema The database name to specify, if any + * + * @return $this + */ + public function joinNatural($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null) + { + $this->select->joinNatural($name, $cols, $schema); + return $this; + } + + /** + * Add a UNION clause to the query + * + * @param array $select Select clauses for the union + * @param string $type Type of UNION to use + * + * @return $this + */ + public function union($select = array(), $type = Zend_Db_Select::SQL_UNION) + { + $this->select->union($select, $type); + return $this; + } +} diff --git a/library/Icinga/Data/Extensible.php b/library/Icinga/Data/Extensible.php new file mode 100644 index 0000000..ad690d8 --- /dev/null +++ b/library/Icinga/Data/Extensible.php @@ -0,0 +1,22 @@ +id = (string) $id; + return $this; + } + + abstract public function isExpression(); + + abstract public function isChain(); + + abstract public function isEmpty(); + + abstract public function toQueryString(); + + abstract public function andFilter(Filter $filter); + + abstract public function orFilter(Filter $filter); + + /** + * Whether the give row matches this Filter + * + * @param mixed $row Preferrably an stdClass instance + * @return bool + */ + abstract public function matches($row); + + public function getUrlParams() + { + return UrlParams::fromQueryString($this->toQueryString()); + } + + public function getById($id) + { + if ((string) $id === $this->getId()) { + return $this; + } + throw new ProgrammingError( + 'Trying to get invalid filter index "%s" from "%s" ("%s")', + $id, + $this, + $this->id + ); + } + + public function getId() + { + return $this->id; + } + + public function isRootNode() + { + return false === strpos($this->id, '-'); + } + + abstract public function listFilteredColumns(); + + public function applyChanges($changes) + { + $filter = $this; + $pairs = array(); + foreach ($changes as $k => $v) { + if (preg_match('/^(column|value|sign|operator)_([\d-]+)$/', $k, $m)) { + $pairs[$m[2]][$m[1]] = $v; + } + } + $operators = array(); + foreach ($pairs as $id => $fs) { + if (array_key_exists('operator', $fs)) { + $operators[$id] = $fs['operator']; + } else { + $f = $filter->getById($id); + $f->setColumn($fs['column']); + if ($f->getSign() !== $fs['sign']) { + if ($f->isRootNode()) { + $filter = $f->setSign($fs['sign']); + } else { + $filter->replaceById($id, $f->setSign($fs['sign'])); + } + } + $f->setExpression($fs['value']); + } + } + + krsort($operators, SORT_NATURAL); + foreach ($operators as $id => $operator) { + $f = $filter->getById($id); + if ($f->getOperatorName() !== $operator) { + if ($f->isRootNode()) { + $filter = $f->setOperatorName($operator); + } else { + $filter->replaceById($id, $f->setOperatorName($operator)); + } + } + } + + return $filter; + } + + public function getParentId() + { + if ($this->isRootNode()) { + throw new ProgrammingError('Filter root nodes have no parent'); + } + return substr($this->id, 0, strrpos($this->id, '-')); + } + + public function getParent() + { + return $this->getById($this->getParentId()); + } + + public function hasId($id) + { + if ($id === $this->getId()) { + return true; + } + return false; + } + + /** + * Where Filter factory + * + * @param string $col Column to be filtered + * @param string $filter Filter expression + * + * @throws FilterException + * @return FilterExpression + */ + public static function where($col, $filter) + { + return new FilterExpression($col, '=', $filter); + } + + public static function expression($col, $op, $expression) + { + switch ($op) { + case '=': + return new FilterMatch($col, $op, $expression); + case '<': + return new FilterLessThan($col, $op, $expression); + case '>': + return new FilterGreaterThan($col, $op, $expression); + case '>=': + return new FilterEqualOrGreaterThan($col, $op, $expression); + case '<=': + return new FilterEqualOrLessThan($col, $op, $expression); + case '!=': + return new FilterMatchNot($col, $op, $expression); + default: + throw new ProgrammingError( + 'There is no such filter sign: %s', + $op + ); + } + } + + /** + * Or FilterOperator factory + * + * @param Filter $filter,... Unlimited optional list of Filters + * + * @return FilterOr + */ + public static function matchAny() + { + $args = func_get_args(); + if (count($args) === 1 && is_array($args[0])) { + $args = $args[0]; + } + return new FilterOr($args); + } + + /** + * Or FilterOperator factory + * + * @param Filter $filter,... Unlimited optional list of Filters + * + * @return FilterAnd + */ + public static function matchAll() + { + $args = func_get_args(); + if (count($args) === 1 && is_array($args[0])) { + $args = $args[0]; + } + return new FilterAnd($args); + } + + /** + * FilterNot factory, negates the given filter + * + * @param Filter $filter Filter to be negated + * + * @return FilterNot + */ + public static function not() + { + $args = func_get_args(); + if (count($args) === 1) { + if (is_array($args[0])) { + $args = $args[0]; + } + } + if (count($args) > 1) { + return new FilterNot(array(new FilterAnd($args))); + } else { + return new FilterNot($args); + } + } + + public static function chain($operator, $filters = array()) + { + switch ($operator) { + case 'AND': + return self::matchAll($filters); + case 'OR': + return self::matchAny($filters); + case 'NOT': + return self::not($filters); + } + throw new ProgrammingError( + '"%s" is not a valid filter chain operator', + $operator + ); + } + + /** + * Create filter from queryString + * + * This is still pretty basic, need improvement + * + * @return static + */ + public static function fromQueryString($query) + { + return FilterQueryString::parse($query); + } +} diff --git a/library/Icinga/Data/Filter/FilterAnd.php b/library/Icinga/Data/Filter/FilterAnd.php new file mode 100644 index 0000000..96b68cc --- /dev/null +++ b/library/Icinga/Data/Filter/FilterAnd.php @@ -0,0 +1,42 @@ +filters as $filter) { + if (! $filter->matches($row)) { + return false; + } + } + return true; + } + + public function andFilter(Filter $filter) + { + return $this->addFilter($filter); + } + + public function orFilter(Filter $filter) + { + return Filter::matchAny($this, $filter); + } +} diff --git a/library/Icinga/Data/Filter/FilterChain.php b/library/Icinga/Data/Filter/FilterChain.php new file mode 100644 index 0000000..0f1e071 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterChain.php @@ -0,0 +1,286 @@ +filters = $filters; + + $this->refreshChildIds(); + + return $this; + } + + public function hasId($id) + { + foreach ($this->filters() as $filter) { + if ($filter->hasId($id)) { + return true; + } + } + return parent::hasId($id); + } + + public function getById($id) + { + foreach ($this->filters() as $filter) { + if ($filter->hasId($id)) { + return $filter->getById($id); + } + } + return parent::getById($id); + } + + public function removeId($id) + { + if ($id === $this->getId()) { + $this->filters = array(); + return $this; + } + $remove = null; + foreach ($this->filters as $key => $filter) { + if ($filter->getId() === $id) { + $remove = $key; + } elseif ($filter instanceof FilterChain) { + $filter->removeId($id); + } + } + if ($remove !== null) { + unset($this->filters[$remove]); + $this->filters = array_values($this->filters); + } + $this->refreshChildIds(); + return $this; + } + + public function replaceById($id, $filter) + { + $found = false; + foreach ($this->filters as $k => $child) { + if ($child->getId() == $id) { + $this->filters[$k] = $filter; + $found = true; + break; + } + if ($child->hasId($id)) { + $child->replaceById($id, $filter); + $found = true; + break; + } + } + if (! $found) { + throw new ProgrammingError('You tried to replace an unexistant child filter'); + } + $this->refreshChildIds(); + return $this; + } + + protected function refreshChildIds() + { + $i = 0; + $id = $this->getId(); + foreach ($this->filters as $filter) { + $i++; + $filter->setId($id . '-' . $i); + } + return $this; + } + + public function setId($id) + { + return parent::setId($id)->refreshChildIds(); + } + + public function getOperatorName() + { + return $this->operatorName; + } + + public function setOperatorName($name) + { + if ($name !== $this->operatorName) { + return Filter::chain($name, $this->filters); + } + return $this; + } + + public function getOperatorSymbol() + { + return $this->operatorSymbol; + } + + public function setAllowedFilterColumns(array $columns) + { + $this->allowedColumns = $columns; + return $this; + } + + /** + * List and return all column names referenced in this filter + * + * @param array $columns The columns listed so far + * + * @return array + */ + public function listFilteredColumns(array $columns = array()) + { + foreach ($this->filters as $filter) { + if ($filter instanceof FilterExpression) { + $column= $filter->getColumn(); + if (! in_array($column, $columns, true)) { + $columns[] = $column; + } + } else { + $columns = $filter->listFilteredColumns($columns); + } + } + + return $columns; + } + + public function toQueryString() + { + $parts = array(); + if (empty($this->filters)) { + return ''; + } + foreach ($this->filters() as $filter) { + if (! $filter->isEmpty()) { + $parts[] = $filter->toQueryString(); + } + } + + // TODO: getLevel?? + if (strpos($this->getId(), '-')) { + return '(' . implode($this->getOperatorSymbol(), $parts) . ')'; + } else { + return implode($this->getOperatorSymbol(), $parts); + } + } + + /** + * Get simple string representation + * + * Useful for debugging only + * + * @return string + */ + public function __toString() + { + if (empty($this->filters)) { + return ''; + } + $parts = array(); + foreach ($this->filters as $filter) { + if ($filter instanceof FilterChain) { + $parts[] = '(' . $filter . ')'; + } else { + $parts[] = (string) $filter; + } + } + $op = ' ' . $this->getOperatorSymbol() . ' '; + return implode($op, $parts); + } + + public function __construct($filters = array()) + { + foreach ($filters as $filter) { + $this->addFilter($filter); + } + } + + public function isExpression() + { + return false; + } + + public function isChain() + { + return true; + } + + public function isEmpty() + { + return empty($this->filters); + } + + public function addFilter(Filter $filter) + { + if (! empty($this->allowedColumns)) { + $this->validateFilterColumns($filter); + } + + $this->filters[] = $filter; + $filter->setId($this->getId() . '-' . $this->count()); + return $this; + } + + protected function validateFilterColumns(Filter $filter) + { + if ($filter->isExpression()) { + $valid = false; + foreach ($this->allowedColumns as $column) { + if (is_callable($column)) { + if (call_user_func($column, $filter->getColumn())) { + $valid = true; + break; + } + } elseif ($filter->getColumn() === $column) { + $valid = true; + break; + } + } + + if (! $valid) { + throw new QueryException('Invalid filter column provided: %s', $filter->getColumn()); + } + } else { + foreach ($filter->filters() as $subFilter) { + $this->validateFilterColumns($subFilter); + } + } + } + + public function &filters() + { + return $this->filters; + } + + public function count() + { + return count($this->filters); + } + + public function __clone() + { + foreach ($this->filters as & $filter) { + $filter = clone $filter; + } + } +} diff --git a/library/Icinga/Data/Filter/FilterEqual.php b/library/Icinga/Data/Filter/FilterEqual.php new file mode 100644 index 0000000..da53d3f --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqual.php @@ -0,0 +1,16 @@ +{$this->column})) { + return false; + } + + return (string) $row->{$this->column} === (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php b/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php new file mode 100644 index 0000000..d7bd5b8 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqualOrGreaterThan.php @@ -0,0 +1,16 @@ +{$this->column})) { + return false; + } + + return (string) $row->{$this->column} >= (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterEqualOrLessThan.php b/library/Icinga/Data/Filter/FilterEqualOrLessThan.php new file mode 100644 index 0000000..8016fc4 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterEqualOrLessThan.php @@ -0,0 +1,26 @@ +column . ' <= ' . $this->expression; + } + + public function toQueryString() + { + return $this->column . '<=' . $this->expression; + } + + public function matches($row) + { + if (! isset($row->{$this->column})) { + return false; + } + + return (string) $row->{$this->column} <= (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterException.php b/library/Icinga/Data/Filter/FilterException.php new file mode 100644 index 0000000..842d7ab --- /dev/null +++ b/library/Icinga/Data/Filter/FilterException.php @@ -0,0 +1,15 @@ +column = $column; + $this->sign = $sign; + $this->expression = $expression; + $this->caseSensitive = true; + } + + public function isExpression() + { + return true; + } + + public function isChain() + { + return false; + } + + public function isEmpty() + { + return false; + } + + public function getColumn() + { + return $this->column; + } + + public function getSign() + { + return $this->sign; + } + + public function setColumn($column) + { + $this->column = $column; + return $this; + } + + public function getExpression() + { + return $this->expression; + } + + /** + * Return whether this filter compares case sensitive + * + * @return bool + */ + public function getCaseSensitive() + { + return $this->caseSensitive; + } + + public function setExpression($expression) + { + $this->expression = $expression; + return $this; + } + + public function setSign($sign) + { + if ($sign !== $this->sign) { + return Filter::expression($this->column, $sign, $this->expression); + } + return $this; + } + + /** + * Set this filter's case sensitivity + * + * @param bool $caseSensitive + * + * @return $this + */ + public function setCaseSensitive($caseSensitive = true) + { + $this->caseSensitive = $caseSensitive; + return $this; + } + + public function listFilteredColumns() + { + return array($this->getColumn()); + } + + public function __toString() + { + if ($this->isBooleanTrue()) { + return $this->column; + } + + $expression = is_array($this->expression) ? + '( ' . implode(' | ', $this->expression) . ' )' : + $this->expression; + + return sprintf( + '%s %s %s', + $this->column, + $this->sign, + $expression + ); + } + + public function toQueryString() + { + if ($this->isBooleanTrue()) { + return $this->column; + } + + $expression = is_array($this->expression) ? + '(' . implode('|', array_map('rawurlencode', $this->expression)) . ')' : + rawurlencode($this->expression); + + return $this->column . $this->sign . $expression; + } + + protected function isBooleanTrue() + { + return $this->sign === '=' && $this->expression === true; + } + + /** + * If $var is a scalar, do the same as strtolower() would do. + * If $var is an array, map $this->strtolowerRecursive() to its elements. + * Otherwise, return $var unchanged. + * + * @param mixed $var + * + * @return mixed + */ + protected function strtolowerRecursive($var) + { + if ($var === null) { + return ''; + } + if (is_scalar($var)) { + return strtolower($var); + } + if (is_array($var)) { + return array_map(array($this, 'strtolowerRecursive'), $var); + } + return $var; + } + + public function matches($row) + { + try { + $rowValue = $row->{$this->column}; + } catch (Exception $e) { + // TODO: REALLY? Exception? + return false; + } + + if ($this->caseSensitive) { + $expression = $this->expression; + } else { + $rowValue = $this->strtolowerRecursive($rowValue); + $expression = $this->strtolowerRecursive($this->expression); + } + + if (is_array($expression)) { + return in_array($rowValue, $expression); + } + + $expression = (string) $expression; + if (strpos($expression, '*') === false) { + if (is_array($rowValue)) { + return in_array($expression, $rowValue); + } + + return (string) $rowValue === $expression; + } + + $parts = array(); + foreach (preg_split('~\*~', $expression) as $part) { + $parts[] = preg_quote($part, '/'); + } + $pattern = '/^' . implode('.*', $parts) . '$/'; + + if (is_array($rowValue)) { + foreach ($rowValue as $candidate) { + if (preg_match($pattern, $candidate)) { + return true; + } + } + + return false; + } + + return $rowValue !== null && preg_match($pattern, $rowValue); + } + + public function andFilter(Filter $filter) + { + return Filter::matchAll($this, $filter); + } + + public function orFilter(Filter $filter) + { + return Filter::matchAny($this, $filter); + } +} diff --git a/library/Icinga/Data/Filter/FilterGreaterThan.php b/library/Icinga/Data/Filter/FilterGreaterThan.php new file mode 100644 index 0000000..92a0e62 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterGreaterThan.php @@ -0,0 +1,16 @@ +{$this->column})) { + // TODO: REALLY? Exception? + return false; + } + return (string) $row->{$this->column} > (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterLessThan.php b/library/Icinga/Data/Filter/FilterLessThan.php new file mode 100644 index 0000000..c13a1ce --- /dev/null +++ b/library/Icinga/Data/Filter/FilterLessThan.php @@ -0,0 +1,26 @@ +column . ' < ' . $this->expression; + } + + public function toQueryString() + { + return $this->column . '<' . $this->expression; + } + + public function matches($row) + { + if (! isset($row->{$this->column})) { + return false; + } + + return (string) $row->{$this->column} < (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterMatch.php b/library/Icinga/Data/Filter/FilterMatch.php new file mode 100644 index 0000000..a3befad --- /dev/null +++ b/library/Icinga/Data/Filter/FilterMatch.php @@ -0,0 +1,8 @@ +caseSensitive = false; + } +} diff --git a/library/Icinga/Data/Filter/FilterMatchNot.php b/library/Icinga/Data/Filter/FilterMatchNot.php new file mode 100644 index 0000000..1e5050e --- /dev/null +++ b/library/Icinga/Data/Filter/FilterMatchNot.php @@ -0,0 +1,12 @@ +caseSensitive = false; + } +} diff --git a/library/Icinga/Data/Filter/FilterNot.php b/library/Icinga/Data/Filter/FilterNot.php new file mode 100644 index 0000000..b61f497 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterNot.php @@ -0,0 +1,58 @@ +filters() as $filter) { + if ($filter->matches($row)) { + return false; + } + } + return true; + } + + public function andFilter(Filter $filter) + { + return Filter::matchAll($this, $filter); + } + + public function orFilter(Filter $filter) + { + return Filter::matchAny($filter); + } + + public function toQueryString() + { + $parts = array(); + if (empty($this->filters)) { + return ''; + } + + foreach ($this->filters() as $filter) { + $parts[] = $filter->toQueryString(); + } + if (count($parts) === 1) { + return '!' . $parts[0]; + } else { + return '!(' . implode('&', $parts) . ')'; + } + } + + public function __toString() + { + if (count($this->filters) === 1) { + return '! ' . $this->filters[0]; + } + return '! (' . implode('&', $this->filters) . ')'; + } +} diff --git a/library/Icinga/Data/Filter/FilterNotEqual.php b/library/Icinga/Data/Filter/FilterNotEqual.php new file mode 100644 index 0000000..8915a3d --- /dev/null +++ b/library/Icinga/Data/Filter/FilterNotEqual.php @@ -0,0 +1,12 @@ +{$this->column} !== (string) $this->expression; + } +} diff --git a/library/Icinga/Data/Filter/FilterOr.php b/library/Icinga/Data/Filter/FilterOr.php new file mode 100644 index 0000000..aca91f3 --- /dev/null +++ b/library/Icinga/Data/Filter/FilterOr.php @@ -0,0 +1,39 @@ +filters as $filter) { + if ($filter->matches($row)) { + return true; + } + } + return false; + } + + public function setOperatorName($name) + { + if ($this->count() > 1 && $name === 'NOT') { + return Filter::not(clone $this); + } + return parent::setOperatorName($name); + } + + public function andFilter(Filter $filter) + { + return Filter::matchAll($this, $filter); + } + + public function orFilter(Filter $filter) + { + return $this->addFilter($filter); + } +} diff --git a/library/Icinga/Data/Filter/FilterParseException.php b/library/Icinga/Data/Filter/FilterParseException.php new file mode 100644 index 0000000..f2b732b --- /dev/null +++ b/library/Icinga/Data/Filter/FilterParseException.php @@ -0,0 +1,10 @@ +debug[] = sprintf( + '%s[%d=%s] (%s): %s', + str_repeat('* ', $level), + $this->pos, + $this->string[$this->pos - 1], + $op, + $msg + ); + } + + public static function parse($string) + { + $parser = new static(); + return $parser->parseQueryString($string); + } + + protected function readNextKey() + { + $str = $this->readUnlessSpecialChar(); + + if ($str === false) { + return $str; + } + return rawurldecode($str); + } + + protected function readNextValue() + { + if ($this->nextChar() === '(') { + $this->readChar(); + $var = preg_split('~\|~', $this->readUnless(')')); + if ($this->readChar() !== ')') { + $this->parseError(null, 'Expected ")"'); + } + } else { + $var = rawurldecode($this->readUnless(array(')', '&', '|', '>', '<'))); + } + return $var; + } + + protected function readNextExpression() + { + if ('' === ($key = $this->readNextKey())) { + return false; + } + + foreach (array('<', '>') as $sign) { + if (false !== ($pos = strpos($key, $sign))) { + if ($this->nextChar() === '=') { + break; + } + $var = substr($key, $pos + 1); + $key = substr($key, 0, $pos); + + if (ctype_digit($var)) { + $var = (float) $var; + } + + return Filter::expression($key, $sign, $var); + } + } + if (in_array($this->nextChar(), array('=', '>', '<', '!'))) { + $sign = $this->readChar(); + } else { + $sign = false; + } + if ($sign === false) { + return Filter::expression($key, '=', true); + } + + $toFloat = false; + if ($sign === '=') { + $last = substr($key, -1); + if ($last === '>' || $last === '<') { + $sign = $last . $sign; + $key = substr($key, 0, -1); + $toFloat = true; + } + // TODO: Same as above for unescaped <> - do we really need this? + } elseif ($sign === '>' || $sign === '<' || $sign === '!') { + $toFloat = $sign === '>' || $sign === '<'; + if ($this->nextChar() === '=') { + $sign .= $this->readChar(); + } + } + + $var = $this->readNextValue(); + if ($toFloat && ctype_digit($var)) { + $var = (float) $var; + } + + return Filter::expression($key, $sign, $var); + } + + protected function parseError($char = null, $extraMsg = null) + { + if ($extraMsg === null) { + $extra = ''; + } else { + $extra = ': ' . $extraMsg; + } + if ($char === null) { + $char = $this->string[$this->pos]; + } + if ($this->reportDebug) { + $extra .= "\n" . implode("\n", $this->debug); + } + + throw new FilterParseException( + 'Invalid filter "%s", unexpected %s at pos %d%s', + $this->string, + $char, + $this->pos, + $extra + ); + } + + protected function readFilters($nestingLevel = 0, $op = null) + { + $filters = array(); + while ($this->pos < $this->length) { + if ($op === '!' && count($filters) === 1) { + break; + } + $filter = $this->readNextExpression(); + $next = $this->readChar(); + + + if ($filter === false) { + $this->debug('Got no next expression, next is ' . $next, $nestingLevel, $op); + if ($next === '!') { + $not = $this->readFilters($nestingLevel + 1, '!'); + $filters[] = $not; + if (in_array($this->nextChar(), array('|', '&', ')'))) { + $next = $this->readChar(); + $this->debug('Got NOT, next is now: ' . $next, $nestingLevel, $op); + } else { + $this->debug('Breaking after NOT: ' . $not, $nestingLevel, $op); + break; + } + } + + if ($op === null && count($filters) > 0 && ($next === '&' || $next === '|')) { + $op = $next; + continue; + } + + if ($next === false) { + // Nothing more to read + break; + } + + if ($next === ')') { + if ($nestingLevel > 0) { + $this->debug('Closing without filter: ' . $next, $nestingLevel, $op); + break; + } + $this->parseError($next); + } + if ($next === '(') { + $filters[] = $this->readFilters($nestingLevel + 1, null); + continue; + } + if ($next === $op) { + continue; + } + $this->parseError($next, "$op level $nestingLevel"); + } else { + $this->debug('Got new expression: ' . $filter, $nestingLevel, $op); + + $filters[] = $filter; + + if ($next === false) { + $this->debug('Next is false, nothing to read but got filter', $nestingLevel, $op); + // Got filter, nothing more to read + break; + } + + if ($op === '!') { + $this->pos--; + break; + } + if ($next === $op) { + $this->debug('Next matches operator', $nestingLevel, $op); + continue; // Break?? + } + + if ($next === ')') { + if ($nestingLevel > 0) { + $this->debug('Closing with filter: ' . $next, $nestingLevel, $op); + break; + } + $this->parseError($next); + } + if ($op === null && in_array($next, array('&', '|'))) { + $this->debug('Setting op to ' . $next, $nestingLevel, $op); + $op = $next; + continue; + } + $this->parseError($next); + } + } + + if ($nestingLevel === 0 && $this->pos < $this->length) { + $this->parseError($op, 'Did not read full filter'); + } + + if ($nestingLevel === 0 && count($filters) === 1 && $op !== '!') { + // There is only one filter expression, no chain + $this->debug('Returning first filter only: ' . $filters[0], $nestingLevel, $op); + return $filters[0]; + } + + if ($op === null && count($filters) === 1) { + $this->debug('No op, single filter, setting AND', $nestingLevel, $op); + $op = '&'; + } + $this->debug(sprintf('Got %d filters, returning', count($filters)), $nestingLevel, $op); + + switch ($op) { + case '&': + return Filter::matchAll($filters); + case '|': + return Filter::matchAny($filters); + case '!': + return Filter::not($filters); + case null: + return Filter::matchAll(); + default: + $this->parseError($op); + } + } + + protected function parseQueryString($string) + { + $this->pos = 0; + + $this->string = $string; + + $this->length = $string ? strlen($string) : 0; + + if ($this->length === 0) { + return Filter::matchAll(); + } + return $this->readFilters(); + } + + protected function readUnless($char) + { + $buffer = ''; + while (false !== ($c = $this->readChar())) { + if (is_array($char)) { + if (in_array($c, $char)) { + $this->pos--; + break; + } + } else { + if ($c === $char) { + $this->pos--; + break; + } + } + $buffer .= $c; + } + + return $buffer; + } + + protected function readUnlessSpecialChar() + { + return $this->readUnless(array('=', '(', ')', '&', '|', '>', '<', '!')); + } + + protected function readExpressionOperator() + { + return $this->readUnless(array('=', '>', '<', '!')); + } + + protected function readChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos++]; + } + return false; + } + + protected function nextChar() + { + if ($this->length > $this->pos) { + return $this->string[$this->pos]; + } + return false; + } +} diff --git a/library/Icinga/Data/FilterColumns.php b/library/Icinga/Data/FilterColumns.php new file mode 100644 index 0000000..7eaacea --- /dev/null +++ b/library/Icinga/Data/FilterColumns.php @@ -0,0 +1,21 @@ +add(). We must no require classes implementing this interface to + * implement redundant methods over and over again. This interface must be moved to the namespace Icinga\Data\Filter. + * It lacks documentation. + */ +interface Filterable +{ + public function applyFilter(Filter $filter); + + public function setFilter(Filter $filter); + + public function getFilter(); + + public function addFilter(Filter $filter); + + public function where($condition, $value = null); +} diff --git a/library/Icinga/Data/Identifiable.php b/library/Icinga/Data/Identifiable.php new file mode 100644 index 0000000..7435026 --- /dev/null +++ b/library/Icinga/Data/Identifiable.php @@ -0,0 +1,17 @@ +description = $description; + } + + /** + * Get the name of this Inspection + * + * @return mixed + */ + public function getDescription() + { + return $this->description; + } + + /** + * Append the given log entry or nested inspection + * + * @throws ProgrammingError When called after erroring + * + * @param $entry string|Inspection A log entry or nested inspection + */ + public function write($entry) + { + if (isset($this->error)) { + throw new ProgrammingError('Inspection object used after error'); + } + if ($entry instanceof Inspection) { + $this->log[$entry->description] = $entry->toArray(); + } else { + Logger::debug($entry); + $this->log[] = $entry; + } + } + + /** + * Append the given log entry and fail this inspection with the given error + * + * @param $entry string|Inspection A log entry or nested inspection + * + * @throws ProgrammingError When called multiple times + * + * @return this fluent interface + */ + public function error($entry) + { + if (isset($this->error)) { + throw new ProgrammingError('Inspection object used after error'); + } + Logger::error($entry); + $this->log[] = $entry; + $this->error = $entry; + return $this; + } + + /** + * If the inspection resulted in an error + * + * @return bool + */ + public function hasError() + { + return isset($this->error); + } + + /** + * The error that caused the inspection to fail + * + * @return Inspection|string + */ + public function getError() + { + return $this->error; + } + + /** + * Convert the inspection to an array + * + * @return array An array of strings that describe the state in a human-readable form, each array element + * represents one log entry about this object. + */ + public function toArray() + { + return $this->log; + } + + /** + * Return a text representation of the inspection log entries + */ + public function __toString() + { + return sprintf( + 'Inspection: description: "%s" error: "%s"', + $this->description, + $this->error + ); + } +} diff --git a/library/Icinga/Data/Limitable.php b/library/Icinga/Data/Limitable.php new file mode 100644 index 0000000..8591a79 --- /dev/null +++ b/library/Icinga/Data/Limitable.php @@ -0,0 +1,48 @@ +baseQuery = $query; + $this->xAxisColumn = $xAxisColumn; + $this->yAxisColumn = $yAxisColumn; + } + + /** + * {@inheritdoc} + */ + public function getOrder() + { + return $this->order; + } + + /** + * {@inheritdoc} + */ + public function hasOrder() + { + return ! empty($this->order); + } + + /** + * {@inheritdoc} + */ + public function order($field, $direction = null) + { + $this->order[$field] = $direction; + return $this; + } + + /** + * Set the filter to apply on the query for the x-axis + * + * @param Filter $filter + * + * @return $this + */ + public function setXAxisFilter(Filter $filter = null) + { + $this->xAxisFilter = $filter; + return $this; + } + + /** + * Set the filter to apply on the query for the y-axis + * + * @param Filter $filter + * + * @return $this + */ + public function setYAxisFilter(Filter $filter = null) + { + $this->yAxisFilter = $filter; + return $this; + } + + /** + * Get the x-axis header + * + * Defaults to {@link $xAxisColumn} in case no x-axis header has been set using {@link setXAxisHeader()} + * + * @return string + */ + public function getXAxisHeader() + { + return $this->xAxisHeader !== null ? $this->xAxisHeader : $this->xAxisColumn; + } + + /** + * Set the x-axis header + * + * @param string $xAxisHeader + * + * @return $this + */ + public function setXAxisHeader($xAxisHeader) + { + $this->xAxisHeader = (string) $xAxisHeader; + return $this; + } + + /** + * Get the y-axis header + * + * Defaults to {@link $yAxisColumn} in case no x-axis header has been set using {@link setYAxisHeader()} + * + * @return string + */ + public function getYAxisHeader() + { + return $this->yAxisHeader !== null ? $this->yAxisHeader : $this->yAxisColumn; + } + + /** + * Set the y-axis header + * + * @param string $yAxisHeader + * + * @return $this + */ + public function setYAxisHeader($yAxisHeader) + { + $this->yAxisHeader = (string) $yAxisHeader; + return $this; + } + + /** + * Return the value for the given request parameter + * + * @param string $axis The axis for which to return the parameter ('x' or 'y') + * @param string $param The parameter name to return + * @param int $default The default value to return + * + * @return int + */ + protected function getPaginationParameter($axis, $param, $default = null) + { + $request = Icinga::app()->getRequest(); + + $value = $request->getParam($param, ''); + if (strpos($value, ',') > 0) { + $parts = explode(',', $value, 2); + return intval($parts[$axis === 'x' ? 0 : 1]); + } + + return $default !== null ? $default : 0; + } + + /** + * Query horizontal (x) axis + * + * @return SimpleQuery + */ + protected function queryXAxis() + { + if ($this->xAxisQuery === null) { + $this->xAxisQuery = clone $this->baseQuery; + $this->xAxisQuery->clearGroupingRules(); + $xAxisHeader = $this->getXAxisHeader(); + $columns = array($this->xAxisColumn, $xAxisHeader); + $this->xAxisQuery->group(array_unique($columns)); // xAxisColumn and header may be the same column + $this->xAxisQuery->columns($columns); + + if ($this->xAxisFilter !== null) { + $this->xAxisQuery->addFilter($this->xAxisFilter); + } + + $this->xAxisQuery->order( + $xAxisHeader, + isset($this->order[$xAxisHeader]) ? $this->order[$xAxisHeader] : self::SORT_ASC + ); + } + + return $this->xAxisQuery; + } + + /** + * Query vertical (y) axis + * + * @return SimpleQuery + */ + protected function queryYAxis() + { + if ($this->yAxisQuery === null) { + $this->yAxisQuery = clone $this->baseQuery; + $this->yAxisQuery->clearGroupingRules(); + $yAxisHeader = $this->getYAxisHeader(); + $columns = array($this->yAxisColumn, $yAxisHeader); + $this->yAxisQuery->group(array_unique($columns)); // yAxisColumn and header may be the same column + $this->yAxisQuery->columns($columns); + + if ($this->yAxisFilter !== null) { + $this->yAxisQuery->addFilter($this->yAxisFilter); + } + + $this->yAxisQuery->order( + $yAxisHeader, + isset($this->order[$yAxisHeader]) ? $this->order[$yAxisHeader] : self::SORT_ASC + ); + } + return $this->yAxisQuery; + } + + /** + * Return a pagination adapter for the x-axis query + * + * $limit and $page are taken from the current request if not given. + * + * @param int $limit The maximum amount of entries to fetch + * @param int $page The page to set as current one + * + * @return Zend_Paginator + */ + public function paginateXAxis($limit = null, $page = null) + { + if ($limit === null || $page === null) { + if ($limit === null) { + $limit = $this->getPaginationParameter('x', 'limit', 20); + } + + if ($page === null) { + $page = $this->getPaginationParameter('x', 'page', 1); + } + } + + $query = $this->queryXAxis(); + $query->limit($limit, $page > 0 ? ($page - 1) * $limit : 0); + + $paginator = new Zend_Paginator(new QueryAdapter($query)); + $paginator->setItemCountPerPage($limit); + $paginator->setCurrentPageNumber($page); + return $paginator; + } + + /** + * Return a pagination adapter for the y-axis query + * + * $limit and $page are taken from the current request if not given. + * + * @param int $limit The maximum amount of entries to fetch + * @param int $page The page to set as current one + * + * @return Zend_Paginator + */ + public function paginateYAxis($limit = null, $page = null) + { + if ($limit === null || $page === null) { + if ($limit === null) { + $limit = $this->getPaginationParameter('y', 'limit', 20); + } + + if ($page === null) { + $page = $this->getPaginationParameter('y', 'page', 1); + } + } + + $query = $this->queryYAxis(); + $query->limit($limit, $page > 0 ? ($page - 1) * $limit : 0); + + $paginator = new Zend_Paginator(new QueryAdapter($query)); + $paginator->setItemCountPerPage($limit); + $paginator->setCurrentPageNumber($page); + return $paginator; + } + + /** + * Return the pivot table as an array of pivot data and pivot header + * + * @return array + */ + public function toArray() + { + if (($this->xAxisFilter === null && $this->yAxisFilter === null) + || ($this->xAxisFilter !== null && $this->yAxisFilter !== null) + ) { + $xAxis = $this->queryXAxis()->fetchPairs(); + $yAxis = $this->queryYAxis()->fetchPairs(); + $xAxisKeys = array_keys($xAxis); + $yAxisKeys = array_keys($yAxis); + } else { + if ($this->xAxisFilter !== null) { + $xAxis = $this->queryXAxis()->fetchPairs(); + $xAxisKeys = array_keys($xAxis); + $yAxis = $this->queryYAxis()->where($this->xAxisColumn, $xAxisKeys)->fetchPairs(); + $yAxisKeys = array_keys($yAxis); + } else { // $this->yAxisFilter !== null + $yAxis = $this->queryYAxis()->fetchPairs(); + $yAxisKeys = array_keys($yAxis); + $xAxis = $this->queryXAxis()->where($this->yAxisColumn, $yAxisKeys)->fetchPairs(); + $xAxisKeys = array_keys($xAxis); + } + } + $pivotData = array(); + $pivotHeader = array( + 'cols' => $xAxis, + 'rows' => $yAxis + ); + if (! empty($xAxis) && ! empty($yAxis)) { + $this->baseQuery + ->where($this->xAxisColumn, array_map( + function ($key) { + return (string) $key; + }, + $xAxisKeys + )) + ->where($this->yAxisColumn, array_map( + function ($key) { + return (string) $key; + }, + $yAxisKeys + )); + + foreach ($yAxisKeys as $yAxisKey) { + foreach ($xAxisKeys as $xAxisKey) { + $pivotData[$yAxisKey][$xAxisKey] = null; + } + } + + foreach ($this->baseQuery as $row) { + $pivotData[$row->{$this->yAxisColumn}][$row->{$this->xAxisColumn}] = $row; + } + } + return array($pivotData, $pivotHeader); + } +} diff --git a/library/Icinga/Data/QueryInterface.php b/library/Icinga/Data/QueryInterface.php new file mode 100644 index 0000000..e723857 --- /dev/null +++ b/library/Icinga/Data/QueryInterface.php @@ -0,0 +1,8 @@ +getSection($resourceName); + if ($resourceConfig->isEmpty()) { + throw new ConfigurationError( + 'Cannot load resource config "%s". Resource does not exist', + $resourceName + ); + } + return $resourceConfig; + } + + /** + * Get the configuration of all existing resources, or all resources of the given type + * + * @param string $type Filter for resource type + * + * @return Config The resources configuration + */ + public static function getResourceConfigs($type = null) + { + self::assertResourcesExist(); + if ($type === null) { + return self::$resources; + } + $resources = array(); + foreach (self::$resources as $name => $resource) { + if ($resource->get('type') === $type) { + $resources[$name] = $resource; + } + } + return Config::fromArray($resources); + } + + /** + * Check if the existing resources are set. If not, load them from resources.ini + * + * @throws ConfigurationError + */ + private static function assertResourcesExist() + { + if (self::$resources === null) { + self::$resources = Config::app('resources'); + } + } + + /** + * Create and return a resource based on the given configuration + * + * @param ConfigObject $config The configuration of the resource to create + * + * @return Selectable The resource + * @throws ConfigurationError In case of an unsupported type or invalid configuration + */ + public static function createResource(ConfigObject $config) + { + switch (strtolower($config->type)) { + case 'db': + $resource = new DbConnection($config); + break; + case 'ldap': + if (empty($config->root_dn)) { + throw new ConfigurationError('LDAP root DN missing'); + } + + $resource = new LdapConnection($config); + break; + case 'file': + $resource = new FileReader($config); + break; + case 'ini': + $resource = Config::fromIni($config->ini); + break; + default: + throw new ConfigurationError( + 'Unsupported resource type "%s"', + $config->type + ); + } + + return $resource; + } + + /** + * Create a resource from name + * + * @param string $resourceName + * @return DbConnection|LdapConnection + */ + public static function create($resourceName) + { + return self::createResource(self::getResourceConfig($resourceName)); + } +} diff --git a/library/Icinga/Data/Selectable.php b/library/Icinga/Data/Selectable.php new file mode 100644 index 0000000..ace4e79 --- /dev/null +++ b/library/Icinga/Data/Selectable.php @@ -0,0 +1,17 @@ +compare *only*. + * + * @var array + */ + protected $flippedColumns; + + /** + * The columns you're using to sort the query result + * + * @var array + */ + protected $order = array(); + + /** + * Number of rows to return + * + * @var int + */ + protected $limitCount; + + /** + * Result starts with this row + * + * @var int + */ + protected $limitOffset; + + /** + * Whether to peek ahead for more results + * + * @var bool + */ + protected $peekAhead; + + /** + * Whether the query did not yield all available results + * + * @var bool + */ + protected $hasMore; + + protected $filter; + + /** + * Constructor + * + * @param mixed $ds + */ + public function __construct($ds, $columns = null) + { + $this->ds = $ds; + $this->filter = Filter::matchAll(); + if ($columns !== null) { + $this->desiredColumns = $columns; + } + $this->init(); + if ($this->desiredColumns !== null) { + $this->columns($this->desiredColumns); + } + } + + /** + * Initialize query + * + * Overwrite this instead of __construct (it's called at the end of the construct) to + * implement custom initialization logic on construction time + */ + protected function init() + { + } + + /** + * Get the data source + * + * @return mixed + */ + public function getDatasource() + { + return $this->ds; + } + + /** + * Return the current position of this query's iterator + * + * @return int + */ + public function getIteratorPosition() + { + return $this->iteratorPosition; + } + + /** + * Start or rewind the iteration + */ + public function rewind(): void + { + if ($this->iterator === null) { + $iterator = $this->ds->query($this); + if ($iterator instanceof IteratorAggregate) { + $this->iterator = $iterator->getIterator(); + } else { + $this->iterator = $iterator; + } + } + + $this->iterator->rewind(); + $this->iteratorPosition = null; + Benchmark::measure('Query result iteration started'); + } + + /** + * Fetch and return the current row of this query's result + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->iterator->current(); + } + + /** + * Return whether the current row of this query's result is valid + * + * @return bool + */ + public function valid(): bool + { + $valid = $this->iterator->valid(); + if ($valid && $this->peekAhead && $this->hasLimit() && $this->iteratorPosition + 1 === $this->getLimit()) { + $this->hasMore = true; + $valid = false; // We arrived at the last result, which is the requested extra row, so stop the iteration + } elseif (! $valid) { + $this->hasMore = false; + } + + if (! $valid) { + Benchmark::measure('Query result iteration finished'); + return false; + } elseif ($this->iteratorPosition === null) { + $this->iteratorPosition = 0; + } + + return true; + } + + /** + * Return the key for the current row of this query's result + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->iterator->key(); + } + + /** + * Advance to the next row of this query's result + */ + public function next(): void + { + $this->iterator->next(); + $this->iteratorPosition += 1; + } + + /** + * Choose a table and the columns you are interested in + * + * Query will return all available columns if none are given here. + * + * @param mixed $target + * @param array $fields + * + * @return $this + */ + public function from($target, array $fields = null) + { + $this->target = $target; + if ($fields !== null) { + $this->columns($fields); + } + return $this; + } + + /** + * Add a where condition to the query by and + * + * The syntax of the condition and valid values are defined by the concrete backend-specific query implementation. + * + * @param string $condition + * @param mixed $value + * + * @return $this + */ + public function where($condition, $value = null) + { + // TODO: more intelligence please + $this->filter->addFilter(Filter::expression($condition, '=', $value)); + return $this; + } + + public function getFilter() + { + return $this->filter; + } + + public function applyFilter(Filter $filter) + { + return $this->addFilter($filter); + } + + public function addFilter(Filter $filter) + { + $this->filter->addFilter($filter); + return $this; + } + + public function setFilter(Filter $filter) + { + $this->filter = $filter; + return $this; + } + + public function setOrderColumns(array $orderColumns) + { + throw new IcingaException('This function does nothing and will be removed'); + } + + /** + * Split order field into its field and sort direction + * + * @param string $field + * + * @return array + */ + public function splitOrder($field) + { + $fieldAndDirection = explode(' ', $field, 2); + if (count($fieldAndDirection) === 1) { + $direction = null; + } else { + $field = $fieldAndDirection[0]; + $direction = (strtoupper(trim($fieldAndDirection[1])) === 'DESC') ? + Sortable::SORT_DESC : Sortable::SORT_ASC; + } + return array($field, $direction); + } + + /** + * Sort result set by the given field (and direction) + * + * Preferred usage: + * + * $query->order('field, 'ASC') + * + * + * @param string $field + * @param string $direction + * + * @return $this + */ + public function order($field, $direction = null) + { + if ($direction === null) { + list($field, $direction) = $this->splitOrder($field); + if ($direction === null) { + $direction = Sortable::SORT_ASC; + } + } else { + switch (($direction = strtoupper($direction))) { + case Sortable::SORT_ASC: + case Sortable::SORT_DESC: + break; + default: + $direction = Sortable::SORT_ASC; + break; + } + } + $this->order[] = array($field, $direction); + return $this; + } + + /** + * Compare $a with $b based on this query's sort rules and column aliases + * + * @param object $a + * @param object $b + * @param int $orderIndex + * + * @return int + */ + public function compare($a, $b, $orderIndex = 0) + { + if (! array_key_exists($orderIndex, $this->order)) { + return 0; // Last column to sort reached, rows are considered being equal + } + + if ($this->flippedColumns === null) { + $this->flippedColumns = array_flip($this->columns); + } + + $column = $this->order[$orderIndex][0]; + if (array_key_exists($column, $this->flippedColumns) && is_string($this->flippedColumns[$column])) { + $column = $this->flippedColumns[$column]; + } + + $result = strcmp(strtolower($a->$column ?? ''), strtolower($b->$column ?? '')); + if ($result === 0) { + return $this->compare($a, $b, ++$orderIndex); + } + + $direction = $this->order[$orderIndex][1]; + if ($direction === self::SORT_ASC) { + return $result; + } else { + return $result * -1; + } + } + + /** + * Clear the order if any + * + * @return $this + */ + public function clearOrder() + { + $this->order = array(); + return $this; + } + + /** + * Whether an order is set + * + * @return bool + */ + public function hasOrder() + { + return ! empty($this->order); + } + + /** + * Get the order + * + * @return array + */ + public function getOrder() + { + return $this->order; + } + + /** + * Set whether this query should peek ahead for more results + * + * Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will + * be removed from the result set. Note that this only applies when fetching multiple results of limited queries. + * + * @return $this + */ + public function peekAhead($state = true) + { + $this->peekAhead = (bool) $state; + return $this; + } + + /** + * Return whether this query did not yield all available results + * + * @return bool + * + * @throws ProgrammingError In case the query did not run yet + */ + public function hasMore() + { + if ($this->hasMore === null) { + throw new ProgrammingError('Query did not run. Cannot determine whether there are more results.'); + } + + return $this->hasMore; + } + + /** + * Return whether this query will or has yielded any result + * + * @return bool + */ + public function hasResult() + { + return $this->cachedCount > 0 || $this->iteratorPosition !== null || $this->fetchRow() !== false; + } + + /** + * Set a limit count and offset to the query + * + * @param int $count Number of rows to return + * @param int $offset Start returning after this many rows + * + * @return $this + */ + public function limit($count = null, $offset = null) + { + $this->limitCount = $count !== null ? (int) $count : null; + $this->limitOffset = (int) $offset; + return $this; + } + + /** + * Whether a limit is set + * + * @return bool + */ + public function hasLimit() + { + return $this->limitCount !== null && $this->limitCount > 0; + } + + /** + * Get the limit if any + * + * @return int|null + */ + public function getLimit() + { + return $this->peekAhead && $this->hasLimit() ? $this->limitCount + 1 : $this->limitCount; + } + + /** + * Whether an offset is set + * + * @return bool + */ + public function hasOffset() + { + return $this->limitOffset > 0; + } + + /** + * Get the offset if any + * + * @return int|null + */ + public function getOffset() + { + return $this->limitOffset; + } + + /** + * Retrieve an array containing all rows of the result set + * + * @return array + */ + public function fetchAll() + { + Benchmark::measure('Fetching all results started'); + $results = $this->ds->fetchAll($this); + Benchmark::measure('Fetching all results finished'); + + if ($this->peekAhead && $this->hasLimit() && count($results) === $this->getLimit()) { + $this->hasMore = true; + array_pop($results); + } else { + $this->hasMore = false; + } + + return $results; + } + + /** + * Fetch the first row of the result set + * + * @return mixed + */ + public function fetchRow() + { + Benchmark::measure('Fetching one row started'); + $row = $this->ds->fetchRow($this); + Benchmark::measure('Fetching one row finished'); + return $row; + } + + /** + * Fetch the first column of all rows of the result set as an array + * + * @return array + */ + public function fetchColumn() + { + Benchmark::measure('Fetching one column started'); + $values = $this->ds->fetchColumn($this); + Benchmark::measure('Fetching one column finished'); + + if ($this->peekAhead && $this->hasLimit() && count($values) === $this->getLimit()) { + $this->hasMore = true; + array_pop($values); + } else { + $this->hasMore = false; + } + + return $values; + } + + /** + * Fetch the first column of the first row of the result set + * + * @return string + */ + public function fetchOne() + { + Benchmark::measure('Fetching one value started'); + $value = $this->ds->fetchOne($this); + Benchmark::measure('Fetching one value finished'); + return $value; + } + + /** + * Fetch all rows of the result set as an array of key-value pairs + * + * The first column is the key, the second column is the value. + * + * @return array + */ + public function fetchPairs() + { + Benchmark::measure('Fetching pairs started'); + $pairs = $this->ds->fetchPairs($this); + Benchmark::measure('Fetching pairs finished'); + + if ($this->peekAhead && $this->hasLimit() && count($pairs) === $this->getLimit()) { + $this->hasMore = true; + array_pop($pairs); + } else { + $this->hasMore = false; + } + + return $pairs; + } + + /** + * Count all rows of the result set, ignoring limit and offset + * + * @return int + */ + public function count(): int + { + $query = clone $this; + $query->limit(0, 0); + Benchmark::measure('Counting all results started'); + $count = $this->ds->count($query); + $this->cachedCount = $count; + Benchmark::measure('Counting all results finished'); + return $count; + } + + /** + * Set columns + * + * @param array $columns + * + * @return $this + */ + public function columns(array $columns) + { + $this->columns = $columns; + $this->flippedColumns = null; // Reset, due to updated columns + return $this; + } + + public function getColumns() + { + return $this->columns; + } + + /** + * Deep clone self::$filter + */ + public function __clone() + { + $this->filter = clone $this->filter; + } +} diff --git a/library/Icinga/Data/SortRules.php b/library/Icinga/Data/SortRules.php new file mode 100644 index 0000000..c93bdda --- /dev/null +++ b/library/Icinga/Data/SortRules.php @@ -0,0 +1,14 @@ + + * $query->order('field, 'ASC') + * + * + * @param string $field + * @param string $direction + * + * @return self + */ + public function order($field, $direction = null); + + /** + * Whether an order is set + * + * @return bool + */ + public function hasOrder(); + + /** + * Get the order if any + * + * @return array|null + */ + public function getOrder(); +} diff --git a/library/Icinga/Data/Tree/SimpleTree.php b/library/Icinga/Data/Tree/SimpleTree.php new file mode 100644 index 0000000..e89f589 --- /dev/null +++ b/library/Icinga/Data/Tree/SimpleTree.php @@ -0,0 +1,90 @@ +sentinel = new TreeNode(); + } + + /** + * Add a child node + * + * @param TreeNode $child + * @param TreeNode $parent + * + * @return $this + */ + public function addChild(TreeNode $child, TreeNode $parent = null) + { + if ($parent === null) { + $parent = $this->sentinel; + } elseif (! isset($this->nodes[$parent->getId()])) { + throw new LogicException(sprintf( + 'Can\'t append child node %s to parent node %s: Parent node does not exist', + $child->getId(), + $parent->getId() + )); + } + if (isset($this->nodes[$child->getId()])) { + throw new LogicException(sprintf( + 'Can\'t append child node %s to parent node %s: Child node does already exist', + $child->getId(), + $parent->getId() + )); + } + $this->nodes[$child->getId()] = $child; + $parent->appendChild($child); + return $this; + } + + /** + * Get a node by its ID + * + * @param mixed $id + * + * @return TreeNode|null + */ + public function getNode($id) + { + if (! isset($this->nodes[$id])) { + return null; + } + return $this->nodes[$id]; + } + + /** + * @return TreeNodeIterator + */ + public function getIterator(): Traversable + { + return new TreeNodeIterator($this->sentinel); + } +} diff --git a/library/Icinga/Data/Tree/TreeNode.php b/library/Icinga/Data/Tree/TreeNode.php new file mode 100644 index 0000000..66bce79 --- /dev/null +++ b/library/Icinga/Data/Tree/TreeNode.php @@ -0,0 +1,109 @@ +id = $id; + return $this; + } + + /** + * (non-PHPDoc) + * @see Identifiable::getId() For the method documentation. + */ + public function getId() + { + return $this->id; + } + + /** + * Set the node's value + * + * @param mixed $value + * + * @return $this + */ + public function setValue($value) + { + $this->value = $value; + return $this; + } + + /** + * Get the node's value + * + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * Append a child node as the last child of this node + * + * @param TreeNode $child The child to append + * + * @return $this + */ + public function appendChild(TreeNode $child) + { + $this->children[] = $child; + return $this; + } + + + /** + * Get whether the node has children + * + * @return bool + */ + public function hasChildren() + { + return ! empty($this->children); + } + + /** + * Get the node's children + * + * @return array + */ + public function getChildren() + { + return $this->children; + } +} diff --git a/library/Icinga/Data/Tree/TreeNodeIterator.php b/library/Icinga/Data/Tree/TreeNodeIterator.php new file mode 100644 index 0000000..cffc9f4 --- /dev/null +++ b/library/Icinga/Data/Tree/TreeNodeIterator.php @@ -0,0 +1,75 @@ +children = new ArrayIterator($node->getChildren()); + } + + public function current(): TreeNode + { + return $this->children->current(); + } + + public function key(): int + { + return $this->children->key(); + } + + public function next(): void + { + $this->children->next(); + } + + public function rewind(): void + { + $this->children->rewind(); + } + + public function valid(): bool + { + return $this->children->valid(); + } + + public function hasChildren(): bool + { + return $this->current()->hasChildren(); + } + + public function getChildren(): TreeNodeIterator + { + return new static($this->current()); + } + + /** + * Get whether the iterator is empty + * + * @return bool + */ + public function isEmpty() + { + return ! $this->children->count(); + } +} diff --git a/library/Icinga/Data/Updatable.php b/library/Icinga/Data/Updatable.php new file mode 100644 index 0000000..ff70b99 --- /dev/null +++ b/library/Icinga/Data/Updatable.php @@ -0,0 +1,24 @@ + 3600 * 24 * 3) { + $type = static::DATE; + if (date('Y') === date('Y', $time)) { + $formatted = date($requireTime ? 'M j H:i' : 'M j', $time); + } else { + $formatted = date($requireTime ? 'Y-m-d H:i' : 'Y-m', $time); + } + } else { + $minutes = floor($diff / 60); + if ($minutes < 60) { + $type = static::RELATIVE; + $formatted = sprintf('%dm %ds', $minutes, $diff % 60); + } else { + $hours = floor($minutes / 60); + if ($hours < 24) { + if (date('d') === date('d', $time)) { + $type = static::TIME; + $formatted = date('H:i', $time); + } else { + $type = static::DATE; + $formatted = date('M j H:i', $time); + } + } else { + $type = static::RELATIVE; + $formatted = sprintf('%dd %dh', floor($hours / 24), $hours % 24); + } + } + } + return array($type, $formatted, $invert); + } + + /** + * Format date + * + * @param int|float $date + * + * @return string + */ + public static function formatDate($date) + { + return date('Y-m-d', (int) $date); + } + + /** + * Format date and time + * + * @param int|float $dateTime + * + * @return string + */ + public static function formatDateTime($dateTime) + { + return date('Y-m-d H:i:s', (int) $dateTime); + } + + /** + * Format a duration + * + * @param int|float $seconds Duration in seconds + * + * @return string + */ + public static function formatDuration($seconds) + { + $minutes = floor((float) $seconds / 60); + if ($minutes < 60) { + $formatted = sprintf('%dm %ds', $minutes, $seconds % 60); + } else { + $hours = floor($minutes / 60); + if ($hours < 24) { + $formatted = sprintf('%dh %dm', $hours, $minutes % 60); + } else { + $formatted = sprintf('%dd %dh', floor($hours / 24), $hours % 24); + } + } + return $formatted; + } + + /** + * Format time + * + * @param int|float $time + * + * @return string + */ + public static function formatTime($time) + { + return date('H:i:s', (int) $time); + } + + /** + * Format time as time ago + * + * @param int|float $time + * @param bool $timeOnly + * @param bool $requireTime + * + * @return string + */ + public static function timeAgo($time, $timeOnly = false, $requireTime = false) + { + list($type, $ago, $invert) = static::diff($time, $requireTime); + if ($timeOnly) { + return $ago; + } + switch ($type) { + case static::DATE: + // Move to next case + case static::DATETIME: + $formatted = sprintf( + t('on %s', 'An event happened on the given date or date and time'), + $ago + ); + break; + case static::RELATIVE: + $formatted = sprintf( + t('%s ago', 'An event that happened the given time interval ago'), + $ago + ); + break; + case static::TIME: + $formatted = sprintf(t('at %s', 'An event happened at the given time'), $ago); + break; + } + return $formatted; + } + + /** + * Format time as time since + * + * @param int|float $time + * @param bool $timeOnly + * @param bool $requireTime + * + * @return string + */ + public static function timeSince($time, $timeOnly = false, $requireTime = false) + { + list($type, $since, $invert) = static::diff($time, $requireTime); + if ($timeOnly) { + return $since; + } + switch ($type) { + case static::RELATIVE: + $formatted = sprintf( + t('for %s', 'A status is lasting for the given time interval'), + $since + ); + break; + case static::DATE: + // Move to next case + case static::DATETIME: + // Move to next case + case static::TIME: + $formatted = sprintf( + t('since %s', 'A status is lasting since the given time, date or date and time'), + $since + ); + break; + } + return $formatted; + } + + /** + * Format time as time until + * + * @param int|float $time + * @param bool $timeOnly + * @param bool $requireTime + * + * @return string + */ + public static function timeUntil($time, $timeOnly = false, $requireTime = false) + { + list($type, $until, $invert) = static::diff($time, $requireTime); + if ($invert && $type === static::RELATIVE) { + $until = '-' . $until; + } + if ($timeOnly) { + return $until; + } + switch ($type) { + case static::DATE: + // Move to next case + case static::DATETIME: + $formatted = sprintf( + t('on %s', 'An event will happen on the given date or date and time'), + $until + ); + break; + case static::RELATIVE: + $formatted = sprintf( + t('in %s', 'An event will happen after the given time interval has elapsed'), + $until + ); + break; + case static::TIME: + $formatted = sprintf(t('at %s', 'An event will happen at the given time'), $until); + break; + } + return $formatted; + } +} diff --git a/library/Icinga/Exception/AlreadyExistsException.php b/library/Icinga/Exception/AlreadyExistsException.php new file mode 100644 index 0000000..d70c58f --- /dev/null +++ b/library/Icinga/Exception/AlreadyExistsException.php @@ -0,0 +1,11 @@ +statusCode; + } + + /** + * Set this exception's HTTP response headers + * + * @param array $headers + * + * @return $this + */ + public function setHeaders(array $headers) + { + $this->headers = $headers; + return $this; + } + + /** + * Set/Add a HTTP response header + * + * @param string $name + * @param string $value + * + * @return $this + */ + public function setHeader($name, $value) + { + $this->headers[$name] = $value; + return $this; + } + + /** + * Return this exception's HTTP response headers + * + * @return array An array where each key is a header name and the value its value + */ + public function getHeaders() + { + return $this->headers ?: array(); + } +} diff --git a/library/Icinga/Exception/Http/HttpBadRequestException.php b/library/Icinga/Exception/Http/HttpBadRequestException.php new file mode 100644 index 0000000..004eabd --- /dev/null +++ b/library/Icinga/Exception/Http/HttpBadRequestException.php @@ -0,0 +1,12 @@ +statusCode = (int) $statusCode; + + $args = func_get_args(); + array_shift($args); + call_user_func_array('parent::__construct', $args); + } +} diff --git a/library/Icinga/Exception/Http/HttpExceptionInterface.php b/library/Icinga/Exception/Http/HttpExceptionInterface.php new file mode 100644 index 0000000..c5e0cc7 --- /dev/null +++ b/library/Icinga/Exception/Http/HttpExceptionInterface.php @@ -0,0 +1,21 @@ +getHeaders(); + return isset($headers['Allow']) ? $headers['Allow'] : null; + } + + /** + * Set the allowed HTTP methods + * + * @param string $allowedMethods + * + * @return $this + */ + public function setAllowedMethods($allowedMethods) + { + $this->setHeader('Allow', (string) $allowedMethods); + return $this; + } +} diff --git a/library/Icinga/Exception/Http/HttpNotFoundException.php b/library/Icinga/Exception/Http/HttpNotFoundException.php new file mode 100644 index 0000000..eb91d63 --- /dev/null +++ b/library/Icinga/Exception/Http/HttpNotFoundException.php @@ -0,0 +1,12 @@ +newInstanceArgs($args); + } + + /** + * Return the given exception formatted as one-liner + * + * The format used is: %class% in %path%:%line% with message: %message% + * + * @param Exception $exception + * + * @return string + */ + public static function describe(Exception $exception) + { + return sprintf( + '%s in %s:%d with message: %s', + get_class($exception), + $exception->getFile(), + $exception->getLine(), + $exception->getMessage() + ); + } + + /** + * Return the same as {@link Exception::getTraceAsString()} for the given exception, + * but show only the types of scalar arguments + * + * @param Exception $exception + * + * @return string + */ + public static function getConfidentialTraceAsString(Exception $exception) + { + $trace = array(); + + foreach ($exception->getTrace() as $index => $frame) { + $trace[] = isset($frame['file']) + ? "#{$index} {$frame['file']}({$frame['line']}): " + : "#{$index} [internal function]: "; + + if (isset($frame['class'])) { + $trace[] = $frame['class']; + } + + if (isset($frame['type'])) { + $trace[] = $frame['type']; + } + + $trace[] = "{$frame['function']}("; + + if (isset($frame['args'])) { + $args = array(); + foreach ($frame['args'] as $arg) { + $type = gettype($arg); + $args[] = $type === 'object' ? 'Object(' . get_class($arg) . ')' : ucfirst($type); + } + + $trace[] = implode(', ', $args); + } + $trace[] = ")\n"; + } + + $trace[] = '#' . ($index + 1) . ' {main}'; + + return implode($trace); + } +} diff --git a/library/Icinga/Exception/InvalidPropertyException.php b/library/Icinga/Exception/InvalidPropertyException.php new file mode 100644 index 0000000..e7bcf32 --- /dev/null +++ b/library/Icinga/Exception/InvalidPropertyException.php @@ -0,0 +1,11 @@ +parameter; + } + + /** + * Set the name of the missing parameter + * + * @param string $name + * + * @return $this + */ + public function setParameter($name) + { + $this->parameter = (string) $name; + return $this; + } +} diff --git a/library/Icinga/Exception/NotFoundError.php b/library/Icinga/Exception/NotFoundError.php new file mode 100644 index 0000000..74e6941 --- /dev/null +++ b/library/Icinga/Exception/NotFoundError.php @@ -0,0 +1,8 @@ +query = $query; + return $csv; + } + + public function dump() + { + header('Content-type: text/csv'); + echo (string) $this; + } + + public function __toString() + { + $first = true; + $csv = ''; + foreach ($this->query as $row) { + if ($first) { + $csv .= implode(',', array_keys((array) $row)) . "\r\n"; + $first = false; + } + $out = array(); + foreach ($row as & $val) { + $out[] = '"' . $val . '"'; + } + $csv .= implode(',', $out) . "\r\n"; + } + + return $csv; + } +} diff --git a/library/Icinga/File/Ini/Dom/Comment.php b/library/Icinga/File/Ini/Dom/Comment.php new file mode 100644 index 0000000..c202d0f --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Comment.php @@ -0,0 +1,37 @@ +content = $content; + } + + /** + * Render this comment into INI markup + * + * @return string + */ + public function render() + { + return ';' . $this->content; + } +} diff --git a/library/Icinga/File/Ini/Dom/Directive.php b/library/Icinga/File/Ini/Dom/Directive.php new file mode 100644 index 0000000..4279a5f --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Directive.php @@ -0,0 +1,166 @@ +key = trim($key); + if (strlen($this->key) < 1) { + throw new ConfigurationError(sprintf('Ini error: empty directive key.')); + } + } + + /** + * Return the name of this directive + * + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * Return the value of this configuration directive + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Set the value of this configuration directive + * + * @param string $value + */ + public function setValue($value) + { + $this->value = trim($value); + } + + /** + * Set the comments to be rendered on the line before this directive + * + * @param Comment[] $comments + */ + public function setCommentsPre(array $comments) + { + $this->commentsPre = $comments; + } + + /** + * Return the comments to be rendered on the line before this directive + * + * @return Comment[] + */ + public function getCommentsPre() + { + return $this->commentsPre; + } + + /** + * Set the comment rendered on the same line of this directive + * + * @param Comment $comment + */ + public function setCommentPost(Comment $comment) + { + $this->commentPost = $comment; + } + + /** + * Render this configuration directive into INI markup + * + * @return string + */ + public function render() + { + $str = ''; + if (! empty($this->commentsPre)) { + $comments = array(); + foreach ($this->commentsPre as $comment) { + $comments[] = $comment->render(); + } + $str = implode(PHP_EOL, $comments) . PHP_EOL; + } + $str .= sprintf('%s = "%s"', $this->sanitizeKey($this->key), $this->sanitizeValue($this->value)); + if (isset($this->commentPost)) { + $str .= ' ' . $this->commentPost->render(); + } + return $str; + } + + /** + * Assure that the given identifier contains no newlines and pending or trailing whitespaces + * + * @param $str The string to sanitize + * + * @return string + */ + protected function sanitizeKey($str) + { + return trim(str_replace(PHP_EOL, ' ', $str)); + } + + /** + * Escape the significant characters in directive values, normalize line breaks and assure that + * the character contains no linebreaks + * + * @param $str The string to sanitize + * + * @return mixed|string + */ + protected function sanitizeValue($str) + { + $str = trim($str); + $str = str_replace('\\', '\\\\', $str); + $str = str_replace('"', '\"', $str); + $str = str_replace("\r", '\r', $str); + $str = str_replace("\n", '\n', $str); + + return $str; + } +} diff --git a/library/Icinga/File/Ini/Dom/Document.php b/library/Icinga/File/Ini/Dom/Document.php new file mode 100644 index 0000000..f38f33e --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Document.php @@ -0,0 +1,132 @@ +sections[$section->getName()] = $section; + } + + /** + * Return whether this INI file has the section with the given key + * + * @param string $name + * + * @return bool + */ + public function hasSection($name) + { + return isset($this->sections[trim($name)]); + } + + /** + * Return the section with the given name + * + * @param string $name + * + * @return Section + */ + public function getSection($name) + { + return $this->sections[trim($name)]; + } + + /** + * Set the section with the given name + * + * @param string $name + * @param Section $section + * + * @return Section + */ + public function setSection($name, Section $section) + { + return $this->sections[trim($name)] = $section; + } + + /** + * Remove the section with the given name + * + * @param string $name + */ + public function removeSection($name) + { + unset($this->sections[trim($name)]); + } + + /** + * Set the dangling comments at file end that belong to no particular directive + * + * @param Comment[] $comments + */ + public function setCommentsDangling(array $comments) + { + $this->commentsDangling = $comments; + } + + /** + * Get the dangling comments at file end that belong to no particular directive + * + * @return array + */ + public function getCommentsDangling() + { + return $this->commentsDangling; + } + + /** + * Render this document into the corresponding INI markup + * + * @return string + */ + public function render() + { + $sections = array(); + foreach ($this->sections as $section) { + $sections []= $section->render(); + } + $str = implode(PHP_EOL, $sections); + if (! empty($this->commentsDangling)) { + foreach ($this->commentsDangling as $comment) { + $str .= PHP_EOL . $comment->render(); + } + } + return $str; + } + + /** + * Convert $this to an array + * + * @return array + */ + public function toArray() + { + $a = array(); + foreach ($this->sections as $section) { + $a[$section->getName()] = $section->toArray(); + } + return $a; + } +} diff --git a/library/Icinga/File/Ini/Dom/Section.php b/library/Icinga/File/Ini/Dom/Section.php new file mode 100644 index 0000000..f877c87 --- /dev/null +++ b/library/Icinga/File/Ini/Dom/Section.php @@ -0,0 +1,190 @@ +name = trim($name); + if (strlen($this->name) < 1) { + throw new ConfigurationError('Ini file error: empty section identifier'); + } elseif (strpos($name, '[') !== false || strpos($name, ']') !== false) { + throw new ConfigurationError( + 'Ini file error: Section name "%s" must not contain any brackets ([, ])', + $name + ); + } + } + + /** + * Append a directive to the end of this section + * + * @param Directive $directive The directive to append + */ + public function addDirective(Directive $directive) + { + $this->directives[$directive->getKey()] = $directive; + } + + /** + * Remove the directive with the given name + * + * @param string $key They name of the directive to remove + */ + public function removeDirective($key) + { + unset($this->directives[$key]); + } + + /** + * Return whether this section has a directive with the given key + * + * @param string $key The name of the directive + * + * @return bool + */ + public function hasDirective($key) + { + return isset($this->directives[$key]); + } + + /** + * Get the directive with the given key + * + * @param $key string + * + * @return Directive + */ + public function getDirective($key) + { + return $this->directives[$key]; + } + + /** + * Return the name of this section + * + * @return string The name + */ + public function getName() + { + return $this->name; + } + + /** + * Set the comments to be rendered on the line before this section + * + * @param Comment[] $comments + */ + public function setCommentsPre(array $comments) + { + $this->commentsPre = $comments; + } + + /** + * Set the comment rendered on the same line of this section + * + * @param Comment $comment + */ + public function setCommentPost(Comment $comment) + { + $this->commentPost = $comment; + } + + /** + * Render this section into INI markup + * + * @return string + */ + public function render() + { + $dirs = ''; + $i = 0; + foreach ($this->directives as $directive) { + $comments = $directive->getCommentsPre(); + $dirs .= (($i++ > 0 && ! empty($comments)) ? PHP_EOL : '') + . $directive->render() . PHP_EOL; + } + $cms = ''; + if (! empty($this->commentsPre)) { + foreach ($this->commentsPre as $comment) { + $comments[] = $comment->render(); + } + $cms = implode(PHP_EOL, $comments) . PHP_EOL; + } + $post = ''; + if (isset($this->commentPost)) { + $post = ' ' . $this->commentPost->render(); + } + return $cms . sprintf('[%s]', $this->sanitize($this->name)) . $post . PHP_EOL . $dirs; + } + + /** + * Escape the significant characters in sections and normalize line breaks + * + * @param $str The string to sanitize + * + * @return mixed + */ + protected function sanitize($str) + { + $str = trim($str); + $str = str_replace('\\', '\\\\', $str); + $str = str_replace('"', '\\"', $str); + $str = str_replace(';', '\\;', $str); + return str_replace(PHP_EOL, ' ', $str); + } + + /** + * Convert $this to an array + * + * @return array + */ + public function toArray() + { + $a = array(); + foreach ($this->directives as $directive) { + $a[$directive->getKey()] = $directive->getValue(); + } + return $a; + } +} diff --git a/library/Icinga/File/Ini/IniParser.php b/library/Icinga/File/Ini/IniParser.php new file mode 100644 index 0000000..279aa45 --- /dev/null +++ b/library/Icinga/File/Ini/IniParser.php @@ -0,0 +1,310 @@ +setCommentsPre($coms); + $doc->addSection($sec); + $dir = null; + $coms = array(); + + $state = self::LINE_END; + $token = ''; + } + break; + + case self::DIRECTIVE_KEY: + if ($s !== '=') { + $token .= $s; + } else { + $dir = new Directive($token); + $dir->setCommentsPre($coms); + if (isset($sec)) { + $sec->addDirective($dir); + } else { + Logger::warning(sprintf( + 'Ini parser warning: section-less directive "%s" ignored. (l. %d)', + $token, + $line + )); + } + + $coms = array(); + $state = self::DIRECTIVE_VALUE_START; + $token = ''; + } + break; + + case self::DIRECTIVE_VALUE_START: + if (ctype_space($s)) { + continue 2; + } elseif ($s === '"') { + $state = self::DIRECTIVE_VALUE_QUOTED; + } else { + $state = self::DIRECTIVE_VALUE; + $token = $s; + } + break; + + case self::DIRECTIVE_VALUE: + /* + Escaping non-quoted values is not supported by php_parse_ini, it might + be reasonable to include in case we are switching completely our own + parser implementation + */ + if ($s === "\n" || $s === ";") { + $dir->setValue($token); + $token = ''; + + if ($s === "\n") { + $state = self::LINE_START; + $line ++; + } elseif ($s === ';') { + $state = self::COMMENT; + } + } else { + $token .= $s; + } + break; + + case self::DIRECTIVE_VALUE_QUOTED: + if ($s === '\\') { + $state = self::ESCAPE; + $escaping = self::DIRECTIVE_VALUE_QUOTED; + } elseif ($s !== '"') { + $token .= $s; + } else { + $dir->setValue($token); + $token = ''; + $state = self::LINE_END; + } + break; + + case self::COMMENT: + case self::COMMENT_END: + if ($s !== "\n") { + $token .= $s; + } else { + $com = new Comment(); + $com->setContent($token); + $token = ''; + + // Comments at the line end belong to the current line's directive or section. Comments + // on empty lines belong to the next directive that shows up. + if ($state === self::COMMENT_END) { + if (isset($dir)) { + $dir->setCommentPost($com); + } else { + $sec->setCommentPost($com); + } + } else { + $coms[] = $com; + } + $state = self::LINE_START; + $line ++; + } + break; + + case self::LINE_END: + if ($s === "\n") { + $state = self::LINE_START; + $line ++; + } elseif ($s === ';') { + $state = self::COMMENT_END; + } + break; + } + } + + // process the last token + switch ($state) { + case self::COMMENT: + case self::COMMENT_END: + $com = new Comment(); + $com->setContent($token); + if ($state === self::COMMENT_END) { + if (isset($dir)) { + $dir->setCommentPost($com); + } else { + $sec->setCommentPost($com); + } + } else { + $coms[] = $com; + } + break; + + case self::DIRECTIVE_VALUE: + $dir->setValue($token); + $sec->addDirective($dir); + break; + + case self::ESCAPE: + case self::DIRECTIVE_VALUE_QUOTED: + case self::DIRECTIVE_KEY: + case self::SECTION: + self::throwParseError('File ended in unterminated state ' . $state, $line); + } + if (! empty($coms)) { + $doc->setCommentsDangling($coms); + } + return $doc; + } + + /** + * Read the ini file and parse it with ::parseIni() + * + * @param string $file The ini file to read + * + * @return Config + * @throws NotReadableError When the file cannot be read + */ + public static function parseIniFile($file) + { + if (($path = realpath($file)) === false) { + throw new NotReadableError('Couldn\'t compute the absolute path of `%s\'', $file); + } + + if (($content = file_get_contents($path)) === false) { + throw new NotReadableError('Couldn\'t read the file `%s\'', $path); + } + + try { + $configArray = parse_ini_string($content, true, INI_SCANNER_RAW); + } catch (ErrorException $e) { + throw new ConfigurationError('Couldn\'t parse the INI file `%s\'', $path, $e); + } + + $unescaped = array(); + foreach ($configArray as $section => $options) { + $unescaped[self::unescapeSectionName($section)] = array_map([__CLASS__, 'unescapeOptionValue'], $options); + } + + return Config::fromArray($unescaped)->setConfigFile($file); + } + + /** + * Unescape significant characters in the given section name + * + * @param string $str + * + * @return string + */ + protected static function unescapeSectionName($str) + { + $str = str_replace('\"', '"', $str); + $str = str_replace('\;', ';', $str); + + return str_replace('\\\\', '\\', $str); + } + + /** + * Unescape significant characters in the given option value + * + * @param string $str + * + * @return string + */ + protected static function unescapeOptionValue($str) + { + $str = str_replace('\n', "\n", $str); + $str = str_replace('\r', "\r", $str); + $str = str_replace('\"', '"', $str); + $str = str_replace('\\\\', '\\', $str); + + // This replacement is a work-around for PHP bug #76965. Fixed with versions 7.1.24, 7.2.12 and 7.3.0. + return preg_replace('~^([\'"])(.*?)\1\s+$~', '$2', $str); + } +} diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php new file mode 100644 index 0000000..f00040e --- /dev/null +++ b/library/Icinga/File/Ini/IniWriter.php @@ -0,0 +1,205 @@ +config = $config; + $this->filename = $filename; + $this->fileMode = $filemode; + $this->options = $options; + } + + /** + * Render the Zend_Config into a config filestring + * + * @return string + */ + public function render() + { + if (file_exists($this->filename)) { + $oldconfig = Config::fromIni($this->filename); + $content = trim(file_get_contents($this->filename)); + } else { + $oldconfig = Config::fromArray(array()); + $content = ''; + } + $doc = IniParser::parseIni($content); + $this->diffPropertyUpdates($this->config, $doc); + $this->diffPropertyDeletions($oldconfig, $this->config, $doc); + $doc = $this->updateSectionOrder($this->config, $doc); + return $doc->render(); + } + + /** + * Write configuration to file and set file mode in case it does not exist yet + * + * @param string $filename + * @param bool $exclusiveLock + * + * @throws Zend_Config_Exception + */ + public function write($filename = null, $exclusiveLock = false) + { + $filePath = isset($filename) ? $filename : $this->filename; + $setMode = false === file_exists($filePath); + + if (file_put_contents($filePath, $this->render(), $exclusiveLock ? LOCK_EX : 0) === false) { + throw new Zend_Config_Exception('Could not write to file "' . $filePath . '"'); + } + + if ($setMode) { + // file was newly created + $mode = $this->fileMode; + if (is_int($this->fileMode) && false === @chmod($filePath, $this->fileMode)) { + throw new Zend_Config_Exception(sprintf('Failed to set file mode "%o" on file "%s"', $mode, $filePath)); + } + } + } + + /** + * Update the order of the sections in the ini file to match the order of the new config + * + * @return Document A new document with the changed section order applied + */ + protected function updateSectionOrder(Config $newconfig, Document $oldDoc) + { + $doc = new Document(); + $dangling = $oldDoc->getCommentsDangling(); + if (isset($dangling)) { + $doc->setCommentsDangling($dangling); + } + foreach ($newconfig->toArray() as $section => $directives) { + $doc->addSection($oldDoc->getSection($section)); + } + return $doc; + } + + /** + * Search for created and updated properties and use the editor to create or update these entries + * + * @param Config $newconfig The config representing the state after the change + * @param Document $doc + * + * @throws ProgrammingError + */ + protected function diffPropertyUpdates(Config $newconfig, Document $doc) + { + foreach ($newconfig->toArray() as $section => $directives) { + if (! is_array($directives)) { + Logger::warning('Section-less property ' . (string)$directives . ' was ignored.'); + continue; + } + if (!$doc->hasSection($section)) { + $domSection = new Section($section); + $doc->addSection($domSection); + } else { + $domSection = $doc->getSection($section); + } + foreach ($directives as $key => $value) { + if ($value === null) { + continue; + } + + if ($value instanceof ConfigObject) { + throw new ProgrammingError('Cannot diff recursive configs'); + } + if ($domSection->hasDirective($key)) { + $domSection->getDirective($key)->setValue($value); + } else { + $dir = new Directive($key); + $dir->setValue($value); + $domSection->addDirective($dir); + } + } + } + } + + /** + * Search for deleted properties and use the editor to delete these entries + * + * @param Config $oldconfig The config representing the state before the change + * @param Config $newconfig The config representing the state after the change + * @param Document $doc + * + * @throws ProgrammingError + */ + protected function diffPropertyDeletions(Config $oldconfig, Config $newconfig, Document $doc) + { + // Iterate over all properties in the old configuration file and remove those that don't + // exist in the new config + foreach ($oldconfig->toArray() as $section => $directives) { + if (! is_array($directives)) { + Logger::warning('Section-less property ' . (string)$directives . ' was ignored.'); + continue; + } + + if ($newconfig->hasSection($section)) { + $newSection = $newconfig->getSection($section); + $oldDomSection = $doc->getSection($section); + foreach ($directives as $key => $value) { + if ($value instanceof ConfigObject) { + throw new ProgrammingError('Cannot diff recursive configs'); + } + if (null === $newSection->get($key) && $oldDomSection->hasDirective($key)) { + $oldDomSection->removeDirective($key); + } + } + } else { + $doc->removeSection($section); + } + } + } +} diff --git a/library/Icinga/File/Pdf.php b/library/Icinga/File/Pdf.php new file mode 100644 index 0000000..e159e2f --- /dev/null +++ b/library/Icinga/File/Pdf.php @@ -0,0 +1,96 @@ + + * @author Fabien Ménager + * @author Alexander A. Klimov + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + */ + + $baseDir = Icinga::app()->getBaseDir('library/vendor/dompdf'); + + require_once "$baseDir/vendor/autoload.php"; +}); + +class Pdf +{ + protected function assertNoHeadersSent() + { + if (headers_sent()) { + throw new ProgrammingError( + 'Could not send pdf-response, content already written to output.' + ); + } + } + + public function renderControllerAction($controller) + { + $this->assertNoHeadersSent(); + + Environment::raiseMemoryLimit('512M'); + Environment::raiseExecutionTime(300); + + $viewRenderer = $controller->getHelper('viewRenderer'); + $viewRenderer->postDispatch(); + + $layoutHelper = $controller->getHelper('layout'); + $oldLayout = $layoutHelper->getLayout(); + $layout = $layoutHelper->setLayout('pdf'); + + $layout->content = $controller->getResponse(); + $html = $layout->render(); + + // Restore previous layout and reset content, to properly show errors + $controller->getResponse()->clearBody($viewRenderer->getResponseSegment()); + $layoutHelper->setLayout($oldLayout); + + $imgDir = Url::fromPath('img'); + $html = preg_replace( + '~src="' . $imgDir . '/~', + 'src="' . Icinga::app()->getBootstrapDirectory() . '/img/', + $html + ); + + $request = $controller->getRequest(); + + if (Hook::has('Pdfexport')) { + $pdfexport = Hook::first('Pdfexport'); + $pdfexport->streamPdfFromHtml($html, sprintf( + '%s-%s-%d', + $request->getControllerName(), + $request->getActionName(), + time() + )); + + return; + } + + $options = new Options(); + $options->set('defaultPaperSize', 'A4'); + $dompdf = new Dompdf($options); + $dompdf->loadHtml($html); + $dompdf->render(); + $dompdf->stream( + sprintf( + '%s-%s-%d', + $request->getControllerName(), + $request->getActionName(), + time() + ) + ); + } +} diff --git a/library/Icinga/File/Storage/LocalFileStorage.php b/library/Icinga/File/Storage/LocalFileStorage.php new file mode 100644 index 0000000..e38167e --- /dev/null +++ b/library/Icinga/File/Storage/LocalFileStorage.php @@ -0,0 +1,158 @@ +baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR); + } + + public function getIterator(): Traversable + { + try { + return new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $this->baseDir, + RecursiveDirectoryIterator::CURRENT_AS_FILEINFO + | RecursiveDirectoryIterator::KEY_AS_PATHNAME + | RecursiveDirectoryIterator::SKIP_DOTS + ) + ); + } catch (UnexpectedValueException $e) { + throw new NotReadableError('Couldn\'t read the directory "%s": %s', $this->baseDir, $e); + } + } + + public function has($path) + { + return is_file($this->resolvePath($path)); + } + + public function create($path, $content) + { + $resolvedPath = $this->resolvePath($path); + + $this->ensureDir(dirname($resolvedPath)); + + try { + $stream = fopen($resolvedPath, 'x'); + } catch (ErrorException $e) { + throw new AlreadyExistsException('Couldn\'t create the file "%s": %s', $path, $e); + } + + try { + fclose($stream); + chmod($resolvedPath, 0664); + file_put_contents($resolvedPath, $content); + } catch (ErrorException $e) { + throw new NotWritableError('Couldn\'t create the file "%s": %s', $path, $e); + } + } + + public function read($path) + { + $resolvedPath = $this->resolvePath($path, true); + + try { + return file_get_contents($resolvedPath); + } catch (ErrorException $e) { + throw new NotReadableError('Couldn\'t read the file "%s": %s', $path, $e); + } + } + + public function update($path, $content) + { + $resolvedPath = $this->resolvePath($path, true); + + try { + file_put_contents($resolvedPath, $content); + } catch (ErrorException $e) { + throw new NotWritableError('Couldn\'t update the file "%s": %s', $path, $e); + } + } + + public function delete($path) + { + $resolvedPath = $this->resolvePath($path, true); + + try { + unlink($resolvedPath); + } catch (ErrorException $e) { + throw new NotWritableError('Couldn\'t delete the file "%s": %s', $path, $e); + } + } + + public function resolvePath($path, $assertExistence = false) + { + if ($assertExistence && ! $this->has($path)) { + throw new NotFoundError('No such file: "%s"', $path); + } + + $steps = preg_split('~/~', $path, -1, PREG_SPLIT_NO_EMPTY); + for ($i = 0; $i < count($steps);) { + if ($steps[$i] === '.') { + array_splice($steps, $i, 1); + } elseif ($steps[$i] === '..' && $i > 0 && $steps[$i - 1] !== '..') { + array_splice($steps, $i - 1, 2); + --$i; + } else { + ++$i; + } + } + + if ($steps[0] === '..') { + throw new InvalidArgumentException('Paths above the base directory are not allowed'); + } + + return $this->baseDir . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $steps); + } + + /** + * Ensure that the given directory exists + * + * @param string $dir + * + * @throws NotWritableError + */ + protected function ensureDir($dir) + { + if (! is_dir($dir)) { + $this->ensureDir(dirname($dir)); + + try { + mkdir($dir, 02770); + } catch (ErrorException $e) { + throw new NotWritableError('Couldn\'t create the directory "%s": %s', $dir, $e); + } + } + } +} diff --git a/library/Icinga/File/Storage/StorageInterface.php b/library/Icinga/File/Storage/StorageInterface.php new file mode 100644 index 0000000..f416b00 --- /dev/null +++ b/library/Icinga/File/Storage/StorageInterface.php @@ -0,0 +1,94 @@ +baseDir)) { + return; + } + + $directoryIterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $this->baseDir, + RecursiveDirectoryIterator::CURRENT_AS_FILEINFO + | RecursiveDirectoryIterator::KEY_AS_PATHNAME + | RecursiveDirectoryIterator::SKIP_DOTS + ), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($directoryIterator as $path => $entry) { + /** @var \SplFileInfo $entry */ + + if ($entry->isDir() && ! $entry->isLink()) { + rmdir($path); + } else { + unlink($path); + } + } + + rmdir($this->baseDir); + } +} diff --git a/library/Icinga/Legacy/DashboardConfig.php b/library/Icinga/Legacy/DashboardConfig.php new file mode 100644 index 0000000..3fb5c2f --- /dev/null +++ b/library/Icinga/Legacy/DashboardConfig.php @@ -0,0 +1,137 @@ +user; + } + + /** + * Set the user + * + * @param User $user + * + * @return $this + */ + public function setUser($user) + { + $this->user = $user; + return $this; + } + + + /** + * List all dashboard configuration files that match the given user + * + * @param User $user + * + * @return string[] + */ + public static function listConfigFilesForUser(User $user) + { + $files = array(); + $dashboards = static::resolvePath('dashboards'); + if ($handle = @opendir($dashboards)) { + while (false !== ($entry = readdir($handle))) { + if ($entry[0] === '.' || ! is_dir($dashboards . '/' . $entry)) { + continue; + } + if (strtolower($entry) === strtolower($user->getUsername())) { + $files[] = $dashboards . '/' . $entry . '/dashboard.ini'; + } + } + closedir($handle); + } + return $files; + } + + /** + * {@inheritdoc} + */ + public function saveIni($filePath = null, $fileMode = 0660) + { + // Preprocessing start, ensures that the non-translated names are used to save module dashboard changes + // TODO: This MUST NOT survive the new dashboard implementation (yes, it's still a thing..) + $dashboardNavigation = new Navigation(); + $dashboardNavigation->load('dashboard-pane'); + $getDashboardPane = function ($label) use ($dashboardNavigation) { + foreach ($dashboardNavigation as $dashboardPane) { + /** @var DashboardPane $dashboardPane */ + if ($dashboardPane->getLabel() === $label) { + return $dashboardPane; + } + + foreach ($dashboardPane->getChildren() as $dashlet) { + /** @var NavigationItem $dashlet */ + if ($dashlet->getLabel() === $label) { + return $dashlet; + } + } + } + }; + + foreach (clone $this->config as $name => $options) { + if (strpos($name, '.') !== false) { + list($dashboardLabel, $dashletLabel) = explode('.', $name, 2); + } else { + $dashboardLabel = $name; + $dashletLabel = null; + } + + $dashboardPane = $getDashboardPane($dashboardLabel); + if ($dashboardPane !== null) { + $dashboardLabel = $dashboardPane->getName(); + } + + if ($dashletLabel !== null) { + $dashletItem = $getDashboardPane($dashletLabel); + if ($dashletItem !== null) { + $dashletLabel = $dashletItem->getName(); + } + } + + unset($this->config[$name]); + $this->config[$dashboardLabel . ($dashletLabel ? '.' . $dashletLabel : '')] = $options; + } + // Preprocessing end + + parent::saveIni($filePath, $fileMode); + if ($filePath === null) { + $filePath = $this->configFile; + } + foreach (static::listConfigFilesForUser($this->user) as $file) { + if ($file !== $filePath) { + @unlink($file); + } + } + } +} diff --git a/library/Icinga/Less/Call.php b/library/Icinga/Less/Call.php new file mode 100644 index 0000000..0a78cb5 --- /dev/null +++ b/library/Icinga/Less/Call.php @@ -0,0 +1,77 @@ +name, $call->args, $call->index, $call->currentFileInfo); + } + + public function compile($env = null) + { + if (! $env) { + // Not sure how to trigger this, but if there is no $env, there is nothing we can do + return parent::compile($env); + } + + foreach ($this->args as $arg) { + if (! is_array($arg->value)) { + continue; + } + + $name = null; + if ($arg->value[0] instanceof Less_Tree_Variable) { + // This is the case when defining a variable with a callable LESS rules such as fade, fadeout.. + // Example: `@foo: #fff; @foo-bar: fade(@foo, 10);` + $name = $arg->value[0]->name; + } elseif ($arg->value[0] instanceof ColorPropOrVariable) { + // This is the case when defining a CSS rule using the LESS functions and passing + // a variable as an argument to them. Example: `... { color: fade(@foo, 10%); }` + $name = $arg->value[0]->getVariable()->name; + } + + if ($name) { + foreach ($env->frames as $frame) { + if (($v = $frame->variable($name))) { + // Variables from the frame stack are always of type LESS Tree Rule + $vr = $v->value; + if ($vr instanceof Less_Tree_Value) { + // Get the actual color prop, otherwise this may cause an invalid argument error + $vr = $vr->compile($env); + } + + if ($vr instanceof DeferredColorProp) { + if (! $vr->hasReference()) { + // Should never happen, though just for safety's sake + $vr->compile($env); + } + + // Get the uppermost variable of the variable references + while (! $vr instanceof ColorProp) { + $vr = $vr->getRef(); + } + } elseif ($vr instanceof Less_Tree_Color) { + $vr = ColorProp::fromColor($vr); + $vr->setName($name); + } + + $arg->value[0] = $vr; + + break; + } + } + } + } + + return parent::compile($env); + } +} diff --git a/library/Icinga/Less/ColorProp.php b/library/Icinga/Less/ColorProp.php new file mode 100644 index 0000000..3f83c5e --- /dev/null +++ b/library/Icinga/Less/ColorProp.php @@ -0,0 +1,109 @@ +color = $color; + + foreach ($color as $k => $v) { + if ($k === 'name') { + $self->setName($v); // Removes the @ char from the name + } else { + $self->$k = $v; + } + } + + return $self; + } + + /** + * @return int + */ + public function getIndex() + { + return $this->index; + } + + /** + * @param int $index + * + * @return $this + */ + public function setIndex($index) + { + $this->index = $index; + + return $this; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) + { + if ($name[0] === '@') { + $name = substr($name, 1); + } + + $this->name = $name; + + return $this; + } + + public function genCSS($output) + { + $css = (new Less_Tree_Call( + 'var', + [ + new Less_Tree_Keyword('--' . $this->getName()), + // Use the Less_Tree_Color with which we created the ColorProp so that we don't get into genCSS() loops. + $this->color + ], + $this->getIndex() + ))->toCSS(); + + $output->add($css); + } +} diff --git a/library/Icinga/Less/ColorPropOrVariable.php b/library/Icinga/Less/ColorPropOrVariable.php new file mode 100644 index 0000000..7918674 --- /dev/null +++ b/library/Icinga/Less/ColorPropOrVariable.php @@ -0,0 +1,71 @@ +variable; + } + + /** + * @param Less_Tree_Variable $variable + * + * @return $this + */ + public function setVariable(Less_Tree_Variable $variable) + { + $this->variable = $variable; + + return $this; + } + + public function compile($env) + { + $v = $this->getVariable(); + + if ($v->name[1] === '@') { + // Evaluate variable variable as in Less_Tree_Variable:28. + $vv = new Less_Tree_Variable(substr($v->name, 1), $v->index + 1, $v->currentFileInfo); + // Overwrite the name so that the variable variable is not evaluated again. + $result = $vv->compile($env); + if ($result instanceof DeferredColorProp) { + $v->name = $result->name; + } else { + $v->name = '@' . $result->value; + } + } + + $compiled = $v->compile($env); + + if ($compiled instanceof ColorProp) { + // We may already have a ColorProp, which is the case with mixin calls. + return $compiled; + } + + if ($compiled instanceof Less_Tree_Color) { + return ColorProp::fromColor($compiled) + ->setIndex($v->index) + ->setName($v->name); + } + + return $compiled; + } +} diff --git a/library/Icinga/Less/DeferredColorProp.php b/library/Icinga/Less/DeferredColorProp.php new file mode 100644 index 0000000..c9c39ad --- /dev/null +++ b/library/Icinga/Less/DeferredColorProp.php @@ -0,0 +1,136 @@ +reference = self::fromVariable($variable); + } + } + + public function isResolved() + { + return $this->resolved; + } + + public function getName() + { + $name = $this->name; + if ($this->name[0] === '@') { + $name = substr($this->name, 1); + } + + return $name; + } + + public function hasReference() + { + return $this->reference !== null; + } + + public function getRef() + { + return $this->reference; + } + + public function setReference($ref) + { + $this->reference = $ref; + + return $this; + } + + public static function fromVariable(Less_Tree_Variable $variable) + { + $static = new static($variable->name, $variable->index, $variable->currentFileInfo); + $static->evaluating = $variable->evaluating; + $static->type = $variable->type; + + return $static; + } + + public function compile($env) + { + if (! $this->hasReference()) { + // This is never supposed to happen, however, we might have a deferred color prop + // without a reference. In this case we can simply use the parent method. + return parent::compile($env); + } + + if ($this->isResolved()) { + // The dependencies are already resolved, no need to traverse the frame stack over again! + return $this; + } + + if ($this->evaluating) { // Just like the parent method + throw new Less_Exception_Compiler( + "Recursive variable definition for " . $this->name, + null, + $this->index, + $this->currentFileInfo + ); + } + + $this->evaluating = true; + + foreach ($env->frames as $frame) { + if (($v = $frame->variable($this->getRef()->name))) { + $rv = $v->value; + if ($rv instanceof Less_Tree_Value) { + $rv = $rv->compile($env); + } + + // As we are at it anyway, let's cast the tree color to our color prop as well! + if ($rv instanceof Less_Tree_Color) { + $rv = ColorProp::fromColor($rv); + $rv->setName($this->getRef()->getName()); + } + + $this->evaluating = false; + $this->resolved = true; + $this->setReference($rv); + + break; + } + } + + return $this; + } + + public function genCSS($output) + { + if (! $this->hasReference()) { + return; // Nothing to generate + } + + $css = (new Less_Tree_Call( + 'var', + [ + new Less_Tree_Keyword('--' . $this->getName()), + $this->getRef() // Each of the references will be generated recursively + ], + $this->index + ))->toCSS(); + + $output->add($css); + } +} diff --git a/library/Icinga/Less/LightMode.php b/library/Icinga/Less/LightMode.php new file mode 100644 index 0000000..b4b72a0 --- /dev/null +++ b/library/Icinga/Less/LightMode.php @@ -0,0 +1,128 @@ +modes)) { + throw new InvalidArgumentException("$mode already exists"); + } + + $this->modes[$mode] = true; + + return $this; + } + + /** + * @param string $mode + * + * @return Less_Environment + * + * @throws InvalidArgumentException If there is no environment for the given mode + */ + public function getEnv($mode) + { + if (! isset($this->envs[$mode])) { + throw new InvalidArgumentException("$mode does not exist"); + } + + return $this->envs[$mode]; + } + + /** + * @param string $mode + * @param Less_Environment $env + * + * @return $this + * + * @throws InvalidArgumentException If an environment for given the mode already exists + */ + public function setEnv($mode, Less_Environment $env) + { + if (array_key_exists($mode, $this->envs)) { + throw new InvalidArgumentException("$mode already exists"); + } + + $this->envs[$mode] = $env; + + return $this; + } + + /** + * @param string $mode + * + * @return bool + */ + public function hasSelector($mode) + { + return isset($this->selectors[$mode]); + } + + /** + * @param string $mode + * + * @return string + * + * @throws InvalidArgumentException If there is no selector for the given mode + */ + public function getSelector($mode) + { + if (! isset($this->selectors[$mode])) { + throw new InvalidArgumentException("$mode does not exist"); + } + + return $this->selectors[$mode]; + } + + /** + * @param string $mode + * @param string $selector + * + * @return $this + * + * @throws InvalidArgumentException If a selector for given the mode already exists + */ + public function setSelector($mode, $selector) + { + if (array_key_exists($mode, $this->selectors)) { + throw new InvalidArgumentException("$mode already exists"); + } + + $this->selectors[$mode] = $selector; + + return $this; + } + + public function getIterator(): Traversable + { + return new ArrayIterator(array_keys($this->modes)); + } +} diff --git a/library/Icinga/Less/LightModeCall.php b/library/Icinga/Less/LightModeCall.php new file mode 100644 index 0000000..d899e3c --- /dev/null +++ b/library/Icinga/Less/LightModeCall.php @@ -0,0 +1,38 @@ +variable); + } + + /** + * @param Less_Environment $env + * + * @return Less_Tree_Ruleset + */ + public function compile($env) + { + return parent::compile( + $env->copyEvalEnv(array_merge($env->frames, $this->getLightMode()->getEnv($this->variable)->frames)) + ); + } +} diff --git a/library/Icinga/Less/LightModeDefinition.php b/library/Icinga/Less/LightModeDefinition.php new file mode 100644 index 0000000..929e95c --- /dev/null +++ b/library/Icinga/Less/LightModeDefinition.php @@ -0,0 +1,75 @@ +ruleset, $drs->frames); + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * @param Less_Environment $env + * + * @return Less_Tree_DetachedRuleset + */ + public function compile($env) + { + $drs = parent::compile($env); + + /** @var $frame Less_Tree_Ruleset */ + foreach ($env->frames as $frame) { + if ($frame->variable($this->getName())) { + if (! empty($frame->first_oelements) && ! isset($frame->first_oelements['.icinga-module'])) { + throw new Less_Exception_Compiler('Light mode definition not allowed in selectors'); + } + + break; + } + } + + $this->getLightMode()->setEnv($this->getName(), $env->copyEvalEnv($env->frames)); + + return $drs; + } +} diff --git a/library/Icinga/Less/LightModeTrait.php b/library/Icinga/Less/LightModeTrait.php new file mode 100644 index 0000000..d328265 --- /dev/null +++ b/library/Icinga/Less/LightModeTrait.php @@ -0,0 +1,30 @@ +lightMode; + } + + /** + * @param LightMode $lightMode + * + * @return $this + */ + public function setLightMode(LightMode $lightMode) + { + $this->lightMode = $lightMode; + + return $this; + } +} diff --git a/library/Icinga/Less/LightModeVisitor.php b/library/Icinga/Less/LightModeVisitor.php new file mode 100644 index 0000000..35758b4 --- /dev/null +++ b/library/Icinga/Less/LightModeVisitor.php @@ -0,0 +1,26 @@ +setLightMode($this->getLightMode()); + } + + public function run($node) + { + return $this->visitObj($node); + } +} diff --git a/library/Icinga/Less/Visitor.php b/library/Icinga/Less/Visitor.php new file mode 100644 index 0000000..48417c8 --- /dev/null +++ b/library/Icinga/Less/Visitor.php @@ -0,0 +1,243 @@ +name !== 'var') { + // We need to use our own tree call class , so that we can precompile the arguments before making + // the actual LESS function calls. Otherwise, it will produce lots of invalid argument exceptions! + $c = Call::fromCall($c); + } + + return $c; + } + + public function visitDetachedRuleset($drs) + { + if ($this->variableOrigin->name === '@' . static::LIGHT_MODE_NAME) { + $this->variableOrigin->name .= '-' . substr(sha1(uniqid(mt_rand(), true)), 0, 7); + + $this->lightMode->add($this->variableOrigin->name); + + if ($this->moduleSelector !== false) { + $this->lightMode->setSelector($this->variableOrigin->name, $this->moduleSelector); + } + + $drs = LightModeDefinition::fromDetachedRuleset($drs) + ->setLightMode($this->lightMode) + ->setName($this->variableOrigin->name); + } + + // Since a detached ruleset is a variable definition in the first place, + // just reset that we define a variable. + $this->definingVariable = false; + + return $drs; + } + + public function visitMixinCall($c) + { + // Less_Tree_Mixin_Call::accept() does not visit arguments, but we have to replace them if necessary. + foreach ($c->arguments as $a) { + $a['value'] = $this->visitObj($a['value']); + } + + return $c; + } + + public function visitMixinDefinition($m) + { + // Less_Tree_Mixin_Definition::accept() does not visit params, but we have to replace them if necessary. + foreach ($m->params as $p) { + if (! isset($p['value'])) { + continue; + } + + $p['value'] = $this->visitObj($p['value']); + } + + return $m; + } + + public function visitRule($r) + { + if ($r->name[0] === '@' && $r->variable) { + if ($this->definingVariable !== false) { + throw new LogicException('Already defining a variable'); + } + + $this->definingVariable = spl_object_hash($r); + $this->variableOrigin = $r; + + if ($r->value instanceof Less_Tree_Value) { + if ($r->value->value[0] instanceof Less_Tree_Expression) { + if ($r->value->value[0]->value[0] instanceof Less_Tree_Variable) { + // Transform the variable definition rule into our own class + $r->value->value[0]->value[0] = new DeferredColorProp($r->name, $r->value->value[0]->value[0]); + } + } + } + } + + return $r; + } + + public function visitRuleOut($r) + { + if ($this->definingVariable !== false && $this->definingVariable === spl_object_hash($r)) { + $this->definingVariable = false; + $this->variableOrigin = null; + } + } + + public function visitRuleset($rs) + { + // Method is required, otherwise visitRulesetOut will not be called. + return $rs; + } + + public function visitRulesetOut($rs) + { + if ($this->moduleScope !== false + && isset($rs->selectors) + && spl_object_hash($rs->selectors[0]) === $this->moduleScope + ) { + $this->moduleSelector = null; + $this->moduleScope = false; + } + } + + public function visitSelector($s) + { + if ($s->_oelements_len === 2 && $s->_oelements[0] === '.icinga-module') { + $this->moduleSelector = implode('', $s->_oelements); + $this->moduleScope = spl_object_hash($s); + } + + return $s; + } + + public function visitVariable($v) + { + if ($this->definingVariable !== false) { + return $v; + } + + return (new ColorPropOrVariable()) + ->setVariable($v); + } + + public function visitColor($c) + { + if ($this->definingVariable !== false) { + // Make sure that all less tree colors do have a proper name + $c->name = $this->variableOrigin->name; + } + + return $c; + } + + public function run($node) + { + $this->lightMode = new LightMode(); + + $evald = $this->visitObj($node); + + // The visitor has registered all light modes in visitDetachedRuleset, but has not called them yet. + // Now the light mode calls are prepared with the appropriate CSS selectors. + $calls = []; + foreach ($this->lightMode as $mode) { + if ($this->lightMode->hasSelector($mode)) { + $calls[] = "{$this->lightMode->getSelector($mode)} {\n$mode();\n}"; + } else { + $calls[] = "$mode();"; + } + } + + if (! empty($calls)) { + // Place and parse light mode calls into a new anonymous file, + // leaving the original Less in which the light modes were defined untouched. + $parser = (new Less_Parser()) + ->parse(sprintf(static::LIGHT_MODE_CSS, implode("\n", $calls))); + + // Because Less variables are block scoped, + // we can't just access the light mode definitions in the calls above. + // The LightModeVisitor ensures that all calls have access to the environment in which the mode was defined. + // Finally, the rules are merged so that the light mode calls are also rendered to CSS. + $rules = new ReflectionProperty(get_class($parser), 'rules'); + $rules->setAccessible(true); + $evald->rules = array_merge( + $evald->rules, + (new LightModeVisitor()) + ->setLightMode($this->lightMode) + ->visitArray($rules->getValue($parser)) + ); + // The LightModeVisitor is used explicitly here instead of using it as a plugin + // since we only need to process the newly created rules for the light mode calls. + } + + return $evald; + } +} diff --git a/library/Icinga/Protocol/Dns.php b/library/Icinga/Protocol/Dns.php new file mode 100644 index 0000000..3d422d7 --- /dev/null +++ b/library/Icinga/Protocol/Dns.php @@ -0,0 +1,89 @@ + 0) { + return $records[0]['ip']; + } + return null; + } + + /** + * Get the IPv6 address of the given hostname. + * + * @param $hostname The hostname to resolve + * + * @return string|null The IPv6 address of the given hostname or null, when no entry exists. + */ + public static function ipv6($hostname) + { + $records = dns_get_record($hostname, DNS_AAAA); + if ($records !== false && count($records) > 0) { + return $records[0]['ip']; + } + return null; + } +} diff --git a/library/Icinga/Protocol/File/Exception/FileReaderException.php b/library/Icinga/Protocol/File/Exception/FileReaderException.php new file mode 100644 index 0000000..237352c --- /dev/null +++ b/library/Icinga/Protocol/File/Exception/FileReaderException.php @@ -0,0 +1,12 @@ + $value) + * + * @var array + */ + protected $currentData; + + public function __construct($filename, $fields) + { + $this->fields = $fields; + $f = new File($filename); + $f->setFlags( + File::DROP_NEW_LINE | + File::READ_AHEAD | + File::SKIP_EMPTY + ); + parent::__construct($f); + } + + /** + * Return the current data + * + * @return array + */ + public function current(): array + { + return $this->currentData; + } + + /** + * Accept lines matching the given PCRE pattern + * + * @return bool + * + * @throws FileReaderException If PHP failed parsing the PCRE pattern + */ + public function accept(): bool + { + $data = array(); + $matched = preg_match( + $this->fields, + $this->getInnerIterator()->current(), + $data + ); + + if ($matched === false) { + throw new FileReaderException('Failed parsing regular expression!'); + } elseif ($matched === 1) { + foreach ($data as $key => $value) { + if (is_int($key)) { + unset($data[$key]); + } + } + $this->currentData = $data; + return true; + } + return false; + } +} diff --git a/library/Icinga/Protocol/File/FileQuery.php b/library/Icinga/Protocol/File/FileQuery.php new file mode 100644 index 0000000..504de2e --- /dev/null +++ b/library/Icinga/Protocol/File/FileQuery.php @@ -0,0 +1,86 @@ +sortDir = ( + $direction === null || strtoupper(trim($direction)) === 'DESC' + ) ? self::SORT_DESC : self::SORT_ASC; + return $this; + } + + /** + * Return true if sorting descending, false otherwise + * + * @return bool + */ + public function sortDesc() + { + return $this->sortDir === self::SORT_DESC; + } + + /** + * Add an mandatory filter expression to be applied on this query + * + * @param string $expression the filter expression to be applied + * + * @return FileQuery + */ + public function andWhere($expression) + { + $this->filters[] = $expression; + return $this; + } + + /** + * Get filters currently applied on this query + * + * @return array + */ + public function getFilters() + { + return $this->filters; + } +} diff --git a/library/Icinga/Protocol/File/FileReader.php b/library/Icinga/Protocol/File/FileReader.php new file mode 100644 index 0000000..d160387 --- /dev/null +++ b/library/Icinga/Protocol/File/FileReader.php @@ -0,0 +1,208 @@ +{$key})) { + $this->{$key} = $config->{$key}; + } else { + throw new FileReaderException('The directive `%s\' is required', $key); + } + } + } + + /** + * Instantiate a FileIterator object with the target file + * + * @return FileIterator + */ + public function iterate() + { + return new LogFileIterator($this->filename, $this->fields); + } + + /** + * Instantiate a FileQuery object + * + * @return FileQuery + */ + public function select() + { + return new FileQuery($this); + } + + /** + * Fetch and return all rows of the given query's result set using an iterator + * + * @param FileQuery $query + * + * @return ArrayIterator + */ + public function query(FileQuery $query) + { + return new ArrayIterator($this->fetchAll($query)); + } + + /** + * Return the number of available valid lines. + * + * @return int + */ + public function count(): int + { + if ($this->count === null) { + $this->count = iterator_count($this->iterate()); + } + return $this->count; + } + + /** + * Fetch result as an array of objects + * + * @param FileQuery $query + * + * @return array + */ + public function fetchAll(FileQuery $query) + { + $all = array(); + foreach ($this->fetchPairs($query) as $index => $value) { + $all[$index] = (object) $value; + } + return $all; + } + + /** + * Fetch result as a key/value pair array + * + * @param FileQuery $query + * + * @return array + */ + public function fetchPairs(FileQuery $query) + { + $skip = $query->getOffset(); + $read = $query->getLimit(); + if ($skip === null) { + $skip = 0; + } + $lines = array(); + if ($query->sortDesc()) { + $count = $this->count($query); + if ($count <= $skip) { + return $lines; + } elseif ($count < ($skip + $read)) { + $read = $count - $skip; + $skip = 0; + } else { + $skip = $count - ($skip + $read); + } + } + foreach ($this->iterate() as $index => $line) { + if ($index >= $skip) { + if ($index >= $skip + $read) { + break; + } + $lines[] = $line; + } + } + if ($query->sortDesc()) { + $lines = array_reverse($lines); + } + return $lines; + } + + /** + * Fetch first result row + * + * @param FileQuery $query + * + * @return object + */ + public function fetchRow(FileQuery $query) + { + $all = $this->fetchAll($query); + if (isset($all[0])) { + return $all[0]; + } + return null; + } + + /** + * Fetch first result column + * + * @param FileQuery $query + * + * @return array + */ + public function fetchColumn(FileQuery $query) + { + $column = array(); + foreach ($this->fetchPairs($query) as $pair) { + foreach ($pair as $value) { + $column[] = $value; + break; + } + } + return $column; + } + + /** + * Fetch first column value from first result row + * + * @param FileQuery $query + * + * @return mixed + */ + public function fetchOne(FileQuery $query) + { + $pairs = $this->fetchPairs($query); + if (isset($pairs[0])) { + foreach ($pairs[0] as $value) { + return $value; + } + } + return null; + } +} diff --git a/library/Icinga/Protocol/File/LogFileIterator.php b/library/Icinga/Protocol/File/LogFileIterator.php new file mode 100644 index 0000000..67a4d99 --- /dev/null +++ b/library/Icinga/Protocol/File/LogFileIterator.php @@ -0,0 +1,149 @@ +file = new SplFileObject($filename); + $this->file->setFlags( + SplFileObject::DROP_NEW_LINE | + SplFileObject::READ_AHEAD + ); + $this->fields = $fields; + } + + public function rewind(): void + { + $this->file->rewind(); + $this->index = 0; + $this->nextMessage(); + } + + public function next(): void + { + $this->file->next(); + ++$this->index; + $this->nextMessage(); + } + + public function current(): array + { + return $this->current; + } + + public function key(): int + { + return $this->index; + } + + public function valid(): bool + { + return $this->valid; + } + + protected function nextMessage() + { + $message = $this->next === null ? array() : array($this->next); + $this->valid = null; + while ($this->file->valid()) { + if (false === ($res = preg_match( + $this->fields, + $current = $this->file->current() + ))) { + throw new IcingaException('Failed at preg_match()'); + } + if (empty($message)) { + if ($res === 1) { + $message[] = $current; + } + } elseif ($res === 1) { + $this->next = $current; + $this->valid = true; + break; + } else { + $message[] = $current; + } + + $this->file->next(); + } + if ($this->valid === null) { + $this->next = null; + $this->valid = ! empty($message); + } + + if ($this->valid) { + while (! empty($message)) { + $matches = array(); + if (false === ($res = preg_match( + $this->fields, + implode(PHP_EOL, $message), + $matches + ))) { + throw new IcingaException('Failed at preg_match()'); + } + if ($res === 1) { + $this->current = $matches; + return; + } + array_pop($message); + } + $this->valid = false; + } + } +} diff --git a/library/Icinga/Protocol/Ldap/Discovery.php b/library/Icinga/Protocol/Ldap/Discovery.php new file mode 100644 index 0000000..d2080aa --- /dev/null +++ b/library/Icinga/Protocol/Ldap/Discovery.php @@ -0,0 +1,143 @@ +connection = $conn; + } + + /** + * Suggests a resource configuration of hostname, port and root_dn + * based on the discovery + * + * @return array The suggested configuration as an array + */ + public function suggestResourceSettings() + { + return array( + 'hostname' => $this->connection->getHostname(), + 'port' => $this->connection->getPort(), + 'root_dn' => $this->connection->getCapabilities()->getDefaultNamingContext() + ); + } + + /** + * Suggests a backend configuration of base_dn, user_class and user_name_attribute + * based on the discovery + * + * @return array The suggested configuration as an array + */ + public function suggestBackendSettings() + { + if ($this->isAd()) { + return array( + 'backend' => 'msldap', + 'base_dn' => $this->connection->getCapabilities()->getDefaultNamingContext(), + 'user_class' => 'user', + 'user_name_attribute' => 'sAMAccountName' + ); + } else { + return array( + 'backend' => 'ldap', + 'base_dn' => $this->connection->getCapabilities()->getDefaultNamingContext(), + 'user_class' => 'inetOrgPerson', + 'user_name_attribute' => 'uid' + ); + } + } + + /** + * Whether the suggested ldap server is an ActiveDirectory + * + * @return boolean + */ + public function isAd() + { + return $this->connection->getCapabilities()->isActiveDirectory(); + } + + /** + * Whether the discovery was successful + * + * @return bool False when the suggestions are guessed + */ + public function isSuccess() + { + return $this->connection->discoverySuccessful(); + } + + /** + * Why the discovery failed + * + * @return \Exception|null + */ + public function getError() + { + return $this->connection->getDiscoveryError(); + } + + /** + * Discover LDAP servers on the given domain + * + * @param string $domain The object containing the form elements + * + * @return Discovery True when the discovery was successful, false when the configuration was guessed + */ + public static function discoverDomain($domain) + { + if (! isset($domain)) { + return false; + } + + // Attempt 1: Connect to the domain directly + $disc = Discovery::discover($domain, 389); + if ($disc->isSuccess()) { + return $disc; + } + + // Attempt 2: Discover all available ldap dns records and connect to the first one + $records = array_merge(Dns::getSrvRecords($domain, 'ldap'), Dns::getSrvRecords($domain, 'ldaps')); + if (isset($records[0])) { + $record = $records[0]; + return Discovery::discover( + isset($record['target']) ? $record['target'] : $domain, + isset($record['port']) ? $record['port'] : $domain + ); + } + + // Return the first failed discovery, which will suggest properties based on guesses + return $disc; + } + + /** + * Convenience method to instantiate a new Discovery + * + * @param $host The host on which to execute the discovery + * @param $port The port on which to execute the discovery + * + * @return Discover The resulting Discovery + */ + public static function discover($host, $port) + { + $conn = new LdapConnection(new ConfigObject(array( + 'hostname' => $host, + 'port' => $port + ))); + return new Discovery($conn); + } +} diff --git a/library/Icinga/Protocol/Ldap/LdapCapabilities.php b/library/Icinga/Protocol/Ldap/LdapCapabilities.php new file mode 100644 index 0000000..0e562b1 --- /dev/null +++ b/library/Icinga/Protocol/Ldap/LdapCapabilities.php @@ -0,0 +1,439 @@ +setAttributes($attributes); + } + + /** + * Set the attributes and (re)build the OIDs + * + * @param $attributes StdClass The attributes returned, may be null for guessing default capabilities + */ + protected function setAttributes($attributes) + { + $this->attributes = $attributes; + $this->oids = array(); + + $keys = array('supportedControl', 'supportedExtension', 'supportedFeatures', 'supportedCapabilities'); + foreach ($keys as $key) { + if (isset($attributes->$key)) { + if (is_array($attributes->$key)) { + foreach ($attributes->$key as $oid) { + $this->oids[$oid] = true; + } + } else { + $this->oids[$attributes->$key] = true; + } + } + } + } + + /** + * Return if the capability object contains support for StartTLS + * + * @return bool Whether StartTLS is supported + */ + public function hasStartTls() + { + return isset($this->oids[self::LDAP_SERVER_START_TLS_OID]); + } + + /** + * Return if the capability object contains support for paged results + * + * @return bool Whether StartTLS is supported + */ + public function hasPagedResult() + { + return isset($this->oids[self::LDAP_PAGED_RESULT_OID_STRING]); + } + + /** + * Whether the ldap server is an ActiveDirectory server + * + * @return boolean + */ + public function isActiveDirectory() + { + return isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_OID]); + } + + /** + * Whether the ldap server is an OpenLDAP server + * + * @return bool + */ + public function isOpenLdap() + { + return isset($this->attributes->structuralObjectClass) && + $this->attributes->structuralObjectClass === 'OpenLDAProotDSE'; + } + + /** + * Return if the capability objects contains support for LdapV3, defaults to true if discovery failed + * + * @return bool + */ + public function hasLdapV3() + { + if (! isset($this->attributes) || ! isset($this->attributes->supportedLDAPVersion)) { + // Default to true, if unknown + return true; + } + + return (is_string($this->attributes->supportedLDAPVersion) + && (int) $this->attributes->supportedLDAPVersion === 3) + || (is_array($this->attributes->supportedLDAPVersion) + && in_array(3, $this->attributes->supportedLDAPVersion)); + } + + /** + * Whether the capability with the given OID is supported + * + * @param $oid string The OID of the capability + * + * @return bool + */ + public function hasOid($oid) + { + return isset($this->oids[$oid]); + } + + /** + * Get the default naming context + * + * @return string|null the default naming context, or null when no contexts are available + */ + public function getDefaultNamingContext() + { + // defaultNamingContext entry has higher priority + if (isset($this->attributes->defaultNamingContext)) { + return $this->attributes->defaultNamingContext; + } + + // if its missing use namingContext + $namingContexts = $this->namingContexts(); + return empty($namingContexts) ? null : $namingContexts[0]; + } + + /** + * Get the configuration naming context + * + * @return string|null + */ + public function getConfigurationNamingContext() + { + if (isset($this->attributes->configurationNamingContext)) { + return $this->attributes->configurationNamingContext; + } + } + + /** + * Get the NetBIOS name + * + * @return string|null + */ + public function getNetBiosName() + { + if (isset($this->attributes->nETBIOSName)) { + return $this->attributes->nETBIOSName; + } + } + + /** + * Fetch the namingContexts + * + * @return array the available naming contexts + */ + public function namingContexts() + { + if (!isset($this->attributes->namingContexts)) { + return array(); + } + if (!is_array($this->attributes->namingContexts)) { + return array($this->attributes->namingContexts); + } + return$this->attributes->namingContexts; + } + + public function getVendor() + { + /* + rfc #3045 specifies that the name of the server MAY be included in the attribute 'verndorName', + AD and OpenLDAP don't do this, but for all all other vendors we follow the standard and + just hope for the best. + */ + + if ($this->isActiveDirectory()) { + return 'Microsoft Active Directory'; + } + + if ($this->isOpenLdap()) { + return 'OpenLDAP'; + } + + if (! isset($this->attributes->vendorName)) { + return null; + } + return $this->attributes->vendorName; + } + + public function getVersion() + { + /* + rfc #3045 specifies that the version of the server MAY be included in the attribute 'vendorVersion', + but AD and OpenLDAP don't do this. For OpenLDAP there is no way to query the server versions, but for all + all other vendors we follow the standard and just hope for the best. + */ + + if ($this->isActiveDirectory()) { + return $this->getAdObjectVersionName(); + } + + if (! isset($this->attributes->vendorVersion)) { + return null; + } + return $this->attributes->vendorVersion; + } + + /** + * Discover the capabilities of the given LDAP server + * + * @param LdapConnection $connection The ldap connection to use + * + * @return LdapCapabilities + * + * @throws LdapException In case the capability query has failed + */ + public static function discoverCapabilities(LdapConnection $connection) + { + $ds = $connection->getConnection(); + + $fields = array( + 'configurationNamingContext', + 'defaultNamingContext', + 'namingContexts', + 'vendorName', + 'vendorVersion', + 'supportedSaslMechanisms', + 'dnsHostName', + 'schemaNamingContext', + 'supportedLDAPVersion', // => array(3, 2) + 'supportedCapabilities', + 'supportedControl', + 'supportedExtension', + 'objectVersion', + '+' + ); + + $result = @ldap_read($ds, '', (string) $connection->select()->from('*', $fields), $fields); + if (! $result) { + throw new LdapException( + 'Capability query failed (%s; Default port: %d): %s. Check if hostname and port' + . ' of the ldap resource are correct and if anonymous access is permitted.', + $connection->getHostname(), + $connection->getPort(), + ldap_error($ds) + ); + } + + $entry = ldap_first_entry($ds, $result); + if ($entry === false) { + throw new LdapException( + 'Capabilities not available (%s; Default port: %d): %s. Discovery of root DSE probably not permitted.', + $connection->getHostname(), + $connection->getPort(), + ldap_error($ds) + ); + } + + $cap = new LdapCapabilities($connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields)); + $cap->discoverAdConfigOptions($connection); + + if (isset($cap->attributes) && Logger::getInstance()->getLevel() === Logger::DEBUG) { + Logger::debug('Capability query discovered the following attributes:'); + foreach ($cap->attributes as $name => $value) { + if ($value !== null) { + Logger::debug(' %s = %s', $name, $value); + } + } + Logger::debug('Capability query attribute listing ended.'); + } + + return $cap; + } + + /** + * Discover the AD-specific configuration options of the given LDAP server + * + * @param LdapConnection $connection The ldap connection to use + * + * @throws LdapException In case the configuration options query has failed + */ + protected function discoverAdConfigOptions(LdapConnection $connection) + { + if ($this->isActiveDirectory()) { + $configurationNamingContext = $this->getConfigurationNamingContext(); + $defaultNamingContext = $this->getDefaultNamingContext(); + if (!($configurationNamingContext === null || $defaultNamingContext === null)) { + $ds = $connection->bind()->getConnection(); + $adFields = array('nETBIOSName'); + $partitions = 'CN=Partitions,' . $configurationNamingContext; + + $result = @ldap_list( + $ds, + $partitions, + (string) $connection->select()->from('*', $adFields)->where('nCName', $defaultNamingContext), + $adFields + ); + if ($result) { + $entry = ldap_first_entry($ds, $result); + if ($entry === false) { + throw new LdapException( + 'Configuration options not available (%s:%d). Discovery of "%s" probably not permitted.', + $connection->getHostname(), + $connection->getPort(), + $partitions + ); + } + + $this->setAttributes((object) array_merge( + (array) $this->attributes, + (array) $connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $adFields) + )); + } else { + if (ldap_errno($ds) !== 1) { + // One stands for "operations error" which occurs if not bound non-anonymously. + + throw new LdapException( + 'Configuration options query failed (%s:%d): %s. Check if hostname and port of the' + . ' ldap resource are correct and if anonymous access is permitted.', + $connection->getHostname(), + $connection->getPort(), + ldap_error($ds) + ); + } + } + } + } + } + + /** + * Determine the active directory version using the available capabillities + * + * @return null|string The server version description or null when unknown + */ + protected function getAdObjectVersionName() + { + if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_W8_OID])) { + return 'Windows Server 2012 (or newer)'; + } + if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_V61_R2_OID])) { + return 'Windows Server 2008 R2 (or newer)'; + } + if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_V60_OID])) { + return 'Windows Server 2008 (or newer)'; + } + return null; + } +} diff --git a/library/Icinga/Protocol/Ldap/LdapConnection.php b/library/Icinga/Protocol/Ldap/LdapConnection.php new file mode 100644 index 0000000..982db42 --- /dev/null +++ b/library/Icinga/Protocol/Ldap/LdapConnection.php @@ -0,0 +1,1584 @@ +config = $config; + $this->hostname = $config->hostname; + $this->bindDn = $config->bind_dn; + $this->bindPw = $config->bind_pw; + $this->rootDn = $config->root_dn; + $this->port = (int) $config->get('port', 389); + $this->timeout = (int) $config->get('timeout', 5); + + $this->encryption = $config->encryption; + if ($this->encryption !== null) { + $this->encryption = strtolower($this->encryption); + } + } + + /** + * Return the ip address, hostname or ldap URI being used to connect with the LDAP server + * + * @return string + */ + public function getHostname() + { + return $this->hostname; + } + + /** + * Return the port being used to connect with the LDAP server + * + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * Return the distinguished name being used as the base path for queries which do not provide one theirselves + * + * @return string + */ + public function getDn() + { + return $this->rootDn; + } + + /** + * Return the root node for this connection + * + * @return Root + */ + public function root() + { + if ($this->root === null) { + $this->root = Root::forConnection($this); + } + + return $this->root; + } + + /** + * Return the LDAP link identifier being used + * + * Establishes a connection if necessary. + * + * @return resource + */ + public function getConnection() + { + if ($this->ds === null) { + $this->ds = $this->prepareNewConnection(); + } + + return $this->ds; + } + + /** + * Return the capabilities of the current connection + * + * @return LdapCapabilities + */ + public function getCapabilities() + { + if ($this->capabilities === null) { + try { + $this->capabilities = LdapCapabilities::discoverCapabilities($this); + $this->discoverySuccess = true; + $this->discoveryError = null; + } catch (LdapException $e) { + Logger::debug($e); + Logger::warning('LADP discovery failed, assuming default LDAP capabilities.'); + $this->capabilities = new LdapCapabilities(); // create empty default capabilities + $this->discoverySuccess = false; + $this->discoveryError = $e; + } + } + + return $this->capabilities; + } + + /** + * Return whether discovery was successful + * + * @return bool true if the capabilities were successfully determined, false if the capabilities were guessed + */ + public function discoverySuccessful() + { + if ($this->discoverySuccess === null) { + $this->getCapabilities(); // Initializes self::$discoverySuccess + } + + return $this->discoverySuccess; + } + + /** + * Get discovery error if any + * + * @return Exception|null + */ + public function getDiscoveryError() + { + return $this->discoveryError; + } + + /** + * Return whether the current connection is encrypted + * + * @return bool + */ + public function isEncrypted() + { + if ($this->encrypted === null) { + return false; + } + + return $this->encrypted; + } + + /** + * Perform a LDAP bind on the current connection + * + * @throws LdapException In case the LDAP bind was unsuccessful or insecure + */ + public function bind() + { + if ($this->bound) { + return $this; + } + + $ds = $this->getConnection(); + + $success = @ldap_bind($ds, $this->bindDn, $this->bindPw); + if (! $success) { + throw new LdapException( + 'LDAP bind (%s / %s) to %s failed: %s', + $this->bindDn, + '***' /* $this->bindPw */, + $this->normalizeHostname($this->hostname), + ldap_error($ds) + ); + } + + $this->bound = true; + return $this; + } + + /** + * Provide a query on this connection + * + * @return LdapQuery + */ + public function select() + { + return new LdapQuery($this); + } + + /** + * Fetch and return all rows of the given query's result set using an iterator + * + * @param LdapQuery $query The query returning the result set + * + * @return ArrayIterator + */ + public function query(LdapQuery $query) + { + return new ArrayIterator($this->fetchAll($query)); + } + + /** + * Count all rows of the given query's result set + * + * @param LdapQuery $query The query returning the result set + * + * @return int + */ + public function count(LdapQuery $query) + { + $this->bind(); + + if (($unfoldAttribute = $query->getUnfoldAttribute()) !== null) { + $desiredColumns = $query->getColumns(); + if (isset($desiredColumns[$unfoldAttribute])) { + $fields = array($unfoldAttribute => $desiredColumns[$unfoldAttribute]); + } elseif (in_array($unfoldAttribute, $desiredColumns, true)) { + $fields = array($unfoldAttribute); + } else { + throw new ProgrammingError( + 'The attribute used to unfold a query\'s result must be selected' + ); + } + + $res = $this->runQuery($query, $fields); + return count($res); + } + + $ds = $this->getConnection(); + $results = $this->ldapSearch($query, array('dn')); + + if ($results === false) { + if (ldap_errno($ds) !== self::LDAP_NO_SUCH_OBJECT) { + throw new LdapException( + 'LDAP count query "%s" (base %s) failed: %s', + (string) $query, + $query->getBase() ?: $this->getDn(), + ldap_error($ds) + ); + } + } + + return ldap_count_entries($ds, $results); + } + + /** + * Retrieve an array containing all rows of the result set + * + * @param LdapQuery $query The query returning the result set + * @param array $fields Request these attributes instead of the ones registered in the given query + * + * @return array + */ + public function fetchAll(LdapQuery $query, array $fields = null) + { + $this->bind(); + + if ($query->getUsePagedResults() && $this->getCapabilities()->hasPagedResult()) { + return $this->runPagedQuery($query, $fields); + } else { + return $this->runQuery($query, $fields); + } + } + + /** + * Fetch the first row of the result set + * + * @param LdapQuery $query The query returning the result set + * @param array $fields Request these attributes instead of the ones registered in the given query + * + * @return mixed + */ + public function fetchRow(LdapQuery $query, array $fields = null) + { + $clonedQuery = clone $query; + $clonedQuery->limit(1); + $clonedQuery->setUsePagedResults(false); + $results = $this->fetchAll($clonedQuery, $fields); + return array_shift($results) ?: false; + } + + /** + * Fetch the first column of all rows of the result set as an array + * + * @param LdapQuery $query The query returning the result set + * @param array $fields Request these attributes instead of the ones registered in the given query + * + * @return array + * + * @throws ProgrammingError In case no attribute is being requested + */ + public function fetchColumn(LdapQuery $query, array $fields = null) + { + if ($fields === null) { + $fields = $query->getColumns(); + } + + if (empty($fields)) { + throw new ProgrammingError('You must request at least one attribute when fetching a single column'); + } + + $alias = key($fields); + $results = $this->fetchAll($query, array($alias => current($fields))); + $column = is_int($alias) ? current($fields) : $alias; + $values = array(); + foreach ($results as $row) { + if (isset($row->$column)) { + $values[] = $row->$column; + } + } + + return $values; + } + + /** + * Fetch the first column of the first row of the result set + * + * @param LdapQuery $query The query returning the result set + * @param array $fields Request these attributes instead of the ones registered in the given query + * + * @return string + */ + public function fetchOne(LdapQuery $query, array $fields = null) + { + $row = $this->fetchRow($query, $fields); + if ($row === false) { + return false; + } + + $values = get_object_vars($row); + if (empty($values)) { + return false; + } + + if ($fields === null) { + // Fetch the desired columns from the query if not explicitly overriden in the method's parameter + $fields = $query->getColumns(); + } + + if (empty($fields)) { + // The desired columns may be empty independently whether provided by the query or the method's parameter + return array_shift($values); + } + + $alias = key($fields); + return $values[is_string($alias) ? $alias : $fields[$alias]]; + } + + /** + * Fetch all rows of the result set as an array of key-value pairs + * + * The first column is the key, the second column is the value. + * + * @param LdapQuery $query The query returning the result set + * @param array $fields Request these attributes instead of the ones registered in the given query + * + * @return array + * + * @throws ProgrammingError In case there are less than two attributes being requested + */ + public function fetchPairs(LdapQuery $query, array $fields = null) + { + if ($fields === null) { + $fields = $query->getColumns(); + } + + if (count($fields) < 2) { + throw new ProgrammingError('You are required to request at least two attributes'); + } + + $columns = $desiredColumnNames = array(); + foreach ($fields as $alias => $column) { + if (is_int($alias)) { + $columns[] = $column; + $desiredColumnNames[] = $column; + } else { + $columns[$alias] = $column; + $desiredColumnNames[] = $alias; + } + + if (count($desiredColumnNames) === 2) { + break; + } + } + + $results = $this->fetchAll($query, $columns); + $pairs = array(); + foreach ($results as $row) { + $colOne = $desiredColumnNames[0]; + $colTwo = $desiredColumnNames[1]; + $pairs[$row->$colOne] = $row->$colTwo; + } + + return $pairs; + } + + /** + * Fetch an LDAP entry by its DN + * + * @param string $dn + * @param array|null $fields + * + * @return StdClass|bool + */ + public function fetchByDn($dn, array $fields = null) + { + return $this->select() + ->from('*', $fields) + ->setBase($dn) + ->setScope('base') + ->fetchRow(); + } + + /** + * Test the given LDAP credentials by establishing a connection and attempting a LDAP bind + * + * @param string $bindDn + * @param string $bindPw + * + * @return bool Whether the given credentials are valid + * + * @throws LdapException In case an error occured while establishing the connection or attempting the bind + */ + public function testCredentials($bindDn, $bindPw) + { + $ds = $this->getConnection(); + $success = @ldap_bind($ds, $bindDn, $bindPw); + if (! $success) { + if (ldap_errno($ds) === self::LDAP_INVALID_CREDENTIALS) { + Logger::debug( + 'Testing LDAP credentials (%s / %s) failed: %s', + $bindDn, + '***', + ldap_error($ds) + ); + return false; + } + + throw new LdapException(ldap_error($ds)); + } + + return true; + } + + /** + * Return whether an entry identified by the given distinguished name exists + * + * @param string $dn + * + * @return bool + */ + public function hasDn($dn) + { + $ds = $this->getConnection(); + $this->bind(); + + $result = ldap_read($ds, $dn, '(objectClass=*)', array('objectClass')); + return ldap_count_entries($ds, $result) > 0; + } + + /** + * Delete a root entry and all of its children identified by the given distinguished name + * + * @param string $dn + * + * @return bool + * + * @throws LdapException In case an error occured while deleting an entry + */ + public function deleteRecursively($dn) + { + $ds = $this->getConnection(); + $this->bind(); + + $result = @ldap_list($ds, $dn, '(objectClass=*)', array('objectClass')); + if ($result === false) { + if (ldap_errno($ds) === self::LDAP_NO_SUCH_OBJECT) { + return false; + } + + throw new LdapException('LDAP list for "%s" failed: %s', $dn, ldap_error($ds)); + } + + $children = ldap_get_entries($ds, $result); + for ($i = 0; $i < $children['count']; $i++) { + $result = $this->deleteRecursively($children[$i]['dn']); + if (! $result) { + // TODO: return result code, if delete fails + throw new LdapException('Recursively deleting "%s" failed', $dn); + } + } + + return $this->deleteDn($dn); + } + + /** + * Delete a single entry identified by the given distinguished name + * + * @param string $dn + * + * @return bool + * + * @throws LdapException In case an error occured while deleting the entry + */ + public function deleteDn($dn) + { + $ds = $this->getConnection(); + $this->bind(); + + $result = @ldap_delete($ds, $dn); + if ($result === false) { + if (ldap_errno($ds) === self::LDAP_NO_SUCH_OBJECT) { + return false; // TODO: Isn't it a success if something i'd like to remove is not existing at all??? + } + + throw new LdapException('LDAP delete for "%s" failed: %s', $dn, ldap_error($ds)); + } + + return true; + } + + /** + * Fetch the distinguished name of the result of the given query + * + * @param LdapQuery $query The query returning the result set + * + * @return string The distinguished name, or false when the given query yields no results + * + * @throws LdapException In case the query yields multiple results + */ + public function fetchDn(LdapQuery $query) + { + $rows = $this->fetchAll($query, array()); + if (count($rows) > 1) { + throw new LdapException('Cannot fetch single DN for %s', $query); + } + + return key($rows); + } + + /** + * Run the given LDAP query and return the resulting entries + * + * @param LdapQuery $query The query to fetch results with + * @param array $fields Request these attributes instead of the ones registered in the given query + * + * @return array + * + * @throws LdapException In case an error occured while fetching the results + */ + protected function runQuery(LdapQuery $query, array $fields = null) + { + $limit = $query->getLimit(); + $offset = $query->hasOffset() ? $query->getOffset() : 0; + + if ($fields === null) { + $fields = $query->getColumns(); + } + + $ds = $this->getConnection(); + + $serverSorting = ! $this->config->disable_server_side_sort + && $this->getCapabilities()->hasOid(LdapCapabilities::LDAP_SERVER_SORT_OID); + + if ($query->hasOrder()) { + if ($serverSorting) { + ldap_set_option($ds, LDAP_OPT_SERVER_CONTROLS, array( + array( + 'oid' => LdapCapabilities::LDAP_SERVER_SORT_OID, + 'value' => $this->encodeSortRules($query->getOrder()) + ) + )); + } elseif (! empty($fields)) { + foreach ($query->getOrder() as $rule) { + if (! in_array($rule[0], $fields, true)) { + $fields[] = $rule[0]; + } + } + } + } + + $unfoldAttribute = $query->getUnfoldAttribute(); + if ($unfoldAttribute) { + foreach ($query->getFilter()->listFilteredColumns() as $filterColumn) { + $fieldKey = array_search($filterColumn, $fields, true); + if ($fieldKey === false || is_string($fieldKey)) { + $fields[] = $filterColumn; + } + } + } + + $results = $this->ldapSearch( + $query, + array_values($fields), + 0, + ($serverSorting || ! $query->hasOrder()) && $limit ? $offset + $limit : 0 + ); + if ($results === false) { + if (ldap_errno($ds) === self::LDAP_NO_SUCH_OBJECT) { + return array(); + } + + throw new LdapException( + 'LDAP query "%s" (base %s) failed. Error: %s', + $query, + $query->getBase() ?: $this->rootDn, + ldap_error($ds) + ); + } elseif (ldap_count_entries($ds, $results) === 0) { + return array(); + } + + $count = 0; + $entries = array(); + $entry = ldap_first_entry($ds, $results); + do { + if ($unfoldAttribute) { + $rows = $this->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields, $unfoldAttribute); + if (is_array($rows)) { + // TODO: Register the DN the same way as a section name in the ArrayDatasource! + foreach ($rows as $row) { + if ($query->getFilter()->matches($row)) { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[] = $row; + } + + if ($serverSorting && $limit > 0 && $limit === count($entries)) { + break; + } + } + } + } else { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[ldap_get_dn($ds, $entry)] = $rows; + } + } + } else { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( + ldap_get_attributes($ds, $entry), + $fields + ); + } + } + } while ((! $serverSorting || $limit === 0 || $limit !== count($entries)) + && ($entry = ldap_next_entry($ds, $entry)) + ); + + if (! $serverSorting) { + if ($query->hasOrder()) { + uasort($entries, array($query, 'compare')); + } + + if ($limit && $count > $limit) { + $entries = array_splice($entries, $query->hasOffset() ? $query->getOffset() : 0, $limit); + } + } + + ldap_free_result($results); + return $entries; + } + + /** + * Run the given LDAP query and return the resulting entries + * + * This utilizes paged search requests as defined in RFC 2696. + * + * @param LdapQuery $query The query to fetch results with + * @param array $fields Request these attributes instead of the ones registered in the given query + * @param int $pageSize The maximum page size, defaults to self::PAGE_SIZE + * + * @return array + * + * @throws LdapException In case an error occured while fetching the results + */ + protected function runPagedQuery(LdapQuery $query, array $fields = null, $pageSize = null) + { + if ($pageSize === null) { + $pageSize = static::PAGE_SIZE; + } + + $limit = $query->getLimit(); + $offset = $query->hasOffset() ? $query->getOffset() : 0; + + if ($fields === null) { + $fields = $query->getColumns(); + } + + $ds = $this->getConnection(); + + $serverSorting = false;//$this->getCapabilities()->hasOid(LdapCapabilities::LDAP_SERVER_SORT_OID); + if (! $serverSorting && $query->hasOrder() && ! empty($fields)) { + foreach ($query->getOrder() as $rule) { + if (! in_array($rule[0], $fields, true)) { + $fields[] = $rule[0]; + } + } + } + + $unfoldAttribute = $query->getUnfoldAttribute(); + if ($unfoldAttribute) { + foreach ($query->getFilter()->listFilteredColumns() as $filterColumn) { + $fieldKey = array_search($filterColumn, $fields, true); + if ($fieldKey === false || is_string($fieldKey)) { + $fields[] = $filterColumn; + } + } + } + + $controls = []; + $legacyControlHandling = version_compare(PHP_VERSION, '7.3.0') < 0; + if ($serverSorting && $query->hasOrder()) { + $control = [ + 'oid' => LDAP_CONTROL_SORTREQUEST, + 'value' => $this->encodeSortRules($query->getOrder()) + ]; + if ($legacyControlHandling) { + ldap_set_option($ds, LDAP_OPT_SERVER_CONTROLS, [$control]); + } else { + $controls[LDAP_CONTROL_SORTREQUEST] = $control; + } + } + + $count = 0; + $cookie = ''; + $entries = array(); + do { + if ($legacyControlHandling) { + // Do not request the pagination control as a critical extension, as we want the + // server to return results even if the paged search request cannot be satisfied + ldap_control_paged_result($ds, $pageSize, false, $cookie); + } else { + $controls[LDAP_CONTROL_PAGEDRESULTS] = [ + 'oid' => LDAP_CONTROL_PAGEDRESULTS, + 'iscritical' => false, // See above + 'value' => [ + 'size' => $pageSize, + 'cookie' => $cookie + ] + ]; + } + + $results = $this->ldapSearch( + $query, + array_values($fields), + 0, + ($serverSorting || ! $query->hasOrder()) && $limit ? $offset + $limit : 0, + 0, + LDAP_DEREF_NEVER, + empty($controls) ? null : $controls + ); + if ($results === false) { + if (ldap_errno($ds) === self::LDAP_NO_SUCH_OBJECT) { + break; + } + + throw new LdapException( + 'LDAP query "%s" (base %s) failed. Error: %s', + (string) $query, + $query->getBase() ?: $this->getDn(), + ldap_error($ds) + ); + } elseif (ldap_count_entries($ds, $results) === 0) { + if (in_array( + ldap_errno($ds), + array(static::LDAP_SIZELIMIT_EXCEEDED, static::LDAP_ADMINLIMIT_EXCEEDED), + true + )) { + Logger::warning( + 'Unable to request more than %u results. Does the server allow paged search requests? (%s)', + $count, + ldap_error($ds) + ); + } + + break; + } + + $entry = ldap_first_entry($ds, $results); + do { + if ($unfoldAttribute) { + $rows = $this->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields, $unfoldAttribute); + if (is_array($rows)) { + // TODO: Register the DN the same way as a section name in the ArrayDatasource! + foreach ($rows as $row) { + if ($query->getFilter()->matches($row)) { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[] = $row; + } + + if ($serverSorting && $limit > 0 && $limit === count($entries)) { + break; + } + } + } + } else { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[ldap_get_dn($ds, $entry)] = $rows; + } + } + } else { + $count += 1; + if (! $serverSorting || $offset === 0 || $offset < $count) { + $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( + ldap_get_attributes($ds, $entry), + $fields + ); + } + } + } while ((! $serverSorting || $limit === 0 || $limit !== count($entries)) + && ($entry = ldap_next_entry($ds, $entry)) + ); + + if ($legacyControlHandling) { + if (false === @ldap_control_paged_result_response($ds, $results, $cookie)) { + // If the page size is greater than or equal to the sizeLimit value, the server should ignore the + // control as the request can be satisfied in a single page: https://www.ietf.org/rfc/rfc2696.txt + // This applies no matter whether paged search requests are permitted or not. You're done once you + // got everything you were out for. + if ($serverSorting && count($entries) !== $limit) { + // The server does not support pagination, but still returned a response by ignoring the + // pagedResultsControl. We output a warning to indicate that the pagination control was ignored. + Logger::warning( + 'Unable to request paged LDAP results. Does the server allow paged search requests?' + ); + } + } + } else { + ldap_parse_result($ds, $results, $errno, $dn, $errmsg, $refs, $controlsReturned); + $cookie = $controlsReturned[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; + } + + ldap_free_result($results); + } while ($cookie && (! $serverSorting || $limit === 0 || count($entries) < $limit)); + + if ($legacyControlHandling && $cookie) { + // A sequence of paged search requests is abandoned by the client sending a search request containing a + // pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by + // the server: https://www.ietf.org/rfc/rfc2696.txt + ldap_control_paged_result($ds, 0, false, $cookie); + // Returns no entries, due to the page size + ldap_search($ds, $query->getBase() ?: $this->getDn(), (string) $query); + } + + if (! $serverSorting) { + if ($query->hasOrder()) { + uasort($entries, array($query, 'compare')); + } + + if ($limit && $count > $limit) { + $entries = array_splice($entries, $query->hasOffset() ? $query->getOffset() : 0, $limit); + } + } + + return $entries; + } + + /** + * Clean up the given attributes and return them as simple object + * + * Applies column aliases, aggregates/unfolds multi-value attributes + * as array and sets null for each missing attribute. + * + * @param array $attributes + * @param array $requestedFields + * @param string $unfoldAttribute + * + * @return object|array An array in case the object has been unfolded + */ + public function cleanupAttributes($attributes, array $requestedFields, $unfoldAttribute = null) + { + // In case the result contains attributes with a differing case than the requested fields, it is + // necessary to create another array to map attributes case insensitively to their requested counterparts. + // This does also apply the virtual alias handling. (Since an LDAP server does not handle such) + $loweredFieldMap = array(); + foreach ($requestedFields as $alias => $name) { + $loweredName = strtolower($name); + if (isset($loweredFieldMap[$loweredName])) { + if (! is_array($loweredFieldMap[$loweredName])) { + $loweredFieldMap[$loweredName] = array($loweredFieldMap[$loweredName]); + } + + $loweredFieldMap[$loweredName][] = is_string($alias) ? $alias : $name; + } else { + $loweredFieldMap[$loweredName] = is_string($alias) ? $alias : $name; + } + } + + $cleanedAttributes = array(); + for ($i = 0; $i < $attributes['count']; $i++) { + $attribute_name = $attributes[$i]; + if ($attributes[$attribute_name]['count'] === 1) { + $attribute_value = $attributes[$attribute_name][0]; + } else { + $attribute_value = array(); + for ($j = 0; $j < $attributes[$attribute_name]['count']; $j++) { + $attribute_value[] = $attributes[$attribute_name][$j]; + } + } + + $requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)]) + ? $loweredFieldMap[strtolower($attribute_name)] + : $attribute_name; + if (is_array($requestedAttributeName)) { + foreach ($requestedAttributeName as $requestedName) { + $cleanedAttributes[$requestedName] = $attribute_value; + } + } else { + $cleanedAttributes[$requestedAttributeName] = $attribute_value; + } + } + + // The result may not contain all requested fields, so populate the cleaned + // result with the missing fields and their value being set to null + foreach ($requestedFields as $alias => $name) { + if (! is_string($alias)) { + $alias = $name; + } + + if (! array_key_exists($alias, $cleanedAttributes)) { + $cleanedAttributes[$alias] = null; + Logger::debug('LDAP query result does not provide the requested field "%s"', $name); + } + } + + if ($unfoldAttribute !== null + && isset($cleanedAttributes[$unfoldAttribute]) + && is_array($cleanedAttributes[$unfoldAttribute]) + ) { + $siblings = array(); + foreach ($loweredFieldMap as $loweredName => $requestedNames) { + if (is_array($requestedNames) && in_array($unfoldAttribute, $requestedNames, true)) { + $siblings = array_diff($requestedNames, array($unfoldAttribute)); + break; + } + } + + $values = $cleanedAttributes[$unfoldAttribute]; + unset($cleanedAttributes[$unfoldAttribute]); + $baseRow = (object) $cleanedAttributes; + $rows = array(); + foreach ($values as $value) { + $row = clone $baseRow; + $row->{$unfoldAttribute} = $value; + foreach ($siblings as $sibling) { + $row->{$sibling} = $value; + } + + $rows[] = $row; + } + + return $rows; + } + + return (object) $cleanedAttributes; + } + + /** + * Encode the given array of sort rules as ASN.1 octet stream according to RFC 2891 + * + * @param array $sortRules + * + * @return string Binary representation of the octet stream + */ + protected function encodeSortRules(array $sortRules) + { + $sequenceOf = ''; + + foreach ($sortRules as $rule) { + if ($rule[1] === Sortable::SORT_DESC) { + $reversed = '8101ff'; + } else { + $reversed = ''; + } + + $attributeType = unpack('H*', $rule[0]); + $attributeType = $attributeType[1]; + $attributeOctets = strlen($attributeType) / 2; + if ($attributeOctets >= 127) { + // Use the indefinite form of the length octets (the long form would be another option) + $attributeType = '0440' . $attributeType . '0000'; + } else { + $attributeType = '04' . str_pad(dechex($attributeOctets), 2, '0', STR_PAD_LEFT) . $attributeType; + } + + $sequence = $attributeType . $reversed; + $sequenceOctects = strlen($sequence) / 2; + if ($sequenceOctects >= 127) { + $sequence = '3040' . $sequence . '0000'; + } else { + $sequence = '30' . str_pad(dechex($sequenceOctects), 2, '0', STR_PAD_LEFT) . $sequence; + } + + $sequenceOf .= $sequence; + } + + $sequenceOfOctets = strlen($sequenceOf) / 2; + if ($sequenceOfOctets >= 127) { + $sequenceOf = '3040' . $sequenceOf . '0000'; + } else { + $sequenceOf = '30' . str_pad(dechex($sequenceOfOctets), 2, '0', STR_PAD_LEFT) . $sequenceOf; + } + + return hex2bin($sequenceOf); + } + + /** + * Prepare and establish a connection with the LDAP server + * + * @param Inspection $info Optional inspection to fill with diagnostic info + * + * @return resource A LDAP link identifier + * + * @throws LdapException In case the connection is not possible + */ + protected function prepareNewConnection(Inspection $info = null) + { + if (! isset($info)) { + $info = new Inspection(''); + } + + $hostname = $this->normalizeHostname($this->hostname); + + $ds = ldap_connect($hostname, $this->port); + + // Set a proper timeout for each connection + ldap_set_option($ds, LDAP_OPT_NETWORK_TIMEOUT, $this->timeout); + + // Usage of ldap_rename, setting LDAP_OPT_REFERRALS to 0 or using STARTTLS requires LDAPv3. + // If this does not work we're probably not in a PHP 5.3+ environment as it is VERY + // unlikely that the server complains about it by itself prior to a bind request + ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); + + // Not setting this results in "Operations error" on AD when using the whole domain as search base + ldap_set_option($ds, LDAP_OPT_REFERRALS, 0); + + if ($this->encryption === static::LDAPS) { + $info->write('Connect using LDAPS'); + } elseif ($this->encryption === static::STARTTLS) { + $this->encrypted = true; + $info->write('Connect using STARTTLS'); + if (! ldap_start_tls($ds)) { + throw new LdapException('LDAP STARTTLS failed: %s', ldap_error($ds)); + } + } elseif ($this->encryption !== static::LDAPS) { + $this->encrypted = false; + $info->write('Connect without encryption'); + } + + return $ds; + } + + /** + * Perform a LDAP search and return the result + * + * @param LdapQuery $query + * @param array $attributes An array of the required attributes + * @param int $attrsonly Should be set to 1 if only attribute types are wanted + * @param int $sizelimit Enables you to limit the count of entries fetched + * @param int $timelimit Sets the number of seconds how long is spend on the search + * @param int $deref + * @param array $controls LDAP Controls to send with the request (Only supported with PHP v7.3+) + * + * @return resource|bool A search result identifier or false on error + * + * @throws LogicException If the LDAP query search scope is unsupported + */ + public function ldapSearch( + LdapQuery $query, + array $attributes = null, + $attrsonly = 0, + $sizelimit = 0, + $timelimit = 0, + $deref = LDAP_DEREF_NEVER, + $controls = null + ) { + $queryString = (string) $query; + $baseDn = $query->getBase() ?: $this->getDn(); + $scope = $query->getScope(); + + if (Logger::getInstance()->getLevel() === Logger::DEBUG) { + // We're checking the level by ourselves to avoid rendering the ldapsearch commandline for nothing + $starttlsParam = $this->encryption === static::STARTTLS ? ' -ZZ' : ''; + + $bindParams = ''; + if ($this->bound) { + $bindParams = ' -D "' . $this->bindDn . '"' . ($this->bindPw ? ' -W' : ''); + } + + if ($deref === LDAP_DEREF_NEVER) { + $derefName = 'never'; + } elseif ($deref === LDAP_DEREF_ALWAYS) { + $derefName = 'always'; + } elseif ($deref === LDAP_DEREF_SEARCHING) { + $derefName = 'search'; + } else { // $deref === LDAP_DEREF_FINDING + $derefName = 'find'; + } + + Logger::debug("Issuing LDAP search. Use '%s' to reproduce.", sprintf( + 'ldapsearch -P 3%s -H "%s"%s -b "%s" -s "%s" -z %u -l %u -a "%s"%s%s%s', + $starttlsParam, + $this->normalizeHostname($this->hostname), + $bindParams, + $baseDn, + $scope, + $sizelimit, + $timelimit, + $derefName, + $attrsonly ? ' -A' : '', + $queryString ? ' "' . $queryString . '"' : '', + $attributes ? ' "' . join('" "', $attributes) . '"' : '' + )); + } + + switch ($scope) { + case LdapQuery::SCOPE_SUB: + $function = 'ldap_search'; + break; + case LdapQuery::SCOPE_ONE: + $function = 'ldap_list'; + break; + case LdapQuery::SCOPE_BASE: + $function = 'ldap_read'; + break; + default: + throw new LogicException('LDAP scope %s not supported by ldapSearch', $scope); + } + + // Explicit calls with and without controls, + // because the parameter is only supported since PHP 7.3. + // Since it is a public method, + // providing controls will naturally fail if the parameter is not supported by PHP. + if ($controls !== null) { + return @$function( + $this->getConnection(), + $baseDn, + $queryString, + $attributes, + $attrsonly, + $sizelimit, + $timelimit, + $deref, + $controls + ); + } else { + return @$function( + $this->getConnection(), + $baseDn, + $queryString, + $attributes, + $attrsonly, + $sizelimit, + $timelimit, + $deref + ); + } + } + + /** + * Create an LDAP entry + * + * @param string $dn The distinguished name to use + * @param array $attributes The entry's attributes + * + * @return bool Whether the operation was successful + */ + public function addEntry($dn, array $attributes) + { + return ldap_add($this->getConnection(), $dn, $attributes); + } + + /** + * Modify an LDAP entry + * + * @param string $dn The distinguished name to use + * @param array $attributes The attributes to update the entry with + * + * @return bool Whether the operation was successful + */ + public function modifyEntry($dn, array $attributes) + { + return ldap_modify($this->getConnection(), $dn, $attributes); + } + + /** + * Change the distinguished name of an LDAP entry + * + * @param string $dn The entry's current distinguished name + * @param string $newRdn The new relative distinguished name + * @param string $newParentDn The new parent or superior entry's distinguished name + * + * @return resource The resulting search result identifier + * + * @throws LdapException In case an error occured + */ + public function moveEntry($dn, $newRdn, $newParentDn) + { + $ds = $this->getConnection(); + $result = ldap_rename($ds, $dn, $newRdn, $newParentDn, false); + if ($result === false) { + throw new LdapException('Could not move entry "%s" to "%s": %s', $dn, $newRdn, ldap_error($ds)); + } + + return $result; + } + + /** + * Return the LDAP specific configuration directory with the given relative path being appended + * + * @param string $sub + * + * @return string + */ + protected function getConfigDir($sub = null) + { + $dir = Config::$configDir . '/ldap'; + if ($sub !== null) { + $dir .= '/' . $sub; + } + + return $dir; + } + + /** + * Render and return a valid LDAP filter representation of the given filter + * + * @param Filter $filter + * @param int $level + * + * @return string + */ + public function renderFilter(Filter $filter, $level = 0) + { + if ($filter->isExpression()) { + /** @var $filter FilterExpression */ + return $this->renderFilterExpression($filter); + } + + /** @var $filter FilterChain */ + $parts = array(); + foreach ($filter->filters() as $filterPart) { + $part = $this->renderFilter($filterPart, $level + 1); + if ($part) { + $parts[] = $part; + } + } + + if (empty($parts)) { + return ''; + } + + $format = '%1$s(%2$s)'; + if (count($parts) === 1 && ! $filter instanceof FilterNot) { + $format = '%2$s'; + } + if ($level === 0) { + $format = '(' . $format . ')'; + } + + return sprintf($format, $filter->getOperatorSymbol(), implode(')(', $parts)); + } + + /** + * Render and return a valid LDAP filter expression of the given filter + * + * @param FilterExpression $filter + * + * @return string + */ + protected function renderFilterExpression(FilterExpression $filter) + { + $column = $filter->getColumn(); + $sign = $filter->getSign(); + $expression = $filter->getExpression(); + $format = '%1$s%2$s%3$s'; + + if ($expression === null || $expression === true) { + $expression = '*'; + } elseif (is_array($expression)) { + $seqFormat = '|(%s)'; + if ($sign === '!=') { + $seqFormat = '!(' . $seqFormat . ')'; + $sign = '='; + } + + $seqParts = array(); + foreach ($expression as $expressionValue) { + $seqParts[] = sprintf( + $format, + LdapUtils::quoteForSearch($column), + $sign, + LdapUtils::quoteForSearch($expressionValue, true) + ); + } + + return sprintf($seqFormat, implode(')(', $seqParts)); + } + + if ($sign === '!=') { + $format = '!(%1$s=%3$s)'; + } + + return sprintf( + $format, + LdapUtils::quoteForSearch($column), + $sign, + LdapUtils::quoteForSearch($expression, true) + ); + } + + /** + * Inspect if this LDAP Connection is working as expected + * + * Check if connection, bind and encryption is working as expected and get additional + * information about the used + * + * @return Inspection Inspection result + */ + public function inspect() + { + $insp = new Inspection('Ldap Connection'); + + // Try to connect to the server with the given connection parameters + try { + $ds = $this->prepareNewConnection($insp); + } catch (Exception $e) { + if ($this->encryption === 'starttls') { + // The Exception does not return any proper error messages in case of certificate errors. Connecting + // by STARTTLS will usually fail at this point when the certificate is unknown, + // so at least try to give some hints. + $insp->write('NOTE: There might be an issue with the chosen encryption. Ensure that the LDAP-Server ' . + 'supports STARTTLS and that the LDAP-Client is configured to accept its certificate.'); + } + return $insp->error($e->getMessage()); + } + + // Try a bind-command with the given user credentials, this must not fail + $success = @ldap_bind($ds, $this->bindDn, $this->bindPw); + $msg = sprintf( + 'LDAP bind (%s / %s) to %s', + $this->bindDn, + '***' /* $this->bindPw */, + $this->normalizeHostname($this->hostname) + ); + if (! $success) { + // ldap_error does not return any proper error messages in case of certificate errors. Connecting + // by LDAPS will usually fail at this point when the certificate is unknown, so at least try to give + // some hints. + if ($this->encryption === 'ldaps') { + $insp->write('NOTE: There might be an issue with the chosen encryption. Ensure that the LDAP-Server ' . + ' supports LDAPS and that the LDAP-Client is configured to accept its certificate.'); + } + return $insp->error(sprintf('%s failed: %s', $msg, ldap_error($ds))); + } + $insp->write(sprintf($msg . ' successful')); + + // Try to execute a schema discovery this may fail if schema discovery is not supported + try { + $cap = LdapCapabilities::discoverCapabilities($this); + $discovery = new Inspection('Discovery Results'); + $vendor = $cap->getVendor(); + if (isset($vendor)) { + $discovery->write($vendor); + } + $version = $cap->getVersion(); + if (isset($version)) { + $discovery->write($version); + } + $discovery->write('Supports STARTTLS: ' . ($cap->hasStartTls() ? 'True' : 'False')); + $discovery->write('Default naming context: ' . $cap->getDefaultNamingContext()); + $insp->write($discovery); + } catch (Exception $e) { + $insp->write('Schema discovery not possible: ' . $e->getMessage()); + } + return $insp; + } + + protected function normalizeHostname($hostname) + { + $scheme = $this->encryption === static::LDAPS ? 'ldaps://' : 'ldap://'; + $normalizeHostname = function ($hostname) use ($scheme) { + if (strpos($hostname, $scheme) === false) { + $hostname = $scheme . $hostname; + } + + if (! preg_match('/:\d+$/', $hostname)) { + $hostname .= ':' . $this->port; + } + + return $hostname; + }; + + $ldapUrls = explode(' ', $hostname); + if (count($ldapUrls) > 1) { + foreach ($ldapUrls as & $uri) { + $uri = $normalizeHostname($uri); + } + + $hostname = implode(' ', $ldapUrls); + } else { + $hostname = $normalizeHostname($hostname); + } + + return $hostname; + } +} diff --git a/library/Icinga/Protocol/Ldap/LdapException.php b/library/Icinga/Protocol/Ldap/LdapException.php new file mode 100644 index 0000000..740ee29 --- /dev/null +++ b/library/Icinga/Protocol/Ldap/LdapException.php @@ -0,0 +1,14 @@ +usePagedResults = false; + } + + /** + * Set the base dn to be used for this query + * + * @param string $base + * + * @return $this + */ + public function setBase($base) + { + $this->base = $base; + return $this; + } + + /** + * Return the base dn being used for this query + * + * @return string + */ + public function getBase() + { + return $this->base; + } + + /** + * Set whether this query is permitted to utilize paged results + * + * @param bool $state + * + * @return $this + */ + public function setUsePagedResults($state = true) + { + $this->usePagedResults = (bool) $state; + return $this; + } + + /** + * Return whether this query is permitted to utilize paged results + * + * @return bool + */ + public function getUsePagedResults() + { + return $this->usePagedResults; + } + + /** + * Set the attribute to be used to unfold the result + * + * @param string $attributeName + * + * @return $this + */ + public function setUnfoldAttribute($attributeName) + { + $this->unfoldAttribute = $attributeName; + return $this; + } + + /** + * Return the attribute to use to unfold the result + * + * @return string + */ + public function getUnfoldAttribute() + { + return $this->unfoldAttribute; + } + + /** + * Set this query's native LDAP filter + * + * @param string $filter + * + * @return $this + */ + public function setNativeFilter($filter) + { + $this->nativeFilter = $filter; + return $this; + } + + /** + * Return this query's native LDAP filter + * + * @return string + */ + public function getNativeFilter() + { + return $this->nativeFilter; + } + + /** + * Choose an objectClass and the columns you are interested in + * + * {@inheritdoc} This creates an objectClass filter. + */ + public function from($target, array $fields = null) + { + $this->where('objectClass', $target); + return parent::from($target, $fields); + } + + public function where($condition, $value = null) + { + $this->addFilter(Filter::expression($condition, '=', $value)); + return $this; + } + + public function addFilter(Filter $filter) + { + $this->makeCaseInsensitive($filter); + return parent::addFilter($filter); + } + + public function setFilter(Filter $filter) + { + $this->makeCaseInsensitive($filter); + return parent::setFilter($filter); + } + + protected function makeCaseInsensitive(Filter $filter) + { + if ($filter->isExpression()) { + /** @var \Icinga\Data\Filter\FilterExpression $filter */ + $filter->setCaseSensitive(false); + } else { + /** @var \Icinga\Data\Filter\FilterChain $filter */ + foreach ($filter->filters() as $subFilter) { + $this->makeCaseInsensitive($subFilter); + } + } + } + + public function compare($a, $b, $orderIndex = 0) + { + if (array_key_exists($orderIndex, $this->order)) { + $column = $this->order[$orderIndex][0]; + $direction = $this->order[$orderIndex][1]; + + $flippedColumns = $this->flippedColumns ?: array_flip($this->columns); + if (array_key_exists($column, $flippedColumns) && is_string($flippedColumns[$column])) { + $column = $flippedColumns[$column]; + } + + if (is_array($a->$column)) { + // rfc2891 states: If a sort key is a multi-valued attribute, and an entry happens to + // have multiple values for that attribute and no other controls are + // present that affect the sorting order, then the server SHOULD use the + // least value (according to the ORDERING rule for that attribute). + $a = clone $a; + $a->$column = array_reduce($a->$column, function ($carry, $item) use ($direction) { + $result = $carry === null ? 0 : strcmp($item, $carry); + return ($direction === self::SORT_ASC ? $result : $result * -1) < 1 ? $item : $carry; + }); + } + + if (is_array($b->$column)) { + $b = clone $b; + $b->$column = array_reduce($b->$column, function ($carry, $item) use ($direction) { + $result = $carry === null ? 0 : strcmp($item, $carry); + return ($direction === self::SORT_ASC ? $result : $result * -1) < 1 ? $item : $carry; + }); + } + } + + return parent::compare($a, $b, $orderIndex); + } + + /** + * Fetch result as tree + * + * @return Root + * + * @todo This is untested waste, not being used anywhere and ignores the query's order and base dn. + * Evaluate whether it's reasonable to properly implement and test it. + */ + public function fetchTree() + { + $result = $this->fetchAll(); + $sorted = array(); + $quotedDn = preg_quote($this->ds->getDn(), '/'); + foreach ($result as $key => & $item) { + $new_key = LdapUtils::implodeDN( + array_reverse( + LdapUtils::explodeDN( + preg_replace('/,' . $quotedDn . '$/', '', $key) + ) + ) + ); + $sorted[$new_key] = $key; + } + unset($groups); + ksort($sorted); + + $tree = Root::forConnection($this->ds); + $root_dn = $tree->getDN(); + foreach ($sorted as $sort_key => & $key) { + if ($key === $root_dn) { + continue; + } + $tree->createChildByDN($key, $result[$key]); + } + return $tree; + } + + /** + * Fetch the distinguished name of the first result + * + * @return string|false The distinguished name or false in case it's not possible to fetch a result + * + * @throws LdapException In case the query returns multiple results + * (i.e. it's not possible to fetch a unique DN) + */ + public function fetchDn() + { + return $this->ds->fetchDn($this); + } + + /** + * Render and return this query's filter + * + * @return string + */ + public function renderFilter() + { + $filter = $this->ds->renderFilter($this->filter); + if ($this->nativeFilter) { + $filter = '(&(' . $this->nativeFilter . ')' . $filter . ')'; + } + + return $filter; + } + + /** + * Return the LDAP filter to be applied on this query + * + * @return string + */ + public function __toString() + { + return $this->renderFilter(); + } + + /** + * Get LDAP search scope + * + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * Set LDAP search scope + * + * Valid: sub one base (Default: sub) + * + * @param string $scope + * + * @return LdapQuery + * + * @throws LogicException If scope value is invalid + */ + public function setScope($scope) + { + if (! in_array($scope, static::$scopes)) { + throw new LogicException( + 'Can\'t set scope %d, it is is invalid. Use one of %s or LdapQuery\'s constants.', + $scope, + implode(', ', static::$scopes) + ); + } + $this->scope = $scope; + return $this; + } +} diff --git a/library/Icinga/Protocol/Ldap/LdapUtils.php b/library/Icinga/Protocol/Ldap/LdapUtils.php new file mode 100644 index 0000000..9c9ae10 --- /dev/null +++ b/library/Icinga/Protocol/Ldap/LdapUtils.php @@ -0,0 +1,148 @@ + + * @author Icinga-Web Team + * @package Icinga\Protocol\Ldap + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class LdapUtils +{ + /** + * Extends PHPs ldap_explode_dn() function + * + * UTF-8 chars like German umlauts would otherwise be escaped and shown + * as backslash-prefixed hexcode-sequenzes. + * + * @param string $dn DN + * @param boolean $with_type Returns 'type=value' when true and 'value' when false + * + * @return array + */ + public static function explodeDN($dn, $with_type = true) + { + $res = ldap_explode_dn($dn, $with_type ? 0 : 1); + + foreach ($res as $k => $v) { + $res[$k] = preg_replace_callback( + '/\\\([0-9a-f]{2})/i', + function ($m) { + return chr(hexdec($m[1])); + }, + $v + ); + } + unset($res['count']); + return $res; + } + + /** + * Implode unquoted RDNs to a DN + * + * TODO: throw away, this is not how it shall be done + * + * @param array $parts DN-component + * + * @return string + */ + public static function implodeDN($parts) + { + $str = ''; + foreach ($parts as $part) { + if ($str !== '') { + $str .= ','; + } + list($key, $val) = preg_split('~=~', $part, 2); + $str .= $key . '=' . self::quoteForDN($val); + } + return $str; + } + + /** + * Test if supplied value looks like a DN + * + * @param mixed $value + * + * @return bool + */ + public static function isDn($value) + { + if (is_string($value)) { + return ldap_dn2ufn($value) !== false; + } + return false; + } + + /** + * Quote a string that should be used in a DN + * + * Special characters will be escaped + * + * @param string $str DN-component + * + * @return string + */ + public static function quoteForDN($str) + { + return self::quoteChars( + $str, + array( + ',', + '=', + '+', + '<', + '>', + ';', + '\\', + '"', + '#' + ) + ); + } + + /** + * Quote a string that should be used in an LDAP search + * + * Special characters will be escaped + * + * @param string String to be escaped + * @param bool $allow_wildcard + * @return string + */ + public static function quoteForSearch($str, $allow_wildcard = false) + { + if ($allow_wildcard) { + return self::quoteChars($str, array('(', ')', '\\', chr(0))); + } + return self::quoteChars($str, array('*', '(', ')', '\\', chr(0))); + } + + /** + * Escape given characters in the given string + * + * Special characters will be escaped + * + * @param $str + * @param $chars + * @internal param String $string to be escaped + * @return string + */ + protected static function quoteChars($str, $chars) + { + $quotedChars = array(); + foreach ($chars as $k => $v) { + // Temporarily prefixing with illegal '(' + $quotedChars[$k] = '(' . str_pad(dechex(ord($v)), 2, '0'); + } + $str = str_replace($chars, $quotedChars, $str); + // Replacing temporary '(' with '\\'. This is a workaround, as + // str_replace behaves pretty strange with leading a backslash: + $str = preg_replace('~\(~', '\\', $str); + return $str; + } +} diff --git a/library/Icinga/Protocol/Ldap/Node.php b/library/Icinga/Protocol/Ldap/Node.php new file mode 100644 index 0000000..176f962 --- /dev/null +++ b/library/Icinga/Protocol/Ldap/Node.php @@ -0,0 +1,69 @@ + + * @author Icinga-Web Team + * @package Icinga\Protocol\Ldap + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Node extends Root +{ + /** + * @var LdapConnection + */ + protected $connection; + + /** + * @var + */ + protected $rdn; + + /** + * @var Root + */ + protected $parent; + + /** + * @param Root $parent + */ + protected function __construct(Root $parent) + { + $this->connection = $parent->getConnection(); + $this->parent = $parent; + } + + /** + * @param $parent + * @param $rdn + * @param array $props + * @return Node + */ + public static function createWithRDN($parent, $rdn, $props = array()) + { + $node = new Node($parent); + $node->rdn = $rdn; + $node->props = $props; + return $node; + } + + /** + * @return mixed + */ + public function getRDN() + { + return $this->rdn; + } + + /** + * @return mixed|string + */ + public function getDN() + { + return $this->getRDN() . ',' . $this->parent->getDN(); + } +} diff --git a/library/Icinga/Protocol/Ldap/Root.php b/library/Icinga/Protocol/Ldap/Root.php new file mode 100644 index 0000000..5d7a63d --- /dev/null +++ b/library/Icinga/Protocol/Ldap/Root.php @@ -0,0 +1,241 @@ + + * @author Icinga-Web Team + * @package Icinga\Protocol\Ldap + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + * @package Icinga\Protocol\Ldap + */ +class Root +{ + /** + * @var string + */ + protected $rdn; + + /** + * @var LdapConnection + */ + protected $connection; + + /** + * @var array + */ + protected $children = array(); + + /** + * @var array + */ + protected $props = array(); + + /** + * @param LdapConnection $connection + */ + protected function __construct(LdapConnection $connection) + { + $this->connection = $connection; + } + + /** + * @return bool + */ + public function hasParent() + { + return false; + } + + /** + * @param LdapConnection $connection + * @return Root + */ + public static function forConnection(LdapConnection $connection) + { + $root = new Root($connection); + return $root; + } + + /** + * @param $dn + * @param array $props + * @return Node + */ + public function createChildByDN($dn, $props = array()) + { + $dn = $this->stripMyDN($dn); + $parts = array_reverse(LdapUtils::explodeDN($dn)); + $parent = $this; + while ($rdn = array_shift($parts)) { + if ($parent->hasChildRDN($rdn)) { + $child = $parent->getChildByRDN($rdn); + } else { + $child = Node::createWithRDN($parent, $rdn, (array)$props); + $parent->addChild($child); + } + $parent = $child; + } + return $child; + } + + /** + * @param $rdn + * @return bool + */ + public function hasChildRDN($rdn) + { + return array_key_exists(strtolower($rdn), $this->children); + } + + /** + * @param $rdn + * @return mixed + * @throws IcingaException + */ + public function getChildByRDN($rdn) + { + if (!$this->hasChildRDN($rdn)) { + throw new IcingaException( + 'The child RDN "%s" is not available', + $rdn + ); + } + return $this->children[strtolower($rdn)]; + } + + /** + * @return array + */ + public function children() + { + return $this->children; + } + + public function countChildren() + { + return count($this->children); + } + + /** + * @return bool + */ + public function hasChildren() + { + return !empty($this->children); + } + + /** + * @param Node $child + * @return $this + */ + public function addChild(Node $child) + { + $this->children[strtolower($child->getRDN())] = $child; + return $this; + } + + /** + * @param $dn + * @return string + */ + protected function stripMyDN($dn) + { + $this->assertSubDN($dn); + return substr($dn, 0, strlen($dn) - strlen($this->getDN()) - 1); + } + + /** + * @param $dn + * @return $this + * @throws IcingaException + */ + protected function assertSubDN($dn) + { + $mydn = $this->getDN(); + $end = substr($dn, -1 * strlen($mydn)); + if (strtolower($end) !== strtolower($mydn)) { + throw new IcingaException( + '"%s" is not a child of "%s"', + $dn, + $mydn + ); + } + if (strlen($dn) === strlen($mydn)) { + throw new IcingaException( + '"%s" is not a child of "%s", they are equal', + $dn, + $mydn + ); + } + return $this; + } + + /** + * @param LdapConnection $connection + * @return $this + */ + public function setConnection(LdapConnection $connection) + { + $this->connection = $connection; + return $this; + } + + /** + * @return LdapConnection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * @return bool + */ + public function hasBeenChanged() + { + return false; + } + + /** + * @return mixed + */ + public function getRDN() + { + return $this->getDN(); + } + + /** + * @return mixed + */ + public function getDN() + { + return $this->connection->getDn(); + } + + /** + * @param $key + * @return null + */ + public function __get($key) + { + if (!array_key_exists($key, $this->props)) { + return null; + } + return $this->props[$key]; + } + + /** + * @param $key + * @return bool + */ + public function __isset($key) + { + return array_key_exists($key, $this->props); + } +} diff --git a/library/Icinga/Protocol/Nrpe/Connection.php b/library/Icinga/Protocol/Nrpe/Connection.php new file mode 100644 index 0000000..491a965 --- /dev/null +++ b/library/Icinga/Protocol/Nrpe/Connection.php @@ -0,0 +1,111 @@ +host = $host; + $this->port = $port; + } + + public function useSsl($use_ssl = true) + { + $this->use_ssl = $use_ssl; + return $this; + } + + public function sendCommand($command, $args = null) + { + if (! empty($args)) { + $command .= '!' . implode('!', $args); + } + + $packet = Packet::createQuery($command); + return $this->send($packet); + } + + public function getLastReturnCode() + { + return $this->lastReturnCode; + } + + public function send(Packet $packet) + { + $conn = $this->connection(); + $bytes = $packet->getBinary(); + fputs($conn, $bytes, strlen($bytes)); + // TODO: Check result checksum! + $result = fread($conn, 8192); + if ($result === false) { + throw new IcingaException('CHECK_NRPE: Error receiving data from daemon.'); + } elseif (strlen($result) === 0) { + throw new IcingaException( + 'CHECK_NRPE: Received 0 bytes from daemon. Check the remote server logs for error messages' + ); + } + // TODO: CHECK_NRPE: Receive underflow - only %d bytes received (%d expected) + $code = unpack('n', substr($result, 8, 2)); + $this->lastReturnCode = $code[1]; + $this->disconnect(); + return rtrim(substr($result, 10, -2)); + } + + protected function connect() + { + $ctx = stream_context_create(); + if ($this->use_ssl) { + // TODO: fail if not ok: + $res = stream_context_set_option($ctx, 'ssl', 'ciphers', 'ADH'); + $uri = sprintf('ssl://%s:%d', $this->host, $this->port); + } else { + $uri = sprintf('tcp://%s:%d', $this->host, $this->port); + } + $this->connection = @stream_socket_client( + $uri, + $errno, + $errstr, + 10, + STREAM_CLIENT_CONNECT, + $ctx + ); + if (! $this->connection) { + throw new IcingaException( + 'NRPE Connection failed: %s', + $errstr + ); + } + } + + protected function connection() + { + if ($this->connection === null) { + $this->connect(); + } + return $this->connection; + } + + protected function disconnect() + { + if (is_resource($this->connection)) { + fclose($this->connection); + $this->connection = null; + } + return $this; + } + + public function __destruct() + { + $this->disconnect(); + } +} diff --git a/library/Icinga/Protocol/Nrpe/Packet.php b/library/Icinga/Protocol/Nrpe/Packet.php new file mode 100644 index 0000000..54c8526 --- /dev/null +++ b/library/Icinga/Protocol/Nrpe/Packet.php @@ -0,0 +1,69 @@ +type = $type; + $this->body = $body; + $this->regenerateRandomBytes(); + } + + // TODO: renew "from time to time" to allow long-running daemons + protected function regenerateRandomBytes() + { + self::$randomBytes = ''; + for ($i = 0; $i < 4096; $i++) { + self::$randomBytes .= pack('N', mt_rand()); + } + } + + public static function createQuery($body) + { + $packet = new Packet(self::QUERY, $body); + return $packet; + } + + protected function getFillString($length) + { + $max = strlen(self::$randomBytes) - $length; + return substr(self::$randomBytes, rand(0, $max), $length); + } + + // TODO: WTF is SR? And 2324? + public function getBinary() + { + $version = pack('n', $this->version); + $type = pack('n', $this->type); + $dummycrc = "\x00\x00\x00\x00"; + $result = "\x00\x00"; + $result = pack('n', 2324); + $body = $this->body + . "\x00" + . $this->getFillString(1023 - strlen($this->body)) + . 'SR'; + + $crc = pack( + 'N', + crc32($version . $type . $dummycrc . $result . $body) + ); + $bytes = $version . $type . $crc . $result . $body; + return $bytes; + } + + public function __toString() + { + return $this->body; + } +} diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php new file mode 100644 index 0000000..039d221 --- /dev/null +++ b/library/Icinga/Repository/DbRepository.php @@ -0,0 +1,1077 @@ + + *
  • Support for table aliases
  • + *
  • Automatic table prefix handling
  • + *
  • Insert, update and delete capabilities
  • + *
  • Differentiation between statement and query columns
  • + *
  • Capability to join additional tables depending on the columns being selected or used in a filter
  • + * + * + * @method DbConnection getDataSource($table = null) + */ +abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible +{ + /** + * The datasource being used + * + * @var DbConnection + */ + protected $ds; + + /** + * The table aliases being applied + * + * This must be initialized by repositories which are going to make use of table aliases. Every table for which + * aliased columns are provided must be defined in this array using its name as key and the alias being used as + * value. Failure to do so will result in invalid queries. + * + * @var array + */ + protected $tableAliases; + + /** + * The join probability rules + * + * This may be initialized by repositories which make use of the table join capability. It allows to define + * probability rules to enhance control how ambiguous column aliases are associated with the correct table. + * To define a rule use the name of a base table as key and another array of table names as probable join + * targets ordered by priority. (Ascending: Lower means higher priority) + * + * array( + * 'table_name' => array('target1', 'target2', 'target3') + * ) + * + * + * @todo Support for tree-ish rules + * + * @var array + */ + protected $joinProbabilities; + + /** + * The statement columns being provided + * + * This may be initialized by repositories which are going to make use of table aliases. It allows to provide + * alias-less column names to be used for a statement. The array needs to be in the following format: + * + * array( + * 'table_name' => array( + * 'column1', + * 'alias1' => 'column2', + * 'alias2' => 'column3' + * ) + * ) + * + * + * @var array + */ + protected $statementColumns; + + /** + * An array to map table names to statement columns/aliases + * + * @var array + */ + protected $statementAliasTableMap; + + /** + * A flattened array to map statement columns to aliases + * + * @var array + */ + protected $statementAliasColumnMap; + + /** + * An array to map table names to statement columns + * + * @var array + */ + protected $statementColumnTableMap; + + /** + * A flattened array to map aliases to statement columns + * + * @var array + */ + protected $statementColumnAliasMap; + + /** + * List of column names or aliases mapped to their table where the COLLATE SQL-instruction has been removed + * + * This list is being populated in case of a PostgreSQL backend only, + * to ensure case-insensitive string comparison in WHERE clauses. + * + * @var array + */ + protected $caseInsensitiveColumns; + + /** + * Create a new DB repository object + * + * In case $this->queryColumns has already been initialized, this initializes + * $this->caseInsensitiveColumns in case of a PostgreSQL connection. + * + * @param DbConnection $ds The datasource to use + */ + public function __construct(DbConnection $ds) + { + parent::__construct($ds); + + if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) { + $this->queryColumns = $this->removeCollateInstruction($this->queryColumns); + } + } + + /** + * Return the query columns being provided + * + * Initializes $this->caseInsensitiveColumns in case of a PostgreSQL connection. + * + * @return array + */ + public function getQueryColumns() + { + if ($this->queryColumns === null) { + $this->queryColumns = parent::getQueryColumns(); + if ($this->ds->getDbType() === 'pgsql') { + $this->queryColumns = $this->removeCollateInstruction($this->queryColumns); + } + } + + return $this->queryColumns; + } + + /** + * Return the table aliases to be applied + * + * Calls $this->initializeTableAliases() in case $this->tableAliases is null. + * + * @return array + */ + public function getTableAliases() + { + if ($this->tableAliases === null) { + $this->tableAliases = $this->initializeTableAliases(); + } + + return $this->tableAliases; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the table aliases lazily + * + * @return array + */ + protected function initializeTableAliases() + { + return array(); + } + + /** + * Return the join probability rules + * + * Calls $this->initializeJoinProbabilities() in case $this->joinProbabilities is null. + * + * @return array + */ + public function getJoinProbabilities() + { + if ($this->joinProbabilities === null) { + $this->joinProbabilities = $this->initializeJoinProbabilities(); + } + + return $this->joinProbabilities; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the join probabilities lazily + * + * @return array + */ + protected function initializeJoinProbabilities() + { + return array(); + } + + /** + * Remove each COLLATE SQL-instruction from all given query columns + * + * @param array $queryColumns + * + * @return array $queryColumns, the updated version + */ + protected function removeCollateInstruction($queryColumns) + { + foreach ($queryColumns as $table => & $columns) { + foreach ($columns as $alias => & $column) { + // Using a regex here because COLLATE may occur anywhere in the string + $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count); + if ($count > 0) { + $this->caseInsensitiveColumns[$table][is_string($alias) ? $alias : $column] = true; + } + } + } + + return $queryColumns; + } + + /** + * Initialize table, column and alias maps + * + * @throws ProgrammingError In case $this->queryColumns does not provide any column information + */ + protected function initializeAliasMaps() + { + parent::initializeAliasMaps(); + + foreach ($this->aliasTableMap as $alias => $table) { + if ($table !== null) { + if (strpos($alias, '.') !== false) { + $prefixedAlias = str_replace('.', '_', $alias); + } else { + $prefixedAlias = $table . '_' . $alias; + } + + if (array_key_exists($prefixedAlias, $this->aliasTableMap)) { + if ($this->aliasTableMap[$prefixedAlias] !== null) { + $existingTable = $this->aliasTableMap[$prefixedAlias]; + $existingColumn = $this->aliasColumnMap[$prefixedAlias]; + $this->aliasTableMap[$existingTable . '.' . $prefixedAlias] = $existingTable; + $this->aliasColumnMap[$existingTable . '.' . $prefixedAlias] = $existingColumn; + $this->aliasTableMap[$prefixedAlias] = null; + $this->aliasColumnMap[$prefixedAlias] = null; + } + + $this->aliasTableMap[$table . '.' . $prefixedAlias] = $table; + $this->aliasColumnMap[$table . '.' . $prefixedAlias] = $this->aliasColumnMap[$alias]; + } else { + $this->aliasTableMap[$prefixedAlias] = $table; + $this->aliasColumnMap[$prefixedAlias] = $this->aliasColumnMap[$alias]; + } + } + } + } + + /** + * Return the given table with the datasource's prefix being prepended + * + * @param array|string $table + * + * @return array|string + * + * @throws IcingaException In case $table is not of a supported type + */ + protected function prependTablePrefix($table) + { + $prefix = $this->ds->getTablePrefix(); + if (! $prefix) { + return $table; + } + + if (is_array($table)) { + foreach ($table as & $tableName) { + if (strpos($tableName, $prefix) === false) { + $tableName = $prefix . $tableName; + } + } + } elseif (is_string($table)) { + $table = (strpos($table, $prefix) === false ? $prefix : '') . $table; + } else { + throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table)); + } + + return $table; + } + + /** + * Remove the datasource's prefix from the given table name and return the remaining part + * + * @param array|string $table + * + * @return array|string + * + * @throws IcingaException In case $table is not of a supported type + */ + protected function removeTablePrefix($table) + { + $prefix = $this->ds->getTablePrefix(); + if (! $prefix) { + return $table; + } + + if (is_array($table)) { + foreach ($table as & $tableName) { + if (strpos($tableName, $prefix) === 0) { + $tableName = str_replace($prefix, '', $tableName); + } + } + } elseif (is_string($table)) { + if (strpos($table, $prefix) === 0) { + $table = str_replace($prefix, '', $table); + } + } else { + throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table)); + } + + return $table; + } + + /** + * Return the given table with its alias being applied + * + * @param array|string $table + * @param string $virtualTable + * + * @return array|string + */ + protected function applyTableAlias($table, $virtualTable = null) + { + if (! is_array($table)) { + $tableAliases = $this->getTableAliases(); + if ($virtualTable !== null && isset($tableAliases[$virtualTable])) { + return array($tableAliases[$virtualTable] => $table); + } + + if (isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) { + return array($tableAliases[$nonPrefixedTable] => $table); + } + } + + return $table; + } + + /** + * Return the given table with its alias being cleared + * + * @param array|string $table + * + * @return string + * + * @throws IcingaException In case $table is not of a supported type + */ + protected function clearTableAlias($table) + { + if (is_string($table)) { + return $table; + } + + if (is_array($table)) { + return reset($table); + } + + throw new IcingaException('Table alias handling for type "%s" is not supported', type($table)); + } + + /** + * Insert a table row with the given data + * + * Note that the base implementation does not perform any quoting on the $table argument. + * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value + * as third parameter $types to define a different type than string for a particular column. + * + * @param string $table + * @param array $bind + * @param array $types + * + * @return int The number of affected rows + */ + public function insert($table, array $bind, array $types = array()) + { + $realTable = $this->clearTableAlias($this->requireTable($table)); + + foreach ($types as $alias => $type) { + unset($types[$alias]); + $types[$this->requireStatementColumn($table, $alias)] = $type; + } + + return $this->ds->insert($realTable, $this->requireStatementColumns($table, $bind), $types); + } + + /** + * Update table rows with the given data, optionally limited by using a filter + * + * Note that the base implementation does not perform any quoting on the $table argument. + * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value + * as fourth parameter $types to define a different type than string for a particular column. + * + * @param string $table + * @param array $bind + * @param Filter $filter + * @param array $types + * + * @return int The number of affected rows + */ + public function update($table, array $bind, Filter $filter = null, array $types = array()) + { + $realTable = $this->clearTableAlias($this->requireTable($table)); + + if ($filter) { + $filter = $this->requireFilter($table, $filter); + } + + foreach ($types as $alias => $type) { + unset($types[$alias]); + $types[$this->requireStatementColumn($table, $alias)] = $type; + } + + return $this->ds->update($realTable, $this->requireStatementColumns($table, $bind), $filter, $types); + } + + /** + * Delete table rows, optionally limited by using a filter + * + * @param string $table + * @param Filter $filter + * + * @return int The number of affected rows + */ + public function delete($table, Filter $filter = null) + { + $realTable = $this->clearTableAlias($this->requireTable($table)); + + if ($filter) { + $filter = $this->requireFilter($table, $filter); + } + + return $this->ds->delete($realTable, $filter); + } + + /** + * Return the statement columns being provided + * + * Calls $this->initializeStatementColumns() in case $this->statementColumns is null. + * + * @return array + */ + public function getStatementColumns() + { + if ($this->statementColumns === null) { + $this->statementColumns = $this->initializeStatementColumns(); + } + + return $this->statementColumns; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the statement columns lazily + * + * @return array + */ + protected function initializeStatementColumns() + { + return array(); + } + + /** + * Return an array to map table names to statement columns/aliases + * + * @return array + */ + protected function getStatementAliasTableMap() + { + if ($this->statementAliasTableMap === null) { + $this->initializeStatementMaps(); + } + + return $this->statementAliasTableMap; + } + + /** + * Return a flattened array to map statement columns to aliases + * + * @return array + */ + protected function getStatementAliasColumnMap() + { + if ($this->statementAliasColumnMap === null) { + $this->initializeStatementMaps(); + } + + return $this->statementAliasColumnMap; + } + + /** + * Return an array to map table names to statement columns + * + * @return array + */ + protected function getStatementColumnTableMap() + { + if ($this->statementColumnTableMap === null) { + $this->initializeStatementMaps(); + } + + return $this->statementColumnTableMap; + } + + /** + * Return a flattened array to map aliases to statement columns + * + * @return array + */ + protected function getStatementColumnAliasMap() + { + if ($this->statementColumnAliasMap === null) { + $this->initializeStatementMaps(); + } + + return $this->statementColumnAliasMap; + } + + /** + * Initialize $this->statementAliasTableMap and $this->statementAliasColumnMap + */ + protected function initializeStatementMaps() + { + $this->statementAliasTableMap = array(); + $this->statementAliasColumnMap = array(); + $this->statementColumnTableMap = array(); + $this->statementColumnAliasMap = array(); + foreach ($this->getStatementColumns() as $table => $columns) { + foreach ($columns as $alias => $column) { + $key = is_string($alias) ? $alias : $column; + if (array_key_exists($key, $this->statementAliasTableMap)) { + if ($this->statementAliasTableMap[$key] !== null) { + $existingTable = $this->statementAliasTableMap[$key]; + $existingColumn = $this->statementAliasColumnMap[$key]; + $this->statementAliasTableMap[$existingTable . '.' . $key] = $existingTable; + $this->statementAliasColumnMap[$existingTable . '.' . $key] = $existingColumn; + $this->statementAliasTableMap[$key] = null; + $this->statementAliasColumnMap[$key] = null; + } + + $this->statementAliasTableMap[$table . '.' . $key] = $table; + $this->statementAliasColumnMap[$table . '.' . $key] = $column; + } else { + $this->statementAliasTableMap[$key] = $table; + $this->statementAliasColumnMap[$key] = $column; + } + + if (array_key_exists($column, $this->statementColumnTableMap)) { + if ($this->statementColumnTableMap[$column] !== null) { + $existingTable = $this->statementColumnTableMap[$column]; + $existingAlias = $this->statementColumnAliasMap[$column]; + $this->statementColumnTableMap[$existingTable . '.' . $column] = $existingTable; + $this->statementColumnAliasMap[$existingTable . '.' . $column] = $existingAlias; + $this->statementColumnTableMap[$column] = null; + $this->statementColumnAliasMap[$column] = null; + } + + $this->statementColumnTableMap[$table . '.' . $column] = $table; + $this->statementColumnAliasMap[$table . '.' . $column] = $key; + } else { + $this->statementColumnTableMap[$column] = $table; + $this->statementColumnAliasMap[$column] = $key; + } + } + } + } + + /** + * Return whether this repository is capable of converting values for the given table and optional column + * + * This does not check whether any conversion for the given table is available if $column is not given, as it + * may be possible that columns from another table where joined in which would otherwise not being converted. + * + * @param string $table + * @param string $column + * + * @return bool + */ + public function providesValueConversion($table, $column = null) + { + if ($column !== null) { + if ($column instanceof Zend_Db_Expr) { + return false; + } + + if ($this->validateQueryColumnAssociation($table, $column)) { + return parent::providesValueConversion($table, $column); + } + + if (($tableName = $this->findTableName($column, $table))) { + return parent::providesValueConversion($tableName, $column); + } + + return false; + } + + $conversionRules = $this->getConversionRules(); + return !empty($conversionRules); + } + + /** + * Return the name of the conversion method for the given alias or column name and context + * + * If a query column or a filter column, which is part of a query filter, needs to be converted, + * you'll need to pass $query, otherwise the column is considered a statement column. + * + * @param string $table The datasource's table + * @param string $name The alias or column name for which to return a conversion method + * @param string $context The context of the conversion: persist or retrieve + * @param RepositoryQuery $query If given the column is considered a query column, + * statement column otherwise + * + * @return string + * + * @throws ProgrammingError In case a conversion rule is found but not any conversion method + */ + protected function getConverter($table, $name, $context, RepositoryQuery $query = null) + { + if ($name instanceof Zend_Db_Expr) { + return; + } + + if (! ($query !== null && $this->validateQueryColumnAssociation($table, $name)) + && !($query === null && $this->validateStatementColumnAssociation($table, $name)) + ) { + $table = $this->findTableName($name, $table); + if (! $table) { + if ($query !== null) { + // It may be an aliased Zend_Db_Expr + $desiredColumns = $query->getColumns(); + if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) { + return; + } + } + + throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?'); + } + } + + return parent::getConverter($table, $name, $context, $query); + } + + /** + * Validate that the requested table exists + * + * This will prepend the datasource's table prefix and will apply the table's alias, if any. + * + * @param string $table The table to validate + * @param RepositoryQuery $query An optional query to pass as context + * (unused by the base implementation) + * + * @return array|string + * + * @throws ProgrammingError In case the given table does not exist + */ + public function requireTable($table, RepositoryQuery $query = null) + { + $virtualTable = null; + $statementColumns = $this->getStatementColumns(); + if (! isset($statementColumns[$table])) { + $newTable = parent::requireTable($table); + if ($newTable !== $table) { + $virtualTable = $table; + } + + $table = $newTable; + } else { + $virtualTables = $this->getVirtualTables(); + if (isset($virtualTables[$table])) { + $virtualTable = $table; + $table = $virtualTables[$table]; + } + } + + return $this->prependTablePrefix($this->applyTableAlias($table, $virtualTable)); + } + + /** + * Return the alias for the given table or null if none has been defined + * + * @param string $table + * + * @return string|null + */ + public function resolveTableAlias($table) + { + $tableAliases = $this->getTableAliases(); + if (isset($tableAliases[$table])) { + return $tableAliases[$table]; + } + } + + /** + * Return the alias for the given query column name or null in case the query column name does not exist + * + * @param string $table + * @param string $column + * + * @return string|null + */ + public function reassembleQueryColumnAlias($table, $column) + { + $alias = parent::reassembleQueryColumnAlias($table, $column); + if ($alias === null + && !$this->validateQueryColumnAssociation($table, $column) + && ($tableName = $this->findTableName($column, $table)) + ) { + return parent::reassembleQueryColumnAlias($tableName, $column); + } + + return $alias; + } + + /** + * Validate that the given column is a valid query target and return it or the actual name if it's an alias + * + * Attempts to join the given column from a different table if its association to the given table cannot be + * verified. + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context, + * if not given no join will be attempted + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid query column + * @throws ProgrammingError In case the given column is not found in $table and cannot be joined in + */ + public function requireQueryColumn($table, $name, RepositoryQuery $query = null) + { + if ($name instanceof Zend_Db_Expr) { + return $name; + } + + if ($query === null || $this->validateQueryColumnAssociation($table, $name)) { + return parent::requireQueryColumn($table, $name, $query); + } + + $column = $this->joinColumn($name, $table, $query); + if ($column === null) { + if ($query !== null) { + // It may be an aliased Zend_Db_Expr + $desiredColumns = $query->getColumns(); + if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) { + $column = $desiredColumns[$name]; + } + } + + if ($column === null) { + throw new ProgrammingError( + 'Unable to find a valid table for column "%s" to join into "%s"', + $name, + $table + ); + } + } + + return $column; + } + + /** + * Validate that the given column is a valid filter target and return it or the actual name if it's an alias + * + * Attempts to join the given column from a different table if its association to the given table cannot be + * verified. In case of a PostgreSQL connection and if a COLLATE SQL-instruction is part of the resolved column, + * this applies LOWER() on the column and, if given, strtolower() on the filter's expression. + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context, + * if not given the column is considered being used for a statement filter + * @param FilterExpression $filter An optional filter to pass as context + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid filter column + * @throws ProgrammingError In case the given column is not found in $table and cannot be joined in + */ + public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = null) + { + if ($name instanceof Zend_Db_Expr) { + return $name; + } + + $joined = false; + if ($query === null) { + $column = $this->requireStatementColumn($table, $name); + } elseif ($this->validateQueryColumnAssociation($table, $name)) { + $column = parent::requireFilterColumn($table, $name, $query, $filter); + } else { + $column = $this->joinColumn($name, $table, $query); + if ($column === null) { + if ($query !== null) { + // It may be an aliased Zend_Db_Expr + $desiredColumns = $query->getColumns(); + if (isset($desiredColumns[$name]) && $desiredColumns[$name] instanceof Zend_Db_Expr) { + $column = $desiredColumns[$name]; + } + } + + if ($column === null) { + throw new ProgrammingError( + 'Unable to find a valid table for column "%s" to join into "%s"', + $name, + $table + ); + } + } else { + $joined = true; + } + } + + if (! empty($this->caseInsensitiveColumns)) { + if ($joined) { + $table = $this->findTableName($name, $table); + } + + if ($column === $name) { + if ($query === null) { + $name = $this->reassembleStatementColumnAlias($table, $name); + } else { + $name = $this->reassembleQueryColumnAlias($table, $name); + } + } + + if (isset($this->caseInsensitiveColumns[$table][$name])) { + $column = 'LOWER(' . $column . ')'; + if ($filter !== null) { + $expression = $filter->getExpression(); + if (is_array($expression)) { + $filter->setExpression(array_map('strtolower', $expression)); + } else { + $filter->setExpression(strtolower($expression)); + } + } + } + } + + return $column; + } + + /** + * Return the statement column name for the given alias or null in case the alias does not exist + * + * @param string $table + * @param string $alias + * + * @return string|null + */ + public function resolveStatementColumnAlias($table, $alias) + { + $statementAliasColumnMap = $this->getStatementAliasColumnMap(); + if (isset($statementAliasColumnMap[$alias])) { + return $statementAliasColumnMap[$alias]; + } + + $prefixedAlias = $table . '.' . $alias; + if (isset($statementAliasColumnMap[$prefixedAlias])) { + return $statementAliasColumnMap[$prefixedAlias]; + } + } + + /** + * Return the alias for the given statement column name or null in case the statement column does not exist + * + * @param string $table + * @param string $column + * + * @return string|null + */ + public function reassembleStatementColumnAlias($table, $column) + { + $statementColumnAliasMap = $this->getStatementColumnAliasMap(); + if (isset($statementColumnAliasMap[$column])) { + return $statementColumnAliasMap[$column]; + } + + $prefixedColumn = $table . '.' . $column; + if (isset($statementColumnAliasMap[$prefixedColumn])) { + return $statementColumnAliasMap[$prefixedColumn]; + } + } + + /** + * Return whether the given alias or statement column name is available in the given table + * + * @param string $table + * @param string $alias + * + * @return bool + */ + public function validateStatementColumnAssociation($table, $alias) + { + $statementAliasTableMap = $this->getStatementAliasTableMap(); + if (isset($statementAliasTableMap[$alias])) { + return $statementAliasTableMap[$alias] === $table; + } + + $prefixedAlias = $table . '.' . $alias; + if (isset($statementAliasTableMap[$prefixedAlias])) { + return true; + } + + $statementColumnTableMap = $this->getStatementColumnTableMap(); + if (isset($statementColumnTableMap[$alias])) { + return $statementColumnTableMap[$alias] === $table; + } + + return isset($statementColumnTableMap[$prefixedAlias]); + } + + /** + * Return whether the given column name or alias of the given table is a valid statement column + * + * @param string $table The table where to look for the column or alias + * @param string $name The column name or alias to check + * + * @return bool + */ + public function hasStatementColumn($table, $name) + { + if (($this->resolveStatementColumnAlias($table, $name) === null + && $this->reassembleStatementColumnAlias($table, $name) === null) + || !$this->validateStatementColumnAssociation($table, $name) + ) { + return parent::hasStatementColumn($table, $name); + } + + return true; + } + + /** + * Validate that the given column is a valid statement column and return it or the actual name if it's an alias + * + * @param string $table The table for which to require the column + * @param string $name The name or alias of the column to validate + * + * @return string The given column's name + * + * @throws StatementException In case the given column is not a statement column + */ + public function requireStatementColumn($table, $name) + { + if (($column = $this->resolveStatementColumnAlias($table, $name)) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleStatementColumnAlias($table, $name)) !== null) { + $column = $name; + } else { + return parent::requireStatementColumn($table, $name); + } + + if (! $this->validateStatementColumnAssociation($table, $alias)) { + throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table); + } + + return $column; + } + + /** + * Join alias or column $name into $table using $query + * + * Attempts to find a valid table for the given alias or column name and a method labelled join + * to process the actual join logic. If neither of those is found, null is returned. + * The method is called with the same parameters but in reversed order. + * + * @param string $name The alias or column name to join into $target + * @param string $target The table to join $name into + * @param RepositoryQUery $query The query to apply the JOIN-clause on + * + * @return string|null The resolved alias or $name, null if no join logic is found + */ + public function joinColumn($name, $target, RepositoryQuery $query) + { + if (! ($tableName = $this->findTableName($name, $target))) { + return; + } + + if (($column = $this->resolveQueryColumnAlias($tableName, $name)) === null) { + $column = $name; + } + + if (($joinIdentifier = $this->resolveTableAlias($tableName)) === null) { + $joinIdentifier = $this->prependTablePrefix($tableName); + } + if ($query->getQuery()->hasJoinedTable($joinIdentifier)) { + return $column; + } + + $joinMethod = 'join' . StringHelper::cname($tableName); + if (! method_exists($this, $joinMethod)) { + throw new ProgrammingError( + 'Unable to join table "%s" into "%s". Method "%s" not found', + $tableName, + $target, + $joinMethod + ); + } + + $this->$joinMethod($query, $target, $name); + return $column; + } + + /** + * Return the table name for the given alias or column name + * + * @param string $column The alias or column name + * @param string $origin The base table of a SELECT query + * + * @return string|null null in case no table is found + */ + protected function findTableName($column, $origin) + { + // First, try to produce an exact match since it's faster and cheaper + $aliasTableMap = $this->getAliasTableMap(); + if (isset($aliasTableMap[$column])) { + $table = $aliasTableMap[$column]; + } else { + $columnTableMap = $this->getColumnTableMap(); + if (isset($columnTableMap[$column])) { + $table = $columnTableMap[$column]; + } + } + + // But only return it if it's a probable join... + $joinProbabilities = $this->getJoinProbabilities(); + if (isset($joinProbabilities[$origin])) { + $probableJoins = $joinProbabilities[$origin]; + } + + // ...if probability can be determined + if (isset($table) && (empty($probableJoins) || in_array($table, $probableJoins, true))) { + return $table; + } + + // Without a proper exact match, there is only one fast and cheap way to find a suitable table.. + if (! empty($probableJoins)) { + foreach ($probableJoins as $table) { + if (isset($aliasTableMap[$table . '.' . $column])) { + return $table; + } + } + } + + // Last chance to find a table. Though, this usually ends up with a QueryException.. + foreach ($aliasTableMap as $prefixedAlias => $table) { + if (strpos($prefixedAlias, '.') !== false) { + list($_, $alias) = explode('.', $prefixedAlias, 2); + if ($alias === $column) { + return $table; + } + } + } + } +} diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php new file mode 100644 index 0000000..7385b3e --- /dev/null +++ b/library/Icinga/Repository/IniRepository.php @@ -0,0 +1,420 @@ + + *
  • Insert, update and delete capabilities
  • + *
  • Triggers for inserts, updates and deletions
  • + *
  • Lazy initialization of table specific configs
  • + * + */ +abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible +{ + /** + * The configuration files used as table specific datasources + * + * This must be initialized by concrete repository implementations, in the following format + * + * array( + * 'table_name' => array( + * 'name' => 'name_of_the_ini_file_without_extension', + * 'keyColumn' => 'the_name_of_the_column_to_use_as_key_column', + * ['module' => 'the_name_of_the_module_if_any'] + * ) + * ) + * + * + * @var array + */ + protected $configs; + + /** + * The tables for which triggers are available when inserting, updating or deleting rows + * + * This may be initialized by concrete repository implementations and describes for which table names triggers + * are available. The repository attempts to find a method depending on the type of event and table for which + * to run the trigger. The name of such a method is expected to be declared using lowerCamelCase. + * (e.g. group_membership will be translated to onUpdateGroupMembership and groupmembership will be translated + * to onUpdateGroupmembership) The available events are onInsert, onUpdate and onDelete. + * + * @var array + */ + protected $triggers; + + /** + * Create a new INI repository object + * + * @param Config|null $ds The data source to use + * + * @throws ProgrammingError In case the given data source does not provide a valid key column + */ + public function __construct(Config $ds = null) + { + parent::__construct($ds); // First! Due to init(). + + if ($ds !== null && !$ds->getConfigObject()->getKeyColumn()) { + throw new ProgrammingError('INI repositories require their data source to provide a valid key column'); + } + } + + /** + * {@inheritDoc} + * + * @return Config + */ + public function getDataSource($table = null) + { + if ($this->ds !== null) { + return parent::getDataSource($table); + } + + $table = $table ?: $this->getBaseTable(); + $configs = $this->getConfigs(); + if (! isset($configs[$table])) { + throw new ProgrammingError('Config for table "%s" missing', $table); + } elseif (! $configs[$table] instanceof Config) { + $configs[$table] = $this->createConfig($configs[$table], $table); + } + + if (! $configs[$table]->getConfigObject()->getKeyColumn()) { + throw new ProgrammingError( + 'INI repositories require their data source to provide a valid key column' + ); + } + + return $configs[$table]; + } + + /** + * Return the configuration files used as table specific datasources + * + * Calls $this->initializeConfigs() in case $this->configs is null. + * + * @return array + */ + public function getConfigs() + { + if ($this->configs === null) { + $this->configs = $this->initializeConfigs(); + } + + return $this->configs; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the configs lazily + * + * @return array + */ + protected function initializeConfigs() + { + return array(); + } + + /** + * Return the tables for which triggers are available when inserting, updating or deleting rows + * + * Calls $this->initializeTriggers() in case $this->triggers is null. + * + * @return array + */ + public function getTriggers() + { + if ($this->triggers === null) { + $this->triggers = $this->initializeTriggers(); + } + + return $this->triggers; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the triggers lazily + * + * @return array + */ + protected function initializeTriggers() + { + return array(); + } + + /** + * Run a trigger for the given table and row which is about to be inserted + * + * @param string $table + * @param ConfigObject $new + * + * @return ConfigObject + */ + public function onInsert($table, ConfigObject $new) + { + $trigger = $this->getTrigger($table, 'onInsert'); + if ($trigger !== null) { + $row = $this->$trigger($new); + if ($row !== null) { + $new = $row; + } + } + + return $new; + } + + /** + * Run a trigger for the given table and row which is about to be updated + * + * @param string $table + * @param ConfigObject $old + * @param ConfigObject $new + * + * @return ConfigObject + */ + public function onUpdate($table, ConfigObject $old, ConfigObject $new) + { + $trigger = $this->getTrigger($table, 'onUpdate'); + if ($trigger !== null) { + $row = $this->$trigger($old, $new); + if ($row !== null) { + $new = $row; + } + } + + return $new; + } + + /** + * Run a trigger for the given table and row which has been deleted + * + * @param string $table + * @param ConfigObject $old + * + * @return ConfigObject + */ + public function onDelete($table, ConfigObject $old) + { + $trigger = $this->getTrigger($table, 'onDelete'); + if ($trigger !== null) { + $this->$trigger($old); + } + } + + /** + * Return the name of the trigger method for the given table and event-type + * + * @param string $table The table name for which to return a trigger method + * @param string $event The name of the event type + * + * @return string + */ + protected function getTrigger($table, $event) + { + if (! in_array($table, $this->getTriggers())) { + return; + } + + $identifier = join('', array_map('ucfirst', explode('_', $table))); + if (method_exists($this, $event . $identifier)) { + return $event . $identifier; + } + } + + /** + * Insert the given data for the given target + * + * $data must provide a proper value for the data source's key column. + * + * @param string $target + * @param array $data + * + * @throws StatementException In case the operation has failed + */ + public function insert($target, array $data) + { + $ds = $this->getDataSource($target); + $newData = $this->requireStatementColumns($target, $data); + + $config = $this->onInsert($target, new ConfigObject($newData)); + $section = $this->extractSectionName($config, $ds->getConfigObject()->getKeyColumn()); + + if ($ds->hasSection($section)) { + throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section); + } + + $ds->setSection($section, $config); + + try { + $ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to insert. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Update the target with the given data and optionally limit the affected entries by using a filter + * + * @param string $target + * @param array $data + * @param Filter $filter + * + * @throws StatementException In case the operation has failed + */ + public function update($target, array $data, Filter $filter = null) + { + $ds = $this->getDataSource($target); + $newData = $this->requireStatementColumns($target, $data); + + $keyColumn = $ds->getConfigObject()->getKeyColumn(); + if ($filter === null && isset($newData[$keyColumn])) { + throw new StatementException( + t('Cannot update. Column "%s" holds a section\'s name which must be unique'), + $keyColumn + ); + } + + $query = $ds->select(); + if ($filter !== null) { + $query->addFilter($this->requireFilter($target, $filter)); + } + + /** @var ConfigObject $config */ + $newSection = null; + foreach ($query as $section => $config) { + if ($newSection !== null) { + throw new StatementException( + t('Cannot update. Column "%s" holds a section\'s name which must be unique'), + $keyColumn + ); + } + + $newConfig = clone $config; + foreach ($newData as $column => $value) { + if ($column === $keyColumn) { + if ($value !== $config->get($keyColumn)) { + $newSection = $value; + } + } else { + $newConfig->$column = $value; + } + } + + // This is necessary as the query result set contains the key column. + unset($newConfig->$keyColumn); + + if ($newSection) { + if ($ds->hasSection($newSection)) { + throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection); + } + + $ds->removeSection($section)->setSection( + $newSection, + $this->onUpdate($target, $config, $newConfig) + ); + } else { + $ds->setSection( + $section, + $this->onUpdate($target, $config, $newConfig) + ); + } + } + + try { + $ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to update. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Delete entries in the given target, optionally limiting the affected entries by using a filter + * + * @param string $target + * @param Filter $filter + * + * @throws StatementException In case the operation has failed + */ + public function delete($target, Filter $filter = null) + { + $ds = $this->getDataSource($target); + + $query = $ds->select(); + if ($filter !== null) { + $query->addFilter($this->requireFilter($target, $filter)); + } + + /** @var ConfigObject $config */ + foreach ($query as $section => $config) { + $ds->removeSection($section); + $this->onDelete($target, $config); + } + + try { + $ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Create and return a Config for the given meta and table + * + * @param array $meta + * @param string $table + * + * @return Config + * + * @throws ProgrammingError In case the given meta is invalid + */ + protected function createConfig(array $meta, $table) + { + if (! isset($meta['name'])) { + throw new ProgrammingError('Config file name missing for table "%s"', $table); + } elseif (! isset($meta['keyColumn'])) { + throw new ProgrammingError('Config key column name missing for table "%s"', $table); + } + + if (isset($meta['module'])) { + $config = Config::module($meta['module'], $meta['name']); + } else { + $config = Config::app($meta['name']); + } + + $config->getConfigObject()->setKeyColumn($meta['keyColumn']); + return $config; + } + + /** + * Extract and return the section name off of the given $config + * + * @param array|ConfigObject $config + * @param string $keyColumn + * + * @return string + * + * @throws ProgrammingError In case no valid section name is available + */ + protected function extractSectionName(&$config, $keyColumn) + { + if (! is_array($config) && !$config instanceof ConfigObject) { + throw new ProgrammingError('$config is neither an array nor a ConfigObject'); + } elseif (! isset($config[$keyColumn])) { + throw new ProgrammingError('$config does not provide a value for key column "%s"', $keyColumn); + } + + $section = $config[$keyColumn]; + unset($config[$keyColumn]); + return $section; + } +} diff --git a/library/Icinga/Repository/LdapRepository.php b/library/Icinga/Repository/LdapRepository.php new file mode 100644 index 0000000..e7a380b --- /dev/null +++ b/library/Icinga/Repository/LdapRepository.php @@ -0,0 +1,71 @@ + + *
  • Attribute name normalization
  • + * + */ +abstract class LdapRepository extends Repository +{ + /** + * The datasource being used + * + * @var LdapConnection + */ + protected $ds; + + /** + * Normed attribute names based on known LDAP environments + * + * @var array + */ + protected $normedAttributes = array( + 'uid' => 'uid', + 'gid' => 'gid', + 'user' => 'user', + 'group' => 'group', + 'member' => 'member', + 'memberuid' => 'memberUid', + 'posixgroup' => 'posixGroup', + 'uniquemember' => 'uniqueMember', + 'groupofnames' => 'groupOfNames', + 'inetorgperson' => 'inetOrgPerson', + 'samaccountname' => 'sAMAccountName', + 'groupofuniquenames' => 'groupOfUniqueNames' + ); + + /** + * Create a new LDAP repository object + * + * @param LdapConnection $ds The data source to use + */ + public function __construct(LdapConnection $ds) + { + parent::__construct($ds); + } + + /** + * Return the given attribute name normed to known LDAP enviroments, if possible + * + * @param string $name + * + * @return string + */ + protected function getNormedAttribute($name) + { + $loweredName = strtolower($name ?? ''); + if (array_key_exists($loweredName, $this->normedAttributes)) { + return $this->normedAttributes[$loweredName]; + } + + return $name; + } +} diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php new file mode 100644 index 0000000..bda93aa --- /dev/null +++ b/library/Icinga/Repository/Repository.php @@ -0,0 +1,1261 @@ + + *
  • Concrete implementations need to initialize Repository::$queryColumns
  • + *
  • The datasource passed to a repository must implement the Selectable interface
  • + *
  • The datasource must yield an instance of Queryable when its select() method is called
  • + * + */ +abstract class Repository implements Selectable +{ + /** + * The format to use when converting values of type date_time + */ + const DATETIME_FORMAT = 'd/m/Y g:i A'; + + /** + * The name of this repository + * + * @var string + */ + protected $name; + + /** + * The datasource being used + * + * @var Selectable + */ + protected $ds; + + /** + * The base table name this repository is responsible for + * + * This will be automatically set to the first key of $queryColumns if not explicitly set. + * + * @var string + */ + protected $baseTable; + + /** + * The virtual tables being provided + * + * This may be initialized by concrete repository implementations with an array + * where a key is the name of a virtual table and its value the real table name. + * + * @var array + */ + protected $virtualTables; + + /** + * The query columns being provided + * + * This must be initialized by concrete repository implementations, in the following format + * + * array( + * 'baseTable' => array( + * 'column1', + * 'alias1' => 'column2', + * 'alias2' => 'column3' + * ) + * ) + * + * + * @var array + */ + protected $queryColumns; + + /** + * The columns (or aliases) which are not permitted to be queried + * + * Blacklisted query columns can still occur in a filter expression or sort rule. + * + * @var array An array of strings + */ + protected $blacklistedQueryColumns; + + /** + * Whether the blacklisted query columns are in the legacy format + * + * @var bool + */ + protected $legacyBlacklistedQueryColumns; + + /** + * The filter columns being provided + * + * This may be intialized by concrete repository implementations, in the following format + * + * array( + * 'alias_or_column_name', + * 'label_to_show_in_the_filter_editor' => 'alias_or_column_name' + * ) + * + * + * @var array + */ + protected $filterColumns; + + /** + * Whether the provided filter columns are in the legacy format + * + * @var bool + */ + protected $legacyFilterColumns; + + /** + * The search columns (or aliases) being provided + * + * @var array An array of strings + */ + protected $searchColumns; + + /** + * Whether the provided search columns are in the legacy format + * + * @var bool + */ + protected $legacySearchColumns; + + /** + * The sort rules to be applied on a query + * + * This may be initialized by concrete repository implementations, in the following format + * + * array( + * 'alias_or_column_name' => array( + * 'order' => 'asc' + * ), + * 'alias_or_column_name' => array( + * 'columns' => array( + * 'once_more_the_alias_or_column_name_as_in_the_parent_key', + * 'an_additional_alias_or_column_name_with_a_specific_direction asc' + * ), + * 'order' => 'desc' + * ), + * 'alias_or_column_name' => array( + * 'columns' => array('a_different_alias_or_column_name_designated_to_act_as_the_only_sort_column') + * // Ascendant sort by default + * ) + * ) + * + * Note that it's mandatory to supply the alias name in case there is one. + * + * @var array + */ + protected $sortRules; + + /** + * Whether the provided sort rules are in the legacy format + * + * @var bool + */ + protected $legacySortRules; + + /** + * The value conversion rules to apply on a query or statement + * + * This may be initialized by concrete repository implementations and describes for which aliases or column + * names what type of conversion is available. For entries, where the key is the alias/column and the value + * is the type identifier, the repository attempts to find a conversion method for the alias/column first and, + * if none is found, then for the type. If an entry only provides a value, which is the alias/column, the + * repository only attempts to find a conversion method for the alias/column. The name of a conversion method + * is expected to be declared using lowerCamelCase. (e.g. user_name will be translated to persistUserName and + * groupname will be translated to retrieveGroupname) + * + * @var array + */ + protected $conversionRules; + + /** + * An array to map table names to aliases + * + * @var array + */ + protected $aliasTableMap; + + /** + * A flattened array to map query columns to aliases + * + * @var array + */ + protected $aliasColumnMap; + + /** + * An array to map table names to query columns + * + * @var array + */ + protected $columnTableMap; + + /** + * A flattened array to map aliases to query columns + * + * @var array + */ + protected $columnAliasMap; + + /** + * Create a new repository object + * + * @param Selectable|null $ds The datasource to use. + * Only pass null if you have overridden {@link getDataSource()}! + */ + public function __construct(Selectable $ds = null) + { + $this->ds = $ds; + $this->aliasTableMap = array(); + $this->aliasColumnMap = array(); + $this->columnTableMap = array(); + $this->columnAliasMap = array(); + + $this->init(); + } + + /** + * Initialize this repository + * + * Supposed to be overwritten by concrete repository implementations. + */ + protected function init() + { + } + + /** + * Set this repository's name + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Return this repository's name + * + * In case no name has been explicitly set yet, the class name is returned. + * + * @return string + */ + public function getName() + { + return $this->name ?: __CLASS__; + } + + /** + * Return the datasource being used for the given table + * + * @param string $table + * + * @return Selectable + * + * @throws ProgrammingError In case no datasource is available + */ + public function getDataSource($table = null) + { + if ($this->ds === null) { + throw new ProgrammingError( + 'No data source available. It is required to either pass it' + . ' at initialization time or by overriding this method.' + ); + } + + return $this->ds; + } + + /** + * Return the base table name this repository is responsible for + * + * @return string + * + * @throws ProgrammingError In case no base table name has been set and + * $this->queryColumns does not provide one either + */ + public function getBaseTable() + { + if ($this->baseTable === null) { + $queryColumns = $this->getQueryColumns(); + reset($queryColumns); + $this->baseTable = key($queryColumns); + if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) { + throw new ProgrammingError('"%s" is not a valid base table', $this->baseTable); + } + } + + return $this->baseTable; + } + + /** + * Return the virtual tables being provided + * + * Calls $this->initializeVirtualTables() in case $this->virtualTables is null. + * + * @return array + */ + public function getVirtualTables() + { + if ($this->virtualTables === null) { + $this->virtualTables = $this->initializeVirtualTables(); + } + + return $this->virtualTables; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the virtual tables lazily + * + * @return array + */ + protected function initializeVirtualTables() + { + return array(); + } + + /** + * Return the query columns being provided + * + * Calls $this->initializeQueryColumns() in case $this->queryColumns is null. + * + * @return array + */ + public function getQueryColumns() + { + if ($this->queryColumns === null) { + $this->queryColumns = $this->initializeQueryColumns(); + } + + return $this->queryColumns; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the query columns lazily + * + * @return array + */ + protected function initializeQueryColumns() + { + return array(); + } + + /** + * Return the columns (or aliases) which are not permitted to be queried + * + * Calls $this->initializeBlacklistedQueryColumns() in case $this->blacklistedQueryColumns is null. + * + * @param string $table + * + * @return array + */ + public function getBlacklistedQueryColumns($table = null) + { + if ($this->blacklistedQueryColumns === null) { + $this->legacyBlacklistedQueryColumns = false; + + $blacklistedQueryColumns = $this->initializeBlacklistedQueryColumns($table); + if (is_int(key($blacklistedQueryColumns))) { + $this->blacklistedQueryColumns[$table] = $blacklistedQueryColumns; + } else { + $this->blacklistedQueryColumns = $blacklistedQueryColumns; + } + } elseif ($this->legacyBlacklistedQueryColumns === null) { + $this->legacyBlacklistedQueryColumns = is_int(key($this->blacklistedQueryColumns)); + } + + if ($this->legacyBlacklistedQueryColumns) { + return $this->blacklistedQueryColumns; + } elseif (! isset($this->blacklistedQueryColumns[$table])) { + $this->blacklistedQueryColumns[$table] = $this->initializeBlacklistedQueryColumns($table); + } + + return $this->blacklistedQueryColumns[$table]; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the + * blacklisted query columns lazily or dependent on a query's current base table + * + * @param string $table + * + * @return array + */ + protected function initializeBlacklistedQueryColumns() + { + // $table is not part of the signature due to PHP strict standards + return array(); + } + + /** + * Return the filter columns being provided + * + * Calls $this->initializeFilterColumns() in case $this->filterColumns is null. + * + * @param string $table + * + * @return array + */ + public function getFilterColumns($table = null) + { + if ($this->filterColumns === null) { + $this->legacyFilterColumns = false; + + $filterColumns = $this->initializeFilterColumns($table); + $foundTables = array_intersect_key($this->getQueryColumns(), $filterColumns); + if (empty($foundTables)) { + $this->filterColumns[$table] = $filterColumns; + } else { + $this->filterColumns = $filterColumns; + } + } elseif ($this->legacyFilterColumns === null) { + $foundTables = array_intersect_key($this->getQueryColumns(), $this->filterColumns); + $this->legacyFilterColumns = empty($foundTables); + } + + if ($this->legacyFilterColumns) { + return $this->filterColumns; + } elseif (! isset($this->filterColumns[$table])) { + $this->filterColumns[$table] = $this->initializeFilterColumns($table); + } + + return $this->filterColumns[$table]; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize + * the filter columns lazily or dependent on a query's current base table + * + * @param string $table + * + * @return array + */ + protected function initializeFilterColumns() + { + // $table is not part of the signature due to PHP strict standards + return array(); + } + + /** + * Return the search columns being provided + * + * Calls $this->initializeSearchColumns() in case $this->searchColumns is null. + * + * @param string $table + * + * @return array + */ + public function getSearchColumns($table = null) + { + if ($this->searchColumns === null) { + $this->legacySearchColumns = false; + + $searchColumns = $this->initializeSearchColumns($table); + if (is_int(key($searchColumns))) { + $this->searchColumns[$table] = $searchColumns; + } else { + $this->searchColumns = $searchColumns; + } + } elseif ($this->legacySearchColumns === null) { + $this->legacySearchColumns = is_int(key($this->searchColumns)); + } + + if ($this->legacySearchColumns) { + return $this->searchColumns; + } elseif (! isset($this->searchColumns[$table])) { + $this->searchColumns[$table] = $this->initializeSearchColumns($table); + } + + return $this->searchColumns[$table]; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize + * the search columns lazily or dependent on a query's current base table + * + * @param string $table + * + * @return array + */ + protected function initializeSearchColumns() + { + // $table is not part of the signature due to PHP strict standards + return array(); + } + + /** + * Return the sort rules to be applied on a query + * + * Calls $this->initializeSortRules() in case $this->sortRules is null. + * + * @param string $table + * + * @return array + */ + public function getSortRules($table = null) + { + if ($this->sortRules === null) { + $this->legacySortRules = false; + + $sortRules = $this->initializeSortRules($table); + $foundTables = array_intersect_key($this->getQueryColumns(), $sortRules); + if (empty($foundTables)) { + $this->sortRules[$table] = $sortRules; + } else { + $this->sortRules = $sortRules; + } + } elseif ($this->legacySortRules === null) { + $foundTables = array_intersect_key($this->getQueryColumns(), $this->sortRules); + $this->legacySortRules = empty($foundTables); + } + + if ($this->legacySortRules) { + return $this->sortRules; + } elseif (! isset($this->sortRules[$table])) { + $this->sortRules[$table] = $this->initializeSortRules($table); + } + + return $this->sortRules[$table]; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize + * the sort rules lazily or dependent on a query's current base table + * + * @param string $table + * + * @return array + */ + protected function initializeSortRules() + { + // $table is not part of the signature due to PHP strict standards + return array(); + } + + /** + * Return the value conversion rules to apply on a query + * + * Calls $this->initializeConversionRules() in case $this->conversionRules is null. + * + * @return array + */ + public function getConversionRules() + { + if ($this->conversionRules === null) { + $this->conversionRules = $this->initializeConversionRules(); + } + + return $this->conversionRules; + } + + /** + * Overwrite this in your repository implementation in case you need to initialize the conversion rules lazily + * + * @return array + */ + protected function initializeConversionRules() + { + return array(); + } + + /** + * Return an array to map table names to aliases + * + * @return array + */ + protected function getAliasTableMap() + { + if (empty($this->aliasTableMap)) { + $this->initializeAliasMaps(); + } + + return $this->aliasTableMap; + } + + /** + * Return a flattened array to map query columns to aliases + * + * @return array + */ + protected function getAliasColumnMap() + { + if (empty($this->aliasColumnMap)) { + $this->initializeAliasMaps(); + } + + return $this->aliasColumnMap; + } + + /** + * Return an array to map table names to query columns + * + * @return array + */ + protected function getColumnTableMap() + { + if (empty($this->columnTableMap)) { + $this->initializeAliasMaps(); + } + + return $this->columnTableMap; + } + + /** + * Return a flattened array to map aliases to query columns + * + * @return array + */ + protected function getColumnAliasMap() + { + if (empty($this->columnAliasMap)) { + $this->initializeAliasMaps(); + } + + return $this->columnAliasMap; + } + + /** + * Initialize $this->aliasTableMap and $this->aliasColumnMap + * + * @throws ProgrammingError In case $this->queryColumns does not provide any column information + */ + protected function initializeAliasMaps() + { + $queryColumns = $this->getQueryColumns(); + if (empty($queryColumns)) { + throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first'); + } + + foreach ($queryColumns as $table => $columns) { + foreach ($columns as $alias => $column) { + if (! is_string($alias)) { + $key = $column; + } else { + $key = $alias; + $column = preg_replace('~\n\s*~', ' ', $column); + } + + if (array_key_exists($key, $this->aliasTableMap)) { + if ($this->aliasTableMap[$key] !== null) { + $existingTable = $this->aliasTableMap[$key]; + $existingColumn = $this->aliasColumnMap[$key]; + $this->aliasTableMap[$existingTable . '.' . $key] = $existingTable; + $this->aliasColumnMap[$existingTable . '.' . $key] = $existingColumn; + $this->aliasTableMap[$key] = null; + $this->aliasColumnMap[$key] = null; + } + + $this->aliasTableMap[$table . '.' . $key] = $table; + $this->aliasColumnMap[$table . '.' . $key] = $column; + } else { + $this->aliasTableMap[$key] = $table; + $this->aliasColumnMap[$key] = $column; + } + + if (array_key_exists($column, $this->columnTableMap)) { + if ($this->columnTableMap[$column] !== null) { + $existingTable = $this->columnTableMap[$column]; + $existingAlias = $this->columnAliasMap[$column]; + $this->columnTableMap[$existingTable . '.' . $column] = $existingTable; + $this->columnAliasMap[$existingTable . '.' . $column] = $existingAlias; + $this->columnTableMap[$column] = null; + $this->columnAliasMap[$column] = null; + } + + $this->columnTableMap[$table . '.' . $column] = $table; + $this->columnAliasMap[$table . '.' . $column] = $key; + } else { + $this->columnTableMap[$column] = $table; + $this->columnAliasMap[$column] = $key; + } + } + } + } + + /** + * Return a new query for the given columns + * + * @param array $columns The desired columns, if null all columns will be queried + * + * @return RepositoryQuery + */ + public function select(array $columns = null) + { + $query = new RepositoryQuery($this); + $query->from($this->getBaseTable(), $columns); + return $query; + } + + /** + * Return whether this repository is capable of converting values for the given table and optional column + * + * @param string $table + * @param string $column + * + * @return bool + */ + public function providesValueConversion($table, $column = null) + { + $conversionRules = $this->getConversionRules(); + if (empty($conversionRules)) { + return false; + } + + if (! isset($conversionRules[$table])) { + return false; + } elseif ($column === null) { + return true; + } + + $alias = $this->reassembleQueryColumnAlias($table, $column) ?: $column; + return array_key_exists($alias, $conversionRules[$table]) || in_array($alias, $conversionRules[$table]); + } + + /** + * Convert a value supposed to be transmitted to the data source + * + * @param string $table The table where to persist the value + * @param string $name The alias or column name + * @param mixed $value The value to convert + * @param RepositoryQuery $query An optional query to pass as context + * (Directly passed through to $this->getConverter) + * + * @return mixed If conversion was possible, the converted value, + * otherwise the unchanged value + */ + public function persistColumn($table, $name, $value, RepositoryQuery $query = null) + { + $converter = $this->getConverter($table, $name, 'persist', $query); + if ($converter !== null) { + $value = $this->$converter($value, $name, $table, $query); + } + + return $value; + } + + /** + * Convert a value which was fetched from the data source + * + * @param string $table The table the value has been fetched from + * @param string $name The alias or column name + * @param mixed $value The value to convert + * @param RepositoryQuery $query An optional query to pass as context + * (Directly passed through to $this->getConverter) + * + * @return mixed If conversion was possible, the converted value, + * otherwise the unchanged value + */ + public function retrieveColumn($table, $name, $value, RepositoryQuery $query = null) + { + $converter = $this->getConverter($table, $name, 'retrieve', $query); + if ($converter !== null) { + $value = $this->$converter($value, $name, $table, $query); + } + + return $value; + } + + /** + * Return the name of the conversion method for the given alias or column name and context + * + * @param string $table The datasource's table + * @param string $name The alias or column name for which to return a conversion method + * @param string $context The context of the conversion: persist or retrieve + * @param RepositoryQuery $query An optional query to pass as context + * (unused by the base implementation) + * + * @return string + * + * @throws ProgrammingError In case a conversion rule is found but not any conversion method + */ + protected function getConverter($table, $name, $context, RepositoryQuery $query = null) + { + $conversionRules = $this->getConversionRules(); + if (! isset($conversionRules[$table])) { + return; + } + + $tableRules = $conversionRules[$table]; + if (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) { + $alias = $name; + } + + // Check for a conversion method for the alias/column first + if (array_key_exists($alias, $tableRules) || in_array($alias, $tableRules)) { + $methodName = $context . join('', array_map('ucfirst', explode('_', $alias))); + if (method_exists($this, $methodName)) { + return $methodName; + } + } + + // The conversion method for the type is just a fallback, but it is required to exist if defined + if (isset($tableRules[$alias])) { + $identifier = join('', array_map('ucfirst', explode('_', $tableRules[$alias]))); + if (! method_exists($this, $context . $identifier)) { + // Do not throw an error in case at least one conversion method exists + if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) { + throw new ProgrammingError( + 'Cannot find any conversion method for type "%s"' + . '. Add a proper conversion method or remove the type definition', + $tableRules[$alias] + ); + } + + Logger::debug( + 'Conversion method "%s" for type definition "%s" does not exist in repository "%s".', + $context . $identifier, + $tableRules[$alias], + $this->getName() + ); + } else { + return $context . $identifier; + } + } + } + + /** + * Convert a timestamp or DateTime object to a string formatted using static::DATETIME_FORMAT + * + * @param mixed $value + * + * @return string + */ + protected function persistDateTime($value) + { + if (is_numeric($value)) { + $value = date(static::DATETIME_FORMAT, $value); + } elseif ($value instanceof DateTime) { + $value = date(static::DATETIME_FORMAT, $value->getTimestamp()); // Using date here, to ignore any timezone + } elseif ($value !== null) { + throw new ProgrammingError( + 'Cannot persist value "%s" as type date_time. It\'s not a timestamp or DateTime object', + $value + ); + } + + return $value; + } + + /** + * Convert a string formatted using static::DATETIME_FORMAT to a unix timestamp + * + * @param string $value + * + * @return int + */ + protected function retrieveDateTime($value) + { + if (is_numeric($value)) { + $value = (int) $value; + } elseif (is_string($value)) { + $dateTime = DateTime::createFromFormat(static::DATETIME_FORMAT, $value); + if ($dateTime === false) { + Logger::debug( + 'Unable to parse string "%s" as type date_time with format "%s" in repository "%s"', + $value, + static::DATETIME_FORMAT, + $this->getName() + ); + $value = null; + } else { + $value = $dateTime->getTimestamp(); + } + } elseif ($value !== null) { + throw new ProgrammingError( + 'Cannot retrieve value "%s" as type date_time. It\'s not a integer or (numeric) string', + $value + ); + } + + return $value; + } + + /** + * Convert the given array to an comma separated string + * + * @param array|string $value + * + * @return string + */ + protected function persistCommaSeparatedString($value) + { + if (is_array($value)) { + $value = join(',', array_map('trim', $value)); + } elseif ($value !== null && !is_string($value)) { + throw new ProgrammingError('Cannot persist value "%s" as comma separated string', $value); + } + + return $value; + } + + /** + * Convert the given comma separated string to an array + * + * @param string $value + * + * @return array + */ + protected function retrieveCommaSeparatedString($value) + { + if ($value && is_string($value)) { + $value = StringHelper::trimSplit($value); + } elseif ($value !== null) { + throw new ProgrammingError('Cannot retrieve value "%s" as array. It\'s not a string', $value); + } + + return $value; + } + + /** + * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation + * + * @param string|null $value + * + * @return int + * + * @see https://tools.ietf.org/html/rfc4517#section-3.3.13 + */ + protected function retrieveGeneralizedTime($value) + { + if ($value === null) { + return $value; + } + + try { + return ASN1::parseGeneralizedTime($value)->getTimeStamp(); + } catch (InvalidArgumentException $e) { + Logger::debug(sprintf('Repository "%s": %s', $this->getName(), $e->getMessage())); + } + } + + /** + * Validate that the requested table exists and resolve it's real name if necessary + * + * @param string $table The table to validate + * @param RepositoryQuery $query An optional query to pass as context + * (unused by the base implementation) + * + * @return string The table's name, may differ from the given one + * + * @throws ProgrammingError In case the given table does not exist + */ + public function requireTable($table, RepositoryQuery $query = null) + { + $queryColumns = $this->getQueryColumns(); + if (! isset($queryColumns[$table])) { + throw new ProgrammingError('Table "%s" not found', $table); + } + + $virtualTables = $this->getVirtualTables(); + if (isset($virtualTables[$table])) { + $table = $virtualTables[$table]; + } + + return $table; + } + + /** + * Recurse the given filter, require each column for the given table and convert all values + * + * @param string $table The table being filtered + * @param Filter $filter The filter to recurse + * @param RepositoryQuery $query An optional query to pass as context + * (Directly passed through to $this->requireFilterColumn) + * @param bool $clone Whether to clone $filter first + * + * @return Filter The udpated filter + */ + public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true) + { + if ($clone) { + $filter = clone $filter; + } + + if ($filter->isExpression()) { + $column = $filter->getColumn(); + $filter->setColumn($this->requireFilterColumn($table, $column, $query, $filter)); + $filter->setExpression($this->persistColumn($table, $column, $filter->getExpression(), $query)); + } elseif ($filter->isChain()) { + foreach ($filter->filters() as $chainOrExpression) { + $this->requireFilter($table, $chainOrExpression, $query, false); + } + } + + return $filter; + } + + /** + * Return this repository's query columns of the given table mapped to their respective aliases + * + * @param string $table + * + * @return array + * + * @throws ProgrammingError In case $table does not exist + */ + public function requireAllQueryColumns($table) + { + $queryColumns = $this->getQueryColumns(); + if (! array_key_exists($table, $queryColumns)) { + throw new ProgrammingError('Table name "%s" not found', $table); + } + + $blacklist = $this->getBlacklistedQueryColumns($table); + $columns = array(); + foreach ($queryColumns[$table] as $alias => $column) { + $name = is_string($alias) ? $alias : $column; + if (! in_array($name, $blacklist)) { + $columns[$alias] = $this->resolveQueryColumnAlias($table, $name); + } + } + + return $columns; + } + + /** + * Return the query column name for the given alias or null in case the alias does not exist + * + * @param string $table + * @param string $alias + * + * @return string|null + */ + public function resolveQueryColumnAlias($table, $alias) + { + $aliasColumnMap = $this->getAliasColumnMap(); + if (isset($aliasColumnMap[$alias])) { + return $aliasColumnMap[$alias]; + } + + $prefixedAlias = $table . '.' . $alias; + if (isset($aliasColumnMap[$prefixedAlias])) { + return $aliasColumnMap[$prefixedAlias]; + } + } + + /** + * Return the alias for the given query column name or null in case the query column name does not exist + * + * @param string $table + * @param string $column + * + * @return string|null + */ + public function reassembleQueryColumnAlias($table, $column) + { + $columnAliasMap = $this->getColumnAliasMap(); + if (isset($columnAliasMap[$column])) { + return $columnAliasMap[$column]; + } + + $prefixedColumn = $table . '.' . $column; + if (isset($columnAliasMap[$prefixedColumn])) { + return $columnAliasMap[$prefixedColumn]; + } + } + + /** + * Return whether the given alias or query column name is available in the given table + * + * @param string $table + * @param string $alias + * + * @return bool + */ + public function validateQueryColumnAssociation($table, $alias) + { + $aliasTableMap = $this->getAliasTableMap(); + if (isset($aliasTableMap[$alias])) { + return $aliasTableMap[$alias] === $table; + } + + $prefixedAlias = $table . '.' . $alias; + if (isset($aliasTableMap[$prefixedAlias])) { + return true; + } + + $columnTableMap = $this->getColumnTableMap(); + if (isset($columnTableMap[$alias])) { + return $columnTableMap[$alias] === $table; + } + + return isset($columnTableMap[$prefixedAlias]); + } + + /** + * Return whether the given column name or alias is a valid query column + * + * @param string $table The table where to look for the column or alias + * @param string $name The column name or alias to check + * + * @return bool + */ + public function hasQueryColumn($table, $name) + { + if ($this->resolveQueryColumnAlias($table, $name) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) { + return false; + } + + return !in_array($alias, $this->getBlacklistedQueryColumns($table)) + && $this->validateQueryColumnAssociation($table, $name); + } + + /** + * Validate that the given column is a valid query target and return it or the actual name if it's an alias + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation) + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid query column + */ + public function requireQueryColumn($table, $name, RepositoryQuery $query = null) + { + if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { + $column = $name; + } else { + throw new QueryException(t('Query column "%s" not found'), $name); + } + + if (in_array($alias, $this->getBlacklistedQueryColumns($table))) { + throw new QueryException(t('Column "%s" cannot be queried'), $name); + } + + if (! $this->validateQueryColumnAssociation($table, $alias)) { + throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table); + } + + return $column; + } + + /** + * Return whether the given column name or alias is a valid filter column + * + * @param string $table The table where to look for the column or alias + * @param string $name The column name or alias to check + * + * @return bool + */ + public function hasFilterColumn($table, $name) + { + return ($this->resolveQueryColumnAlias($table, $name) !== null + || $this->reassembleQueryColumnAlias($table, $name) !== null) + && $this->validateQueryColumnAssociation($table, $name); + } + + /** + * Validate that the given column is a valid filter target and return it or the actual name if it's an alias + * + * @param string $table The table where to look for the column or alias + * @param string $name The name or alias of the column to validate + * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation) + * @param FilterExpression $filter An optional filter to pass as context (unused by the base implementation) + * + * @return string The given column's name + * + * @throws QueryException In case the given column is not a valid filter column + */ + public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = null) + { + if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { + $column = $name; + } else { + throw new QueryException(t('Filter column "%s" not found'), $name); + } + + if (! $this->validateQueryColumnAssociation($table, $alias)) { + throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table); + } + + return $column; + } + + /** + * Return whether the given column name or alias of the given table is a valid statement column + * + * @param string $table The table where to look for the column or alias + * @param string $name The column name or alias to check + * + * @return bool + */ + public function hasStatementColumn($table, $name) + { + return $this->hasQueryColumn($table, $name); + } + + /** + * Validate that the given column is a valid statement column and return it or the actual name if it's an alias + * + * @param string $table The table for which to require the column + * @param string $name The name or alias of the column to validate + * + * @return string The given column's name + * + * @throws StatementException In case the given column is not a statement column + */ + public function requireStatementColumn($table, $name) + { + if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) { + $alias = $name; + } elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) { + $column = $name; + } else { + throw new StatementException('Statement column "%s" not found', $name); + } + + if (in_array($alias, $this->getBlacklistedQueryColumns($table))) { + throw new StatementException('Column "%s" cannot be referenced in a statement', $name); + } + + if (! $this->validateQueryColumnAssociation($table, $alias)) { + throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table); + } + + return $column; + } + + /** + * Resolve the given aliases or column names of the given table supposed to be persisted and convert their values + * + * @param string $table + * @param array $data + * + * @return array + */ + public function requireStatementColumns($table, array $data) + { + $resolved = array(); + foreach ($data as $alias => $value) { + $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($table, $alias, $value); + } + + return $resolved; + } +} diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php new file mode 100644 index 0000000..84f7c6e --- /dev/null +++ b/library/Icinga/Repository/RepositoryQuery.php @@ -0,0 +1,797 @@ +repository = $repository; + } + + /** + * Clone all state relevant properties of this query + */ + public function __clone() + { + if ($this->query !== null) { + $this->query = clone $this->query; + } + if ($this->iterator !== null) { + $this->iterator = clone $this->iterator; + } + } + + /** + * Return a string representation of this query + * + * @return string + */ + public function __toString() + { + return (string) $this->query; + } + + /** + * Return the real query being used + * + * @return QueryInterface + */ + public function getQuery() + { + return $this->query; + } + + /** + * Set where to fetch which columns + * + * This notifies the repository about each desired query column. + * + * @param mixed $target The target from which to fetch the columns + * @param array $columns If null or an empty array, all columns will be fetched + * + * @return $this + */ + public function from($target, array $columns = null) + { + $this->query = $this->repository->getDataSource($target)->select(); + $this->query->from($this->repository->requireTable($target, $this)); + $this->query->columns($this->prepareQueryColumns($target, $columns)); + $this->target = $target; + return $this; + } + + /** + * Return the columns to fetch + * + * @return array + */ + public function getColumns() + { + return $this->query->getColumns(); + } + + /** + * Set which columns to fetch + * + * This notifies the repository about each desired query column. + * + * @param array $columns If null or an empty array, all columns will be fetched + * + * @return $this + */ + public function columns(array $columns) + { + $this->query->columns($this->prepareQueryColumns($this->target, $columns)); + return $this; + } + + /** + * Resolve the given columns supposed to be fetched + * + * This notifies the repository about each desired query column. + * + * @param mixed $target The target where to look for each column + * @param array $desiredColumns Pass null or an empty array to require all query columns + * + * @return array The desired columns indexed by their respective alias + */ + protected function prepareQueryColumns($target, array $desiredColumns = null) + { + $this->customAliases = array(); + if (empty($desiredColumns)) { + $columns = $this->repository->requireAllQueryColumns($target); + } else { + $columns = array(); + foreach ($desiredColumns as $customAlias => $columnAlias) { + $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this); + if ($resolvedColumn !== $columnAlias) { + if (is_string($customAlias)) { + $columns[$customAlias] = $resolvedColumn; + $this->customAliases[$customAlias] = $columnAlias; + } else { + $columns[$columnAlias] = $resolvedColumn; + } + } elseif (is_string($customAlias)) { + $columns[$customAlias] = $columnAlias; + $this->customAliases[$customAlias] = $columnAlias; + } else { + $columns[] = $columnAlias; + } + } + } + + return $columns; + } + + /** + * Return the native column alias for the given custom alias + * + * If no custom alias is found with the given name, it is returned unchanged. + * + * @param string $customAlias + * + * @return string + */ + protected function getNativeAlias($customAlias) + { + if (isset($this->customAliases[$customAlias])) { + return $this->customAliases[$customAlias]; + } + + return $customAlias; + } + + /** + * Return this query's available filter columns with their optional label as key + * + * @return array + */ + public function getFilterColumns() + { + return $this->repository->getFilterColumns($this->target); + } + + /** + * Return this query's available search columns + * + * @return array + */ + public function getSearchColumns() + { + return $this->repository->getSearchColumns($this->target); + } + + /** + * Filter this query using the given column and value + * + * This notifies the repository about the required filter column. + * + * @param string $column + * @param mixed $value + * + * @return $this + */ + public function where($column, $value = null) + { + $this->addFilter(Filter::where($column, $value)); + return $this; + } + + /** + * Add an additional filter expression to this query + * + * This notifies the repository about each required filter column. + * + * @param Filter $filter + * + * @return $this + */ + public function applyFilter(Filter $filter) + { + return $this->addFilter($filter); + } + + /** + * Set a filter for this query + * + * This notifies the repository about each required filter column. + * + * @param Filter $filter + * + * @return $this + */ + public function setFilter(Filter $filter) + { + $this->query->setFilter($this->repository->requireFilter($this->target, $filter, $this)); + return $this; + } + + /** + * Add an additional filter expression to this query + * + * This notifies the repository about each required filter column. + * + * @param Filter $filter + * + * @return $this + */ + public function addFilter(Filter $filter) + { + $this->query->addFilter($this->repository->requireFilter($this->target, $filter, $this)); + return $this; + } + + /** + * Return the current filter + * + * @return Filter + */ + public function getFilter() + { + return $this->query->getFilter(); + } + + /** + * Return the sort rules being applied on this query + * + * @return array + */ + public function getSortRules() + { + return $this->repository->getSortRules($this->target); + } + + /** + * Add a sort rule for this query + * + * If called without a specific column, the repository's defaul sort rules will be applied. + * This notifies the repository about each column being required as filter column. + * + * @param string $field The name of the column by which to sort the query's result + * @param string $direction The direction to use when sorting (asc or desc, default is asc) + * @param bool $ignoreDefault Whether to ignore any default sort rules if $field is given + * + * @return $this + */ + public function order($field = null, $direction = null, $ignoreDefault = false) + { + $sortRules = $this->getSortRules(); + if ($field === null) { + // Use first available sort rule as default + if (empty($sortRules)) { + // Return early in case of no sort defaults and no given $field + return $this; + } + + $sortColumns = reset($sortRules); + if (! array_key_exists('columns', $sortColumns)) { + $sortColumns['columns'] = array(key($sortRules)); + } + if ($direction !== null || !array_key_exists('order', $sortColumns)) { + $sortColumns['order'] = $direction ?: static::SORT_ASC; + } + } else { + $alias = $this->repository->reassembleQueryColumnAlias($this->target, $field) ?: $field; + if (! $ignoreDefault && array_key_exists($alias, $sortRules)) { + $sortColumns = $sortRules[$alias]; + if (! array_key_exists('columns', $sortColumns)) { + $sortColumns['columns'] = array($alias); + } + if ($direction !== null || !array_key_exists('order', $sortColumns)) { + $sortColumns['order'] = $direction ?: static::SORT_ASC; + } + } else { + $sortColumns = array( + 'columns' => array($alias), + 'order' => $direction + ); + } + } + + $baseDirection = isset($sortColumns['order']) && strtoupper($sortColumns['order']) === static::SORT_DESC + ? static::SORT_DESC + : static::SORT_ASC; + + foreach ($sortColumns['columns'] as $column) { + list($column, $specificDirection) = $this->splitOrder($column); + + if ($this->hasLimit() && $this->repository->providesValueConversion($this->target, $column)) { + Logger::debug( + 'Cannot order by column "%s" in repository "%s". The query is' + . ' limited and applies value conversion rules on the column', + $column, + $this->repository->getName() + ); + continue; + } + + try { + $this->query->order( + $this->repository->requireFilterColumn($this->target, $column, $this), + $specificDirection ?: $baseDirection + // I would have liked the following solution, but hey, a coder should be allowed to produce crap... + // $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection + ); + } catch (QueryException $_) { + Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName()); + } + } + + return $this; + } + + /** + * Extract and return the name and direction of the given sort column definition + * + * @param string $field + * + * @return array An array of two items: $columnName, $direction + */ + protected function splitOrder($field) + { + $columnAndDirection = explode(' ', $field, 2); + if (count($columnAndDirection) === 1) { + $column = $field; + $direction = null; + } else { + $column = $columnAndDirection[0]; + $direction = strtoupper($columnAndDirection[1]) === static::SORT_DESC + ? static::SORT_DESC + : static::SORT_ASC; + } + + return array($column, $direction); + } + + /** + * Return whether any sort rules were applied to this query + * + * @return bool + */ + public function hasOrder() + { + return $this->query->hasOrder(); + } + + /** + * Return the sort rules applied to this query + * + * @return array + */ + public function getOrder() + { + return $this->query->getOrder(); + } + + /** + * Set whether this query should peek ahead for more results + * + * Enabling this causes the current query limit to be increased by one. The potential extra row being yielded will + * be removed from the result set. Note that this only applies when fetching multiple results of limited queries. + * + * @param bool $state + * + * @return $this + */ + public function peekAhead($state = true) + { + $this->query->peekAhead($state); + return $this; + } + + /** + * Return whether this query did not yield all available results + * + * @return bool + */ + public function hasMore() + { + return $this->query->hasMore(); + } + + /** + * Return whether this query will or has yielded any result + * + * @return bool + */ + public function hasResult() + { + return $this->query->hasResult(); + } + + /** + * Limit this query's results + * + * @param int $count When to stop returning results + * @param int $offset When to start returning results + * + * @return $this + */ + public function limit($count = null, $offset = null) + { + $this->query->limit($count, $offset); + return $this; + } + + /** + * Return whether this query does not return all available entries from its result + * + * @return bool + */ + public function hasLimit() + { + return $this->query->hasLimit(); + } + + /** + * Return the limit when to stop returning results + * + * @return int + */ + public function getLimit() + { + return $this->query->getLimit(); + } + + /** + * Return whether this query does not start returning results at the very first entry + * + * @return bool + */ + public function hasOffset() + { + return $this->query->hasOffset(); + } + + /** + * Return the offset when to start returning results + * + * @return int + */ + public function getOffset() + { + return $this->query->getOffset(); + } + + /** + * Fetch and return the first column of this query's first row + * + * @return mixed|false False in case of no result + */ + public function fetchOne() + { + if (! $this->hasOrder()) { + $this->order(); + } + + $result = $this->query->fetchOne(); + if ($result !== false && $this->repository->providesValueConversion($this->target)) { + $columns = $this->getColumns(); + $column = isset($columns[0]) ? $columns[0] : $this->getNativeAlias(key($columns)); + return $this->repository->retrieveColumn($this->target, $column, $result, $this); + } + + return $result; + } + + /** + * Fetch and return the first row of this query's result + * + * @return object|false False in case of no result + */ + public function fetchRow() + { + if (! $this->hasOrder()) { + $this->order(); + } + + $result = $this->query->fetchRow(); + if ($result !== false && $this->repository->providesValueConversion($this->target)) { + foreach ($this->getColumns() as $alias => $column) { + if (! is_string($alias)) { + $alias = $column; + } + + $result->$alias = $this->repository->retrieveColumn( + $this->target, + $this->getNativeAlias($alias), + $result->$alias, + $this + ); + } + } + + return $result; + } + + /** + * Fetch and return the first column of all rows of the result set as an array + * + * @return array + */ + public function fetchColumn() + { + if (! $this->hasOrder()) { + $this->order(); + } + + $results = $this->query->fetchColumn(); + if (! empty($results) && $this->repository->providesValueConversion($this->target)) { + $columns = $this->getColumns(); + $aliases = array_keys($columns); + $column = is_int($aliases[0]) ? $columns[0] : $this->getNativeAlias($aliases[0]); + if ($this->repository->providesValueConversion($this->target, $column)) { + foreach ($results as & $value) { + $value = $this->repository->retrieveColumn($this->target, $column, $value, $this); + } + } + } + + return $results; + } + + /** + * Fetch and return all rows of this query's result set as an array of key-value pairs + * + * The first column is the key, the second column is the value. + * + * @return array + */ + public function fetchPairs() + { + if (! $this->hasOrder()) { + $this->order(); + } + + $results = $this->query->fetchPairs(); + if (! empty($results) && $this->repository->providesValueConversion($this->target)) { + $columns = $this->getColumns(); + $aliases = array_keys($columns); + $colOne = $aliases[0] !== 0 ? $this->getNativeAlias($aliases[0]) : $columns[0]; + $colTwo = count($aliases) < 2 ? $colOne : ( + $aliases[1] !== 1 ? $this->getNativeAlias($aliases[1]) : $columns[1] + ); + + if ($this->repository->providesValueConversion($this->target, $colOne) + || $this->repository->providesValueConversion($this->target, $colTwo) + ) { + $newResults = array(); + foreach ($results as $colOneValue => $colTwoValue) { + $colOneValue = $this->repository->retrieveColumn($this->target, $colOne, $colOneValue, $this); + $newResults[$colOneValue] = $this->repository->retrieveColumn( + $this->target, + $colTwo, + $colTwoValue, + $this + ); + } + + $results = $newResults; + } + } + + return $results; + } + + /** + * Fetch and return all results of this query + * + * @return array + */ + public function fetchAll() + { + if (! $this->hasOrder()) { + $this->order(); + } + + $results = $this->query->fetchAll(); + if (! empty($results) && $this->repository->providesValueConversion($this->target)) { + $updateOrder = false; + $columns = $this->getColumns(); + $flippedColumns = array_flip($columns); + foreach ($results as $row) { + foreach ($columns as $alias => $column) { + if (! is_string($alias)) { + $alias = $column; + } + + $row->$alias = $this->repository->retrieveColumn( + $this->target, + $this->getNativeAlias($alias), + $row->$alias, + $this + ); + } + + foreach (($this->getOrder() ?: array()) as $rule) { + $nativeAlias = $this->getNativeAlias($rule[0]); + if (! array_key_exists($rule[0], $flippedColumns) && property_exists($row, $rule[0])) { + if ($this->repository->providesValueConversion($this->target, $nativeAlias)) { + $updateOrder = true; + $row->{$rule[0]} = $this->repository->retrieveColumn( + $this->target, + $nativeAlias, + $row->{$rule[0]}, + $this + ); + } + } elseif (array_key_exists($rule[0], $flippedColumns)) { + if ($this->repository->providesValueConversion($this->target, $nativeAlias)) { + $updateOrder = true; + } + } + } + } + + if ($updateOrder) { + uasort($results, array($this->query, 'compare')); + } + } + + return $results; + } + + /** + * Count all results of this query + * + * @return int + */ + public function count(): int + { + return $this->query->count(); + } + + /** + * Return the current position of this query's iterator + * + * @return int + */ + public function getIteratorPosition() + { + return $this->query->getIteratorPosition(); + } + + /** + * Start or rewind the iteration + */ + public function rewind(): void + { + if ($this->iterator === null) { + if (! $this->hasOrder()) { + $this->order(); + } + + if ($this->query instanceof Traversable) { + $iterator = $this->query; + } else { + $iterator = $this->repository->getDataSource($this->target)->query($this->query); + } + + if ($iterator instanceof IteratorAggregate) { + $this->iterator = $iterator->getIterator(); + } else { + $this->iterator = $iterator; + } + } + + $this->iterator->rewind(); + Benchmark::measure('Query result iteration started'); + } + + /** + * Fetch and return the current row of this query's result + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + $row = $this->iterator->current(); + if ($this->repository->providesValueConversion($this->target)) { + foreach ($this->getColumns() as $alias => $column) { + if (! is_string($alias)) { + $alias = $column; + } + + $row->$alias = $this->repository->retrieveColumn( + $this->target, + $this->getNativeAlias($alias), + $row->$alias, + $this + ); + } + } + + return $row; + } + + /** + * Return whether the current row of this query's result is valid + * + * @return bool + */ + public function valid(): bool + { + if (! $this->iterator->valid()) { + Benchmark::measure('Query result iteration finished'); + return false; + } + + return true; + } + + /** + * Return the key for the current row of this query's result + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->iterator->key(); + } + + /** + * Advance to the next row of this query's result + */ + public function next(): void + { + $this->iterator->next(); + } +} diff --git a/library/Icinga/Security/SecurityException.php b/library/Icinga/Security/SecurityException.php new file mode 100644 index 0000000..861dcf1 --- /dev/null +++ b/library/Icinga/Security/SecurityException.php @@ -0,0 +1,13 @@ + array( + 'type' => 'db', + 'db' => 'mysql', + 'host' => '127.0.0.1', + 'port' => 3306, + 'dbname' => 'icinga_unittest', + 'username' => 'icinga_unittest', + 'password' => 'icinga_unittest' + ), + 'pgsql' => array( + 'type' => 'db', + 'db' => 'pgsql', + 'host' => '127.0.0.1', + 'port' => 5432, + 'dbname' => 'icinga_unittest', + 'username' => 'icinga_unittest', + 'password' => 'icinga_unittest' + ), + ); + + /** + * Setup the default timezone + */ + public static function setupTimezone() + { + date_default_timezone_set('UTC'); + } + + /** + * Setup test path environment + * + * @throws RuntimeException + */ + public static function setupDirectories() + { + $baseDir = getenv('ICINGAWEB_BASEDIR') ?: realpath(__DIR__ . '/../../../'); + if ($baseDir === false) { + throw new RuntimeException('Application base dir not found'); + } + + $libDir = getenv('ICINGAWEB_ICINGA_LIB') ?: realpath($baseDir . '/library/Icinga'); + if ($libDir === false) { + throw new RuntimeException('Icinga library dir not found'); + } + + self::$appDir = $baseDir . '/application'; + self::$libDir = $libDir; + self::$etcDir = $baseDir . '/etc'; + self::$testDir = $baseDir . '/test/php'; + self::$shareDir = $baseDir . '/share/icinga2-web'; + self::$moduleDir = getenv('ICINGAWEB_MODULES_DIR') ?: $baseDir . '/modules'; + } + + /** + * Setup MVC bootstrapping and ensure that the Icinga-Mock gets reinitialized + */ + public function setUp(): void + { + parent::setUp(); + + StaticTranslator::$instance = new NoopTranslator(); + $this->setupIcingaMock(); + } + + /** + * Setup mock object for the application's bootstrap + * + * @return Mockery\Mock + */ + protected function setupIcingaMock() + { + $requestMock = Mockery::mock('Icinga\Web\Request')->shouldDeferMissing(); + $requestMock->shouldReceive('getPathInfo')->andReturn('')->byDefault() + ->shouldReceive('getBaseUrl')->andReturn('/')->byDefault() + ->shouldReceive('getQuery')->andReturn(array())->byDefault() + ->shouldReceive('getParam')->with(Mockery::type('string'), Mockery::type('string')) + ->andReturnUsing(function ($name, $default) { + return $default; + })->byDefault(); + + $responseMock = Mockery::mock('Icinga\Web\Response')->shouldDeferMissing(); + // Can't express this as demeter chains. See: https://github.com/padraic/mockery/issues/59 + $bootstrapMock = Mockery::mock('Icinga\Application\ApplicationBootstrap')->shouldDeferMissing(); + $libDir = dirname(self::$libDir); + $bootstrapMock->shouldReceive('getFrontController')->andReturn($bootstrapMock) + ->shouldReceive('getApplicationDir')->andReturn(self::$appDir) + ->shouldReceive('getLibraryDir')->andReturnUsing(function ($subdir = null) use ($libDir) { + if ($subdir !== null) { + $libDir .= '/' . ltrim($subdir, '/'); + } + return $libDir; + }) + ->shouldReceive('getRequest')->andReturn($requestMock) + ->shouldReceive('getResponse')->andReturn($responseMock); + + Icinga::setApp($bootstrapMock, true); + return $bootstrapMock; + } + + /** + * Return the currently active request mock object + * + * @return Icinga\Web\Request + */ + public function getRequestMock() + { + return Icinga::app()->getRequest(); + } + + /** + * Return the currently active response mock object + * + * @return Icinga\Web\Response + */ + public function getResponseMock() + { + return Icinga::app()->getFrontController()->getResponse(); + } + + /** + * Create Config for database configuration + * + * @param string $name + * + * @return ConfigObject + * @throws RuntimeException + */ + protected function createDbConfigFor($name) + { + if (array_key_exists($name, self::$dbConfiguration)) { + $config = new ConfigObject(self::$dbConfiguration[$name]); + + $host = getenv(sprintf('ICINGAWEB_TEST_%s_HOST', strtoupper($name))); + if ($host) { + $config['host'] = $host; + } + + $port = getenv(sprintf('ICINGAWEB_TEST_%s_PORT', strtoupper($name))); + if ($port) { + $config['port'] = $port; + } + + return $config; + } + + throw new RuntimeException('Configuration for database type not available: ' . $name); + } + + /** + * Creates an array of Icinga\Data\Db\DbConnection + * + * @param string $name + * + * @return array + */ + protected function createDbConnectionFor($name) + { + try { + $conn = ResourceFactory::createResource($this->createDbConfigFor($name)); + } catch (Exception $e) { + $conn = $e->getMessage(); + } + + return array( + array($conn) + ); + } + + /** + * PHPUnit provider for mysql + * + * @return DbConnection + */ + public function mysqlDb() + { + return $this->createDbConnectionFor('mysql'); + } + + /** + * PHPUnit provider for pgsql + * + * @return DbConnection + */ + public function pgsqlDb() + { + return $this->createDbConnectionFor('pgsql'); + } + + /** + * PHPUnit provider for oracle + * + * @return DbConnection + */ + public function oracleDb() + { + return $this->createDbConnectionFor('oracle'); + } + + /** + * Executes sql file by using the database connection + * + * @param DbConnection $resource + * @param string $filename + * + * @throws RuntimeException + */ + public function loadSql(DbConnection $resource, $filename) + { + if (!is_file($filename)) { + throw new RuntimeException( + 'Sql file not found: ' . $filename . ' (test=' . $this->getName() . ')' + ); + } + + $sqlData = file_get_contents($filename); + + if (!$sqlData) { + throw new RuntimeException( + 'Sql file is empty: ' . $filename . ' (test=' . $this->getName() . ')' + ); + } + + $resource->getDbAdapter()->exec($sqlData); + } + + /** + * Setup provider for testcase + * + * @param string|DbConnection|null $resource + */ + public function setupDbProvider($resource) + { + if (!$resource instanceof DbConnection) { + if (is_string($resource)) { + $this->markTestSkipped('Could not initialize provider: ' . $resource); + } else { + $this->markTestSkipped('Could not initialize provider'); + } + return; + } + + $adapter = $resource->getDbAdapter(); + + try { + $adapter->getConnection(); + } catch (Exception $e) { + $this->markTestSkipped('Could not connect to provider: '. $e->getMessage()); + } + + $tables = $adapter->listTables(); + foreach ($tables as $table) { + $adapter->exec('DROP TABLE ' . $table . ';'); + } + } + + /** + * Add assertMatchesRegularExpression() method for phpunit >= 8.0 < 9.0 for compatibility with PHP 7.2. + * + * @TODO Remove once PHP 7.2 support is not needed for testing anymore. + */ + public static function assertMatchesRegularExpression( + string $pattern, + string $string, + string $message = '' + ): void { + if (method_exists(parent::class, 'assertMatchesRegularExpression')) { + parent::assertMatchesRegularExpression($pattern, $string, $message); + } else { + static::assertRegExp($pattern, $string, $message); + } + } + } + + BaseTestCase::setupTimezone(); + BaseTestCase::setupDirectories(); +} diff --git a/library/Icinga/Test/ClassLoader.php b/library/Icinga/Test/ClassLoader.php new file mode 100644 index 0000000..af90a7e --- /dev/null +++ b/library/Icinga/Test/ClassLoader.php @@ -0,0 +1,113 @@ +namespaces[$namespace] = $directory; + + return $this; + } + + /** + * Test whether a namespace exists + * + * @param string $namespace + * + * @return bool + */ + public function hasNamespace($namespace) + { + return array_key_exists($namespace, $this->namespaces); + } + + /** + * Get the source file of the given class or interface + * + * @param string $class Name of the class or interface + * + * @return string|null + */ + public function getSourceFile($class) + { + foreach ($this->namespaces as $namespace => $dir) { + if ($class === strstr($class, $namespace)) { + $classPath = str_replace( + self::NAMESPACE_SEPARATOR, + DIRECTORY_SEPARATOR, + substr($class, strlen($namespace)) + ) . '.php'; + if (file_exists($file = $dir . $classPath)) { + return $file; + } + } + } + return null; + } + + /** + * Load the given class or interface + * + * @param string $class Name of the class or interface + * + * @return bool Whether the class or interface has been loaded + */ + public function loadClass($class) + { + if ($file = $this->getSourceFile($class)) { + require $file; + return true; + } + return false; + } + + /** + * Register {@link loadClass()} as an autoloader + */ + public function register() + { + spl_autoload_register(array($this, 'loadClass')); + } + + /** + * Unregister {@link loadClass()} as an autoloader + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Unregister this as an autoloader + */ + public function __destruct() + { + $this->unregister(); + } +} diff --git a/library/Icinga/Test/DbTest.php b/library/Icinga/Test/DbTest.php new file mode 100644 index 0000000..d1b1ff0 --- /dev/null +++ b/library/Icinga/Test/DbTest.php @@ -0,0 +1,47 @@ +setUsername($username); + + if ($firstname !== null) { + $this->setFirstname($firstname); + } + + if ($lastname !== null) { + $this->setLastname($lastname); + } + + if ($email !== null) { + $this->setEmail($email); + } + } + + /** + * Setter for preferences + * + * @param Preferences $preferences + * + * @return $this + */ + public function setPreferences(Preferences $preferences) + { + $this->preferences = $preferences; + return $this; + } + + /** + * Getter for preferences + * + * @return Preferences + */ + public function getPreferences() + { + if ($this->preferences === null) { + $this->preferences = new Preferences(); + } + + return $this->preferences; + } + + /** + * Return all groups this user belongs to + * + * @return array + */ + public function getGroups() + { + return $this->groups; + } + + /** + * Set the groups this user belongs to + * + * @param array $groups + * + * @return $this + */ + public function setGroups(array $groups) + { + $this->groups = $groups; + return $this; + } + + /** + * Return true if the user is a member of this group + * + * @param string $group + * + * @return boolean + */ + public function isMemberOf($group) + { + return in_array($group, $this->groups); + } + + /** + * Get whether restrictions should not apply to this user + * + * @return bool + */ + public function isUnrestricted() + { + return $this->unrestricted; + } + + /** + * Set whether restrictions should not apply to this user + * + * @param bool $state + * + * @return $this + */ + public function setIsUnrestricted($state) + { + $this->unrestricted = (bool) $state; + + return $this; + } + + /** + * Get the user's permissions + * + * @return array + */ + public function getPermissions() + { + return $this->permissions; + } + + /** + * Set the user's permissions + * + * @param array $permissions + * + * @return $this + */ + public function setPermissions(array $permissions) + { + if (! empty($permissions)) { + natcasesort($permissions); + $this->permissions = array_combine($permissions, $permissions); + } + return $this; + } + + /** + * Return restriction information for this user + * + * @param string $name + * + * @return array + */ + public function getRestrictions($name) + { + if (array_key_exists($name, $this->restrictions)) { + return $this->restrictions[$name]; + } + + return array(); + } + + /** + * Set the user's restrictions + * + * @param string[] $restrictions + * + * @return $this + */ + public function setRestrictions(array $restrictions) + { + $this->restrictions = $restrictions; + return $this; + } + + /** + * Get the roles of the user + * + * @return Role[] + */ + public function getRoles() + { + return $this->roles; + } + + /** + * Set the roles of the user + * + * @param Role[] $roles + * + * @return $this + */ + public function setRoles(array $roles) + { + $this->roles = $roles; + return $this; + } + + /** + * Getter for username + * + * @return string + */ + public function getUsername() + { + return $this->domain === null ? $this->localUsername : $this->localUsername . '@' . $this->domain; + } + + /** + * Setter for username + * + * @param string $name + * + * @return $this + */ + public function setUsername($name) + { + $parts = explode('\\', $name, 2); + if (count($parts) === 2) { + list($this->domain, $this->localUsername) = $parts; + } else { + $parts = explode('@', $name, 2); + if (count($parts) === 2) { + list($this->localUsername, $this->domain) = $parts; + } else { + $this->localUsername = $name; + $this->domain = null; + } + } + + return $this; + } + + /** + * Getter for firstname + * + * @return string + */ + public function getFirstname() + { + return $this->firstname; + } + + /** + * Setter for firstname + * + * @param string $name + * + * @return $this + */ + public function setFirstname($name) + { + $this->firstname = $name; + return $this; + } + + /** + * Getter for lastname + * + * @return string + */ + public function getLastname() + { + return $this->lastname; + } + + /** + * Setter for lastname + * + * @param string $name + * + * @return $this + */ + public function setLastname($name) + { + $this->lastname = $name; + return $this; + } + + /** + * Getter for email + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * Setter for mail + * + * @param string $mail + * + * @return $this + * + * @throws InvalidArgumentException When an invalid mail is provided + */ + public function setEmail($mail) + { + if ($mail !== null && !filter_var($mail, FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException( + sprintf('Invalid mail given for user %s: %s', $this->getUsername(), $mail) + ); + } + + $this->email = $mail; + return $this; + } + + /** + * Set the domain + * + * @param string $domain + * + * @return $this + */ + public function setDomain($domain) + { + if ($domain && ($domain = trim($domain))) { + $this->domain = $domain; + } + + return $this; + } + + /** + * Get whether the user has a domain + * + * @return bool + */ + public function hasDomain() + { + return $this->domain !== null; + } + + /** + * Get the domain + * + * @return string + * + * @throws ProgrammingError If the user does not have a domain + */ + public function getDomain() + { + if ($this->domain === null) { + throw new ProgrammingError( + 'User does not have a domain.' + . ' Use User::hasDomain() to check whether the user has a domain beforehand.' + ); + } + return $this->domain; + } + + /** + * Get the local username, ie. the username without its domain + * + * @return string + */ + public function getLocalUsername() + { + return $this->localUsername; + } + + /** + * Set additional information about user + * + * @param string $key + * @param string $value + * + * @return $this + */ + public function setAdditional($key, $value) + { + $this->additionalInformation[$key] = $value; + return $this; + } + + /** + * Getter for additional information + * + * @param string $key + * @return mixed|null + */ + public function getAdditional($key) + { + if (isset($this->additionalInformation[$key])) { + return $this->additionalInformation[$key]; + } + + return null; + } + + /** + * Retrieve the user's timezone + * + * If the user did not set a timezone, the default timezone set via config.ini is returned + * + * @return DateTimeZone + */ + public function getTimeZone() + { + $tz = $this->preferences->get('timezone'); + if ($tz === null) { + $tz = date_default_timezone_get(); + } + + return new DateTimeZone($tz); + } + + /** + * Set additional external user information + * + * @param string $username + * @param string $field + * + * @return $this + */ + public function setExternalUserInformation($username, $field) + { + $this->externalUserInformation = array($username, $field); + return $this; + } + + /** + * Get additional external user information + * + * @return array + */ + public function getExternalUserInformation() + { + return $this->externalUserInformation; + } + + /** + * Return true if user has external user information set + * + * @return bool + */ + public function isExternalUser() + { + return ! empty($this->externalUserInformation); + } + + /** + * Get whether the user is authenticated using a HTTP authentication mechanism + * + * @return bool + */ + public function getIsHttpUser() + { + return $this->isHttpUser; + } + + /** + * Set whether the user is authenticated using a HTTP authentication mechanism + * + * @param bool $isHttpUser + * + * @return $this + */ + public function setIsHttpUser($isHttpUser = true) + { + $this->isHttpUser = (bool) $isHttpUser; + return $this; + } + + /** + * Whether the user has a given permission + * + * @param string $requiredPermission + * + * @return bool + */ + public function can($requiredPermission) + { + list($permissions, $refusals) = AdmissionLoader::migrateLegacyPermissions([$requiredPermission]); + if (! empty($permissions)) { + $requiredPermission = array_pop($permissions); + } elseif (! empty($refusals)) { + throw new InvalidArgumentException( + 'Refusals are not supported anymore. Check for a grant instead!' + ); + } + + $granted = false; + foreach ($this->getRoles() as $role) { + if ($role->denies($requiredPermission)) { + return false; + } + + if (! $granted && $role->grants($requiredPermission)) { + $granted = true; + } + } + + return $granted; + } + + /** + * Load and return this user's configured navigation of the given type + * + * @param string $type + * + * @return Navigation + */ + public function getNavigation($type) + { + $config = Config::navigation($type === 'dashboard-pane' ? 'dashlet' : $type, $this->getUsername()); + + if ($type === 'dashboard-pane') { + $panes = array(); + foreach ($config as $dashletName => $dashletConfig) { + // TODO: Throw ConfigurationError if pane or url is missing + $panes[$dashletConfig->pane][$dashletName] = $dashletConfig->url; + } + + $navigation = new Navigation(); + foreach ($panes as $paneName => $dashlets) { + $navigation->addItem( + $paneName, + array( + 'type' => 'dashboard-pane', + 'dashlets' => $dashlets + ) + ); + } + } else { + $navigation = Navigation::fromConfig($config); + } + + return $navigation; + } +} diff --git a/library/Icinga/User/Preferences.php b/library/Icinga/User/Preferences.php new file mode 100644 index 0000000..b09462b --- /dev/null +++ b/library/Icinga/User/Preferences.php @@ -0,0 +1,169 @@ + + * 'value')); // Start with initial preferences + * + * $preferences->aNewPreference = 'value'; // Set a preference + * + * unset($preferences->aPreference); // Unset a preference + * + * // Retrieve a preference and return a default value if the preference does not exist + * $anotherPreference = $preferences->get('anotherPreference', 'defaultValue'); + */ +class Preferences implements Countable +{ + /** + * Preferences key-value array + * + * @var array + */ + protected $preferences = array(); + + /** + * Constructor + * + * @param array $preferences Preferences key-value array + */ + public function __construct(array $preferences = array()) + { + $this->preferences = $preferences; + } + + /** + * Count all preferences + * + * @return int The number of preferences + */ + public function count(): int + { + return count($this->preferences); + } + + /** + * Determine whether a preference exists + * + * @param string $name + * + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->preferences); + } + + /** + * Write data to a preference + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + $this->preferences[$name] = $value; + } + + /** + * Retrieve a preference section + * + * @param string $name + * + * @return array|null + */ + public function get($name) + { + if (array_key_exists($name, $this->preferences)) { + return $this->preferences[$name]; + } + + return null; + } + + /** + * Retrieve a value from a specific section + * + * @param string $section + * @param string $name + * @param null $default + * + * @return array|null + */ + public function getValue($section, $name, $default = null) + { + if (array_key_exists($section, $this->preferences) + && array_key_exists($name, $this->preferences[$section]) + ) { + return $this->preferences[$section][$name]; + } + + return $default; + } + + /** + * Magic method so that $obj->value will work. + * + * @param string $name + * + * @return mixed + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * Remove a given preference + * + * @param string $name Preference name + */ + public function remove($name) + { + unset($this->preferences[$name]); + } + + /** + * Determine if a preference is set and is not NULL + * + * @param string $name Preference name + * + * @return bool + */ + public function __isset($name) + { + return isset($this->preferences[$name]); + } + + /** + * Unset a given preference + * + * @param string $name Preference name + */ + public function __unset($name) + { + $this->remove($name); + } + + /** + * Get preferences as array + * + * @return array + */ + public function toArray() + { + return $this->preferences; + } +} diff --git a/library/Icinga/User/Preferences/PreferencesStore.php b/library/Icinga/User/Preferences/PreferencesStore.php new file mode 100644 index 0000000..8ecc677 --- /dev/null +++ b/library/Icinga/User/Preferences/PreferencesStore.php @@ -0,0 +1,344 @@ + + * 'resource name' + * ), + * $user // Instance of \Icinga\User + * ); + * + * $preferences = new Preferences($store->load()); + * $preferences->aPreference = 'value'; + * $store->save($preferences); + * + */ +class PreferencesStore +{ + /** + * Column name for username + */ + const COLUMN_USERNAME = 'username'; + + /** + * Column name for section + */ + const COLUMN_SECTION = 'section'; + + /** + * Column name for preference + */ + const COLUMN_PREFERENCE = 'name'; + + /** + * Column name for value + */ + const COLUMN_VALUE = 'value'; + + /** + * Column name for created time + */ + const COLUMN_CREATED_TIME = 'ctime'; + + /** + * Column name for modified time + */ + const COLUMN_MODIFIED_TIME = 'mtime'; + + /** + * Table name + * + * @var string + */ + protected $table = 'icingaweb_user_preference'; + + /** + * Stored preferences + * + * @var array + */ + protected $preferences = []; + + /** + * Store config + * + * @var ConfigObject + */ + protected $config; + + /** + * Given user + * + * @var User + */ + protected $user; + + /** + * Create a new store + * + * @param ConfigObject $config The config for this adapter + * @param User $user The user to which these preferences belong + */ + public function __construct(ConfigObject $config, User $user) + { + $this->config = $config; + $this->user = $user; + $this->init(); + } + + /** + * Getter for the store config + * + * @return ConfigObject + */ + public function getStoreConfig(): ConfigObject + { + return $this->config; + } + + /** + * Getter for the user + * + * @return User + */ + public function getUser(): User + { + return $this->user; + } + + /** + * Initialize the store + */ + protected function init(): void + { + } + + /** + * Load preferences from the database + * + * @return array + * + * @throws NotReadableError In case the database operation failed + */ + public function load(): array + { + try { + $select = $this->getStoreConfig()->connection->getDbAdapter()->select(); + $result = $select + ->from($this->table, [self::COLUMN_SECTION, self::COLUMN_PREFERENCE, self::COLUMN_VALUE]) + ->where(self::COLUMN_USERNAME . ' = ?', $this->getUser()->getUsername()) + ->query() + ->fetchAll(); + } catch (Exception $e) { + throw new NotReadableError( + 'Cannot fetch preferences for user %s from database', + $this->getUser()->getUsername(), + $e + ); + } + + if ($result !== false) { + $values = []; + foreach ($result as $row) { + $values[$row->{self::COLUMN_SECTION}][$row->{self::COLUMN_PREFERENCE}] = $row->{self::COLUMN_VALUE}; + } + + $this->preferences = $values; + } + + return $this->preferences; + } + + /** + * Save the given preferences in the database + * + * @param Preferences $preferences The preferences to save + */ + public function save(Preferences $preferences): void + { + $preferences = $preferences->toArray(); + + $sections = array_keys($preferences); + + foreach ($sections as $section) { + if (! array_key_exists($section, $this->preferences)) { + $this->preferences[$section] = []; + } + + if (! array_key_exists($section, $preferences)) { + $preferences[$section] = []; + } + + $toBeInserted = array_diff_key($preferences[$section], $this->preferences[$section]); + if (!empty($toBeInserted)) { + $this->insert($toBeInserted, $section); + } + + $toBeUpdated = array_intersect_key( + array_diff_assoc($preferences[$section], $this->preferences[$section]), + array_diff_assoc($this->preferences[$section], $preferences[$section]) + ); + + if (!empty($toBeUpdated)) { + $this->update($toBeUpdated, $section); + } + + $toBeDeleted = array_keys(array_diff_key($this->preferences[$section], $preferences[$section])); + if (!empty($toBeDeleted)) { + $this->delete($toBeDeleted, $section); + } + } + } + + /** + * Insert the given preferences into the database + * + * @param array $preferences The preferences to insert + * @param string $section The preferences in section to update + * + * @throws NotWritableError In case the database operation failed + */ + protected function insert(array $preferences, string $section): void + { + /** @var \Zend_Db_Adapter_Abstract $db */ + $db = $this->getStoreConfig()->connection->getDbAdapter(); + + try { + foreach ($preferences as $key => $value) { + $db->insert( + $this->table, + [ + self::COLUMN_USERNAME => $this->getUser()->getUsername(), + $db->quoteIdentifier(self::COLUMN_SECTION) => $section, + $db->quoteIdentifier(self::COLUMN_PREFERENCE) => $key, + self::COLUMN_VALUE => $value, + self::COLUMN_CREATED_TIME => new Zend_Db_Expr('NOW()'), + self::COLUMN_MODIFIED_TIME => new Zend_Db_Expr('NOW()') + ] + ); + } + } catch (Exception $e) { + throw new NotWritableError( + 'Cannot insert preferences for user %s into database', + $this->getUser()->getUsername(), + $e + ); + } + } + + /** + * Update the given preferences in the database + * + * @param array $preferences The preferences to update + * @param string $section The preferences in section to update + * + * @throws NotWritableError In case the database operation failed + */ + protected function update(array $preferences, string $section): void + { + /** @var \Zend_Db_Adapter_Abstract $db */ + $db = $this->getStoreConfig()->connection->getDbAdapter(); + + try { + foreach ($preferences as $key => $value) { + $db->update( + $this->table, + [ + self::COLUMN_VALUE => $value, + self::COLUMN_MODIFIED_TIME => new Zend_Db_Expr('NOW()') + ], + [ + self::COLUMN_USERNAME . '=?' => $this->getUser()->getUsername(), + $db->quoteIdentifier(self::COLUMN_SECTION) . '=?' => $section, + $db->quoteIdentifier(self::COLUMN_PREFERENCE) . '=?' => $key + ] + ); + } + } catch (Exception $e) { + throw new NotWritableError( + 'Cannot update preferences for user %s in database', + $this->getUser()->getUsername(), + $e + ); + } + } + + /** + * Delete the given preference names from the database + * + * @param array $preferenceKeys The preference names to delete + * @param string $section The preferences in section to update + * + * @throws NotWritableError In case the database operation failed + */ + protected function delete(array $preferenceKeys, string $section): void + { + /** @var \Zend_Db_Adapter_Abstract $db */ + $db = $this->getStoreConfig()->connection->getDbAdapter(); + + try { + $db->delete( + $this->table, + [ + self::COLUMN_USERNAME . '=?' => $this->getUser()->getUsername(), + $db->quoteIdentifier(self::COLUMN_SECTION) . '=?' => $section, + $db->quoteIdentifier(self::COLUMN_PREFERENCE) . ' IN (?)' => $preferenceKeys + ] + ); + } catch (Exception $e) { + throw new NotWritableError( + 'Cannot delete preferences for user %s from database', + $this->getUser()->getUsername(), + $e + ); + } + } + + /** + * Create preferences storage adapter from config + * + * @param ConfigObject $config The config for the adapter + * @param User $user The user to which these preferences belong + * + * @return self + * + * @throws ConfigurationError When the configuration defines an invalid storage type + */ + public static function create(ConfigObject $config, User $user): self + { + $resourceConfig = ResourceFactory::getResourceConfig($config->resource); + if ($resourceConfig->db === 'mysql') { + $resourceConfig->charset = 'utf8mb4'; + } + + $config->connection = ResourceFactory::createResource($resourceConfig); + + return new self($config, $user); + } +} diff --git a/library/Icinga/Util/ASN1.php b/library/Icinga/Util/ASN1.php new file mode 100644 index 0000000..9e00258 --- /dev/null +++ b/library/Icinga/Util/ASN1.php @@ -0,0 +1,102 @@ + + [0-9]{4} # century year + (?:0[1-9]|1[0-2]) # month + (?:0[1-9]|[12][0-9]|3[0-1]) # day + (?:[01][0-9]|2[0-3]) # hour + ) + (?: + (?P[0-5][0-9]) # minute + (?P[0-5][0-9]|60)? # second or leap-second + )? + (?:[.,](?P[0-9]+))? # fraction + (?P # g-time-zone + Z + | + [-+] + (?:[01][0-9]|2[0-3]) # hour + (?:[0-5][0-9])? # minute + ) +\z/x +EOD; + + $matches = array(); + + if (preg_match($generalizedTimePattern, $value, $matches)) { + $dateTimeRaw = $matches['YmdH']; + $dateTimeFormat = 'YmdH'; + + if ($matches['i'] !== '') { + $dateTimeRaw .= $matches['i']; + $dateTimeFormat .= 'i'; + + if ($matches['s'] !== '') { + $dateTimeRaw .= $matches['s']; + $dateTimeFormat .= 's'; + $fractionOfSeconds = 1; + } else { + $fractionOfSeconds = 60; + } + } else { + $fractionOfSeconds = 3600; + } + + $dateTimeFormat .= 'O'; + + if ($matches['tz'] === 'Z') { + $dateTimeRaw .= '+0000'; + } else { + $dateTimeRaw .= $matches['tz']; + + if (strlen($matches['tz']) === 3) { + $dateTimeRaw .= '00'; + } + } + + $dateTime = DateTime::createFromFormat($dateTimeFormat, $dateTimeRaw); + + if ($dateTime !== false) { + if (isset($matches['frac'])) { + $dateTime->add(new DateInterval( + 'PT' . round((float) ('0.' . $matches['frac']) * $fractionOfSeconds) . 'S' + )); + } + + return $dateTime; + } + } + + throw new InvalidArgumentException(sprintf( + 'Failed to parse %s based on the ASN.1 standard (GeneralizedTime)', + var_export($value, true) + )); + } +} diff --git a/library/Icinga/Util/Color.php b/library/Icinga/Util/Color.php new file mode 100644 index 0000000..cf88f41 --- /dev/null +++ b/library/Icinga/Util/Color.php @@ -0,0 +1,121 @@ + 1 ? $r : '0' . $r) + . (strlen($g) > 1 ? $g : '0' . $g) + . (strlen($b) > 1 ? $b : '0' . $b); + } + + /** + * Change the saturation for a given color. + * + * @param $color string The color to change + * @param $change float The change. + * 0.0 creates a black-and-white image. + * 0.5 reduces the color saturation by half. + * 1.0 causes no change. + * 2.0 doubles the color saturation. + * @return string + */ + public static function changeSaturation($color, $change) + { + return self::arrayToRgb(self::changeRgbSaturation(self::rgbAsArray($color), $change)); + } + + /** + * Change the brightness for a given color + * + * @param $color string The color to change + * @param $change float The change in percent + * + * @return string + */ + public static function changeBrightness($color, $change) + { + return self::arrayToRgb(self::changeRgbBrightness(self::rgbAsArray($color), $change)); + } + + /** + * @param $rgb array The rgb-array to change + * @param $change float The factor + * + * @return array The updated rgb-array + */ + private static function changeRgbSaturation(array $rgb, $change) + { + $pr = 0.499; // 0.299 + $pg = 0.387; // 0.587 + $pb = 0.114; // 0.114 + $r = $rgb[0]; + $g = $rgb[1]; + $b = $rgb[2]; + $p = sqrt( + $r * $r * $pr + + $g * $g * $pg + + $b * $b * $pb + ); + $rgb[0] = (int)($p + ($r - $p) * $change); + $rgb[1] = (int)($p + ($g - $p) * $change); + $rgb[2] = (int)($p + ($b - $p) * $change); + return $rgb; + } + + /** + * @param $rgb array The rgb-array to change + * @param $change float The factor + * + * @return array The updated rgb-array + */ + private static function changeRgbBrightness(array $rgb, $change) + { + $red = $rgb[0] + ($rgb[0] * $change); + $green = $rgb[1] + ($rgb[1] * $change); + $blue = $rgb[2] + ($rgb[2] * $change); + $rgb[0] = $red < 255 ? (int) $red : 255; + $rgb[1] = $green < 255 ? (int) $green : 255; + $rgb[2] = $blue < 255 ? (int) $blue : 255; + return $rgb; + } +} diff --git a/library/Icinga/Util/ConfigAwareFactory.php b/library/Icinga/Util/ConfigAwareFactory.php new file mode 100644 index 0000000..133887a --- /dev/null +++ b/library/Icinga/Util/ConfigAwareFactory.php @@ -0,0 +1,18 @@ +setValue($value, $unit); + } + + /** + * Change the value and unit of this dimension + * + * @param int $value The new value + * @param string $unit The unit to use (default: px) + */ + public function setValue($value, $unit = self::UNIT_PX) + { + $this->value = intval($value); + $this->unit = $unit; + } + + /** + * Return true when the value is > 0 + * + * @return bool + */ + public function isDefined() + { + return $this->value > 0; + } + + /** + * Return the underlying value without unit information + * + * @return int + */ + public function getValue() + { + return $this->value; + } + + /** + * Return the unit used for the value + * + * @return string + */ + public function getUnit() + { + return $this->unit; + } + + /** + * Return this value with it's according unit as a string + * + * @return string + */ + public function __toString() + { + if (!$this->isDefined()) { + return ""; + } + return $this->value.$this->unit; + } + + /** + * Create a new Dimension object from a string containing the numeric value and the dimension (e.g. 200px, 20%) + * + * @param $string The string to parse + * + * @return Dimension + */ + public static function fromString($string) + { + $matches = array(); + if (!preg_match_all('/^ *([0-9]+)(px|pt|em|\%) */i', $string, $matches)) { + return new Dimension(0); + } + return new Dimension(intval($matches[1][0]), $matches[2][0]); + } +} diff --git a/library/Icinga/Util/DirectoryIterator.php b/library/Icinga/Util/DirectoryIterator.php new file mode 100644 index 0000000..a2bed1c --- /dev/null +++ b/library/Icinga/Util/DirectoryIterator.php @@ -0,0 +1,213 @@ +path = $path; + if (! empty($extension)) { + $this->extension = '.' . ltrim($extension, '.'); + } + if ($flags !== null) { + $this->flags = $flags; + } + } + + /** + * Check whether the given path is a directory and is readable + * + * @param string $path The path of the directory + * + * @return bool + */ + public static function isReadable($path) + { + return is_dir($path) && is_readable($path); + } + + public function hasChildren(): bool + { + return static::isReadable($this->current); + } + + public function getChildren(): DirectoryIterator + { + return new static($this->current, $this->extension, $this->flags); + } + + #[\ReturnTypeWillChange] + public function current() + { + return $this->current; + } + + public function next(): void + { + do { + $this->files->next(); + $skip = false; + if (! $this->files->valid()) { + $file = false; + $path = false; + break; + } else { + $file = $this->files->current(); + do { + if ($this->skipHidden && $file[0] === '.') { + $skip = true; + break; + } + + $path = $this->path . '/' . $file; + + if (is_dir($path)) { + if ($this->flags & static::FILES_FIRST === static::FILES_FIRST) { + $this->queue[] = array($path, $file); + $skip = true; + } + break; + } + + if ($this->skipEmpty && ! filesize($path)) { + $skip = true; + break; + } + + if ($this->extension && ! StringHelper::endsWith($file, $this->extension)) { + $skip = true; + break; + } + } while (0); + } + } while ($skip); + + /** @noinspection PhpUndefinedVariableInspection */ + + if ($path === false && ! empty($this->queue)) { + list($path, $file) = array_shift($this->queue); + } + + $this->current = $path; + $this->key = $file; + } + + #[\ReturnTypeWillChange] + public function key() + { + return $this->key; + } + + public function valid(): bool + { + return $this->current !== false; + } + + public function rewind(): void + { + if ($this->files === null) { + $files = scandir($this->path); + natcasesort($files); + $this->files = new ArrayIterator($files); + } + $this->files->rewind(); + $this->queue = array(); + $this->next(); + } +} diff --git a/library/Icinga/Util/EnumeratingFilterIterator.php b/library/Icinga/Util/EnumeratingFilterIterator.php new file mode 100644 index 0000000..0659961 --- /dev/null +++ b/library/Icinga/Util/EnumeratingFilterIterator.php @@ -0,0 +1,30 @@ +index = 0; + } + + public function key(): int + { + return $this->index++; + } +} diff --git a/library/Icinga/Util/Environment.php b/library/Icinga/Util/Environment.php new file mode 100644 index 0000000..8d47b84 --- /dev/null +++ b/library/Icinga/Util/Environment.php @@ -0,0 +1,42 @@ +openMode = $openMode; + if ($context === null) { + parent::__construct($filename, $openMode, $useIncludePath); + } else { + parent::__construct($filename, $openMode, $useIncludePath, $context); + } + } + + /** + * Create a file using the given access mode and return a instance of File open for writing + * + * @param string $path The path to the file + * @param int $accessMode The access mode to set + * @param bool $recursive Whether missing nested directories of the given path should be created + * + * @return File + * + * @throws RuntimeException In case the file cannot be created or the access mode cannot be set + * @throws NotWritableError In case the path's (existing) parent is not writable + */ + public static function create($path, $accessMode, $recursive = true) + { + $dirPath = dirname($path); + if ($recursive && !is_dir($dirPath)) { + static::createDirectories($dirPath); + } elseif (! is_writable($dirPath)) { + throw new NotWritableError(sprintf('Path "%s" is not writable', $dirPath)); + } + + $file = new static($path, 'x+'); + + if (! @chmod($path, $accessMode)) { + $error = error_get_last(); + throw new RuntimeException(sprintf( + 'Cannot set access mode "%s" on file "%s" (%s)', + decoct($accessMode), + $path, + $error['message'] + )); + } + + return $file; + } + + /** + * Create missing directories + * + * @param string $path + * + * @throws RuntimeException In case a directory cannot be created or the access mode cannot be set + */ + protected static function createDirectories($path) + { + $part = strpos($path, DIRECTORY_SEPARATOR) === 0 ? DIRECTORY_SEPARATOR : ''; + foreach (explode(DIRECTORY_SEPARATOR, ltrim($path, DIRECTORY_SEPARATOR)) as $dir) { + $part .= $dir . DIRECTORY_SEPARATOR; + + if (! is_dir($part)) { + if (! @mkdir($part, static::$dirMode)) { + $error = error_get_last(); + throw new RuntimeException(sprintf( + 'Failed to create missing directory "%s" (%s)', + $part, + $error['message'] + )); + } + + if (! @chmod($part, static::$dirMode)) { + $error = error_get_last(); + throw new RuntimeException(sprintf( + 'Failed to set access mode "%s" for directory "%s" (%s)', + decoct(static::$dirMode), + $part, + $error['message'] + )); + } + } + } + } + + #[\ReturnTypeWillChange] + public function fwrite($str, $length = null) + { + $this->assertOpenForWriting(); + $this->setupErrorHandler(); + $retVal = $length === null ? parent::fwrite($str) : parent::fwrite($str, $length); + restore_error_handler(); + return $retVal; + } + + public function ftruncate($size): bool + { + $this->assertOpenForWriting(); + $this->setupErrorHandler(); + $retVal = parent::ftruncate($size); + restore_error_handler(); + return $retVal; + } + + #[\ReturnTypeWillChange] + public function ftell() + { + $this->setupErrorHandler(); + $retVal = parent::ftell(); + restore_error_handler(); + return $retVal; + } + + public function flock($operation, &$wouldblock = null): bool + { + $this->setupErrorHandler(); + $retVal = parent::flock($operation, $wouldblock); + restore_error_handler(); + return $retVal; + } + + #[\ReturnTypeWillChange] + public function fgetc() + { + $this->setupErrorHandler(); + $retVal = parent::fgetc(); + restore_error_handler(); + return $retVal; + } + + public function fflush(): bool + { + $this->setupErrorHandler(); + $retVal = parent::fflush(); + restore_error_handler(); + return $retVal; + } + + /** + * Setup an error handler that throws a RuntimeException for every emitted E_WARNING + */ + protected function setupErrorHandler() + { + set_error_handler( + function ($errno, $errstr, $errfile, $errline) { + restore_error_handler(); + throw new ErrorException($errstr, 0, $errno, $errfile, $errline); + }, + E_WARNING + ); + } + + /** + * Assert that the file was opened for writing and throw an exception otherwise + * + * @throws NotWritableError In case the file was not opened for writing + */ + protected function assertOpenForWriting() + { + if (!preg_match('@w|a|\+@', $this->openMode)) { + throw new NotWritableError('File not open for writing'); + } + } +} diff --git a/library/Icinga/Util/Format.php b/library/Icinga/Util/Format.php new file mode 100644 index 0000000..1158208 --- /dev/null +++ b/library/Icinga/Util/Format.php @@ -0,0 +1,197 @@ + 0 && $result < 2) { + $result = $value / pow($base, --$pow); + } + + return sprintf( + '%s%0.2f %s', + $sign, + $result, + $units[abs($pow)] + ); + } + + /** + * Return the amount of seconds based on the given month + * + * @param DateTime|int $dateTimeOrTimestamp The date and time to use + * + * @return int + */ + public static function secondsByMonth($dateTimeOrTimestamp) + { + if ($dateTimeOrTimestamp === null) { + return 0; + } + + if (!($dt = $dateTimeOrTimestamp) instanceof DateTime) { + $dt = new DateTime(); + $dt->setTimestamp($dateTimeOrTimestamp); + } + + return (int) $dt->format('t') * 24 * 3600; + } + + /** + * Return the amount of seconds based on the given year + * + * @param DateTime|int $dateTimeOrTimestamp The date and time to use + * + * @return int + */ + public static function secondsByYear($dateTimeOrTimestamp) + { + if ($dateTimeOrTimestamp === null) { + return 0; + } + + return (self::isLeapYear($dateTimeOrTimestamp) ? 366 : 365) * 24 * 3600; + } + + /** + * Return whether the given year is a leap year + * + * @param DateTime|int $dateTimeOrTimestamp The date and time to use + * + * @return bool + */ + public static function isLeapYear($dateTimeOrTimestamp) + { + if ($dateTimeOrTimestamp === null) { + return false; + } + + if (!($dt = $dateTimeOrTimestamp) instanceof DateTime) { + $dt = new DateTime(); + $dt->setTimestamp($dateTimeOrTimestamp); + } + + return $dt->format('L') == 1; + } + + /** + * Unpack shorthand bytes PHP directives to bytes + * + * @param string $subject + * + * @return int + */ + public static function unpackShorthandBytes($subject) + { + $base = (int) $subject; + + if ($base <= -1) { + return INF; + } + + switch (strtoupper($subject[strlen($subject) - 1])) { + case 'K': + $multiplier = 1024; + break; + case 'M': + $multiplier = 1024 ** 2; + break; + case 'G': + $multiplier = 1024 ** 3; + break; + default: + $multiplier = 1; + break; + } + + return $base * $multiplier; + } +} diff --git a/library/Icinga/Util/GlobFilter.php b/library/Icinga/Util/GlobFilter.php new file mode 100644 index 0000000..15cbe0b --- /dev/null +++ b/library/Icinga/Util/GlobFilter.php @@ -0,0 +1,182 @@ + array( + * 'bar' => array( + * 'baz' => 'deadbeef' // <--- + * ) + * ) + * ) + */ +class GlobFilter +{ + /** + * The prepared filters + * + * @var array + */ + protected $filters; + + /** + * Create a new filter from a comma-separated list of GLOB-like filters or an array of such lists. + * + * @param string|\Traversable $filters + */ + public function __construct($filters) + { + $patterns = array(array('')); + $lastIndex1 = $lastIndex2 = 0; + + foreach ((is_string($filters) ? array($filters) : $filters) as $rawPatterns) { + $escape = false; + + foreach (str_split($rawPatterns) as $c) { + if ($escape) { + $escape = false; + $patterns[$lastIndex1][$lastIndex2] .= preg_quote($c, '/'); + } else { + switch ($c) { + case '\\': + $escape = true; + break; + case ',': + $patterns[] = array(''); + ++$lastIndex1; + $lastIndex2 = 0; + break; + case '.': + $patterns[$lastIndex1][] = ''; + ++$lastIndex2; + break; + case '*': + $patterns[$lastIndex1][$lastIndex2] .= '.*'; + break; + default: + $patterns[$lastIndex1][$lastIndex2] .= preg_quote($c, '/'); + } + } + } + + if ($escape) { + $patterns[$lastIndex1][$lastIndex2] .= '\\\\'; + } + } + + $this->filters = array(); + + foreach ($patterns as $pattern) { + foreach ($pattern as $i => $subPattern) { + if ($subPattern === '') { + unset($pattern[$i]); + } elseif ($subPattern === '.*.*') { + $pattern[$i] = '**'; + } elseif ($subPattern === '.*') { + $pattern[$i] = '/^' . $subPattern . '$/'; + } else { + $pattern[$i] = '/^' . trim($subPattern) . '$/i'; + } + } + + if (! empty($pattern)) { + $found = false; + foreach ($pattern as $i => $v) { + if ($found) { + if ($v === '**') { + unset($pattern[$i]); + } else { + $found = false; + } + } elseif ($v === '**') { + $found = true; + } + } + + if (end($pattern) === '**') { + $pattern[] = '/^.*$/'; + } + + $this->filters[] = array_values($pattern); + } + } + } + + /** + * Remove all keys/attributes matching any of $this->filters from $dataStructure + * + * @param stdClass|array $dataStructure + * + * @return stdClass|array The modified copy of $dataStructure + */ + public function removeMatching($dataStructure) + { + foreach ($this->filters as $filter) { + $dataStructure = static::removeMatchingRecursive($dataStructure, $filter); + } + return $dataStructure; + } + + /** + * Helper method for removeMatching() + * + * @param stdClass|array $dataStructure + * @param array $filter + * + * @return stdClass|array + */ + protected static function removeMatchingRecursive($dataStructure, $filter) + { + $multiLevelPattern = $filter[0] === '**'; + if ($multiLevelPattern) { + $dataStructure = static::removeMatchingRecursive($dataStructure, array_slice($filter, 1)); + } + + $isObject = $dataStructure instanceof stdClass; + if ($isObject || is_array($dataStructure)) { + if ($isObject) { + $dataStructure = (array) $dataStructure; + } + + if ($multiLevelPattern) { + foreach ($dataStructure as $k => & $v) { + $v = static::removeMatchingRecursive($v, $filter); + unset($v); + } + } else { + $currentLevel = $filter[0]; + $nextLevels = count($filter) === 1 ? null : array_slice($filter, 1); + foreach ($dataStructure as $k => & $v) { + if (preg_match($currentLevel, (string) $k)) { + if ($nextLevels === null) { + unset($dataStructure[$k]); + } else { + $v = static::removeMatchingRecursive($v, $nextLevels); + } + } + unset($v); + } + } + + if ($isObject) { + $dataStructure = (object) $dataStructure; + } + } + + return $dataStructure; + } +} diff --git a/library/Icinga/Util/Json.php b/library/Icinga/Util/Json.php new file mode 100644 index 0000000..0b89dcc --- /dev/null +++ b/library/Icinga/Util/Json.php @@ -0,0 +1,151 @@ + $val) { + if (is_string($key)) { + $key = static::sanitizeUtf8String($key); + } + + $sanitized[$key] = static::sanitizeUtf8Recursive($val); + } + + return $sanitized; + + case 'object': + $sanitized = array(); + + foreach ($value as $key => $val) { + if (is_string($key)) { + $key = static::sanitizeUtf8String($key); + } + + $sanitized[$key] = static::sanitizeUtf8Recursive($val); + } + + return (object) $sanitized; + + default: + return $value; + } + } + + /** + * Replace bad byte sequences in the given UTF-8 string with question marks + * + * @param string $string + * + * @return string + */ + protected static function sanitizeUtf8String($string) + { + return mb_convert_encoding($string, 'UTF-8', 'UTF-8'); + } +} diff --git a/library/Icinga/Util/LessParser.php b/library/Icinga/Util/LessParser.php new file mode 100644 index 0000000..46aa061 --- /dev/null +++ b/library/Icinga/Util/LessParser.php @@ -0,0 +1,17 @@ +setOption('plugins', [new Visitor()]); + } +} diff --git a/library/Icinga/Util/StringHelper.php b/library/Icinga/Util/StringHelper.php new file mode 100644 index 0000000..67a836b --- /dev/null +++ b/library/Icinga/Util/StringHelper.php @@ -0,0 +1,184 @@ + $maxLength) { + return substr($string, 0, $maxLength - strlen($ellipsis)) . $ellipsis; + } + + return $string; + } + + /** + * Add ellipsis in the center of a string when a string is longer than max length + * + * @param string $string + * @param int $maxLength + * @param string $ellipsis + * + * @return string + */ + public static function ellipsisCenter($string, $maxLength, $ellipsis = '...') + { + if ($string === null) { + return ''; + } + + $start = ceil($maxLength / 2.0); + $end = floor($maxLength / 2.0); + if (strlen($string) > $maxLength) { + return substr($string, 0, $start - strlen($ellipsis)) . $ellipsis . substr($string, - $end); + } + + return $string; + } + + /** + * Find and return all similar strings in $possibilites matching $string with the given minimum $similarity + * + * @param string $string + * @param array $possibilities + * @param float $similarity + * + * @return array + */ + public static function findSimilar($string, array $possibilities, $similarity = 0.33) + { + if (empty($string)) { + return array(); + } + + $matches = array(); + foreach ($possibilities as $possibility) { + $distance = levenshtein($string, $possibility); + if ($distance / strlen($string) <= $similarity) { + $matches[] = $possibility; + } + } + + return $matches; + } + + /** + * Test whether the given string ends with the given suffix + * + * @param string $string The string to test + * @param string $suffix The suffix the string must end with + * + * @return bool + */ + public static function endsWith($string, $suffix) + { + if ($string === null) { + return false; + } + + $stringSuffix = substr($string, -strlen($suffix)); + return $stringSuffix !== false ? $stringSuffix === $suffix : false; + } + + /** + * Generates an array of strings that constitutes the cartesian product of all passed sets, with all + * string combinations concatenated using the passed join-operator. + * + *
    +     *  cartesianProduct(
    +     *      array(array('foo', 'bar'), array('mumble', 'grumble', null)),
    +     *      '_'
    +     *  );
    +     *     => array('foo_mumble', 'foo_grumble', 'bar_mumble', 'bar_grumble', 'foo', 'bar')
    +     * 
    + * + * @param array $sets An array of arrays containing all sets for which the cartesian + * product should be calculated. + * @param string $glue The glue used to join the strings, defaults to ''. + * + * @returns array The cartesian product in one array of strings. + */ + public static function cartesianProduct(array $sets, $glue = '') + { + $product = null; + foreach ($sets as $set) { + if (! isset($product)) { + $product = $set; + } else { + $newProduct = array(); + foreach ($product as $strA) { + foreach ($set as $strB) { + if ($strB === null) { + $newProduct []= $strA; + } else { + $newProduct []= $strA . $glue . $strB; + } + } + } + $product = $newProduct; + } + } + return $product; + } +} diff --git a/library/Icinga/Util/TimezoneDetect.php b/library/Icinga/Util/TimezoneDetect.php new file mode 100644 index 0000000..4967c7f --- /dev/null +++ b/library/Icinga/Util/TimezoneDetect.php @@ -0,0 +1,107 @@ + $value) { + $method = 'set' . ucfirst($key); + if (method_exists($this, $method)) { + $this->$method($value); + } + } + } + + /** + * Get the author of the acknowledged + * + * @return string + */ + public function getAuthor() + { + return $this->author; + } + + /** + * Set the author of the acknowledged + * + * @param string $author + * + * @return $this + */ + public function setAuthor($author) + { + $this->author = $author; + return $this; + } + + /** + * Get the message of the acknowledged + * + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * Set the message of the acknowledged + * + * @param string $message + * + * @return $this + */ + public function setMessage($message) + { + $this->message = $message; + $this->hash = null; + return $this; + } + + /** + * Get the start date and time of the acknowledged + * + * @return int + */ + public function getStart() + { + return $this->start; + } + + /** + * Set the start date and time of the acknowledged + * + * @param int $start + * + * @return $this + */ + public function setStart($start) + { + $this->start = $start; + return $this; + } + + /** + * Get the end date and time of the acknowledged + * + * @return int + */ + public function getEnd() + { + return $this->end; + } + + /** + * Set the end date and time of the acknowledged + * + * @param int $end + * + * @return $this + */ + public function setEnd($end) + { + $this->end = $end; + return $this; + } + + /** + * Get the hash of the acknowledgement + * + * @return string + */ + public function getHash() + { + if ($this->hash === null) { + $this->hash = md5($this->message); + } + return $this->hash; + } +} diff --git a/library/Icinga/Web/Announcement/AnnouncementCookie.php b/library/Icinga/Web/Announcement/AnnouncementCookie.php new file mode 100644 index 0000000..f4398ba --- /dev/null +++ b/library/Icinga/Web/Announcement/AnnouncementCookie.php @@ -0,0 +1,138 @@ +setExpire(2147483648); + if (isset($_COOKIE['icingaweb2-announcements'])) { + $cookie = json_decode($_COOKIE['icingaweb2-announcements'], true); + if ($cookie !== null) { + if (isset($cookie['acknowledged'])) { + $this->setAcknowledged($cookie['acknowledged']); + } + if (isset($cookie['etag'])) { + $this->setEtag($cookie['etag']); + } + if (isset($cookie['next'])) { + $this->setNextActive($cookie['next']); + } + } + } + } + + /** + * Get the hashes of the acknowledged announcements + * + * @return string[] + */ + public function getAcknowledged() + { + return $this->acknowledged; + } + + /** + * Set the hashes of the acknowledged announcements + * + * @param string[] $acknowledged + * + * @return $this + */ + public function setAcknowledged(array $acknowledged) + { + $this->acknowledged = $acknowledged; + return $this; + } + + /** + * Get the ETag + * + * @return string + */ + public function getEtag() + { + return $this->etag; + } + + /** + * Set the ETag + * + * @param string $etag + * + * @return $this + */ + public function setEtag($etag) + { + $this->etag = $etag; + return $this; + } + + /** + * Get the timestamp of the next active announcement + * + * @return int + */ + public function getNextActive() + { + return $this->nextActive; + } + + /** + * Set the timestamp of the next active announcement + * + * @param int $nextActive + * + * @return $this + */ + public function setNextActive($nextActive) + { + $this->nextActive = $nextActive; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getValue() + { + return Json::encode(array( + 'acknowledged' => $this->getAcknowledged(), + 'etag' => $this->getEtag(), + 'next' => $this->getNextActive() + )); + } +} diff --git a/library/Icinga/Web/Announcement/AnnouncementIniRepository.php b/library/Icinga/Web/Announcement/AnnouncementIniRepository.php new file mode 100644 index 0000000..d972a1d --- /dev/null +++ b/library/Icinga/Web/Announcement/AnnouncementIniRepository.php @@ -0,0 +1,152 @@ + array('id', 'author', 'message', 'hash', 'start', 'end')); + + protected $triggers = array('announcement'); + + protected $configs = array('announcement' => array( + 'name' => 'announcements', + 'keyColumn' => 'id' + )); + + protected $conversionRules = array('announcement' => array( + 'start' => 'timestamp', + 'end' => 'timestamp' + )); + + /** + * Get a DateTime's timestamp + * + * @param DateTime $datetime + * + * @return int|null + */ + protected function persistTimestamp(DateTime $datetime) + { + return $datetime === null ? null : $datetime->getTimestamp(); + } + + /** + * Before-insert trigger (per row) + * + * @param ConfigObject $new The original data to insert + * + * @return ConfigObject The eventually modified data to insert + */ + protected function onInsertAnnouncement(ConfigObject $new) + { + if (! isset($new->id)) { + $new->id = uniqid(); + } + + if (! isset($new->hash)) { + $announcement = new Announcement($new->toArray()); + $new->hash = $announcement->getHash(); + } + + return $new; + } + + /** + * Before-update trigger (per row) + * + * @param ConfigObject $old The original data as currently stored + * @param ConfigObject $new The original data to update + * + * @return ConfigObject The eventually modified data to update + */ + protected function onUpdateAnnouncement(ConfigObject $old, ConfigObject $new) + { + if ($new->message !== $old->message) { + $announcement = new Announcement($new->toArray()); + $new->hash = $announcement->getHash(); + } + + return $new; + } + + /** + * Get the ETag of the announcements.ini file + * + * @return string + */ + public function getEtag() + { + $file = $this->getDataSource('announcement')->getConfigFile(); + + if (@is_readable($file)) { + $mtime = filemtime($file); + $size = filesize($file); + + return hash('crc32', $mtime . $size); + } + + return null; + } + + /** + * Get the query for all active announcements + * + * @return SimpleQuery + */ + public function findActive() + { + $now = new DateTime(); + + $query = $this + ->select(array('hash', 'message', 'start')) + ->setFilter(new FilterAnd(array( + Filter::expression('start', '<=', $now), + Filter::expression('end', '>=', $now) + ))) + ->order('start'); + + return $query; + } + + /** + * Get the timestamp of the next active announcement + * + * @return int|null + */ + public function findNextActive() + { + $now = new DateTime(); + + $query = $this + ->select(array('start', 'end')) + ->setFilter(Filter::matchAny(array( + Filter::expression('start', '>', $now), Filter::expression('end', '>', $now) + ))); + + $refresh = null; + + foreach ($query as $row) { + $min = min($row->start, $row->end); + + if ($refresh === null) { + $refresh = $min; + } else { + $refresh = min($refresh, $min); + } + } + + return $refresh; + } +} diff --git a/library/Icinga/Web/ApplicationStateCookie.php b/library/Icinga/Web/ApplicationStateCookie.php new file mode 100644 index 0000000..e40c17b --- /dev/null +++ b/library/Icinga/Web/ApplicationStateCookie.php @@ -0,0 +1,74 @@ +setExpire(2147483648); + + if (isset($_COOKIE['icingaweb2-application-state'])) { + try { + $cookie = Json::decode($_COOKIE['icingaweb2-application-state'], true); + } catch (JsonDecodeException $e) { + Logger::error( + "Can't decode the application state cookie of user '%s'. An error occurred: %s", + Auth::getInstance()->getUser()->getUsername(), + $e + ); + + return; + } + + if (isset($cookie['acknowledged-messages'])) { + $this->setAcknowledgedMessages($cookie['acknowledged-messages']); + } + } + } + + /** + * Get the acknowledged messages + * + * @return array + */ + public function getAcknowledgedMessages() + { + return $this->acknowledgedMessages; + } + + /** + * Set the acknowledged messages + * + * @param array $acknowledged + * + * @return $this + */ + public function setAcknowledgedMessages(array $acknowledged) + { + $this->acknowledgedMessages = $acknowledged; + + return $this; + } + + public function getValue() + { + return Json::encode([ + 'acknowledged-messages' => $this->getAcknowledgedMessages() + ]); + } +} diff --git a/library/Icinga/Web/Controller.php b/library/Icinga/Web/Controller.php new file mode 100644 index 0000000..a2730d5 --- /dev/null +++ b/library/Icinga/Web/Controller.php @@ -0,0 +1,264 @@ +handleSortControlSubmit(); + } + + /** + * Check whether the sort control has been submitted and redirect using GET parameters + */ + protected function handleSortControlSubmit() + { + $request = $this->getRequest(); + if (! $request->isPost()) { + return; + } + + if (($sort = $request->getPost('sort')) || ($direction = $request->getPost('dir'))) { + $url = Url::fromRequest(); + if ($sort) { + $url->setParam('sort', $sort); + $url->remove('dir'); + } else { + $url->setParam('dir', $direction); + } + + $this->redirectNow($url); + } + } + + /** + * Immediately respond w/ HTTP 400 + * + * @param string $message Exception message or exception format string + * @param mixed ...$arg Format string argument + * + * @throws HttpBadRequestException + */ + public function httpBadRequest($message) + { + throw HttpBadRequestException::create(func_get_args()); + } + + /** + * Immediately respond w/ HTTP 404 + * + * @param string $message Exception message or exception format string + * @param mixed ...$arg Format string argument + * + * @throws HttpNotFoundException + */ + public function httpNotFound($message) + { + throw HttpNotFoundException::create(func_get_args()); + } + + /** + * Render the given form using a simple view script + * + * @param Form $form + * @param string $tab + */ + public function renderForm(Form $form, $tab) + { + $this->getTabs()->add(uniqid(), array( + 'active' => true, + 'label' => $tab, + 'url' => Url::fromRequest() + )); + $this->view->form = $form; + $this->render('simple-form', null, true); + } + + /** + * Create a SortBox widget and apply its sort rules on the given query + * + * The widget is set on the `sortBox' view property only if the current view has not been requested as compact + * + * @param array $columns An array containing the sort columns, with the + * submit value as the key and the label as the value + * @param Sortable $query Query to apply the user chosen sort rules on + * @param array $defaults An array containing default sort directions for specific columns + * + * @return $this + */ + protected function setupSortControl(array $columns, Sortable $query = null, array $defaults = null) + { + $request = $this->getRequest(); + $sortBox = SortBox::create('sortbox-' . $request->getActionName(), $columns, $defaults); + $sortBox->setRequest($request); + + if ($query) { + $sortBox->setQuery($query); + $sortBox->handleRequest($request); + } + + if (! $this->view->compact) { + $this->view->sortBox = $sortBox; + } + + return $this; + } + + /** + * Create a Limiter widget at the `limiter' view property + * + * In case the current view has been requested as compact this method does nothing. + * + * @param int $itemsPerPage Default number of items per page + * + * @return $this + */ + protected function setupLimitControl($itemsPerPage = 25) + { + if (! $this->view->compact) { + $this->view->limiter = new Limiter(); + $this->view->limiter->setDefaultLimit($this->getPageSize($itemsPerPage)); + } + + return $this; + } + + /** + * Get the page size configured via user preferences or return the default value + * + * @param int $default + * + * @return int + */ + protected function getPageSize($default) + { + if ($this->userPageSize === null) { + $user = $this->Auth()->getUser(); + if ($user !== null) { + $pageSize = $user->getPreferences()->getValue('icingaweb', 'default_page_size'); + $this->userPageSize = $pageSize ? (int) $pageSize : false; + } else { + $this->userPageSize = false; + } + } + + return $this->userPageSize !== false ? $this->userPageSize : $default; + } + + /** + * Apply the given page limit and number on the given query and setup a paginator for it + * + * The $itemsPerPage and $pageNumber parameters are only applied if not available in the current request. + * The paginator is set on the `paginator' view property only if the current view has not been requested as compact. + * + * @param QueryInterface $query The query to create a paginator for + * @param int $itemsPerPage Default number of items per page + * @param int $pageNumber Default page number + * + * @return $this + */ + protected function setupPaginationControl(QueryInterface $query, $itemsPerPage = 25, $pageNumber = 0) + { + $request = $this->getRequest(); + $limit = $request->getParam('limit', $this->getPageSize($itemsPerPage)); + $page = $request->getParam('page', $pageNumber); + $query->limit($limit, $page > 0 ? ($page - 1) * $limit : 0); + + if (! $this->view->compact) { + $paginator = new Paginator(); + $paginator->setQuery($query); + $this->view->paginator = $paginator; + } + + return $this; + } + + /** + * Create a FilterEditor widget and apply the user's chosen filter options on the given filterable + * + * The widget is set on the `filterEditor' view property only if the current view has not been requested as compact. + * The optional $filterColumns parameter should be an array of key-value pairs where the key is the name of the + * column and the value the label to show to the user. The optional $searchColumns parameter should be an array + * of column names to be used to handle quick searches. + * + * If the given filterable is an instance of Icinga\Data\FilterColumns, $filterable->getFilterColumns() and + * $filterable->getSearchColumns() is called to provide the respective columns if $filterColumns or $searchColumns + * is not given. + * + * @param Filterable $filterable The filterable to create a filter editor for + * @param array $filterColumns The filter columns to offer to the user + * @param array $searchColumns The search columns to utilize for quick searches + * @param array $preserveParams The url parameters to preserve + * + * @return $this + * + * @todo Preserving and ignoring parameters should be configurable (another two method params? property magic?) + */ + protected function setupFilterControl( + Filterable $filterable, + array $filterColumns = null, + array $searchColumns = null, + array $preserveParams = null + ) { + $defaultPreservedParams = array( + 'limit', // setupPaginationControl() + 'sort', // setupSortControl() + 'dir', // setupSortControl() + 'backend', // Framework + 'showCompact', // Framework + '_dev' // Framework + ); + + $editor = Widget::create('filterEditor'); + /** @var \Icinga\Web\Widget\FilterEditor $editor */ + call_user_func_array( + array($editor, 'preserveParams'), + array_merge($defaultPreservedParams, $preserveParams ?: array()) + ); + + $editor + ->setQuery($filterable) + ->ignoreParams('page') // setupPaginationControl() + ->setColumns($filterColumns) + ->setSearchColumns($searchColumns) + ->handleRequest($this->getRequest()); + + if ($this->view->compact) { + $editor->setVisible(false); + } + + $this->view->filterEditor = $editor; + + return $this; + } +} diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php new file mode 100644 index 0000000..76ff650 --- /dev/null +++ b/library/Icinga/Web/Controller/ActionController.php @@ -0,0 +1,587 @@ +params = UrlParams::fromQueryString(); + + $this->setRequest($request) + ->setResponse($response) + ->_setInvokeArgs($invokeArgs); + $this->_helper = new Zend_Controller_Action_HelperBroker($this); + + $moduleName = $this->getModuleName(); + $this->view->defaultTitle = static::DEFAULT_TITLE; + $this->translationDomain = $moduleName !== 'default' ? $moduleName : 'icinga'; + $this->view->translationDomain = $this->translationDomain; + $this->_helper->layout()->isIframe = $request->getUrl()->shift('isIframe'); + $this->_helper->layout()->showFullscreen = $request->getUrl()->shift('showFullscreen'); + $this->_helper->layout()->moduleName = $moduleName; + + $this->view->compact = false; + if ($request->getUrl()->getParam('view') === 'compact') { + $request->getUrl()->remove('view'); + $this->view->compact = true; + } + if ($request->getUrl()->shift('showCompact')) { + $this->view->compact = true; + } + $this->rerenderLayout = $request->getUrl()->shift('renderLayout'); + if ($request->getUrl()->shift('_disableLayout')) { + $this->_helper->layout()->disableLayout(); + } + + // $auth->authenticate($request, $response, $this->requiresLogin()); + if ($this->requiresLogin()) { + if (! $request->isXmlHttpRequest() && $request->isApiRequest()) { + Auth::getInstance()->challengeHttp(); + } + $this->redirectToLogin(Url::fromRequest()); + } + + $this->view->tabs = new Tabs(); + $this->prepareInit(); + $this->init(); + } + + /** + * Prepare controller initialization + * + * As it should not be required for controllers to call the parent's init() method, base controllers should use + * prepareInit() in order to prepare the controller initialization. + * + * @see \Zend_Controller_Action::init() For the controller initialization method. + */ + protected function prepareInit() + { + } + + /** + * Get the authentication manager + * + * @return Auth + */ + public function Auth() + { + if ($this->auth === null) { + $this->auth = Auth::getInstance(); + } + return $this->auth; + } + + /** + * Whether the current user has the given permission + * + * @param string $permission Name of the permission + * + * @return bool + */ + public function hasPermission($permission) + { + return $this->Auth()->hasPermission($permission); + } + + /** + * Assert that the current user has the given permission + * + * @param string $permission Name of the permission + * + * @throws SecurityException If the current user lacks the given permission + */ + public function assertPermission($permission) + { + if (! $this->Auth()->hasPermission($permission)) { + throw new SecurityException('No permission for %s', $permission); + } + } + + /** + * Return the current module's name + * + * @return string + */ + public function getModuleName() + { + if ($this->moduleName === null) { + $this->moduleName = $this->getRequest()->getModuleName(); + } + + return $this->moduleName; + } + + public function Config($file = null) + { + if ($file === null) { + return Config::app(); + } else { + return Config::app($file); + } + } + + public function Window() + { + if ($this->window === null) { + $this->window = Window::getInstance(); + } + + return $this->window; + } + + protected function reloadCss() + { + $this->reloadCss = true; + return $this; + } + + /** + * Respond with HTTP 405 if the current request's method is not one of the given methods + * + * @param string $httpMethod Unlimited number of allowed HTTP methods + * + * @throws HttpMethodNotAllowedException If the request method is not one of the given methods + */ + public function assertHttpMethod($httpMethod) + { + $httpMethods = array_flip(array_map('strtoupper', func_get_args())); + if (! isset($httpMethods[$this->getRequest()->getMethod()])) { + $e = new HttpMethodNotAllowedException($this->translate('Method Not Allowed')); + $e->setAllowedMethods(implode(', ', array_keys($httpMethods))); + throw $e; + } + } + + /** + * Return restriction information for an eventually authenticated user + * + * @param string $name Restriction name + * + * @return array + */ + public function getRestrictions($name) + { + return $this->Auth()->getRestrictions($name); + } + + /** + * Check whether the controller requires a login. That is when the controller requires authentication and the + * user is currently not authenticated + * + * @return bool + */ + protected function requiresLogin() + { + if (! $this->requiresAuthentication) { + return false; + } + + return ! $this->Auth()->isAuthenticated(); + } + + /** + * Return the tabs + * + * @return Tabs + */ + public function getTabs() + { + return $this->view->tabs; + } + + protected function ignoreXhrBody() + { + if ($this->isXhr()) { + $this->getResponse()->setHeader('X-Icinga-Container', 'ignore'); + } + } + + /** + * Set the interval (in seconds) at which the page should automatically refresh + * + * This may be adjusted based on the user's preferences. The result could be a + * lower or higher rate of the page's automatic refresh. If this is not desired, + * the only way to bypass this is to initialize the {@see ActionController::$autorefreshInterval} + * property or to set the `autorefreshInterval` property of the layout directly. + * + * @param int $interval + * + * @return $this + */ + public function setAutorefreshInterval($interval) + { + if (! is_int($interval) || $interval < 1) { + throw new ProgrammingError( + 'Setting autorefresh interval smaller than 1 second is not allowed' + ); + } + + $user = $this->getRequest()->getUser(); + if ($user !== null) { + $speed = (float) $user->getPreferences()->getValue('icingaweb', 'auto_refresh_speed', 1.0); + $interval = max(round($interval * $speed), min($interval, 5)); + } + + $this->autorefreshInterval = $interval; + + return $this; + } + + public function disableAutoRefresh() + { + $this->autorefreshInterval = null; + + return $this; + } + + /** + * Redirect to login + * + * XHR will always redirect to __SELF__ if an URL to redirect to after successful login is set. __SELF__ instructs + * JavaScript to redirect to the current window's URL if it's an auto-refresh request or to redirect to the URL + * which required login if it's not an auto-refreshing one. + * + * XHR will respond with HTTP status code 403 Forbidden. + * + * @param Url|string $redirect URL to redirect to after successful login + */ + protected function redirectToLogin($redirect = null) + { + $login = Url::fromPath(static::LOGIN_ROUTE); + if ($this->isXhr()) { + if ($redirect !== null) { + $login->setParam('redirect', '__SELF__'); + } + + $this->_response->setHttpResponseCode(403); + } elseif ($redirect !== null) { + if (! $redirect instanceof Url) { + $redirect = Url::fromPath($redirect); + } + + if (($relativeUrl = $redirect->getRelativeUrl())) { + $login->setParam('redirect', $relativeUrl); + } + } + + $this->rerenderLayout()->redirectNow($login); + } + + protected function rerenderLayout() + { + $this->rerenderLayout = true; + return $this; + } + + public function isXhr() + { + return $this->getRequest()->isXmlHttpRequest(); + } + + protected function redirectXhr($url) + { + $response = $this->getResponse(); + + if ($this->reloadCss) { + $response->setReloadCss(true); + } + + if ($this->rerenderLayout) { + $response->setRerenderLayout(true); + } + + $response->redirectAndExit($url); + } + + protected function redirectHttp($url) + { + if ($this->isXhr()) { + $this->getResponse()->setHeader('X-Icinga-Redirect-Http', 'yes'); + } + + $this->getResponse()->redirectAndExit($url); + } + + /** + * Redirect to a specific url, updating the browsers URL field + * + * @param Url|string $url The target to redirect to + **/ + public function redirectNow($url) + { + if ($this->isXhr()) { + $this->redirectXhr($url); + } else { + $this->redirectHttp($url); + } + } + + /** + * @see Zend_Controller_Action::preDispatch() + */ + public function preDispatch() + { + $form = new AutoRefreshForm(); + if (! $this->getRequest()->isApiRequest()) { + $form->handleRequest(); + } + $this->_helper->layout()->autoRefreshForm = $form; + } + + /** + * Detect whether the current request requires changes in the layout and apply them before rendering + * + * @see Zend_Controller_Action::postDispatch() + */ + public function postDispatch() + { + Benchmark::measure('Action::postDispatch()'); + + $req = $this->getRequest(); + $layout = $this->_helper->layout(); + $layout->innerLayout = $this->innerLayout; + $layout->inlineLayout = $this->inlineLayout; + + if ($user = $req->getUser()) { + if ((bool) $user->getPreferences()->getValue('icingaweb', 'show_benchmark', false)) { + if ($this->_helper->layout()->isEnabled()) { + $layout->benchmark = $this->renderBenchmark(); + } + } + + if (! (bool) $user->getPreferences()->getValue('icingaweb', 'auto_refresh', true)) { + $this->disableAutoRefresh(); + } + } + + if ($this->autorefreshInterval !== null) { + $layout->autorefreshInterval = $this->autorefreshInterval; + } + + if ($req->getParam('error_handler') === null && $req->getParam('format') === 'pdf') { + $this->sendAsPdf(); + $this->shutdownSession(); + exit; + } + + if ($this->isXhr()) { + $this->postDispatchXhr(); + } + + $this->shutdownSession(); + } + + protected function postDispatchXhr() + { + $resp = $this->getResponse(); + + if ($this->reloadCss) { + $resp->setReloadCss(true); + } + + if ($this->view->title) { + if (preg_match('~[\r\n]~', $this->view->title)) { + // TODO: Innocent exception and error log for hack attempts + throw new IcingaException('No way, guy'); + } + $resp->setHeader( + 'X-Icinga-Title', + rawurlencode($this->view->title . ' :: ' . $this->view->defaultTitle), + true + ); + } else { + $resp->setHeader('X-Icinga-Title', rawurlencode($this->view->defaultTitle), true); + } + + $layout = $this->_helper->layout(); + if ($this->rerenderLayout) { + $layout->setLayout($this->innerLayout); + $resp->setRerenderLayout(true); + } else { + // The layout may be disabled and there's no indication that the layout is explicitly desired, + // that's why we're passing false as second parameter to setLayout + $layout->setLayout($this->inlineLayout, false); + } + + if ($this->autorefreshInterval !== null) { + $resp->setAutoRefreshInterval($this->autorefreshInterval); + } + } + + protected function sendAsPdf() + { + if (Module::exists('pdfexport')) { + $this->newSendAsPdf(); + } else { + $pdf = new Pdf(); + $pdf->renderControllerAction($this); + } + } + + protected function shutdownSession() + { + $session = Session::getSession(); + if ($session->hasChanged()) { + $session->write(); + } + } + + /** + * Render the benchmark + * + * @return string Benchmark HTML + */ + protected function renderBenchmark() + { + $this->_helper->viewRenderer->postDispatch(); + Benchmark::measure('Response ready'); + return Benchmark::renderToHtml(); + } + + /** + * Try to call compatible methods from older zend versions + * + * Methods like getParam and redirect are _getParam/_redirect in older Zend versions (which reside for example + * in Debian Wheezy). Using those methods without the "_" causes the application to fail on those platforms, but + * using the version with "_" forces us to use deprecated code. So we try to catch this issue by looking for methods + * with the same name, but with a "_" prefix prepended. + * + * @param string $name The method name to check + * @param mixed $params The method parameters + * @return mixed Anything the method returns + */ + public function __call($name, $params) + { + $deprecatedMethod = '_' . $name; + + if (method_exists($this, $deprecatedMethod)) { + return call_user_func_array(array($this, $deprecatedMethod), $params); + } + + return parent::__call($name, $params); + } +} diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php new file mode 100644 index 0000000..97dc4b3 --- /dev/null +++ b/library/Icinga/Web/Controller/AuthBackendController.php @@ -0,0 +1,149 @@ +tabs->disableLegacyExtensions(); + } + + /** + * Redirect to this controller's list action + */ + public function indexAction() + { + $this->redirectNow($this->getRequest()->getControllerName() . '/list'); + } + + /** + * Return all user backends implementing the given interface + * + * @param string $interface The class path of the interface, or null if no interface check should be made + * + * @return array + */ + protected function loadUserBackends($interface = null) + { + $backends = array(); + foreach (Config::app('authentication') as $backendName => $backendConfig) { + $candidate = UserBackend::create($backendName, $backendConfig); + if (! $interface || $candidate instanceof $interface) { + $backends[] = $candidate; + } + } + + return $backends; + } + + /** + * Return the given user backend or the first match in order + * + * @param string $name The name of the backend, or null in case the first match should be returned + * @param string $interface The interface the backend should implement, no interface check if null + * + * @return UserBackendInterface + * + * @throws Zend_Controller_Action_Exception In case the given backend name is invalid + */ + protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable') + { + if ($name !== null) { + $config = Config::app('authentication'); + if (! $config->hasSection($name)) { + $this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name)); + } else { + $backend = UserBackend::create($name, $config->getSection($name)); + if ($interface && !$backend instanceof $interface) { + $interfaceParts = explode('\\', strtolower($interface)); + throw new Zend_Controller_Action_Exception( + sprintf( + $this->translate('Authentication backend "%s" is not %s'), + $name, + array_pop($interfaceParts) + ), + 400 + ); + } + } + } else { + $backends = $this->loadUserBackends($interface); + $backend = array_shift($backends); + } + + return $backend; + } + + /** + * Return all user group backends implementing the given interface + * + * @param string $interface The class path of the interface, or null if no interface check should be made + * + * @return array + */ + protected function loadUserGroupBackends($interface = null) + { + $backends = array(); + foreach (Config::app('groups') as $backendName => $backendConfig) { + $candidate = UserGroupBackend::create($backendName, $backendConfig); + if (! $interface || $candidate instanceof $interface) { + $backends[] = $candidate; + } + } + + return $backends; + } + + /** + * Return the given user group backend or the first match in order + * + * @param string $name The name of the backend, or null in case the first match should be returned + * @param string $interface The interface the backend should implement, no interface check if null + * + * @return UserGroupBackendInterface + * + * @throws Zend_Controller_Action_Exception In case the given backend name is invalid + */ + protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable') + { + if ($name !== null) { + $config = Config::app('groups'); + if (! $config->hasSection($name)) { + $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name)); + } else { + $backend = UserGroupBackend::create($name, $config->getSection($name)); + if ($interface && !$backend instanceof $interface) { + $interfaceParts = explode('\\', strtolower($interface)); + throw new Zend_Controller_Action_Exception( + sprintf( + $this->translate('User group backend "%s" is not %s'), + $name, + array_pop($interfaceParts) + ), + 400 + ); + } + } + } else { + $backends = $this->loadUserGroupBackends($interface); + $backend = array_shift($backends); + } + + return $backend; + } +} diff --git a/library/Icinga/Web/Controller/BasePreferenceController.php b/library/Icinga/Web/Controller/BasePreferenceController.php new file mode 100644 index 0000000..8f2da8f --- /dev/null +++ b/library/Icinga/Web/Controller/BasePreferenceController.php @@ -0,0 +1,39 @@ +view->tabs = ControllerTabCollector::collectControllerTabs('PreferenceController'); + } +} diff --git a/library/Icinga/Web/Controller/ControllerTabCollector.php b/library/Icinga/Web/Controller/ControllerTabCollector.php new file mode 100644 index 0000000..b452a20 --- /dev/null +++ b/library/Icinga/Web/Controller/ControllerTabCollector.php @@ -0,0 +1,97 @@ + $tab) { + $tabs->add($name, $tab); + } + + foreach ($moduleTabs as $name => $tab) { + // Don't overwrite application tabs if the module wants to + if ($tabs->has($name)) { + continue; + } + $tabs->add($name, $tab); + } + return $tabs; + } + + /** + * Collect module tabs for all modules containing the given controller + * + * @param string $controller The controller name to use for tab collection + * + * @return array An array of Tabs objects or arrays containing Tab descriptions + */ + private static function collectModuleTabs($controller) + { + $moduleManager = Icinga::app()->getModuleManager(); + $modules = $moduleManager->listEnabledModules(); + $tabs = array(); + foreach ($modules as $module) { + $tabs += self::createModuleConfigurationTabs($controller, $moduleManager->getModule($module)); + } + + return $tabs; + } + + /** + * Collects the tabs from the createProvidedTabs() method in the configuration controller + * + * If the module doesn't have the given controller or createProvidedTabs method in the controller an empty array + * will be returned + * + * @param string $controllerName The name of the controller that provides tabs via createProvidedTabs + * @param Module $module The module instance that provides the controller + * + * @return array + */ + private static function createModuleConfigurationTabs($controllerName, Module $module) + { + // TODO(el): Only works for controllers w/o namepsace: https://dev.icinga.com/issues/4149 + $controllerDir = $module->getControllerDir(); + $name = $module->getName(); + + $controllerDir = $controllerDir . '/' . $controllerName . '.php'; + $controllerName = ucfirst($name) . '_' . $controllerName; + + if (is_readable($controllerDir)) { + require_once(realpath($controllerDir)); + if (! method_exists($controllerName, 'createProvidedTabs')) { + return array(); + } + $tab = $controllerName::createProvidedTabs(); + if (! is_array($tab)) { + $tab = array($name => $tab); + } + return $tab; + } + return array(); + } +} diff --git a/library/Icinga/Web/Controller/Dispatcher.php b/library/Icinga/Web/Controller/Dispatcher.php new file mode 100644 index 0000000..e2dfb80 --- /dev/null +++ b/library/Icinga/Web/Controller/Dispatcher.php @@ -0,0 +1,93 @@ +setResponse($response); + $controllerName = $request->getControllerName(); + if (! $controllerName) { + parent::dispatch($request, $response); + return; + } + $controllerName = StringHelper::cname($controllerName, '-') . 'Controller'; + $moduleName = $request->getModuleName(); + if ($moduleName === null || $moduleName === $this->_defaultModule) { + $controllerClass = 'Icinga\\' . self::CONTROLLER_NAMESPACE . '\\' . $controllerName; + } else { + $controllerClass = 'Icinga\\Module\\' . ucfirst($moduleName) . '\\' . self::CONTROLLER_NAMESPACE . '\\' + . $controllerName; + } + if (! class_exists($controllerClass)) { + parent::dispatch($request, $response); + return; + } + $controller = new $controllerClass($request, $response, $this->getParams()); + if (! $controller instanceof Zend_Controller_Action + && ! $controller instanceof Zend_Controller_Action_Interface + ) { + throw new Zend_Controller_Dispatcher_Exception( + 'Controller "' . $controllerClass . '" is not an instance of Zend_Controller_Action_Interface' + ); + } + $action = $this->getActionMethod($request); + $request->setDispatched(true); + // Buffer output by default + $disableOb = $this->getParam('disableOutputBuffering'); + $obLevel = ob_get_level(); + if (empty($disableOb)) { + ob_start(); + } + try { + $controller->dispatch($action); + } catch (Exception $e) { + // Clean output buffer on error + $curObLevel = ob_get_level(); + if ($curObLevel > $obLevel) { + do { + ob_get_clean(); + $curObLevel = ob_get_level(); + } while ($curObLevel > $obLevel); + } + throw $e; + } + if (empty($disableOb)) { + $content = ob_get_clean(); + $response->appendBody($content); + } + } +} diff --git a/library/Icinga/Web/Controller/ModuleActionController.php b/library/Icinga/Web/Controller/ModuleActionController.php new file mode 100644 index 0000000..ad66264 --- /dev/null +++ b/library/Icinga/Web/Controller/ModuleActionController.php @@ -0,0 +1,80 @@ +moduleInit(); + if (($this->Auth()->isAuthenticated() || $this->requiresLogin()) + && $this->getFrontController()->getDefaultModule() !== $this->getModuleName()) { + $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->getModuleName()); + } + } + + /** + * Prepare module action controller initialization + */ + protected function moduleInit() + { + } + + public function Config($file = null) + { + if ($file === null) { + if ($this->config === null) { + $this->config = Config::module($this->getModuleName()); + } + return $this->config; + } else { + if (! array_key_exists($file, $this->configs)) { + $this->configs[$file] = Config::module($this->getModuleName(), $file); + } + return $this->configs[$file]; + } + } + + /** + * Return this controller's module + * + * @return Module + */ + public function Module() + { + if ($this->module === null) { + $this->module = Icinga::app()->getModuleManager()->getModule($this->getModuleName()); + } + + return $this->module; + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Controller\ActionController::postDispatchXhr() For the method documentation. + */ + public function postDispatchXhr() + { + parent::postDispatchXhr(); + $this->getResponse()->setHeader('X-Icinga-Module', $this->getModuleName(), true); + } +} diff --git a/library/Icinga/Web/Controller/StaticController.php b/library/Icinga/Web/Controller/StaticController.php new file mode 100644 index 0000000..f5ce163 --- /dev/null +++ b/library/Icinga/Web/Controller/StaticController.php @@ -0,0 +1,87 @@ +getRequestUri(), strlen($request->getBaseUrl()) + 4), '/'); + + $library = null; + foreach ($app->getLibraries() as $candidate) { + if (substr($assetPath, 0, strlen($candidate->getName())) === $candidate->getName()) { + $library = $candidate; + $assetPath = ltrim(substr($assetPath, strlen($candidate->getName())), '/'); + break; + } + } + + if ($library === null) { + $app->getResponse() + ->setHttpResponseCode(404); + + return; + } + + $assetRoot = $library->getStaticAssetPath(); + if (empty($assetRoot)) { + $app->getResponse() + ->setHttpResponseCode(404); + + return; + } + + $filePath = $assetRoot . DIRECTORY_SEPARATOR . $assetPath; + $dirPath = realpath(dirname($filePath)); // dirname, because the file may be a link + + if ($dirPath === false + || substr($dirPath, 0, strlen($assetRoot)) !== $assetRoot + || ! is_file($filePath) + ) { + $app->getResponse() + ->setHttpResponseCode(404); + + return; + } + + $fileStat = stat($filePath); + $eTag = sprintf( + '%x-%x-%x', + $fileStat['ino'], + $fileStat['size'], + (float) str_pad($fileStat['mtime'], 16, '0') + ); + + $app->getResponse()->setHeader( + 'Cache-Control', + 'public, max-age=1814400, stale-while-revalidate=604800', + true + ); + + if ($request->getServer('HTTP_IF_NONE_MATCH') === $eTag) { + $app->getResponse() + ->setHttpResponseCode(304); + } else { + $app->getResponse() + ->setHeader('ETag', $eTag) + ->setHeader('Content-Type', mime_content_type($filePath), true) + ->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $fileStat['mtime']) . ' GMT') + ->setBody(file_get_contents($filePath)); + } + } +} diff --git a/library/Icinga/Web/Cookie.php b/library/Icinga/Web/Cookie.php new file mode 100644 index 0000000..283f07a --- /dev/null +++ b/library/Icinga/Web/Cookie.php @@ -0,0 +1,299 @@ +name = $name; + $this->value = $value; + } + + /** + * Get the domain of the cookie + * + * @return string + */ + public function getDomain() + { + if ($this->domain === null) { + $this->domain = Config::app()->get('cookie', 'domain'); + } + return $this->domain; + } + + /** + * Set the domain of the cookie + * + * @param string $domain + * + * @return $this + */ + public function setDomain($domain) + { + $this->domain = $domain; + return $this; + } + + /** + * Get the timestamp at which the cookie expires + * + * @return int + */ + public function getExpire() + { + return $this->expire; + } + + /** + * Set the timestamp at which the cookie expires + * + * @param int $expire + * + * @return $this + */ + public function setExpire($expire) + { + $this->expire = $expire; + return $this; + } + + /** + * Get whether to protect the cookie against client side script code attempts to read the cookie + * + * @return bool + */ + public function isHttpOnly() + { + return $this->httpOnly; + } + + /** + * Set whether to protect the cookie against client side script code attempts to read the cookie + * + * @param bool $httpOnly + * + * @return $this + */ + public function setHttpOnly($httpOnly) + { + $this->httpOnly = $httpOnly; + return $this; + } + + /** + * Get the name of the cookie + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the path on the web server where the cookie is available + * + * If the path has not been set either via {@link setPath()} or via config, the base URL will be returned. + * + * @return string + */ + public function getPath() + { + if ($this->path === null) { + $path = Config::app()->get('cookie', 'path'); + if ($path === null) { + // The following call could be used as default for ConfigObject::get(), but we prevent unnecessary + // function calls here, if the path is set in the config + $path = Icinga::app()->getRequest()->getBaseUrl() . '/'; // Zend has rtrim($baseUrl, '/') + } + $this->path = $path; + } + return $this->path; + } + + /** + * Set the path on the web server where the cookie is available + * + * @param string $path + * + * @return $this + */ + public function setPath($path) + { + $this->path = $path; + return $this; + } + + /** + * Get whether to send the cookie only over a secure connection + * + * If the secure flag has not been set either via {@link setSecure()} or via config and if the current request was + * sent over a secure connection, true will be returned. + * + * @return bool + */ + public function isSecure() + { + if ($this->secure === null) { + $secure = Config::app()->get('cookie', 'secure'); + if ($secure === null) { + // The following call could be used as default for ConfigObject::get(), but we prevent unnecessary + // function calls here, if the secure flag is set in the config + $secure = Icinga::app()->getRequest()->isSecure(); + } + $this->secure = $secure; + } + return $this->secure; + } + + /** + * Set whether to send the cookie only over a secure connection + * + * @param bool $secure + * + * @return $this + */ + public function setSecure($secure) + { + $this->secure = $secure; + return $this; + } + + /** + * Get the value of the cookie + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Set the value of the cookie + * + * @param string $value + * + * @return $this + */ + public function setValue($value) + { + $this->value = $value; + return $this; + } + + /** + * Create invalidation cookie + * + * This method clones the current cookie and sets its value to null and expire time to 1. + * That way, the cookie removes itself when it has been sent to and processed by the client. + * + * We're cloning the current cookie in order to meet the [RFC6265 spec](https://tools.ietf.org/search/rfc6265) + * regarding the `Path` and `Domain` attribute: + * + * > Finally, to remove a cookie, the server returns a Set-Cookie header with an expiration date in the past. + * > The server will be successful in removing the cookie only if the Path and the Domain attribute in the + * > Set-Cookie header match the values used when the cookie was created. + * + * Note that the cookie has to be sent to the client. + * + * # Example Usage + * + * ```php + * $response->setCookie( + * $cookie->forgetMe() + * ); + * ``` + * + * @return static + */ + public function forgetMe() + { + $forgetMe = clone $this; + + return $forgetMe + ->setValue(null) + ->setExpire(1); + } +} diff --git a/library/Icinga/Web/CookieSet.php b/library/Icinga/Web/CookieSet.php new file mode 100644 index 0000000..019be29 --- /dev/null +++ b/library/Icinga/Web/CookieSet.php @@ -0,0 +1,58 @@ +cookies); + } + + /** + * Add a cookie to the set + * + * If a cookie with the same name already exists, the cookie will be overridden. + * + * @param Cookie $cookie The cookie to add + * + * @return $this + */ + public function add(Cookie $cookie) + { + $this->cookies[$cookie->getName()] = $cookie; + return $this; + } + + /** + * Get the cookie with the given name from the set + * + * @param string $name The name of the cookie + * + * @return Cookie|null The cookie with the given name or null if the cookie does not exist + */ + public function get($name) + { + return isset($this->cookies[$name]) ? $this->cookies[$name] : null; + } +} diff --git a/library/Icinga/Web/Dom/DomNodeIterator.php b/library/Icinga/Web/Dom/DomNodeIterator.php new file mode 100644 index 0000000..1ea20b8 --- /dev/null +++ b/library/Icinga/Web/Dom/DomNodeIterator.php @@ -0,0 +1,84 @@ + + * loadHTML(...); + * $dom = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST); + * foreach ($dom as $node) { + * .... + * } + * + */ +class DomNodeIterator implements RecursiveIterator +{ + /** + * The node's children + * + * @var IteratorIterator + */ + protected $children; + + /** + * Create a new iterator over a DOMNode's children + * + * @param DOMNode $node + */ + public function __construct(DOMNode $node) + { + $this->children = new IteratorIterator($node->childNodes); + } + + public function current(): ?DOMNode + { + return $this->children->current(); + } + + public function key(): int + { + return $this->children->key(); + } + + public function next(): void + { + $this->children->next(); + } + + public function rewind(): void + { + $this->children->rewind(); + } + + public function valid(): bool + { + return $this->children->valid(); + } + + public function hasChildren(): bool + { + return $this->current()->hasChildNodes(); + } + + public function getChildren(): DomNodeIterator + { + return new static($this->current()); + } +} diff --git a/library/Icinga/Web/FileCache.php b/library/Icinga/Web/FileCache.php new file mode 100644 index 0000000..03f0c19 --- /dev/null +++ b/library/Icinga/Web/FileCache.php @@ -0,0 +1,293 @@ +name = $name; + $tmpDir = sys_get_temp_dir(); + $runtimePath = $tmpDir . '/FileCache_' . $name; + if (is_dir($runtimePath)) { + // Don't combine the following if with the above because else the elseif path will be evaluated if the + // runtime path exists and is not writeable + if (is_writeable($runtimePath)) { + $this->basedir = $runtimePath; + $this->enabled = true; + } + } elseif (is_dir($tmpDir) && is_writeable($tmpDir) && @mkdir($runtimePath, octdec('1750'), true)) { + // Suppress mkdir errors because it may error w/ no such file directory if the systemd private tmp directory + // for the web server has been removed + $this->basedir = $runtimePath; + $this->enabled = true; + } + } + + /** + * Store the given content to the desired file name + * + * @param string $file new (relative) filename + * @param string $content the content to be stored + * + * @return bool whether the file has been stored + */ + public function store($file, $content) + { + if (! $this->enabled) { + return false; + } + + return file_put_contents($this->filename($file), $content); + } + + /** + * Find out whether a given file exists + * + * @param string $file the (relative) filename + * @param int $newerThan optional timestamp to compare against + * + * @return bool whether such file exists + */ + public function has($file, $newerThan = null) + { + if (! $this->enabled) { + return false; + } + + $filename = $this->filename($file); + + if (! file_exists($filename) || ! is_readable($filename)) { + return false; + } + + if ($newerThan === null) { + return true; + } + + $info = stat($filename); + + if ($info === false) { + return false; + } + + return (int) $newerThan < $info['mtime']; + } + + /** + * Get a specific file or false if no such file available + * + * @param string $file the disired file name + * + * @return string|bool Filename content or false + */ + public function get($file) + { + if ($this->has($file)) { + return file_get_contents($this->filename($file)); + } + + return false; + } + + /** + * Send a specific file to the browser (output) + * + * @param string $file the disired file name + * + * @return bool Whether the file has been sent + */ + public function send($file) + { + if ($this->has($file)) { + readfile($this->filename($file)); + + return true; + } + + return false; + } + + /** + * Get absolute filename for a given file + * + * @param string $file the disired file name + * + * @return string absolute filename + */ + protected function filename($file) + { + return $this->basedir . '/' . $file; + } + + /** + * Prepare a sub directory with the given name and return its path + * + * @param string $name + * + * @return string|false Returns FALSE in case the cache is not enabled or an error occurred + */ + public function directory($name) + { + if (! $this->enabled) { + return false; + } + + $path = $this->filename($name); + if (! is_dir($path) && ! @mkdir($path, octdec('1750'), true)) { + return false; + } + + return $path; + } + + /** + * Whether the given ETag matches a cached file + * + * If no ETag is given we'll try to fetch the one from the current + * HTTP request. + * + * @param string $file The cached file you want to check + * @param string $match The ETag to match against + * + * @return string|bool ETag on match, otherwise false + */ + public function etagMatchesCachedFile($file, $match = null) + { + return self::etagMatchesFiles($this->filename($file), $match); + } + + /** + * Create an ETag for the given file + * + * @param string $file The desired cache file + * + * @return string your ETag + */ + public function etagForCachedFile($file) + { + return self::etagForFiles($this->filename($file)); + } + + /** + * Whether the given ETag matchesspecific file(s) on disk + * + * @param string|array $files file(s) to check + * @param string $match ETag to match against + * + * @return string|bool ETag on match, otherwise false + */ + public static function etagMatchesFiles($files, $match = null) + { + if ($match === null) { + $match = isset($_SERVER['HTTP_IF_NONE_MATCH']) + ? trim($_SERVER['HTTP_IF_NONE_MATCH'], '"') + : false; + } + if (! $match) { + return false; + } + + if (preg_match('/([0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8})-\w+/i', $match, $matches)) { + // Removes compression suffixes as our custom algorithm can't handle compressed cache files anyway + $match = $matches[1]; + } + + $etag = self::etagForFiles($files); + return $match === $etag ? $etag : false; + } + + /** + * Create ETag for the given files + * + * Custom algorithm creating an ETag based on filenames, mtimes + * and file sizes. Supports single files or a list of files. This + * way we are able to create ETags for virtual files depending on + * multiple source files (e.g. compressed JS, CSS). + * + * @param string|array $files Single file or a list of such + * + * @return string The generated ETag + */ + public static function etagForFiles($files) + { + if (is_string($files)) { + $files = array($files); + } + + $sizes = array(); + $mtimes = array(); + + foreach ($files as $file) { + $file = realpath($file); + if ($file !== false && $info = stat($file)) { + $mtimes[] = $info['mtime']; + $sizes[] = $info['size']; + } else { + $mtimes[] = time(); + $sizes[] = 0; + } + } + + return sprintf( + '%s-%s-%s', + hash('crc32', implode('|', $files)), + hash('crc32', implode('|', $sizes)), + hash('crc32', implode('|', $mtimes)) + ); + } + + /** + * Factory creating your cache instance + * + * @param string $name Instance name + * + * @return FileCache + */ + public static function instance($name = 'icingaweb') + { + if ($name !== 'icingaweb') { + $name = 'icingaweb/modules/' . $name; + } + + if (!array_key_exists($name, self::$instances)) { + self::$instances[$name] = new static($name); + } + + return self::$instances[$name]; + } +} diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php new file mode 100644 index 0000000..b421849 --- /dev/null +++ b/library/Icinga/Web/Form.php @@ -0,0 +1,1666 @@ +'span', 'separator' => '', 'class' => 'control-label')), + array(array('labelWrap' => 'HtmlTag'), array('tag' => 'div', 'class' => 'control-label-group')), + array('ViewHelper', array('separator' => '')), + array('Help', array()), + array('Errors', array('separator' => '')), + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group')) + ); + + /** + * (non-PHPDoc) + * @see \Zend_Form::construct() For the method documentation. + */ + public function __construct($options = null) + { + // Zend's plugin loader reverses the order of added prefix paths thus trying our paths first before trying + // Zend paths + $this->addPrefixPaths(array( + array( + 'prefix' => 'Icinga\\Web\\Form\\Element\\', + 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Element'), + 'type' => static::ELEMENT + ), + array( + 'prefix' => 'Icinga\\Web\\Form\\Decorator\\', + 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Decorator'), + 'type' => static::DECORATOR + ) + )); + + if (! isset($options['attribs']['class'])) { + $options['attribs']['class'] = static::DEFAULT_CLASSES; + } + + parent::__construct($options); + } + + /** + * Set this form's parent + * + * @param Form $form + * + * @return $this + */ + public function setParent(Form $form) + { + $this->_parent = $form; + return $this; + } + + /** + * Return this form's parent + * + * @return Form + */ + public function getParent() + { + return $this->_parent; + } + + /** + * Set a callback that is called instead of this form's onSuccess method + * + * It is called using the following signature: (Form $this). + * + * @param callable $onSuccess Callback + * + * @return $this + * + * @throws ProgrammingError If the callback is not callable + */ + public function setOnSuccess($onSuccess) + { + if (! is_callable($onSuccess)) { + throw new ProgrammingError('The option `onSuccess\' is not callable'); + } + $this->onSuccess = $onSuccess; + return $this; + } + + /** + * Set the label to use for the standard submit button + * + * @param string $label The label to use for the submit button + * + * @return $this + */ + public function setSubmitLabel($label) + { + $this->submitLabel = $label; + return $this; + } + + /** + * Return the label being used for the standard submit button + * + * @return string + */ + public function getSubmitLabel() + { + return $this->submitLabel; + } + + /** + * Set the label to use for showing the user an activity indicator when submitting the form + * + * @param string $label + * + * @return $this + */ + public function setProgressLabel($label) + { + $this->progressLabel = $label; + return $this; + } + + /** + * Return the label to use for showing the user an activity indicator when submitting the form + * + * @return string + */ + public function getProgressLabel() + { + return $this->progressLabel; + } + + /** + * Set the url to redirect to upon success + * + * @param string|Url $url The url to redirect to + * + * @return $this + * + * @throws ProgrammingError In case $url is neither a string nor a instance of Icinga\Web\Url + */ + public function setRedirectUrl($url) + { + if (is_string($url)) { + $url = Url::fromPath($url, array(), $this->getRequest()); + } elseif (! $url instanceof Url) { + throw new ProgrammingError('$url must be a string or instance of Icinga\Web\Url'); + } + + $this->redirectUrl = $url; + return $this; + } + + /** + * Return the url to redirect to upon success + * + * @return Url + */ + public function getRedirectUrl() + { + if ($this->redirectUrl === null) { + $this->redirectUrl = $this->getRequest()->getUrl(); + if ($this->getMethod() === 'get') { + // Be sure to remove all form dependent params because we do not want to submit it again + $this->redirectUrl = $this->redirectUrl->without(array_keys($this->getElements())); + } + } + + return $this->redirectUrl; + } + + /** + * Set the view script to use when rendering this form + * + * @param string $viewScript The view script to use + * + * @return $this + */ + public function setViewScript($viewScript) + { + $this->viewScript = $viewScript; + return $this; + } + + /** + * Return the view script being used when rendering this form + * + * @return string + */ + public function getViewScript() + { + return $this->viewScript; + } + + /** + * Disable CSRF counter measure and remove its field if already added + * + * @param bool $disabled Set true in order to disable CSRF protection for this form, otherwise false + * + * @return $this + */ + public function setTokenDisabled($disabled = true) + { + $this->tokenDisabled = (bool) $disabled; + + if ($disabled && $this->getElement($this->tokenElementName) !== null) { + $this->removeElement($this->tokenElementName); + } + + return $this; + } + + /** + * Return whether CSRF counter measures are disabled for this form + * + * @return bool + */ + public function getTokenDisabled() + { + return $this->tokenDisabled; + } + + /** + * Set the name to use for the CSRF element + * + * @param string $name The name to set + * + * @return $this + */ + public function setTokenElementName($name) + { + $this->tokenElementName = $name; + return $this; + } + + /** + * Return the name of the CSRF element + * + * @return string + */ + public function getTokenElementName() + { + return $this->tokenElementName; + } + + /** + * Disable form identification and remove its field if already added + * + * @param bool $disabled Set true in order to disable identification for this form, otherwise false + * + * @return $this + */ + public function setUidDisabled($disabled = true) + { + $this->uidDisabled = (bool) $disabled; + + if ($disabled && $this->getElement($this->uidElementName) !== null) { + $this->removeElement($this->uidElementName); + } + + return $this; + } + + /** + * Return whether identification is disabled for this form + * + * @return bool + */ + public function getUidDisabled() + { + return $this->uidDisabled; + } + + /** + * Set the name to use for the form identification element + * + * @param string $name The name to set + * + * @return $this + */ + public function setUidElementName($name) + { + $this->uidElementName = $name; + return $this; + } + + /** + * Return the name of the form identification element + * + * @return string + */ + public function getUidElementName() + { + return $this->uidElementName; + } + + /** + * Set whether this form should validate the sent data when being automatically submitted + * + * @param bool $state + * + * @return $this + */ + public function setValidatePartial($state) + { + $this->validatePartial = $state; + return $this; + } + + /** + * Return whether this form should validate the sent data when being automatically submitted + * + * @return bool + */ + public function getValidatePartial() + { + return $this->validatePartial; + } + + /** + * Set whether each element's id should be altered to avoid duplicates + * + * @param bool $value + * + * @return Form + */ + public function setProtectIds($value = true) + { + $this->protectIds = (bool) $value; + return $this; + } + + /** + * Return whether each element's id is being altered to avoid duplicates + * + * @return bool + */ + public function getProtectIds() + { + return $this->protectIds; + } + + /** + * Set the cue to append to each element's label if it's required + * + * @param string $cue + * + * @return Form + */ + public function setRequiredCue($cue) + { + $this->requiredCue = $cue; + return $this; + } + + /** + * Return the cue being appended to each element's label if it's required + * + * @return string + */ + public function getRequiredCue() + { + return $this->requiredCue; + } + + /** + * Set the descriptions for this form + * + * @param array $descriptions + * + * @return Form + */ + public function setDescriptions(array $descriptions) + { + $this->descriptions = $descriptions; + return $this; + } + + /** + * Add a description for this form + * + * If $description is an array the second value should be + * an array as well containing additional HTML properties. + * + * @param string|array $description + * + * @return Form + */ + public function addDescription($description) + { + $this->descriptions[] = $description; + return $this; + } + + /** + * Return the descriptions of this form + * + * @return array + */ + public function getDescriptions() + { + if ($this->descriptions === null) { + return array(); + } + + return $this->descriptions; + } + + /** + * Set the notifications for this form + * + * @param array $notifications + * + * @return $this + */ + public function setNotifications(array $notifications) + { + $this->notifications = $notifications; + return $this; + } + + /** + * Add a notification for this form + * + * If $notification is an array the second value should be + * an array as well containing additional HTML properties. + * + * @param string|array $notification + * @param int $type + * + * @return $this + */ + public function addNotification($notification, $type) + { + $this->notifications[$type][] = $notification; + return $this; + } + + /** + * Return the notifications of this form + * + * @return array + */ + public function getNotifications() + { + if ($this->notifications === null) { + return array(); + } + + return $this->notifications; + } + + /** + * Set the hints for this form + * + * @param array $hints + * + * @return $this + */ + public function setHints(array $hints) + { + $this->hints = $hints; + return $this; + } + + /** + * Add a hint for this form + * + * If $hint is an array the second value should be an + * array as well containing additional HTML properties. + * + * @param string|array $hint + * + * @return $this + */ + public function addHint($hint) + { + $this->hints[] = $hint; + return $this; + } + + /** + * Return the hints of this form + * + * @return array + */ + public function getHints() + { + if ($this->hints === null) { + return array(); + } + + return $this->hints; + } + + /** + * Set whether the Autosubmit decorator should be applied to this form + * + * If true, the Autosubmit decorator is being applied to this form instead of to each of its elements. + * + * @param bool $state + * + * @return Form + */ + public function setUseFormAutosubmit($state = true) + { + $this->useFormAutosubmit = (bool) $state; + if ($this->useFormAutosubmit) { + $this->setAttrib('data-progress-element', 'header-' . $this->getId()); + } else { + $this->removeAttrib('data-progress-element'); + } + + return $this; + } + + /** + * Return whether the Autosubmit decorator is being applied to this form + * + * @return bool + */ + public function getUseFormAutosubmit() + { + return $this->useFormAutosubmit; + } + + /** + * Get whether the form is an API target + * + * @todo This should probably only return true if the request is also an api request + * @return bool + */ + public function getIsApiTarget() + { + return $this->isApiTarget; + } + + /** + * Set whether the form is an API target + * + * @param bool $isApiTarget + * + * @return $this + */ + public function setIsApiTarget($isApiTarget = true) + { + $this->isApiTarget = (bool) $isApiTarget; + return $this; + } + + /** + * Create this form + * + * @param array $formData The data sent by the user + * + * @return $this + */ + public function create(array $formData = array()) + { + if (! $this->created) { + $this->createElements($formData); + $this->addFormIdentification() + ->addCsrfCounterMeasure() + ->addSubmitButton(); + + // Use Form::getAttrib() instead of Form::getAction() here because we want to explicitly check against + // null. Form::getAction() would return the empty string '' if the action is not set. + // For not setting the action attribute use Form::setAction(''). This is required for for the + // accessibility's enable/disable auto-refresh mechanic + if ($this->getAttrib('action') === null) { + $action = $this->getRequest()->getUrl(); + if ($this->getMethod() === 'get') { + $action = $action->without(array_keys($this->getElements())); + } + + // TODO(el): Re-evalute this necessity. + // JavaScript could use the container'sURL if there's no action set. + // We MUST set an action as JS gets confused otherwise, if + // this form is being displayed in an additional column + $this->setAction($action); + } + + $this->created = true; + } + + return $this; + } + + /** + * Create and add elements to this form + * + * Intended to be implemented by concrete form classes. + * + * @param array $formData The data sent by the user + */ + public function createElements(array $formData) + { + } + + /** + * Perform actions after this form was submitted using a valid request + * + * Intended to be implemented by concrete form classes. The base implementation returns always FALSE. + * + * @return null|bool Return FALSE in case no redirect should take place + */ + public function onSuccess() + { + return false; + } + + /** + * Perform actions when no form dependent data was sent + * + * Intended to be implemented by concrete form classes. + */ + public function onRequest() + { + } + + /** + * Add a submit button to this form + * + * Uses the label previously set with Form::setSubmitLabel(). Overwrite this + * method in order to add multiple submit buttons or one with a custom name. + * + * @return $this + */ + public function addSubmitButton() + { + $submitLabel = $this->getSubmitLabel(); + if ($submitLabel) { + $this->addElement( + 'submit', + 'btn_submit', + array( + 'class' => 'btn-primary', + 'ignore' => true, + 'label' => $submitLabel, + 'data-progress-label' => $this->getProgressLabel(), + 'decorators' => array( + 'ViewHelper', + array('Spinner', array('separator' => '')), + array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls')) + ) + ) + ); + } + + return $this; + } + + /** + * Add a subform + * + * @param Zend_Form $form The subform to add + * @param string $name The name of the subform or null to use the name of $form + * @param int $order The location where to insert the form + * + * @return Zend_Form + */ + public function addSubForm(Zend_Form $form, $name = null, $order = null) + { + if ($form instanceof self) { + $form->setDecorators(array('FormElements')); // TODO: Makes it difficult to customise subform decorators.. + $form->setSubmitLabel(''); + $form->setTokenDisabled(); + $form->setUidDisabled(); + $form->setParent($this); + } + + if ($name === null) { + $name = $form->getName(); + } + + return parent::addSubForm($form, $name, $order); + } + + /** + * Create a new element + * + * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set the + * `disableLoadDefaultDecorators' option to any other value than `true'. For loading custom element decorators use + * the 'decorators' option. + * + * @param string $type The type of the element + * @param string $name The name of the element + * @param mixed $options The options for the element + * + * @return Zend_Form_Element + * + * @see Form::$defaultElementDecorators For Icinga Web 2's default element decorators. + */ + public function createElement($type, $name, $options = null) + { + if ($options !== null) { + if ($options instanceof Zend_Config) { + $options = $options->toArray(); + } + if (! isset($options['decorators']) + && ! array_key_exists('disabledLoadDefaultDecorators', $options) + ) { + $options['decorators'] = static::$defaultElementDecorators; + if (! isset($options['data-progress-label']) && ($type === 'submit' + || ($type === 'button' && isset($options['type']) && $options['type'] === 'submit')) + ) { + array_splice($options['decorators'], 1, 0, array(array('Spinner', array('separator' => '')))); + } elseif ($type === 'hidden') { + $options['decorators'] = array('ViewHelper'); + } + } + } else { + $options = array('decorators' => static::$defaultElementDecorators); + if ($type === 'submit') { + array_splice($options['decorators'], 1, 0, array(array('Spinner', array('separator' => '')))); + } elseif ($type === 'hidden') { + $options['decorators'] = array('ViewHelper'); + } + } + + $el = parent::createElement($type, $name, $options); + $el->setTranslator(new ErrorLabeller(array('element' => $el))); + + $el->addPrefixPaths(array( + array( + 'prefix' => 'Icinga\\Web\\Form\\Validator\\', + 'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Validator'), + 'type' => $el::VALIDATE + ) + )); + + if ($this->protectIds) { + $el->setAttrib('id', $this->getRequest()->protectId($this->getId(false) . '_' . $el->getId())); + } + + if ($el->getAttrib('autosubmit')) { + if ($this->getUseFormAutosubmit()) { + $warningId = 'autosubmit_warning_' . $el->getId(); + $warningText = $this->getView()->escape($this->translate( + 'This page will be automatically updated upon change of the value' + )); + $autosubmitDecorator = $this->_getDecorator('Callback', array( + 'placement' => 'PREPEND', + 'callback' => function ($content) use ($warningId, $warningText) { + return '' . $warningText . ''; + } + )); + } else { + $autosubmitDecorator = new Autosubmit(); + $autosubmitDecorator->setAccessible(); + $warningId = $autosubmitDecorator->getWarningId($el); + } + + $decorators = $el->getDecorators(); + $pos = array_search('Zend_Form_Decorator_ViewHelper', array_keys($decorators), true) + 1; + $el->setDecorators( + array_slice($decorators, 0, $pos, true) + + array('autosubmit' => $autosubmitDecorator) + + array_slice($decorators, $pos, count($decorators) - $pos, true) + ); + + if (($describedBy = $el->getAttrib('aria-describedby')) !== null) { + $el->setAttrib('aria-describedby', $describedBy . ' ' . $warningId); + } else { + $el->setAttrib('aria-describedby', $warningId); + } + + $class = $el->getAttrib('class'); + if (is_array($class)) { + $class[] = 'autosubmit'; + } elseif ($class === null) { + $class = 'autosubmit'; + } else { + $class .= ' autosubmit'; + } + $el->setAttrib('class', $class); + + unset($el->autosubmit); + } + + if ($el->getAttrib('preserveDefault')) { + $el->addDecorator( + array('preserveDefault' => 'HtmlTag'), + array( + 'tag' => 'input', + 'type' => 'hidden', + 'name' => $name . static::DEFAULT_SUFFIX, + 'value' => $el instanceof DateTimePicker + ? $el->getValue()->format($el->getFormat()) + : $el->getValue() + ) + ); + + unset($el->preserveDefault); + } + + return $this->ensureElementAccessibility($el); + } + + /** + * Add accessibility related attributes + * + * @param Zend_Form_Element $element + * + * @return Zend_Form_Element + */ + public function ensureElementAccessibility(Zend_Form_Element $element) + { + if ($element->isRequired()) { + $element->setAttrib('aria-required', 'true'); // ARIA + $element->setAttrib('required', ''); // HTML5 + if (($cue = $this->getRequiredCue()) !== null && ($label = $element->getDecorator('label')) !== false) { + $element->setLabel($this->getView()->escape($element->getLabel())); + $label->setOption('escape', false); + $label->setRequiredSuffix(sprintf(' ', $cue)); + } + } + + if ($element->getDescription() !== null && ($help = $element->getDecorator('help')) !== false) { + if (($describedBy = $element->getAttrib('aria-describedby')) !== null) { + // Assume that it's because of the element being of type autosubmit or + // that one who did set the property manually removes the help decorator + // in case it has already an aria-describedby property set + $element->setAttrib( + 'aria-describedby', + $help->setAccessible()->getDescriptionId($element) . ' ' . $describedBy + ); + } else { + $element->setAttrib('aria-describedby', $help->setAccessible()->getDescriptionId($element)); + } + } + + return $element; + } + + /** + * Add a field with a unique and form specific ID + * + * @return $this + */ + public function addFormIdentification() + { + if (! $this->uidDisabled && $this->getElement($this->uidElementName) === null) { + $this->addElement( + 'hidden', + $this->uidElementName, + array( + 'ignore' => true, + 'value' => $this->getName(), + 'decorators' => array('ViewHelper') + ) + ); + } + + return $this; + } + + /** + * Add CSRF counter measure field to this form + * + * @return $this + */ + public function addCsrfCounterMeasure() + { + if (! $this->tokenDisabled) { + $request = $this->getRequest(); + if (! $request->isXmlHttpRequest() + && ($this->getIsApiTarget() || $request->isApiRequest()) + ) { + return $this; + } + if ($this->getElement($this->tokenElementName) === null) { + $this->addElement('CsrfCounterMeasure', $this->tokenElementName); + } + } + return $this; + } + + /** + * {@inheritdoc} + * + * Creates the form if not created yet. + * + * @param array $values + * + * @return $this + */ + public function setDefaults(array $values) + { + $this->create($values); + return parent::setDefaults($values); + } + + /** + * Populate the elements with the given values + * + * @param array $defaults The values to populate the elements with + * + * @return $this + */ + public function populate(array $defaults) + { + $this->create($defaults); + $this->preserveDefaults($this, $defaults); + return parent::populate($defaults); + } + + /** + * Recurse the given form and unset all unchanged default values + * + * @param Zend_Form $form + * @param array $defaults + */ + protected function preserveDefaults(Zend_Form $form, array &$defaults) + { + foreach ($form->getElements() as $name => $element) { + if ((array_key_exists($name, $defaults) + && array_key_exists($name . static::DEFAULT_SUFFIX, $defaults) + && $defaults[$name] === $defaults[$name . static::DEFAULT_SUFFIX]) + || $element->getAttrib('disabled') + ) { + unset($defaults[$name]); + } + } + + foreach ($form->getSubForms() as $_ => $subForm) { + $this->preserveDefaults($subForm, $defaults); + } + } + + /** + * Process the given request using this form + * + * Redirects to the url set with setRedirectUrl() upon success. See onSuccess() + * and onRequest() wherewith you can customize the processing logic. + * + * @param Request $request The request to be processed + * + * @return Request The request supposed to be processed + */ + public function handleRequest(Request $request = null) + { + if ($request === null) { + $request = $this->getRequest(); + } else { + $this->request = $request; + } + + $formData = $this->getRequestData(); + if ($this->getIsApiTarget() + // TODO: Very very bad, wasSent() must not be bypassed if it's only an api request but not an qpi target + || $this->getRequest()->isApiRequest() + || $this->getUidDisabled() + || $this->wasSent($formData) + ) { + $this->populate($formData); // Necessary to get isSubmitted() to work + if (! $this->getSubmitLabel() || $this->isSubmitted()) { + if ($this->isValid($formData) + && (($this->onSuccess !== null && false !== call_user_func($this->onSuccess, $this)) + || ($this->onSuccess === null && false !== $this->onSuccess())) + ) { + // TODO: Still bad. An api target must not behave as one if it's not an api request + if ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) { + // API targets and API requests will never redirect but immediately respond w/ JSON-encoded + // notifications + $notifications = Notification::getInstance()->popMessages(); + $message = null; + foreach ($notifications as $notification) { + if ($notification->type === Notification::SUCCESS) { + $message = $notification->message; + break; + } + } + $this->getResponse()->json() + ->setSuccessData($message !== null ? array('message' => $message) : null) + ->sendResponse(); + } else { + $this->getResponse()->redirectAndExit($this->getRedirectUrl()); + } + // TODO: Still bad. An api target must not behave as one if it's not an api request + } elseif ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) { + $this->getResponse()->json()->setFailData($this->getMessages())->sendResponse(); + } + } elseif ($this->getValidatePartial()) { + // The form can't be processed but we may want to show validation errors though + $this->isValidPartial($formData); + } + } else { + $this->onRequest(); + } + + return $request; + } + + /** + * Return whether the submit button of this form was pressed + * + * When overwriting Form::addSubmitButton() be sure to overwrite this method as well. + * + * @return bool True in case it was pressed, False otherwise or no submit label was set + */ + public function isSubmitted() + { + $requestMethod = $this->getRequest()->getMethod(); + if (strtolower($requestMethod ?: '') !== $this->getMethod()) { + return false; + } + if ($this->getIsApiTarget() || $this->getRequest()->isApiRequest()) { + return true; + } + if ($this->getSubmitLabel()) { + return $this->getElement('btn_submit')->isChecked(); + } + + return false; + } + + /** + * Return whether the data sent by the user refers to this form + * + * Ensures that the correct form gets processed in case there are multiple forms + * with equal submit button names being posted against the same route. + * + * @param array $formData The data sent by the user + * + * @return bool Whether the given data refers to this form + */ + public function wasSent(array $formData) + { + return isset($formData[$this->uidElementName]) && $formData[$this->uidElementName] === $this->getName(); + } + + /** + * Return whether the given values (possibly incomplete) are valid + * + * Unlike Zend_Form::isValid() this will not set NULL as value for + * an element that is not present in the given data. + * + * @param array $formData The data to validate + * + * @return bool + */ + public function isValidPartial(array $formData) + { + $this->create($formData); + + foreach ($this->getElements() as $name => $element) { + if (array_key_exists($name, $formData)) { + if ($element->getAttrib('disabled')) { + // Ensure that disabled elements are not overwritten + // (http://www.zendframework.com/issues/browse/ZF-6909) + $formData[$name] = $element->getValue(); + } elseif (array_key_exists($name . static::DEFAULT_SUFFIX, $formData) + && $formData[$name] === $formData[$name . static::DEFAULT_SUFFIX] + ) { + unset($formData[$name]); + } + } + } + + return parent::isValidPartial($formData); + } + + /** + * Return whether the given values are valid + * + * @param array $formData The data to validate + * + * @return bool + */ + public function isValid($formData) + { + $this->create($formData); + + // Ensure that disabled elements are not overwritten (http://www.zendframework.com/issues/browse/ZF-6909) + foreach ($this->getElements() as $name => $element) { + if ($element->getAttrib('disabled')) { + $formData[$name] = $element->getValue(); + } + } + + return parent::isValid($formData); + } + + /** + * Remove all elements of this form + * + * @return self + */ + public function clearElements() + { + $this->created = false; + return parent::clearElements(); + } + + /** + * Load the default decorators + * + * Overwrites Zend_Form::loadDefaultDecorators to avoid having + * the HtmlTag-Decorator added and to provide view script usage + * + * @return $this + */ + public function loadDefaultDecorators() + { + if ($this->loadDefaultDecoratorsIsDisabled()) { + return $this; + } + + $decorators = $this->getDecorators(); + if (empty($decorators)) { + if ($this->viewScript) { + $this->addDecorator('ViewScript', array( + 'viewScript' => $this->viewScript, + 'form' => $this + )); + } else { + $this->addDecorator('Description', array('tag' => 'h1')); + if ($this->getUseFormAutosubmit()) { + $this->getDecorator('Description')->setEscape(false); + $this->addDecorator( + 'HtmlTag', + array( + 'tag' => 'div', + 'class' => 'header', + 'id' => 'header-' . $this->getId() + ) + ); + } + + $this->addDecorator('FormDescriptions') + ->addDecorator('FormNotifications') + ->addDecorator('FormErrors', array('onlyCustomFormErrors' => true)) + ->addDecorator('FormElements') + ->addDecorator('FormHints') + //->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form')) + ->addDecorator('Form'); + } + } + + return $this; + } + + /** + * Get element id + * + * Returns the protected id, in case id protection is enabled. + * + * @param bool $protect + * + * @return string + */ + public function getId($protect = true) + { + $id = parent::getId(); + return $protect && $this->protectIds ? $this->getRequest()->protectId($id) : $id; + } + + /** + * Return the name of this form + * + * @return string + */ + public function getName() + { + $name = parent::getName(); + if (! $name) { + $name = get_class($this); + $this->setName($name); + $name = parent::getName(); + } + return $name; + } + + /** + * Retrieve form description + * + * This will return the escaped description with the autosubmit warning icon if form autosubmit is enabled. + * + * @return string + */ + public function getDescription() + { + $description = parent::getDescription(); + if ($description && $this->getUseFormAutosubmit()) { + $autosubmit = $this->_getDecorator('Autosubmit', array('accessible' => true)); + $autosubmit->setElement($this); + $description = $autosubmit->render($this->getView()->escape($description)); + } + + return $description; + } + + /** + * Set the action to submit this form against + * + * Note that if you'll pass a instance of URL, Url::getAbsoluteUrl('&') is called to set the action. + * + * @param Url|string $action + * + * @return $this + */ + public function setAction($action) + { + if ($action instanceof Url) { + $action = $action->getAbsoluteUrl('&'); + } + + return parent::setAction($action); + } + + /** + * Set form description + * + * Alias for Zend_Form::setDescription(). + * + * @param string $value + * + * @return Form + */ + public function setTitle($value) + { + return $this->setDescription($value); + } + + /** + * Return the request associated with this form + * + * Returns the global request if none has been set for this form yet. + * + * @return Request + */ + public function getRequest() + { + if ($this->request === null) { + $this->request = Icinga::app()->getRequest(); + } + + return $this->request; + } + + /** + * Set the request + * + * @param Request $request + * + * @return $this + */ + public function setRequest(Request $request) + { + $this->request = $request; + return $this; + } + + /** + * Return the current Response + * + * @return Response + */ + public function getResponse() + { + return Icinga::app()->getFrontController()->getResponse(); + } + + /** + * Return the request data based on this form's request method + * + * @return array + */ + protected function getRequestData() + { + $requestMethod = $this->getRequest()->getMethod(); + if (strtolower($requestMethod ?: '') === $this->getMethod()) { + return $this->request->{'get' . ($this->request->isPost() ? 'Post' : 'Query')}(); + } + + return array(); + } + + /** + * Get the translation domain for this form + * + * The returned translation domain is either determined based on this form's qualified name or it is the default + * 'icinga' domain + * + * @return string + */ + protected function getTranslationDomain() + { + $parts = explode('\\', get_called_class()); + if (count($parts) > 1 && $parts[1] === 'Module') { + // Assume format Icinga\Module\ModuleName\Forms\... + return strtolower($parts[2]); + } + + return 'icinga'; + } + + /** + * Translate a string + * + * @param string $text The string to translate + * @param string|null $context Optional parameter for context based translation + * + * @return string The translated string + */ + protected function translate($text, $context = null) + { + $this->translationDomain = $this->getTranslationDomain(); + + return $this->i18nTranslate($text, $context); + } + + /** + * Translate a plural string + * + * @param string $textSingular The string in singular form to translate + * @param string $textPlural The string in plural form to translate + * @param integer $number The amount to determine from whether to return singular or plural + * @param string|null $context Optional parameter for context based translation + * + * @return string The translated string + */ + protected function translatePlural($textSingular, $textPlural, $number, $context = null) + { + $this->translationDomain = $this->getTranslationDomain(); + + return $this->i18nTranslatePlural($textSingular, $textPlural, $number, $context); + } + + /** + * Render this form + * + * @param Zend_View_Interface $view The view context to use + * + * @return string + */ + public function render(Zend_View_Interface $view = null) + { + $this->create(); + return parent::render($view); + } + + /** + * Get the authentication manager + * + * @return Auth + */ + public function Auth() + { + if ($this->auth === null) { + $this->auth = Auth::getInstance(); + } + return $this->auth; + } + + /** + * Whether the current user has the given permission + * + * @param string $permission Name of the permission + * + * @return bool + */ + public function hasPermission($permission) + { + return $this->Auth()->hasPermission($permission); + } + + /** + * Assert that the current user has the given permission + * + * @param string $permission Name of the permission + * + * @throws SecurityException If the current user lacks the given permission + */ + public function assertPermission($permission) + { + if (! $this->Auth()->hasPermission($permission)) { + throw new SecurityException('No permission for %s', $permission); + } + } + + /** + * Add a error notification + * + * @param string|array $message The notification message + * @param bool $markAsError Whether to prevent the form from being successfully validated or not + * + * @return $this + */ + public function error($message, $markAsError = true) + { + if ($this->getIsApiTarget()) { + $this->addErrorMessage($message); + } else { + $this->addNotification($message, self::NOTIFICATION_ERROR); + } + + if ($markAsError) { + $this->markAsError(); + } + + return $this; + } + + /** + * Add a warning notification + * + * @param string|array $message The notification message + * @param bool $markAsError Whether to prevent the form from being successfully validated or not + * + * @return $this + */ + public function warning($message, $markAsError = true) + { + if ($this->getIsApiTarget()) { + $this->addErrorMessage($message); + } else { + $this->addNotification($message, self::NOTIFICATION_WARNING); + } + + if ($markAsError) { + $this->markAsError(); + } + + return $this; + } + + /** + * Add a info notification + * + * @param string|array $message The notification message + * @param bool $markAsError Whether to prevent the form from being successfully validated or not + * + * @return $this + */ + public function info($message, $markAsError = true) + { + if ($this->getIsApiTarget()) { + $this->addErrorMessage($message); + } else { + $this->addNotification($message, self::NOTIFICATION_INFO); + } + + if ($markAsError) { + $this->markAsError(); + } + + return $this; + } +} diff --git a/library/Icinga/Web/Form/Decorator/Autosubmit.php b/library/Icinga/Web/Form/Decorator/Autosubmit.php new file mode 100644 index 0000000..4405d0b --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/Autosubmit.php @@ -0,0 +1,133 @@ + should be created with the same warning as in the icon label + * + * @var bool + */ + protected $accessible; + + /** + * The id used to identify the auto-submit warning associated with the decorated form element + * + * @var string + */ + protected $warningId; + + /** + * Set whether a hidden should be created with the same warning as in the icon label + * + * @param bool $state + * + * @return Autosubmit + */ + public function setAccessible($state = true) + { + $this->accessible = (bool) $state; + return $this; + } + + /** + * Return whether a hidden is being created with the same warning as in the icon label + * + * @return bool + */ + public function getAccessible() + { + if ($this->accessible === null) { + $this->accessible = $this->getOption('accessible') ?: false; + } + + return $this->accessible; + } + + /** + * Return the id used to identify the auto-submit warning associated with the decorated element + * + * @param mixed $element The element for which to generate a id + * + * @return string + */ + public function getWarningId($element = null) + { + if ($this->warningId === null) { + $element = $element ?: $this->getElement(); + $this->warningId = 'autosubmit_warning_' . $element->getId(); + } + + return $this->warningId; + } + + /** + * Return the current view + * + * @return View + */ + protected function getView() + { + return Icinga::app()->getViewRenderer()->view; + } + + /** + * Add a auto-submit icon and submit button encapsulated in noscript-tags to the element + * + * @param string $content The html rendered so far + * + * @return string The updated html + */ + public function render($content = '') + { + if ($content) { + $isForm = $this->getElement() instanceof Form; + $warning = $isForm + ? t('This page will be automatically updated upon change of any of this form\'s fields') + : t('This page will be automatically updated upon change of the value'); + $content .= $this->getView()->icon('cw', $warning, array( + 'aria-hidden' => $isForm ? 'false' : 'true', + 'class' => 'spinner autosubmit-info' + )); + if (! $isForm && $this->getAccessible()) { + $content = '' + . $warning + . '' + . $content; + } + + $content .= sprintf( + '', + $isForm + ? t('Push this button to update the form to reflect the changes that were made below') + : t('Push this button to update the form to reflect the change' + . ' that was made in the field on the left'), + $this->getView()->icon('cw') . t('Apply') + ); + } + + return $content; + } +} diff --git a/library/Icinga/Web/Form/Decorator/ConditionalHidden.php b/library/Icinga/Web/Form/Decorator/ConditionalHidden.php new file mode 100644 index 0000000..0f84535 --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/ConditionalHidden.php @@ -0,0 +1,35 @@ + tag if the + * "condition" attribute is set and false or 0 + * + * @param string $content The tag's content + * + * @return string The generated tag + */ + public function render($content = '') + { + $attributes = $this->getElement()->getAttribs(); + $condition = isset($attributes['condition']) ? $attributes['condition'] : 1; + if ($condition != 1) { + $content = ''; + } + return $content; + } +} diff --git a/library/Icinga/Web/Form/Decorator/ElementDoubler.php b/library/Icinga/Web/Form/Decorator/ElementDoubler.php new file mode 100644 index 0000000..2da5646 --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/ElementDoubler.php @@ -0,0 +1,63 @@ +getElement(); + if ($group->getElement($this->getOption('condition')) !== null) { + if ($this->getPlacement() === static::APPEND) { + return $content . $this->applyAttributes($group->getElement($this->getOption('double')))->render(); + } else { // $this->getPlacement() === static::PREPEND + return $this->applyAttributes($group->getElement($this->getOption('double')))->render() . $content; + } + } + + return $content; + } + + /** + * Apply all element attributes + * + * @param Zend_Form_Element $element The element to apply the attributes to + * + * @return Zend_Form_Element + */ + protected function applyAttributes(Zend_Form_Element $element) + { + $attributes = $this->getOption('attributes'); + if ($attributes !== null) { + foreach ($attributes as $name => $value) { + $element->setAttrib($name, $value); + } + } + + return $element; + } +} diff --git a/library/Icinga/Web/Form/Decorator/FormDescriptions.php b/library/Icinga/Web/Form/Decorator/FormDescriptions.php new file mode 100644 index 0000000..88ea5d9 --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/FormDescriptions.php @@ -0,0 +1,76 @@ +getElement(); + if (! $form instanceof Form) { + return $content; + } + + $view = $form->getView(); + if ($view === null) { + return $content; + } + + $descriptions = $this->recurseForm($form); + if (empty($descriptions)) { + return $content; + } + + $html = '
    ' + . Icinga::app()->getViewRenderer()->view->icon('info-circled', '', ['class' => 'form-description-icon']) + . '
      '; + + foreach ($descriptions as $description) { + if (is_array($description)) { + list($description, $properties) = $description; + $html .= 'propertiesToString($properties) . '>' . $view->escape($description) . ''; + } else { + $html .= '
    • ' . $view->escape($description) . '
    • '; + } + } + + switch ($this->getPlacement()) { + case self::APPEND: + return $content . $html . '
    '; + case self::PREPEND: + return $html . '' . $content; + } + } + + /** + * Recurse the given form and return the descriptions for it and all of its subforms + * + * @param Form $form The form to recurse + * + * @return array + */ + protected function recurseForm(Form $form) + { + $descriptions = array($form->getDescriptions()); + foreach ($form->getSubForms() as $subForm) { + $descriptions[] = $this->recurseForm($subForm); + } + + return call_user_func_array('array_merge', $descriptions); + } +} diff --git a/library/Icinga/Web/Form/Decorator/FormHints.php b/library/Icinga/Web/Form/Decorator/FormHints.php new file mode 100644 index 0000000..797be26 --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/FormHints.php @@ -0,0 +1,142 @@ +blacklist = array( + 'Zend_Form_Element_Hidden', + 'Zend_Form_Element_Submit', + 'Zend_Form_Element_Button', + 'Icinga\Web\Form\Element\Note', + 'Icinga\Web\Form\Element\Button', + 'Icinga\Web\Form\Element\CsrfCounterMeasure' + ); + } + + /** + * Render form hints + * + * @param string $content The html rendered so far + * + * @return string The updated html + */ + public function render($content = '') + { + $form = $this->getElement(); + if (! $form instanceof Form) { + return $content; + } + + $view = $form->getView(); + if ($view === null) { + return $content; + } + + $hints = $this->recurseForm($form, $entirelyRequired); + if ($entirelyRequired !== null) { + $hints[] = sprintf( + $form->getView()->translate('%s Required field'), + $form->getRequiredCue() + ); + } + + if (empty($hints)) { + return $content; + } + + $html = '
      '; + foreach ($hints as $hint) { + if (is_array($hint)) { + list($hint, $properties) = $hint; + $html .= 'propertiesToString($properties) . '>' . $view->escape($hint) . ''; + } else { + $html .= '
    • ' . $view->escape($hint) . '
    • '; + } + } + + switch ($this->getPlacement()) { + case self::APPEND: + return $content . $html . '
    '; + case self::PREPEND: + return $html . '' . $content; + } + } + + /** + * Recurse the given form and return the hints for it and all of its subforms + * + * @param Form $form The form to recurse + * @param mixed $entirelyRequired Set by reference, true means all elements in the hierarchy are + * required, false only a partial subset and null none at all + * @param bool $elementsPassed Whether there were any elements passed during the recursion until now + * + * @return array + */ + protected function recurseForm(Form $form, &$entirelyRequired = null, $elementsPassed = false) + { + $requiredLabels = array(); + if ($form->getRequiredCue() !== null) { + $partiallyRequired = $partiallyOptional = false; + foreach ($form->getElements() as $element) { + if (! in_array($element->getType(), $this->blacklist)) { + if (! $element->isRequired()) { + $partiallyOptional = true; + if ($entirelyRequired) { + $entirelyRequired = false; + } + } else { + $partiallyRequired = true; + if (($label = $element->getDecorator('label')) !== false) { + $requiredLabels[] = $label; + } + } + } + } + + if (! $elementsPassed) { + $elementsPassed = $partiallyRequired || $partiallyOptional; + if ($entirelyRequired === null && $partiallyRequired) { + $entirelyRequired = ! $partiallyOptional; + } + } elseif ($entirelyRequired === null && $partiallyRequired) { + $entirelyRequired = false; + } + } + + $hints = array($form->getHints()); + foreach ($form->getSubForms() as $subForm) { + $hints[] = $this->recurseForm($subForm, $entirelyRequired, $elementsPassed); + } + + if ($entirelyRequired) { + foreach ($requiredLabels as $label) { + $label->setRequiredSuffix(''); + } + } + + return call_user_func_array('array_merge', $hints); + } +} diff --git a/library/Icinga/Web/Form/Decorator/FormNotifications.php b/library/Icinga/Web/Form/Decorator/FormNotifications.php new file mode 100644 index 0000000..46734df --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/FormNotifications.php @@ -0,0 +1,125 @@ +getElement(); + if (! $form instanceof Form) { + return $content; + } + + $view = $form->getView(); + if ($view === null) { + return $content; + } + + $notifications = $this->recurseForm($form); + if (empty($notifications)) { + return $content; + } + + $html = '
      '; + foreach (array(Form::NOTIFICATION_ERROR, Form::NOTIFICATION_WARNING, Form::NOTIFICATION_INFO) as $type) { + if (isset($notifications[$type])) { + $html .= '
      • '; + foreach ($notifications[$type] as $message) { + if (is_array($message)) { + list($message, $properties) = $message; + $html .= 'propertiesToString($properties) . '>' + . $view->escape($message) + . ''; + } else { + $html .= '
      • ' . $view->escape($message) . '
      • '; + } + } + + $html .= '
    • '; + } + } + + if (isset($notifications[Form::NOTIFICATION_ERROR])) { + $icon = 'cancel'; + $class = 'error'; + } elseif (isset($notifications[Form::NOTIFICATION_WARNING])) { + $icon = 'warning-empty'; + $class = 'warning'; + } else { + $icon = 'info'; + $class = 'info'; + } + + $html = "
      " + . Icinga::app()->getViewRenderer()->view->icon($icon, '', ['class' => 'form-notification-icon']) + . $html; + + switch ($this->getPlacement()) { + case self::APPEND: + return $content . $html . '
    '; + case self::PREPEND: + return $html . '' . $content; + } + } + + /** + * Recurse the given form and return the notifications for it and all of its subforms + * + * @param Form $form The form to recurse + * + * @return array + */ + protected function recurseForm(Form $form) + { + $notifications = $form->getNotifications(); + foreach ($form->getSubForms() as $subForm) { + foreach ($this->recurseForm($subForm) as $type => $messages) { + foreach ($messages as $message) { + $notifications[$type][] = $message; + } + } + } + + return $notifications; + } + + /** + * Return the name for the given notification type + * + * @param int $type + * + * @return string + * + * @throws ProgrammingError In case the given type is invalid + */ + protected function getNotificationTypeName($type) + { + switch ($type) { + case Form::NOTIFICATION_ERROR: + return 'error'; + case Form::NOTIFICATION_WARNING: + return 'warning'; + case Form::NOTIFICATION_INFO: + return 'info'; + default: + throw new ProgrammingError('Invalid notification type "%s" provided', $type); + } + } +} diff --git a/library/Icinga/Web/Form/Decorator/Help.php b/library/Icinga/Web/Form/Decorator/Help.php new file mode 100644 index 0000000..c521abe --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/Help.php @@ -0,0 +1,113 @@ + should be created to describe the decorated form element + * + * @var bool + */ + protected $accessible = false; + + /** + * The id used to identify the description associated with the decorated form element + * + * @var string + */ + protected $descriptionId; + + /** + * Set whether a hidden should be created to describe the decorated form element + * + * @param bool $state + * + * @return Help + */ + public function setAccessible($state = true) + { + $this->accessible = (bool) $state; + return $this; + } + + /** + * Return the id used to identify the description associated with the decorated element + * + * @param Zend_Form_Element $element The element for which to generate a id + * + * @return string + */ + public function getDescriptionId(Zend_Form_Element $element = null) + { + if ($this->descriptionId === null) { + $element = $element ?: $this->getElement(); + $this->descriptionId = 'desc_' . $element->getId(); + } + + return $this->descriptionId; + } + + /** + * Return the current view + * + * @return View + */ + protected function getView() + { + return Icinga::app()->getViewRenderer()->view; + } + + /** + * Add a help icon to the left of an element + * + * @param string $content The html rendered so far + * + * @return string The updated html + */ + public function render($content = '') + { + $element = $this->getElement(); + $description = $element->getDescription(); + $requirement = $element->getAttrib('requirement'); + unset($element->requirement); + + $helpContent = ''; + if ($description || $requirement) { + if ($this->accessible) { + $helpContent = '' + . $description + . ($description && $requirement ? ' ' : '') + . $requirement + . ''; + } + + $helpContent = $this->getView()->icon( + 'info-circled', + $description . ($description && $requirement ? ' ' : '') . $requirement, + array( + 'class' => 'control-info', + 'aria-hidden' => $this->accessible ? 'true' : 'false' + ) + ) . $helpContent; + } + + switch ($this->getPlacement()) { + case self::APPEND: + return $content . $helpContent; + case self::PREPEND: + return $helpContent . $content; + } + } +} diff --git a/library/Icinga/Web/Form/Decorator/Spinner.php b/library/Icinga/Web/Form/Decorator/Spinner.php new file mode 100644 index 0000000..9cfa568 --- /dev/null +++ b/library/Icinga/Web/Form/Decorator/Spinner.php @@ -0,0 +1,48 @@ +getViewRenderer()->view; + } + + /** + * Add a spinner icon to a form element + * + * @param string $content The html rendered so far + * + * @return string The updated html + */ + public function render($content = '') + { + $spinner = '
    getOption('id') !== null ? ' id="' . $this->getOption('id') . '"' : '') + . 'class="spinner ' . ($this->getOption('class') ?: '') . '"' + . '>' + . $this->getView()->icon('spin6') + . '
    '; + + switch ($this->getPlacement()) { + case self::APPEND: + return $content . $spinner; + case self::PREPEND: + return $spinner . $content; + } + } +} diff --git a/library/Icinga/Web/Form/Element/Button.php b/library/Icinga/Web/Form/Element/Button.php new file mode 100644 index 0000000..9cbe915 --- /dev/null +++ b/library/Icinga/Web/Form/Element/Button.php @@ -0,0 +1,80 @@ + $options); + } + + if (!isset($options['ignore'])) { + $options['ignore'] = true; + } + + parent::__construct($spec, $options); + + if ($label = $this->getLabel()) { + // Necessary to get the label shown on the generated HTML + $this->content = $label; + } + } + + /** + * Validate element value (pseudo) + * + * There is no need to reset the value + * + * @param mixed $value Is always ignored + * @param mixed $context Is always ignored + * + * @return bool Returns always TRUE + */ + public function isValid($value, $context = null) + { + return true; + } + + /** + * Has this button been selected? + * + * @return bool + */ + public function isChecked() + { + return $this->getRequest()->getParam($this->getName()) === $this->getValue(); + } + + /** + * Return the current request + * + * @return Request + */ + protected function getRequest() + { + return Icinga::app()->getRequest(); + } +} diff --git a/library/Icinga/Web/Form/Element/Checkbox.php b/library/Icinga/Web/Form/Element/Checkbox.php new file mode 100644 index 0000000..d4499a0 --- /dev/null +++ b/library/Icinga/Web/Form/Element/Checkbox.php @@ -0,0 +1,9 @@ +setDecorators(['ViewHelper']); + $this->setValue($this->generateCsrfToken()); + } + + /** + * Check whether $value is a valid CSRF token + * + * @param string $value The value to check + * @param mixed $context Context to use + * + * @return bool True, in case the CSRF token is valid + * + * @throws InvalidCSRFTokenException In case the CSRF token is not valid + */ + public function isValid($value, $context = null) + { + if (parent::isValid($value, $context) && $this->isValidCsrfToken($value)) { + return true; + } + + throw new InvalidCSRFTokenException(); + } + + /** + * Check whether the given value is a valid CSRF token for the current session + * + * @param string $token The CSRF token + * + * @return bool + */ + protected function isValidCsrfToken($token) + { + if (strpos($token, '|') === false) { + return false; + } + + list($seed, $hash) = explode('|', $token); + + if (false === is_numeric($seed)) { + return false; + } + + return $hash === hash('sha256', Session::getSession()->getId() . $seed); + } + + /** + * Generate a new (seed, token) pair + * + * @return string + */ + protected function generateCsrfToken() + { + $seed = mt_rand(); + $hash = hash('sha256', Session::getSession()->getId() . $seed); + return sprintf('%s|%s', $seed, $hash); + } +} diff --git a/library/Icinga/Web/Form/Element/Date.php b/library/Icinga/Web/Form/Element/Date.php new file mode 100644 index 0000000..8e0985c --- /dev/null +++ b/library/Icinga/Web/Form/Element/Date.php @@ -0,0 +1,19 @@ +addValidator( + new DateTimeValidator($this->local), + true // true for breaking the validator chain on failure + ); + } + + /** + * Get the expected date and time format of any user input + * + * @return string + */ + public function getFormat() + { + return $this->local ? 'Y-m-d\TH:i:s' : DateTime::RFC3339; + } + + /** + * Is the date and time valid? + * + * @param string|DateTime $value + * @param mixed $context + * + * @return bool + */ + public function isValid($value, $context = null) + { + if (is_scalar($value) && $value !== '' && ! preg_match('/\D/', $value)) { + $dateTime = new DateTime(); + $value = $dateTime->setTimestamp($value)->format($this->getFormat()); + } + + if (! parent::isValid($value, $context)) { + return false; + } + + if (! $value instanceof DateTime) { + $format = $this->getFormat(); + $dateTime = DateTime::createFromFormat($format, $value); + if ($dateTime === false) { + $dateTime = DateTime::createFromFormat(substr($format, 0, strrpos($format, ':')), $value); + } + + $this->setValue($dateTime); + } + + return true; + } +} diff --git a/library/Icinga/Web/Form/Element/Note.php b/library/Icinga/Web/Form/Element/Note.php new file mode 100644 index 0000000..9569dee --- /dev/null +++ b/library/Icinga/Web/Form/Element/Note.php @@ -0,0 +1,55 @@ +getDecorators()) === 0) { + $this->setDecorators(array( + 'ViewHelper', + array( + 'HtmlTag', + array('tag' => 'p') + ) + )); + } + } + + /** + * Validate element value (pseudo) + * + * @param mixed $value Ignored + * + * @return bool Always true + */ + public function isValid($value, $context = null) + { + return true; + } +} diff --git a/library/Icinga/Web/Form/Element/Number.php b/library/Icinga/Web/Form/Element/Number.php new file mode 100644 index 0000000..afbd07d --- /dev/null +++ b/library/Icinga/Web/Form/Element/Number.php @@ -0,0 +1,144 @@ +min !== null || $this->max !== null) { + $this->addValidator('Between', true, array( + 'min' => $this->min === null ? -INF : $this->min, + 'max' => $this->max === null ? INF : $this->max, + 'inclusive' => true + )); + } + } + + /** + * Set the expected lower bound for the element’s value + * + * @param float $min + * + * @return $this + */ + public function setMin($min) + { + $this->min = (float) $min; + return $this; + } + + /** + * Get the expected lower bound for the element’s value + * + * @return float|null + */ + public function getMin() + { + return $this->min; + } + + /** + * Set the expected upper bound for the element’s value + * + * @param float $max + * + * @return $this + */ + public function setMax($max) + { + $this->max = (float) $max; + return $this; + } + + /** + * Get the expected upper bound for the element’s value + * + * @return float|null + */ + public function getMax() + { + return $this->max; + } + + /** + * Set the value granularity of the element’s value + * + * @param float|string $step + * + * @return $this + */ + public function setStep($step) + { + if ($step !== 'any') { + $step = (float) $step; + } + $this->step = $step; + return $this; + } + + /** + * Get the value granularity of the element’s value + * + * @return float|string|null + */ + public function getStep() + { + return $this->step; + } + + /** + * (non-PHPDoc) + * @see \Zend_Form_Element::isValid() For the method documentation. + */ + public function isValid($value, $context = null) + { + $this->setValue($value); + $value = $this->getValue(); + if ($value !== null && $value !== '' && ! is_numeric($value)) { + $this->addError(sprintf(t('\'%s\' is not a valid number'), $value)); + return false; + } + return parent::isValid($value, $context); + } +} diff --git a/library/Icinga/Web/Form/Element/Textarea.php b/library/Icinga/Web/Form/Element/Textarea.php new file mode 100644 index 0000000..119cd56 --- /dev/null +++ b/library/Icinga/Web/Form/Element/Textarea.php @@ -0,0 +1,20 @@ +getAttrib('rows') === null) { + $this->setAttrib('rows', 3); + } + } +} diff --git a/library/Icinga/Web/Form/Element/Time.php b/library/Icinga/Web/Form/Element/Time.php new file mode 100644 index 0000000..4b76a33 --- /dev/null +++ b/library/Icinga/Web/Form/Element/Time.php @@ -0,0 +1,19 @@ +messages = $this->createMessages($options['element']); + } + + public function isTranslated($messageId, $original = false, $locale = null) + { + return array_key_exists($messageId, $this->messages); + } + + public function translate($messageId, $locale = null) + { + if (array_key_exists($messageId, $this->messages)) { + return $this->messages[$messageId]; + } + + return $messageId; + } + + protected function createMessages($element) + { + $label = $element->getLabel() ?: $element->getName(); + + return array( + Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label), + Zend_Validate_File_MimeType::FALSE_TYPE => sprintf( + t('%s (%%value%%) has a false MIME type of "%%type%%"'), + $label + ), + Zend_Validate_File_MimeType::NOT_DETECTED => sprintf(t('%s (%%value%%) has no MIME type'), $label), + WritablePathValidator::NOT_WRITABLE => sprintf(t('%s is not writable', 'config.path'), $label), + WritablePathValidator::DOES_NOT_EXIST => sprintf(t('%s does not exist', 'config.path'), $label), + ReadablePathValidator::NOT_READABLE => sprintf(t('%s is not readable', 'config.path'), $label), + DateTimeValidator::INVALID_DATETIME_FORMAT => sprintf( + t('%s not in the expected format: %%value%%'), + $label + ) + ); + } + + protected function _loadTranslationData($data, $locale, array $options = array()) + { + // nonsense, required as being abstract otherwise... + } + + public function toString() + { + return 'ErrorLabeller'; // nonsense, required as being abstract otherwise... + } +} diff --git a/library/Icinga/Web/Form/FormElement.php b/library/Icinga/Web/Form/FormElement.php new file mode 100644 index 0000000..0ac2ee4 --- /dev/null +++ b/library/Icinga/Web/Form/FormElement.php @@ -0,0 +1,61 @@ +_disableLoadDefaultDecorators === true; + } + + /** + * Load default decorators + * + * Icinga Web 2 loads its own default element decorators. For loading Zend's default element decorators set + * FormElement::$_disableLoadDefaultDecorators to false. + * + * @return this + * @see Form::$defaultElementDecorators For Icinga Web 2's default element decorators. + */ + public function loadDefaultDecorators() + { + if ($this->loadDefaultDecoratorsIsDisabled()) { + return $this; + } + + if (! isset($this->_disableLoadDefaultDecorators)) { + $decorators = $this->getDecorators(); + if (empty($decorators)) { + // Load Icinga Web 2's default element decorators + $this->addDecorators(Form::$defaultElementDecorators); + } + } else { + // Load Zend's default decorators + parent::loadDefaultDecorators(); + } + return $this; + } +} diff --git a/library/Icinga/Web/Form/InvalidCSRFTokenException.php b/library/Icinga/Web/Form/InvalidCSRFTokenException.php new file mode 100644 index 0000000..d0eb68a --- /dev/null +++ b/library/Icinga/Web/Form/InvalidCSRFTokenException.php @@ -0,0 +1,11 @@ + 'Invalid date format' + ); + + /** + * Validate the input value + * + * @param string $value The format string to validate + * @param null $context The form context (ignored) + * + * @return bool True when the input is valid, otherwise false + * + * @see Zend_Validate_Abstract::isValid() + */ + public function isValid($value, $context = null) + { + $rest = trim($value, join(' ', array_merge($this->validChars, $this->validSeparators))); + if (strlen($rest) > 0) { + $this->_error('INVALID_CHARACTERS'); + return false; + } + return true; + } +} diff --git a/library/Icinga/Web/Form/Validator/DateTimeValidator.php b/library/Icinga/Web/Form/Validator/DateTimeValidator.php new file mode 100644 index 0000000..5ef327d --- /dev/null +++ b/library/Icinga/Web/Form/Validator/DateTimeValidator.php @@ -0,0 +1,77 @@ + 'Invalid type given. Instance of DateTime or date/time string expected', + self::INVALID_DATETIME_FORMAT => 'Date/time string not in the expected format: %value%' + ); + + protected $local; + + /** + * Create a new date-and-time input control validator + * + * @param bool $local + */ + public function __construct($local) + { + $this->local = (bool) $local; + } + + /** + * Is the date and time valid? + * + * @param string|DateTime $value + * @param mixed $context + * + * @return bool + * + * @see \Zend_Validate_Interface::isValid() + */ + public function isValid($value, $context = null) + { + if (! $value instanceof DateTime && ! is_string($value)) { + $this->_error(self::INVALID_DATETIME_TYPE); + return false; + } + + if (! $value instanceof DateTime) { + $format = $baseFormat = $this->local === true ? 'Y-m-d\TH:i:s' : DateTime::RFC3339; + $dateTime = DateTime::createFromFormat($format, $value); + + if ($dateTime === false) { + $format = substr($format, 0, strrpos($format, ':')); + $dateTime = DateTime::createFromFormat($format, $value); + } + + if ($dateTime === false || $dateTime->format($format) !== $value) { + $this->_error(self::INVALID_DATETIME_FORMAT, $baseFormat); + return false; + } + } + + return true; + } +} diff --git a/library/Icinga/Web/Form/Validator/InArray.php b/library/Icinga/Web/Form/Validator/InArray.php new file mode 100644 index 0000000..5d3925e --- /dev/null +++ b/library/Icinga/Web/Form/Validator/InArray.php @@ -0,0 +1,28 @@ +_value, $this->_haystack); + if (empty($matches)) { + $this->_messages[$messageKey] = sprintf(t('"%s" is not in the list of allowed values.'), $this->_value); + } else { + $this->_messages[$messageKey] = sprintf( + t('"%s" is not in the list of allowed values. Did you mean one of the following?: %s'), + $this->_value, + implode(', ', $matches) + ); + } + } else { + parent::_error($messageKey, $value); + } + } +} diff --git a/library/Icinga/Web/Form/Validator/InternalUrlValidator.php b/library/Icinga/Web/Form/Validator/InternalUrlValidator.php new file mode 100644 index 0000000..f936bb5 --- /dev/null +++ b/library/Icinga/Web/Form/Validator/InternalUrlValidator.php @@ -0,0 +1,41 @@ +getRelativeUrl() === '' || $url->isExternal()) { + $this->_error('IS_EXTERNAL'); + + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function _error($messageKey, $value = null) + { + if ($messageKey === 'IS_EXTERNAL') { + $this->_messages[$messageKey] = t('The url must not be external.'); + } else { + parent::_error($messageKey, $value); + } + } +} diff --git a/library/Icinga/Web/Form/Validator/ReadablePathValidator.php b/library/Icinga/Web/Form/Validator/ReadablePathValidator.php new file mode 100644 index 0000000..826421c --- /dev/null +++ b/library/Icinga/Web/Form/Validator/ReadablePathValidator.php @@ -0,0 +1,53 @@ + 'Path is not readable', + self::DOES_NOT_EXIST => 'Path does not exist' + ); + + /** + * Check whether the given value is a readable filepath + * + * @param string $value The value submitted in the form + * @param mixed $context The context of the form + * + * @return bool Whether the value was successfully validated + */ + public function isValid($value, $context = null) + { + if (false === file_exists($value)) { + $this->_error(self::DOES_NOT_EXIST); + return false; + } + + if (false === is_readable($value)) { + $this->_error(self::NOT_READABLE); + return false; + } + + return true; + } +} diff --git a/library/Icinga/Web/Form/Validator/TimeFormatValidator.php b/library/Icinga/Web/Form/Validator/TimeFormatValidator.php new file mode 100644 index 0000000..9c1c99a --- /dev/null +++ b/library/Icinga/Web/Form/Validator/TimeFormatValidator.php @@ -0,0 +1,58 @@ + 'Invalid time format' + ); + + /** + * Validate the input value + * + * @param string $value The format string to validate + * @param null $context The form context (ignored) + * + * @return bool True when the input is valid, otherwise false + * + * @see Zend_Validate_Abstract::isValid() + */ + public function isValid($value, $context = null) + { + $rest = trim($value, join(' ', array_merge($this->validChars, $this->validSeparators))); + if (strlen($rest) > 0) { + $this->_error('INVALID_CHARACTERS'); + return false; + } + return true; + } +} diff --git a/library/Icinga/Web/Form/Validator/UrlValidator.php b/library/Icinga/Web/Form/Validator/UrlValidator.php new file mode 100644 index 0000000..b1b578f --- /dev/null +++ b/library/Icinga/Web/Form/Validator/UrlValidator.php @@ -0,0 +1,40 @@ +_messageTemplates = array('HAS_QUOTES' => t( + 'The url must not contain raw double quotes. If you really need double quotes, use %22 instead.' + )); + } + + /** + * Validate the input value + * + * @param string $value The string to validate + * + * @return bool true if and only if the input is valid, otherwise false + * + * @see Zend_Validate_Abstract::isValid() + */ + public function isValid($value) + { + $hasQuotes = false === strpos($value, '"'); + if (! $hasQuotes) { + $this->_error('HAS_QUOTES'); + } + return $hasQuotes; + } +} diff --git a/library/Icinga/Web/Form/Validator/WritablePathValidator.php b/library/Icinga/Web/Form/Validator/WritablePathValidator.php new file mode 100644 index 0000000..76efb58 --- /dev/null +++ b/library/Icinga/Web/Form/Validator/WritablePathValidator.php @@ -0,0 +1,72 @@ + 'Path is not writable', + self::DOES_NOT_EXIST => 'Path does not exist' + ); + + /** + * When true, the file or directory must exist + * + * @var bool + */ + private $requireExistence = false; + + /** + * Set this validator to require the target file to exist + */ + public function setRequireExistence() + { + $this->requireExistence = true; + } + + /** + * Check whether the given value is writable path + * + * @param string $value The value submitted in the form + * @param mixed $context The context of the form + * + * @return bool True when validation worked, otherwise false + * + * @see Zend_Validate_Abstract::isValid() + */ + public function isValid($value, $context = null) + { + $value = (string) $value; + + $this->_setValue($value); + if ($this->requireExistence && !file_exists($value)) { + $this->_error(self::DOES_NOT_EXIST); + return false; + } + + if ((file_exists($value) && is_writable($value)) || + (is_dir(dirname($value)) && is_writable(dirname($value))) + ) { + return true; + } + + $this->_error(self::NOT_WRITABLE); + return false; + } +} diff --git a/library/Icinga/Web/Helper/CookieHelper.php b/library/Icinga/Web/Helper/CookieHelper.php new file mode 100644 index 0000000..cc7c448 --- /dev/null +++ b/library/Icinga/Web/Helper/CookieHelper.php @@ -0,0 +1,81 @@ +request = $request; + } + + /** + * Check whether cookies are supported or not + * + * @return bool + */ + public function isSupported() + { + if (! empty($_COOKIE)) { + $this->cleanupCheck(); + return true; + } + + $url = $this->request->getUrl(); + + if ($url->hasParam('_checkCookie') && empty($_COOKIE)) { + return false; + } + + if (! $url->hasParam('_checkCookie')) { + $this->provideCheck(); + } + + return false; + } + + /** + * Prepare check to detect cookie support + */ + public function provideCheck() + { + setcookie(self::CHECK_COOKIE, '1'); + + $requestUri = $this->request->getUrl()->addParams(array('_checkCookie' => 1)); + $this->request->getResponse()->redirectAndExit($requestUri); + } + + /** + * Cleanup the cookie support check + */ + public function cleanupCheck() + { + if ($this->request->getUrl()->hasParam('_checkCookie') && isset($_COOKIE[self::CHECK_COOKIE])) { + $requestUri =$this->request->getUrl()->without('_checkCookie'); + $this->request->getResponse()->redirectAndExit($requestUri); + } + } +} diff --git a/library/Icinga/Web/Helper/HtmlPurifier.php b/library/Icinga/Web/Helper/HtmlPurifier.php new file mode 100644 index 0000000..f0c292f --- /dev/null +++ b/library/Icinga/Web/Helper/HtmlPurifier.php @@ -0,0 +1,99 @@ +set('Core.EscapeNonASCIICharacters', true); + $purifierConfig->set('Attr.AllowedFrameTargets', array('_blank')); + + if (($cachePath = FileCache::instance()->directory('htmlpurifier.cache')) !== false) { + $purifierConfig->set('Cache.SerializerPath', $cachePath); + } else { + $purifierConfig->set('Cache.DefinitionImpl', null); + } + + // This avoids permission problems: + // $purifierConfig->set('Core.DefinitionCache', null); + + // $purifierConfig->set('URI.Base', 'http://www.example.com'); + // $purifierConfig->set('URI.MakeAbsolute', true); + + $this->configure($purifierConfig); + + if ($config instanceof Closure) { + call_user_func($config, $purifierConfig); + } elseif (is_array($config)) { + $purifierConfig->loadArray($config); + } elseif ($config !== null) { + throw new InvalidArgumentException('$config must be either a Closure or array'); + } + + $this->purifier = new \HTMLPurifier($purifierConfig); + } + + /** + * Apply additional default configuration + * + * May be overwritten by more concrete purifier implementations. + * + * @param \HTMLPurifier_Config $config + */ + protected function configure($config) + { + } + + /** + * Purify and return the given HTML string + * + * @param string $html + * @param array|Closure $config Configuration to use instead of the default + * + * @return string + */ + public function purify($html, $config = null) + { + return $this->purifier->purify($html, $config); + } + + /** + * Purify and return the given HTML string + * + * Convenience method to bypass object creation. + * + * @param string $html + * @param array|Closure $config Additional configuration + * + * @return string + */ + public static function process($html, $config = null) + { + $purifier = new static($config); + + return $purifier->purify($html); + } +} diff --git a/library/Icinga/Web/Helper/Markdown.php b/library/Icinga/Web/Helper/Markdown.php new file mode 100644 index 0000000..e6509d0 --- /dev/null +++ b/library/Icinga/Web/Helper/Markdown.php @@ -0,0 +1,38 @@ +set('HTML.Parent', 'span'); // Only allow inline elements + + LinkTransformer::attachTo($config); + }; + } + + return HtmlPurifier::process(Parsedown::instance()->line($content), $config); + } + + public static function text($content, $config = null) + { + require_once 'Parsedown/Parsedown.php'; + + if ($config === null) { + $config = function (\HTMLPurifier_Config $config) { + LinkTransformer::attachTo($config); + }; + } + + return HtmlPurifier::process(Parsedown::instance()->text($content), $config); + } +} diff --git a/library/Icinga/Web/Helper/Markdown/LinkTransformer.php b/library/Icinga/Web/Helper/Markdown/LinkTransformer.php new file mode 100644 index 0000000..f323085 --- /dev/null +++ b/library/Icinga/Web/Helper/Markdown/LinkTransformer.php @@ -0,0 +1,73 @@ +getPath()); + + $ext = null; + if (($extAt = strrpos($fileName, '.')) !== false) { + $ext = substr($fileName, $extAt + 1); + } + + $hasThumbnail = $ext !== null && in_array($ext, static::$IMAGE_FILES, true); + if ($hasThumbnail) { + // I would have liked to not only base this off of the extension, but also by + // whether there is an actual img tag inside the anchor. Seems not possible :( + $attr['class'] = 'with-thumbnail'; + } + + if (! isset($attr['target'])) { + if ($url->isExternal()) { + $attr['target'] = '_blank'; + } else { + $attr['data-base-target'] = '_next'; + } + } + + return $attr; + } + + public static function attachTo(HTMLPurifier_Config $config) + { + $module = $config->getHTMLDefinition(true) + ->getAnonymousModule(); + + if (isset($module->info['a'])) { + $a = $module->info['a']; + } else { + $a = $module->addBlankElement('a'); + } + + $a->attr_transform_post[] = new self(); + } +} diff --git a/library/Icinga/Web/Hook.php b/library/Icinga/Web/Hook.php new file mode 100644 index 0000000..b098518 --- /dev/null +++ b/library/Icinga/Web/Hook.php @@ -0,0 +1,16 @@ +function\s*\([^)]*\)|[^=]*=>|\w+).*)/'; + + protected static $jsFiles = [ + 'js/helpers.js', + 'js/icinga.js', + 'js/icinga/logger.js', + 'js/icinga/storage.js', + 'js/icinga/utils.js', + 'js/icinga/ui.js', + 'js/icinga/timer.js', + 'js/icinga/loader.js', + 'js/icinga/eventlistener.js', + 'js/icinga/events.js', + 'js/icinga/history.js', + 'js/icinga/module.js', + 'js/icinga/timezone.js', + 'js/icinga/behavior/application-state.js', + 'js/icinga/behavior/autofocus.js', + 'js/icinga/behavior/collapsible.js', + 'js/icinga/behavior/detach.js', + 'js/icinga/behavior/dropdown.js', + 'js/icinga/behavior/navigation.js', + 'js/icinga/behavior/form.js', + 'js/icinga/behavior/actiontable.js', + 'js/icinga/behavior/flyover.js', + 'js/icinga/behavior/filtereditor.js', + 'js/icinga/behavior/selectable.js', + 'js/icinga/behavior/modal.js', + 'js/icinga/behavior/input-enrichment.js', + 'js/icinga/behavior/datetime-picker.js' + ]; + + protected static $vendorFiles = []; + + protected static $baseFiles = [ + 'js/define.js' + ]; + + public static function sendMinified() + { + self::send(true); + } + + /** + * Send the client side script code to the client + * + * Does not cache the client side script code if the HTTP header Cache-Control or Pragma is set to no-cache. + * + * @param bool $minified Whether to compress the client side script code + */ + public static function send($minified = false) + { + header('Content-Type: application/javascript'); + $basedir = Icinga::app()->getBootstrapDirectory(); + $moduleManager = Icinga::app()->getModuleManager(); + + $files = []; + $js = $out = ''; + $min = $minified ? '.min' : ''; + + // Prepare vendor file list + $vendorFiles = []; + foreach (self::$vendorFiles as $file) { + $filePath = $basedir . '/' . $file . $min . '.js'; + $vendorFiles[] = $filePath; + $files[] = $filePath; + } + + // Prepare base file list + $baseFiles = []; + foreach (self::$baseFiles as $file) { + $filePath = $basedir . '/' . $file; + $baseFiles[] = $filePath; + $files[] = $filePath; + } + + // Prepare library file list + foreach (Icinga::app()->getLibraries() as $library) { + $files = array_merge($files, $library->getJsAssets()); + } + + // Prepare core file list + $coreFiles = []; + foreach (self::$jsFiles as $file) { + $filePath = $basedir . '/' . $file; + $coreFiles[] = $filePath; + $files[] = $filePath; + } + + $moduleFiles = []; + foreach ($moduleManager->getLoadedModules() as $name => $module) { + if ($module->hasJs()) { + $jsDir = $module->getJsDir(); + foreach ($module->getJsFiles() as $path) { + if (file_exists($path)) { + $moduleFiles[$name][$jsDir][] = $path; + $files[] = $path; + } + } + } + } + + $request = Icinga::app()->getRequest(); + $noCache = $request->getHeader('Cache-Control') === 'no-cache' || $request->getHeader('Pragma') === 'no-cache'; + + header('Cache-Control: public,no-cache,must-revalidate'); + + if (! $noCache && FileCache::etagMatchesFiles($files)) { + header("HTTP/1.1 304 Not Modified"); + return; + } else { + $etag = FileCache::etagForFiles($files); + } + + header('ETag: "' . $etag . '"'); + header('Content-Type: application/javascript'); + + $cacheFile = 'icinga-' . $etag . $min . '.js'; + $cache = FileCache::instance(); + if (! $noCache && $cache->has($cacheFile)) { + $cache->send($cacheFile); + return; + } + + // We do not minify vendor files + foreach ($vendorFiles as $file) { + $out .= ';' . ltrim(trim(file_get_contents($file)), ';') . "\n"; + } + + $baseJs = ''; + foreach ($baseFiles as $file) { + $baseJs .= file_get_contents($file) . "\n\n\n"; + } + + // Library files need to be namespaced first before they can be included + foreach (Icinga::app()->getLibraries() as $library) { + foreach ($library->getJsAssets() as $file) { + $alreadyMinified = false; + if ($minified && file_exists(($minFile = substr($file, 0, -3) . '.min.js'))) { + $alreadyMinified = true; + $file = $minFile; + } + + $content = self::optimizeDefine( + file_get_contents($file), + $file, + $library->getJsAssetPath(), + $library->getName() + ); + + if ($alreadyMinified) { + $out .= ';' . ltrim(trim($content), ';') . "\n"; + } else { + $js .= $content . "\n\n\n"; + } + } + } + + foreach ($coreFiles as $file) { + $js .= file_get_contents($file) . "\n\n\n"; + } + + foreach ($moduleFiles as $name => $paths) { + foreach ($paths as $basePath => $filePaths) { + foreach ($filePaths as $file) { + $content = self::optimizeDefine(file_get_contents($file), $file, $basePath, $name); + if (substr($file, -7, 7) === '.min.js') { + $out .= ';' . ltrim(trim($content), ';') . "\n"; + } else { + $js .= $content . "\n\n\n"; + } + } + } + } + + if ($minified) { + require_once 'JShrink/Minifier.php'; + $out .= Minifier::minify($js, ['flaggedComments' => false]); + $baseOut = Minifier::minify($baseJs, ['flaggedComments' => false]); + $out = ';' . ltrim($baseOut, ';') . "\n" . $out; + } else { + $out = $baseJs . $out . $js; + } + + $cache->store($cacheFile, $out); + echo $out; + } + + /** + * Optimize define() calls in the given JS + * + * @param string $js + * @param string $filePath + * @param string $basePath + * @param string $packageName + * + * @return string + */ + public static function optimizeDefine($js, $filePath, $basePath, $packageName) + { + if (! preg_match(self::DEFINE_RE, $js, $match) || strpos($js, 'define.amd') !== false) { + return $js; + } + + try { + $assetName = $match[1] ? Json::decode($match[1]) : ''; + if (! $assetName) { + $assetName = explode('.', basename($filePath))[0]; + } + + $assetName = join(DIRECTORY_SEPARATOR, array_filter([ + $packageName, + ltrim(substr(dirname($filePath), strlen($basePath)), DIRECTORY_SEPARATOR), + $assetName + ])); + + $assetName = Json::encode($assetName, JSON_UNESCAPED_SLASHES); + } catch (JsonDecodeException $_) { + $assetName = $match[1]; + Logger::debug('Can\'t optimize name of "%s". Are single quotes used instead of double quotes?', $filePath); + } + + try { + $dependencies = $match[2] ? Json::decode($match[2]) : []; + foreach ($dependencies as &$dependencyName) { + if ($dependencyName === 'exports') { + // exports is a special keyword and doesn't need optimization + continue; + } + + if (preg_match('~^((?:\.\.?/)+)*(.*)~', $dependencyName, $natch)) { + $dependencyName = join(DIRECTORY_SEPARATOR, array_filter([ + $packageName, + ltrim(substr( + realpath(join(DIRECTORY_SEPARATOR, [dirname($filePath), $natch[1]])), + strlen(realpath($basePath)) + ), DIRECTORY_SEPARATOR), + $natch[2] + ])); + } + } + + $dependencies = Json::encode($dependencies, JSON_UNESCAPED_SLASHES); + } catch (JsonDecodeException $_) { + $dependencies = $match[2]; + Logger::debug( + 'Can\'t optimize dependencies of "%s". Are single quotes used instead of double quotes?', + $filePath + ); + } + + return str_replace($match[0], sprintf("define(%s, %s, %s", $assetName, $dependencies, $match[3]), $js); + } +} diff --git a/library/Icinga/Web/LessCompiler.php b/library/Icinga/Web/LessCompiler.php new file mode 100644 index 0000000..1f72560 --- /dev/null +++ b/library/Icinga/Web/LessCompiler.php @@ -0,0 +1,257 @@ +lessc = new LessParser(); + // Discourage usage of import because we're caching based on an explicit list of LESS files to compile + $this->lessc->importDisabled = true; + } + + /** + * Add a Web 2 LESS file + * + * @param string $lessFile Path to the LESS file + * + * @return $this + */ + public function addLessFile($lessFile) + { + $this->lessFiles[] = realpath($lessFile); + return $this; + } + + /** + * Add a module LESS file + * + * @param string $moduleName Name of the module + * @param string $lessFile Path to the LESS file + * + * @return $this + */ + public function addModuleLessFile($moduleName, $lessFile) + { + if (! isset($this->moduleLessFiles[$moduleName])) { + $this->moduleLessFiles[$moduleName] = array(); + } + $this->moduleLessFiles[$moduleName][] = realpath($lessFile); + return $this; + } + + /** + * Get the list of LESS files added to the compiler + * + * @return string[] + */ + public function getLessFiles() + { + $lessFiles = $this->lessFiles; + + foreach ($this->moduleLessFiles as $moduleLessFiles) { + $lessFiles = array_merge($lessFiles, $moduleLessFiles); + } + + if ($this->theme !== null) { + $lessFiles[] = $this->theme; + } + + if ($this->themeMode !== null) { + $lessFiles[] = $this->themeMode; + } + + return $lessFiles; + } + + /** + * Set the path to the LESS theme + * + * @param ?string $theme Path to the LESS theme + * + * @return $this + */ + public function setTheme($theme) + { + if ($theme === null || (is_file($theme) && is_readable($theme))) { + $this->theme = $theme; + } else { + Logger::error('Can\t load theme %s. Make sure that the theme exists and is readable', $theme); + } + return $this; + } + + /** + * Set the path to the LESS theme mode + * + * @param string $themeMode Path to the LESS theme mode + * + * @return $this + */ + public function setThemeMode($themeMode) + { + if (is_file($themeMode) && is_readable($themeMode)) { + $this->themeMode = $themeMode; + } else { + Logger::error('Can\t load theme mode %s. Make sure that the theme mode exists and is readable', $themeMode); + } + return $this; + } + + /** + * Instruct the compiler to minify CSS + * + * @return $this + */ + public function compress() + { + $this->lessc->setFormatter('compressed'); + return $this; + } + + /** + * Render to CSS + * + * @return string + */ + public function render() + { + foreach ($this->lessFiles as $lessFile) { + $this->source .= file_get_contents($lessFile); + } + + $moduleCss = ''; + $exportedVars = []; + foreach ($this->moduleLessFiles as $moduleName => $moduleLessFiles) { + $moduleCss .= '.icinga-module.module-' . $moduleName . ' {'; + + foreach ($moduleLessFiles as $moduleLessFile) { + $content = file_get_contents($moduleLessFile); + + $pattern = '/^@exports:\s*{((?:\s*@[^:}]+:[^;]*;\s+)+)};$/m'; + if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $content = str_replace($match[0], '', $content); + foreach (explode("\n", trim($match[1])) as $line) { + list($name, $value) = explode(':', $line, 2); + $exportedVars[trim($name)] = trim($value, ' ;'); + } + } + } + + $moduleCss .= $content; + } + + $moduleCss .= '}'; + } + + $this->source .= $moduleCss; + + $varExports = ''; + foreach ($exportedVars as $name => $value) { + $varExports .= sprintf("%s: %s;\n", $name, $value); + } + + // exported vars are injected at the beginning to avoid that they are + // able to override other variables, that's what themes are for + $this->source = $varExports . "\n\n" . $this->source; + + if ($this->theme !== null) { + $this->source .= file_get_contents($this->theme); + } + + if ($this->themeMode !== null) { + $this->source .= file_get_contents($this->themeMode); + } + + try { + return preg_replace( + '/(\.icinga-module\.module-[^\s]+) (#layout\.[^\s]+)/m', + '\2 \1', + $this->lessc->compile($this->source) + ); + } catch (Less_Exception_Parser $e) { + $excerpt = substr($this->source, $e->index - 500, 1000); + + $lines = []; + $found = false; + $pos = $e->index - 500; + foreach (explode("\n", $excerpt) as $i => $line) { + if ($i === 0) { + $pos += strlen($line); + $lines[] = '.. ' . $line; + } else { + $pos += strlen($line) + 1; + $sep = ' '; + if (! $found && $pos > $e->index) { + $found = true; + $sep = '!! '; + } + + $lines[] = $sep . $line; + } + } + + $lines[] = '..'; + $excerpt = join("\n", $lines); + + return sprintf("%s\n%s\n\n\n%s", $e->getMessage(), $e->getTraceAsString(), $excerpt); + } + } +} diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php new file mode 100644 index 0000000..dc1cdc8 --- /dev/null +++ b/library/Icinga/Web/Menu.php @@ -0,0 +1,152 @@ +init(); + $this->load('menu-item'); + } + + /** + * Setup the main menu + */ + public function init() + { + $this->addItem('dashboard', [ + 'label' => t('Dashboard'), + 'url' => 'dashboard', + 'icon' => 'dashboard', + 'priority' => 10 + ]); + $this->addItem('system', [ + 'cssClass' => 'system-nav-item', + 'label' => t('System'), + 'icon' => 'services', + 'priority' => 700, + 'renderer' => [ + 'SummaryNavigationItemRenderer', + 'state' => 'critical' + ], + 'children' => [ + 'about' => [ + 'icon' => 'info', + 'description' => t('Open about page'), + 'label' => t('About'), + 'url' => 'about', + 'priority' => 700 + ], + 'health' => [ + 'icon' => 'eye', + 'description' => t('Open health overview'), + 'label' => t('Health'), + 'url' => 'health', + 'priority' => 710, + 'renderer' => 'HealthNavigationRenderer' + ], + 'announcements' => [ + 'icon' => 'megaphone', + 'description' => t('List announcements'), + 'label' => t('Announcements'), + 'url' => 'announcements', + 'priority' => 720 + ], + 'sessions' => [ + 'icon' => 'host', + 'description' => t('List of users who stay logged in'), + 'label' => t('User Sessions'), + 'permission' => 'application/sessions', + 'url' => 'manage-user-devices', + 'priority' => 730 + ] + ] + ]); + $this->addItem('configuration', [ + 'cssClass' => 'configuration-nav-item', + 'label' => t('Configuration'), + 'icon' => 'wrench', + 'permission' => 'config/*', + 'priority' => 800, + 'children' => [ + 'application' => [ + 'icon' => 'wrench', + 'description' => t('Open application configuration'), + 'label' => t('Application'), + 'url' => 'config', + 'priority' => 810 + ], + 'authentication' => [ + 'icon' => 'users', + 'description' => t('Open access control configuration'), + 'label' => t('Access Control'), + 'permission' => 'config/access-control/*', + 'priority' => 830, + 'url' => 'role' + ], + 'navigation' => [ + 'icon' => 'sitemap', + 'description' => t('Open shared navigation configuration'), + 'label' => t('Shared Navigation'), + 'url' => 'navigation/shared', + 'permission' => 'config/navigation', + 'priority' => 840, + ], + 'modules' => [ + 'icon' => 'cubes', + 'description' => t('Open module configuration'), + 'label' => t('Modules'), + 'url' => 'config/modules', + 'permission' => 'config/modules', + 'priority' => 890 + ] + ] + ]); + $this->addItem('user', [ + 'cssClass' => 'user-nav-item', + 'label' => Auth::getInstance()->getUser()->getUsername(), + 'icon' => 'user', + 'priority' => 900, + 'children' => [ + 'account' => [ + 'icon' => 'sliders', + 'description' => t('Open your account preferences'), + 'label' => t('My Account'), + 'priority' => 100, + 'url' => 'account' + ], + 'logout' => [ + 'icon' => 'off', + 'description' => t('Log out'), + 'label' => t('Logout'), + 'priority' => 200, + 'attributes' => ['target' => '_self'], + 'url' => 'authentication/logout' + ] + ] + ]); + + if (Logger::writesToFile()) { + $this->getItem('system')->addChild($this->createItem('application_log', [ + 'icon' => 'doc-text', + 'description' => t('Open Application Log'), + 'label' => t('Application Log'), + 'url' => 'list/applicationlog', + 'permission' => 'application/log', + 'priority' => 900 + ])); + } + } +} diff --git a/library/Icinga/Web/Navigation/ConfigMenu.php b/library/Icinga/Web/Navigation/ConfigMenu.php new file mode 100644 index 0000000..db3f566 --- /dev/null +++ b/library/Icinga/Web/Navigation/ConfigMenu.php @@ -0,0 +1,302 @@ + 'nav']; + + protected $children; + + protected $selected; + + protected $state; + + public function __construct() + { + $this->children = [ + 'system' => [ + 'title' => t('System'), + 'items' => [ + 'about' => [ + 'label' => t('About'), + 'url' => 'about' + ], + 'health' => [ + 'label' => t('Health'), + 'url' => 'health', + ], + 'announcements' => [ + 'label' => t('Announcements'), + 'url' => 'announcements' + ], + 'sessions' => [ + 'label' => t('User Sessions'), + 'permission' => 'application/sessions', + 'url' => 'manage-user-devices' + ] + ] + ], + 'configuration' => [ + 'title' => t('Configuration'), + 'permission' => 'config/*', + 'items' => [ + 'application' => [ + 'label' => t('Application'), + 'url' => 'config/general' + ], + 'authentication' => [ + 'label' => t('Access Control'), + 'permission' => 'config/access-control/*', + 'url' => 'role/list' + ], + 'navigation' => [ + 'label' => t('Shared Navigation'), + 'permission' => 'config/navigation', + 'url' => 'navigation/shared' + ], + 'modules' => [ + 'label' => t('Modules'), + 'permission' => 'config/modules', + 'url' => 'config/modules' + ] + ] + ], + 'logout' => [ + 'items' => [ + 'logout' => [ + 'label' => t('Logout'), + 'atts' => [ + 'target' => '_self', + 'class' => 'nav-item-logout' + ], + 'url' => 'authentication/logout' + ] + ] + ] + ]; + + if (Logger::writesToFile()) { + $this->children['system']['items']['application_log'] = [ + 'label' => t('Application Log'), + 'url' => 'list/applicationlog', + 'permission' => 'application/log' + ]; + } + } + + protected function assembleUserMenuItem(BaseHtmlElement $userMenuItem) + { + $username = Auth::getInstance()->getUser()->getUsername(); + + $userMenuItem->add( + new HtmlElement( + 'a', + Attributes::create(['href' => Url::fromPath('account')]), + new HtmlElement( + 'i', + Attributes::create(['class' => 'user-ball']), + Text::create($username[0]) + ), + Text::create($username) + ) + ); + + if (Icinga::app()->getRequest()->getUrl()->matches('account')) { + $userMenuItem->addAttributes(['class' => 'selected active']); + } + } + + protected function assembleCogMenuItem($cogMenuItem) + { + $cogMenuItem->add([ + HtmlElement::create( + 'button', + null, + [ + new Icon('cog'), + $this->createHealthBadge(), + ] + ), + $this->createLevel2Menu() + ]); + } + + protected function assembleLevel2Nav(BaseHtmlElement $level2Nav) + { + $navContent = HtmlElement::create('div', ['class' => 'flyout-content']); + foreach ($this->children as $c) { + if (isset($c['permission']) && ! Auth::getInstance()->hasPermission($c['permission'])) { + continue; + } + + if (isset($c['title'])) { + $navContent->add(HtmlElement::create( + 'h3', + null, + $c['title'] + )); + } + + $ul = HtmlElement::create('ul', ['class' => 'nav']); + foreach ($c['items'] as $key => $item) { + $ul->add($this->createLevel2MenuItem($item, $key)); + } + + $navContent->add($ul); + } + + $level2Nav->add($navContent); + } + + protected function getHealthCount() + { + $count = 0; + $title = null; + $worstState = null; + foreach (HealthHook::collectHealthData()->select() as $result) { + if ($worstState === null || $result->state > $worstState) { + $worstState = $result->state; + $title = $result->message; + $count = 1; + } elseif ($worstState === $result->state) { + $count++; + } + } + + switch ($worstState) { + case HealthHook::STATE_OK: + $count = 0; + break; + case HealthHook::STATE_WARNING: + $this->state = self::STATE_WARNING; + break; + case HealthHook::STATE_CRITICAL: + $this->state = self::STATE_CRITICAL; + break; + case HealthHook::STATE_UNKNOWN: + $this->state = self::STATE_UNKNOWN; + break; + } + + $this->title = $title; + + return $count; + } + + protected function isSelectedItem($item) + { + if ($item !== null && Icinga::app()->getRequest()->getUrl()->matches($item['url'])) { + $this->selected = $item; + return true; + } + + return false; + } + + protected function createHealthBadge() + { + $stateBadge = null; + if ($this->getHealthCount() > 0) { + $stateBadge = new StateBadge($this->getHealthCount(), $this->state); + $stateBadge->addAttributes(['class' => 'disabled']); + } + + return $stateBadge; + } + + protected function createLevel2Menu() + { + $level2Nav = HtmlElement::create( + 'div', + Attributes::create(['class' => 'nav-level-1 flyout']) + ); + + $this->assembleLevel2Nav($level2Nav); + + return $level2Nav; + } + + protected function createLevel2MenuItem($item, $key) + { + if (isset($item['permission']) && ! Auth::getInstance()->hasPermission($item['permission'])) { + return null; + } + + $healthBadge = null; + $class = null; + if ($key === 'health') { + $class = 'badge-nav-item'; + $healthBadge = $this->createHealthBadge(); + } + + $li = HtmlElement::create( + 'li', + isset($item['atts']) ? $item['atts'] : [], + [ + HtmlElement::create( + 'a', + Attributes::create(['href' => Url::fromPath($item['url'])]), + [ + $item['label'], + isset($healthBadge) ? $healthBadge : '' + ] + ), + ] + ); + $li->addAttributes(['class' => $class]); + + if ($this->isSelectedItem($item)) { + $li->addAttributes(['class' => 'selected']); + } + + return $li; + } + + protected function createUserMenuItem() + { + $userMenuItem = HtmlElement::create('li', ['class' => 'user-nav-item']); + + $this->assembleUserMenuItem($userMenuItem); + + return $userMenuItem; + } + + protected function createCogMenuItem() + { + $cogMenuItem = HtmlElement::create('li', ['class' => 'config-nav-item']); + + $this->assembleCogMenuItem($cogMenuItem); + + return $cogMenuItem; + } + + protected function assemble() + { + $this->add([ + $this->createUserMenuItem(), + $this->createCogMenuItem() + ]); + } +} diff --git a/library/Icinga/Web/Navigation/DashboardPane.php b/library/Icinga/Web/Navigation/DashboardPane.php new file mode 100644 index 0000000..71b3215 --- /dev/null +++ b/library/Icinga/Web/Navigation/DashboardPane.php @@ -0,0 +1,84 @@ +dashlets = $dashlets; + return $this; + } + + /** + * Return this pane's dashlets + * + * @param bool $ordered Whether to order the dashlets first + * + * @return array + */ + public function getDashlets($ordered = true) + { + if ($this->dashlets === null) { + return array(); + } + + if ($ordered) { + $dashlets = $this->dashlets; + ksort($dashlets); + return $dashlets; + } + + return $this->dashlets; + } + + /** + * {@inheritdoc} + */ + public function init() + { + $this->setUrl(Url::fromPath('dashboard', array('pane' => $this->getName()))); + } + + /** + * Set disabled state for pane + * + * @param bool $disabled + */ + public function setDisabled($disabled = true) + { + $this->disabled = (bool) $disabled; + } + + /** + * Get disabled state for pane + * + * @return bool + */ + public function getDisabled() + { + return $this->disabled; + } +} diff --git a/library/Icinga/Web/Navigation/DropdownItem.php b/library/Icinga/Web/Navigation/DropdownItem.php new file mode 100644 index 0000000..2342b96 --- /dev/null +++ b/library/Icinga/Web/Navigation/DropdownItem.php @@ -0,0 +1,20 @@ +children->setLayout(Navigation::LAYOUT_DROPDOWN); + } +} diff --git a/library/Icinga/Web/Navigation/Navigation.php b/library/Icinga/Web/Navigation/Navigation.php new file mode 100644 index 0000000..8cf3f68 --- /dev/null +++ b/library/Icinga/Web/Navigation/Navigation.php @@ -0,0 +1,571 @@ +items[$offset]); + } + + public function offsetGet($offset): ?NavigationItem + { + return $this->items[$offset] ?? null; + } + + public function offsetSet($offset, $value): void + { + $this->items[$offset] = $value; + } + + public function offsetUnset($offset): void + { + unset($this->items[$offset]); + } + + public function count(): int + { + return count($this->items); + } + + public function getIterator(): Traversable + { + $this->order(); + return new ArrayIterator($this->items); + } + + /** + * Create and return a new navigation item for the given configuration + * + * @param string $name + * @param array|ConfigObject $properties + * + * @return NavigationItem + * + * @throws InvalidArgumentException If the $properties argument is neither an array nor a ConfigObject + */ + public function createItem($name, $properties) + { + if ($properties instanceof ConfigObject) { + $properties = $properties->toArray(); + } elseif (! is_array($properties)) { + throw new InvalidArgumentException('Argument $properties must be of type array or ConfigObject'); + } + + $itemType = isset($properties['type']) ? StringHelper::cname($properties['type'], '-') : 'NavigationItem'; + if (! empty(static::$types) && isset(static::$types[$itemType])) { + return new static::$types[$itemType]($name, $properties); + } + + $item = null; + foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) { + $classPath = 'Icinga\\Module\\' + . ucfirst($module->getName()) + . '\\' + . static::NAVIGATION_NS + . '\\' + . $itemType; + if (class_exists($classPath)) { + $item = new $classPath($name, $properties); + break; + } + } + + if ($item === null) { + $classPath = 'Icinga\\' . static::NAVIGATION_NS . '\\' . $itemType; + if (class_exists($classPath)) { + $item = new $classPath($name, $properties); + } + } + + if ($item === null) { + if ($itemType !== 'MenuItem') { + Logger::debug( + 'Failed to find custom navigation item class %s for item %s. Using base class NavigationItem now', + $itemType, + $name + ); + } + + $item = new NavigationItem($name, $properties); + static::$types[$itemType] = 'Icinga\\Web\\Navigation\\NavigationItem'; + } elseif (! $item instanceof NavigationItem) { + throw new ProgrammingError('Class %s must inherit from NavigationItem', $classPath); + } else { + static::$types[$itemType] = $classPath; + } + + return $item; + } + + /** + * Add a navigation item + * + * If you do not pass an instance of NavigationItem, this will only add the item + * if it does not require a permission or the current user has the permission. + * + * @param string|NavigationItem $name The name of the item or an instance of NavigationItem + * @param array $properties The properties of the item to add (Ignored if $name is not a string) + * + * @return bool Whether the item was added or not + * + * @throws InvalidArgumentException In case $name is neither a string nor an instance of NavigationItem + */ + public function addItem($name, array $properties = array()) + { + if (is_string($name)) { + if (isset($properties['permission'])) { + if (! Auth::getInstance()->hasPermission($properties['permission'])) { + return false; + } + + unset($properties['permission']); + } + + $item = $this->createItem($name, $properties); + } elseif (! $name instanceof NavigationItem) { + throw new InvalidArgumentException('Argument $name must be of type string or NavigationItem'); + } else { + $item = $name; + } + + $this->items[$item->getName()] = $item; + return true; + } + + /** + * Return the item with the given name + * + * @param string $name + * @param mixed $default + * + * @return NavigationItem|mixed + */ + public function getItem($name, $default = null) + { + return isset($this->items[$name]) ? $this->items[$name] : $default; + } + + /** + * Return the currently active item or the first one if none is active + * + * @return NavigationItem + */ + public function getActiveItem() + { + foreach ($this->items as $item) { + if ($item->getActive()) { + return $item; + } + } + + $firstItem = reset($this->items); + return $firstItem ? $firstItem->setActive() : null; + } + + /** + * Return this navigation's items + * + * @return array + */ + public function getItems() + { + return $this->items; + } + + /** + * Return whether this navigation is empty + * + * @return bool + */ + public function isEmpty() + { + return empty($this->items); + } + + /** + * Return whether this navigation has any renderable items + * + * @return bool + */ + public function hasRenderableItems() + { + foreach ($this->getItems() as $item) { + if ($item->shouldRender()) { + return true; + } + } + + return false; + } + + /** + * Return this navigation's layout + * + * @return int + */ + public function getLayout() + { + return $this->layout; + } + + /** + * Set this navigation's layout + * + * @param int $layout + * + * @return $this + */ + public function setLayout($layout) + { + $this->layout = (int) $layout; + return $this; + } + + /** + * Create and return the renderer for this navigation + * + * @return RecursiveNavigationRenderer + */ + public function getRenderer() + { + return new RecursiveNavigationRenderer($this); + } + + /** + * Return this navigation rendered to HTML + * + * @return string + */ + public function render() + { + return $this->getRenderer()->render(); + } + + /** + * Order this navigation's items + * + * @return $this + */ + public function order() + { + uasort($this->items, array($this, 'compareItems')); + foreach ($this->items as $item) { + if ($item->hasChildren()) { + $item->getChildren()->order(); + } + } + + return $this; + } + + /** + * Return whether the first item is less than, more than or equal to the second one + * + * @param NavigationItem $a + * @param NavigationItem $b + * + * @return int + */ + protected function compareItems(NavigationItem $a, NavigationItem $b) + { + if ($a->getPriority() === $b->getPriority()) { + return strcasecmp($a->getLabel(), $b->getLabel()); + } + + return $a->getPriority() > $b->getPriority() ? 1 : -1; + } + + /** + * Try to find and return a item with the given or a similar name + * + * @param string $name + * + * @return NavigationItem + */ + public function findItem($name) + { + $item = $this->getItem($name); + if ($item !== null) { + return $item; + } + + $loweredName = strtolower($name); + foreach ($this->getItems() as $item) { + if (strtolower($item->getName()) === $loweredName) { + return $item; + } + } + } + + /** + * Merge this navigation with the given one + * + * Any duplicate items of this navigation will be overwritten by the given navigation's items. + * + * @param Navigation $navigation + * + * @return $this + */ + public function merge(Navigation $navigation) + { + foreach ($navigation as $item) { + /** @var $item NavigationItem */ + if (($existingItem = $this->findItem($item->getName())) !== null) { + if ($existingItem->conflictsWith($item)) { + $name = $item->getName(); + do { + if (preg_match('~_(\d+)$~', $name, $matches)) { + $name = preg_replace('~_\d+$~', $matches[1] + 1, $name); + } else { + $name .= '_2'; + } + } while ($this->getItem($name) !== null); + + $this->addItem($item->setName($name)); + } else { + $existingItem->merge($item); + } + } else { + $this->addItem($item); + } + } + + return $this; + } + + /** + * Extend this navigation set with all additional items of the given type + * + * This will fetch navigation items from the following sources: + * * User Shareables + * * User Preferences + * * Modules + * Any existing entry will be overwritten by one that is coming later in order. + * + * @param string $type + * + * @return $this + */ + public function load($type) + { + $user = Auth::getInstance()->getUser(); + if ($type !== 'dashboard-pane') { + // Shareables + $this->merge(Icinga::app()->getSharedNavigation($type)); + + // User Preferences + $this->merge($user->getNavigation($type)); + } + + // Modules + $moduleManager = Icinga::app()->getModuleManager(); + foreach ($moduleManager->getLoadedModules() as $module) { + if ($user->can($moduleManager::MODULE_PERMISSION_NS . $module->getName())) { + if ($type === 'menu-item') { + $this->merge($module->getMenu()); + } elseif ($type === 'dashboard-pane') { + $this->merge($module->getDashboard()); + } + } + } + + return $this; + } + + /** + * Return the global navigation item type configuration + * + * @return array + */ + public static function getItemTypeConfiguration() + { + $defaultItemTypes = array( + 'menu-item' => array( + 'label' => t('Menu Entry'), + 'config' => 'menu' + )/*, // Disabled, until it is able to fully replace the old implementation + 'dashlet' => array( + 'label' => 'Dashlet', + 'config' => 'dashboard' + )*/ + ); + + $moduleItemTypes = array(); + $moduleManager = Icinga::app()->getModuleManager(); + foreach ($moduleManager->getLoadedModules() as $module) { + if (Auth::getInstance()->hasPermission($moduleManager::MODULE_PERMISSION_NS . $module->getName())) { + foreach ($module->getNavigationItems() as $type => $options) { + if (! isset($moduleItemTypes[$type])) { + $moduleItemTypes[$type] = $options; + } + } + } + } + + return array_merge($defaultItemTypes, $moduleItemTypes); + } + + /** + * Create and return a new set of navigation items for the given configuration + * + * Note that this is supposed to be utilized for one dimensional structures + * only. Multi dimensional structures can be processed by fromArray(). + * + * @param Traversable|array $config + * + * @return Navigation + * + * @throws InvalidArgumentException In case the given configuration is invalid + * @throws ConfigurationError In case a referenced parent does not exist + */ + public static function fromConfig($config) + { + if (! is_array($config) && !$config instanceof Traversable) { + throw new InvalidArgumentException('Argument $config must be an array or a instance of Traversable'); + } + + $flattened = $orphans = $topLevel = array(); + foreach ($config as $sectionName => $sectionConfig) { + $parentName = $sectionConfig->parent; + unset($sectionConfig->parent); + + if (! $parentName) { + $topLevel[$sectionName] = $sectionConfig->toArray(); + $flattened[$sectionName] = & $topLevel[$sectionName]; + } elseif (isset($flattened[$parentName])) { + $flattened[$parentName]['children'][$sectionName] = $sectionConfig->toArray(); + $flattened[$sectionName] = & $flattened[$parentName]['children'][$sectionName]; + } else { + $orphans[$parentName][$sectionName] = $sectionConfig->toArray(); + $flattened[$sectionName] = & $orphans[$parentName][$sectionName]; + } + } + + do { + $match = false; + foreach ($orphans as $parentName => $children) { + if (isset($flattened[$parentName])) { + if (isset($flattened[$parentName]['children'])) { + $flattened[$parentName]['children'] = array_merge( + $flattened[$parentName]['children'], + $children + ); + } else { + $flattened[$parentName]['children'] = $children; + } + + unset($orphans[$parentName]); + $match = true; + } + } + } while ($match && !empty($orphans)); + + if (! empty($orphans)) { + throw new ConfigurationError( + t( + 'Failed to fully parse navigation configuration. Ensure that' + . ' all referenced parents are existing navigation items: %s' + ), + join(', ', array_keys($orphans)) + ); + } + + return static::fromArray($topLevel); + } + + /** + * Create and return a new set of navigation items for the given array + * + * @param array $array + * + * @return Navigation + */ + public static function fromArray(array $array) + { + $navigation = new static(); + foreach ($array as $name => $properties) { + $navigation->addItem((string) $name, $properties); + } + + return $navigation; + } + + /** + * Return this navigation rendered to HTML + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return IcingaException::describe($e); + } + } +} diff --git a/library/Icinga/Web/Navigation/NavigationItem.php b/library/Icinga/Web/Navigation/NavigationItem.php new file mode 100644 index 0000000..e262f5a --- /dev/null +++ b/library/Icinga/Web/Navigation/NavigationItem.php @@ -0,0 +1,947 @@ +setName($name); + $this->children = new Navigation(); + + if (! empty($properties)) { + $this->setProperties($properties); + } + + $this->init(); + } + + /** + * Initialize this NavigationItem + */ + public function init() + { + } + + /** + * @return Navigation + */ + public function getIterator(): Traversable + { + return $this->getChildren(); + } + + /** + * Return whether this item is active + * + * @return bool + */ + public function getActive() + { + if ($this->active === null) { + $this->active = false; + if ($this->getUrl() !== null && Icinga::app()->getRequest()->getUrl()->matches($this->getUrl())) { + $this->setActive(); + } elseif ($this->hasChildren()) { + foreach ($this->getChildren() as $item) { + /** @var NavigationItem $item */ + if ($item->getActive()) { + // Do nothing, a true active state is automatically passed to all parents + } + } + } + } + + return $this->active; + } + + /** + * Set whether this item is active + * + * If it's active and has a parent, the parent gets activated as well. + * + * @param bool $active + * + * @return $this + */ + public function setActive($active = true) + { + $this->active = (bool) $active; + if ($this->active && $this->getParent() !== null) { + $this->getParent()->setActive(); + } + + return $this; + } + + /** + * Return whether this item is selected + * + * @return bool + */ + public function getSelected() + { + if ($this->selected === null) { + $this->active = false; + if ($this->getUrl() !== null && Icinga::app()->getRequest()->getUrl()->matches($this->getUrl())) { + $this->setSelected(); + } + } + + return $this->selected; + } + + /** + * Set whether this item is active + * + * If it's active and has a parent, the parent gets activated as well. + * + * @param bool $selected + * + * @return $this + */ + public function setSelected($selected = true) + { + $this->selected = (bool) $selected; + + return $this; + } + + /** + * Get the CSS class used for the outer li element + * + * @return string + */ + public function getCssClass() + { + return $this->cssClass; + } + + /** + * Set the CSS class to use for the outer li element + * + * @param string $class + * + * @return $this + */ + public function setCssClass($class) + { + $this->cssClass = (string) $class; + return $this; + } + + /** + * Return this item's priority + * + * @return int + */ + public function getPriority() + { + return $this->priority !== null ? $this->priority : 100; + } + + /** + * Set this item's priority + * + * @param int $priority + * + * @return $this + */ + public function setPriority($priority) + { + $this->priority = (int) $priority; + return $this; + } + + /** + * Return the value of the given element attribute + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getAttribute($name, $default = null) + { + $attributes = $this->getAttributes(); + return array_key_exists($name, $attributes) ? $attributes[$name] : $default; + } + + /** + * Set the value of the given element attribute + * + * @param string $name + * @param mixed $value + * + * @return $this + */ + public function setAttribute($name, $value) + { + $this->attributes[$name] = $value; + return $this; + } + + /** + * Return the attributes of this item's element + * + * @return array + */ + public function getAttributes() + { + return $this->attributes ?: array(); + } + + /** + * Set the attributes of this item's element + * + * @param array $attributes + * + * @return $this + */ + public function setAttributes(array $attributes) + { + $this->attributes = $attributes; + return $this; + } + + /** + * Add a child to this item + * + * If the child is active this item gets activated as well. + * + * @param NavigationItem $child + * + * @return $this + */ + public function addChild(NavigationItem $child) + { + $this->getChildren()->addItem($child->setParent($this)); + if ($child->getActive()) { + $this->setActive(); + } + + return $this; + } + + /** + * Return this item's children + * + * @return Navigation + */ + public function getChildren() + { + return $this->children; + } + + /** + * Return whether this item has any children + * + * @return bool + */ + public function hasChildren() + { + return ! $this->getChildren()->isEmpty(); + } + + /** + * Set this item's children + * + * @param array|Navigation $children + * + * @return $this + */ + public function setChildren($children) + { + if (is_array($children)) { + $children = Navigation::fromArray($children); + } elseif (! $children instanceof Navigation) { + throw new InvalidArgumentException('Argument $children must be of type array or Navigation'); + } + + foreach ($children as $item) { + $item->setParent($this); + } + + $this->children = $children; + return $this; + } + + /** + * Return this item's icon + * + * @return string + */ + public function getIcon() + { + return $this->icon; + } + + /** + * Set this item's icon + * + * @param string $icon + * + * @return $this + */ + public function setIcon($icon) + { + $this->icon = $icon; + return $this; + } + + /** + * Return this item's name escaped with only ASCII chars and/or digits + * + * @return string + */ + protected function getEscapedName() + { + return preg_replace('~[^a-zA-Z0-9]~', '_', $this->getName()); + } + + /** + * Return a unique version of this item's name + * + * @return string + */ + public function getUniqueName() + { + if ($this->getParent() === null) { + return 'navigation-' . $this->getEscapedName(); + } + + return $this->getParent()->getUniqueName() . '-' . $this->getEscapedName(); + } + + /** + * Return this item's name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set this item's name + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Set this item's parent + * + * @param NavigationItem $parent + * + * @return $this + */ + public function setParent(NavigationItem $parent) + { + $this->parent = $parent; + return $this; + } + + /** + * Return this item's parent + * + * @return NavigationItem + */ + public function getParent() + { + return $this->parent; + } + + /** + * Return this item's label + * + * @return string + */ + public function getLabel() + { + return $this->label !== null ? $this->label : $this->getName(); + } + + /** + * Set this item's label + * + * @param string $label + * + * @return $this + */ + public function setLabel($label) + { + $this->label = $label; + return $this; + } + + /** + * Get the item's description + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Set the item's description + * + * @param string $description + * + * @return $this + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Set this item's url target + * + * @param string $target + * + * @return $this + */ + public function setTarget($target) + { + $this->target = $target; + return $this; + } + + /** + * Return this item's url target + * + * @return string + */ + public function getTarget() + { + return $this->target; + } + + /** + * Return this item's url + * + * @return Url + */ + public function getUrl() + { + if ($this->url === null && $this->hasChildren()) { + $this->setUrl(Url::fromPath('navigation/dashboard', array('name' => strtolower($this->getName())))); + } + + return $this->url; + } + + /** + * Set this item's url + * + * @param Url|string $url + * + * @return $this + * + * @throws InvalidArgumentException If the given url is neither of type + */ + public function setUrl($url) + { + if (is_string($url)) { + $url = Url::fromPath($this->resolveMacros($url)); + } elseif ($url instanceof Url) { + $url = Url::fromPath($this->resolveMacros($url->getAbsoluteUrl())); + } else { + throw new InvalidArgumentException('Argument $url must be of type string or Url'); + } + + $this->url = $url; + + return $this; + } + + /** + * Return the value of the given url parameter + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getUrlParameter($name, $default = null) + { + $parameters = $this->getUrlParameters(); + return isset($parameters[$name]) ? $parameters[$name] : $default; + } + + /** + * Set the value of the given url parameter + * + * @param string $name + * @param mixed $value + * + * @return $this + */ + public function setUrlParameter($name, $value) + { + $this->urlParameters[$name] = $value; + return $this; + } + + /** + * Return all additional parameters for this item's url + * + * @return array + */ + public function getUrlParameters() + { + return $this->urlParameters ?: array(); + } + + /** + * Set additional parameters for this item's url + * + * @param array $urlParameters + * + * @return $this + */ + public function setUrlParameters(array $urlParameters) + { + $this->urlParameters = $urlParameters; + return $this; + } + + /** + * Set this item's properties + * + * Unknown properties (no matching setter) are considered as element attributes. + * + * @param array $properties + * + * @return $this + */ + public function setProperties(array $properties) + { + foreach ($properties as $name => $value) { + $setter = 'set' . ucfirst($name); + if (method_exists($this, $setter)) { + $this->$setter($value); + } else { + $this->setAttribute($name, $value); + } + } + + return $this; + } + + /** + * Merge this item with the given one + * + * @param NavigationItem $item + * + * @return $this + */ + public function merge(NavigationItem $item) + { + if ($this->conflictsWith($item)) { + throw new ProgrammingError('Cannot merge, conflict detected.'); + } + + if ($this->priority === null) { + $priority = $item->getPriority(); + if ($priority !== 100) { + $this->setPriority($priority); + } + } + + if (! $this->getIcon()) { + $this->setIcon($item->getIcon()); + } + + if ($this->getLabel() === $this->getName() && $item->getLabel() !== $item->getName()) { + $this->setLabel($item->getLabel()); + } + + if ($this->target === null && ($target = $item->getTarget()) !== null) { + $this->setTarget($target); + } + + if ($this->renderer === null) { + $renderer = $item->getRenderer(); + if (get_class($renderer) !== 'NavigationItemRenderer') { + $this->setRenderer($renderer); + } + } + + foreach ($item->getAttributes() as $name => $value) { + $this->setAttribute($name, $value); + } + + foreach ($item->getUrlParameters() as $name => $value) { + $this->setUrlParameter($name, $value); + } + + if ($item->hasChildren()) { + $this->getChildren()->merge($item->getChildren()); + } + + return $this; + } + + /** + * Return whether it's possible to merge this item with the given one + * + * @param NavigationItem $item + * + * @return bool + */ + public function conflictsWith(NavigationItem $item) + { + if (! $item instanceof $this) { + return true; + } + + if ($this->getUrl() === null || $item->getUrl() === null) { + return false; + } + + return !$this->getUrl()->matches($item->getUrl()); + } + + /** + * Create and return the given renderer + * + * @param string|array $name + * + * @return NavigationItemRenderer + */ + protected function createRenderer($name) + { + if (is_array($name)) { + $options = array_splice($name, 1); + $name = $name[0]; + } else { + $options = array(); + } + + $renderer = null; + foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) { + $classPath = 'Icinga\\Module\\' . ucfirst($module->getName()) . '\\' . static::RENDERER_NS . '\\' . $name; + if (class_exists($classPath)) { + $renderer = new $classPath($options); + break; + } + } + + if ($renderer === null) { + $classPath = 'Icinga\\' . static::RENDERER_NS . '\\' . $name; + if (class_exists($classPath)) { + $renderer = new $classPath($options); + } + } + + if ($renderer === null) { + throw new ProgrammingError( + 'Cannot find renderer "%s" for navigation item "%s"', + $name, + $this->getName() + ); + } elseif (! $renderer instanceof NavigationItemRenderer) { + throw new ProgrammingError('Class %s must inherit from NavigationItemRenderer', $classPath); + } + + return $renderer; + } + + /** + * Set this item's renderer + * + * @param string|array|NavigationItemRenderer $renderer + * + * @return $this + * + * @throws InvalidArgumentException If the $renderer argument is neither a string nor a NavigationItemRenderer + */ + public function setRenderer($renderer) + { + if (is_string($renderer) || is_array($renderer)) { + $renderer = $this->createRenderer($renderer); + } elseif (! $renderer instanceof NavigationItemRenderer) { + throw new InvalidArgumentException( + 'Argument $renderer must be of type string, array or NavigationItemRenderer' + ); + } + + $this->renderer = $renderer; + return $this; + } + + /** + * Return this item's renderer + * + * @return NavigationItemRenderer + */ + public function getRenderer() + { + if ($this->renderer === null) { + $this->setRenderer('NavigationItemRenderer'); + } + + return $this->renderer; + } + + /** + * Set whether this item should be rendered + * + * @param bool $state + * + * @return $this + */ + public function setRender($state = true) + { + $this->render = (bool) $state; + return $this; + } + + /** + * Return whether this item should be rendered + * + * @return bool + */ + public function getRender() + { + if ($this->render === null) { + return $this->getUrl() !== null; + } + + return $this->render; + } + + /** + * Return whether this item should be rendered + * + * Alias for NavigationItem::getRender(). + * + * @return bool + */ + public function shouldRender() + { + return $this->getRender(); + } + + /** + * Return this item rendered to HTML + * + * @return string + */ + public function render() + { + try { + return $this->getRenderer()->setItem($this)->render(); + } catch (Exception $e) { + Logger::error( + 'Could not invoke custom navigation item renderer. %s in %s:%d with message: %s', + get_class($e), + $e->getFile(), + $e->getLine(), + $e->getMessage() + ); + + $renderer = new NavigationItemRenderer(); + return $renderer->render($this); + } + } + + /** + * Return this item rendered to HTML + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return IcingaException::describe($e); + } + } + + /** + * Resolve all macros in the given URL + * + * @param string $url + * + * @return string + */ + protected function resolveMacros($url) + { + if (strpos($url, '$') === false) { + return $url; + } + + $macros = []; + if (Auth::getInstance()->isAuthenticated()) { + $macros['$user.local_name$'] = Auth::getInstance()->getUser()->getLocalUsername(); + } + if (! empty($macros)) { + $url = str_replace(array_keys($macros), array_values($macros), $url); + } + + return $url; + } +} diff --git a/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php new file mode 100644 index 0000000..8510f70 --- /dev/null +++ b/library/Icinga/Web/Navigation/Renderer/BadgeNavigationItemRenderer.php @@ -0,0 +1,139 @@ +title = $title; + return $this; + } + + /** + * Return the tooltip text for the badge + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set the state identifier to use + * + * @param string $state + * + * @return $this + */ + public function setState($state) + { + $this->state = $state; + return $this; + } + + /** + * Return the state identifier to use + * + * @return string + */ + public function getState() + { + return $this->state; + } + + /** + * Return the amount of items represented by the badge + * + * @return int + */ + abstract public function getCount(); + + /** + * Render the given navigation item as HTML anchor with a badge + * + * @param NavigationItem $item + * + * @return string + */ + public function render(NavigationItem $item = null) + { + if ($item === null) { + $item = $this->getItem(); + } + + $cssClass = ''; + if ($item->getCssClass() !== null) { + $cssClass = ' ' . $item->getCssClass(); + } + + $item->setCssClass('badge-nav-item' . $cssClass); + $this->setEscapeLabel(false); + $label = $this->view()->escape($item->getLabel()); + $item->setLabel($this->renderBadge() . $label); + $html = parent::render($item); + return $html; + } + + /** + * Render the badge + * + * @return string + */ + protected function renderBadge() + { + if ($count = $this->getCount()) { + if ($count > 1000000) { + $count = round($count, -6) / 1000000 . 'M'; + } elseif ($count > 1000) { + $count = round($count, -3) / 1000 . 'k'; + } + + $view = $this->view(); + return sprintf( + '%s', + $view->escape($this->getTitle()), + $view->escape($this->getState()), + $count + ); + } + + return ''; + } +} diff --git a/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php new file mode 100644 index 0000000..577895b --- /dev/null +++ b/library/Icinga/Web/Navigation/Renderer/HealthNavigationRenderer.php @@ -0,0 +1,44 @@ +select() as $result) { + if ($worstState === null || $result->state > $worstState) { + $worstState = $result->state; + $title = $result->message; + $count = 1; + } elseif ($worstState === $result->state) { + $count++; + } + } + + switch ($worstState) { + case HealthHook::STATE_OK: + $count = 0; + break; + case HealthHook::STATE_WARNING: + $this->state = self::STATE_WARNING; + break; + case HealthHook::STATE_CRITICAL: + $this->state = self::STATE_CRITICAL; + break; + case HealthHook::STATE_UNKNOWN: + $this->state = self::STATE_UNKNOWN; + break; + } + + $this->title = $title; + + return $count; + } +} diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php new file mode 100644 index 0000000..74de8f6 --- /dev/null +++ b/library/Icinga/Web/Navigation/Renderer/NavigationItemRenderer.php @@ -0,0 +1,233 @@ +setOptions($options); + } + + $this->internalLinkTargets = array('_main', '_self', '_next'); + $this->init(); + } + + /** + * Initialize this renderer + */ + public function init() + { + } + + /** + * Set the given options + * + * @param array $options + * + * @return $this + */ + public function setOptions(array $options) + { + foreach ($options as $name => $value) { + $setter = 'set' . StringHelper::cname($name); + if (method_exists($this, $setter)) { + $this->$setter($value); + } + } + } + + /** + * Set the view + * + * @param View $view + * + * @return $this + */ + public function setView(View $view) + { + $this->view = $view; + return $this; + } + + /** + * Return the view + * + * @return View + */ + public function view() + { + if ($this->view === null) { + $this->setView(Icinga::app()->getViewRenderer()->view); + } + + return $this->view; + } + + /** + * Set the navigation item to render + * + * @param NavigationItem $item + * + * @return $this + */ + public function setItem(NavigationItem $item) + { + $this->item = $item; + return $this; + } + + /** + * Return the navigation item being rendered + * + * @return NavigationItem + */ + public function getItem() + { + return $this->item; + } + + /** + * Set whether to escape the label + * + * @param bool $state + * + * @return $this + */ + public function setEscapeLabel($state = true) + { + $this->escapeLabel = (bool) $state; + return $this; + } + + /** + * Return whether to escape the label + * + * @return bool + */ + public function getEscapeLabel() + { + return $this->escapeLabel !== null ? $this->escapeLabel : true; + } + + /** + * Render the given navigation item as HTML anchor + * + * @param NavigationItem $item + * + * @return string + */ + public function render(NavigationItem $item = null) + { + if ($item !== null) { + $this->setItem($item); + } elseif (($item = $this->getItem()) === null) { + throw new ProgrammingError( + 'Cannot render nothing. Pass the item to render as part' + . ' of the call to render() or set it with setItem()' + ); + } + + $label = $this->getEscapeLabel() + ? $this->view()->escape($item->getLabel()) + : $item->getLabel(); + if (($icon = $item->getIcon()) !== null) { + $label = $this->view()->icon($icon) . $label; + } elseif ($item->getName()) { + $firstLetter = $item->getName()[0]; + $label = $this->view()->icon('letter', null, ['data-letter' => strtolower($firstLetter)]) . $label; + } + + if (($url = $item->getUrl()) !== null) { + $url->overwriteParams($item->getUrlParameters()); + + $target = $item->getTarget(); + if ($url->isExternal() && (!$target || in_array($target, $this->internalLinkTargets, true))) { + $url = Url::fromPath('iframe', array('url' => $url)); + } + + $content = sprintf( + '%s', + $this->view()->propertiesToString($item->getAttributes()), + $this->view()->escape($url->getAbsoluteUrl('&')), + $this->renderTargetAttribute(), + $label + ); + } elseif ($label) { + $content = sprintf( + '<%1$s%2$s>%3$s', + $item::LINK_ALTERNATIVE, + $this->view()->propertiesToString($item->getAttributes()), + $label + ); + } else { + $content = ''; + } + + return $content; + } + + /** + * Render and return the attribute to provide a non-default target for the url + * + * @return string + */ + protected function renderTargetAttribute() + { + $target = $this->getItem()->getTarget(); + if ($target === null || $this->getItem()->getUrl()->getAbsoluteUrl() == '#') { + return ''; + } + + if (! in_array($target, $this->internalLinkTargets, true)) { + return ' target="' . $this->view()->escape($target) . '"'; + } + + return ' data-base-target="' . $target . '"'; + } +} diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php b/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php new file mode 100644 index 0000000..00c0f9a --- /dev/null +++ b/library/Icinga/Web/Navigation/Renderer/NavigationRenderer.php @@ -0,0 +1,356 @@ +skipOuterElement = $skipOuterElement; + $this->iterator = $navigation->getIterator(); + $this->navigation = $navigation; + $this->content = array(); + } + + /** + * {@inheritdoc} + */ + public function setElementTag($tag) + { + $this->elementTag = $tag; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getElementTag() + { + return $this->elementTag ?: static::OUTER_ELEMENT_TAG; + } + + /** + * {@inheritdoc} + */ + public function setCssClass($class) + { + $this->cssClass = $class; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCssClass() + { + return $this->cssClass; + } + + /** + * {@inheritdoc} + */ + public function setHeading($heading) + { + $this->heading = $heading; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getHeading() + { + return $this->heading; + } + + /** + * Return the view + * + * @return View + */ + public function view() + { + if ($this->view === null) { + $this->setView(Icinga::app()->getViewRenderer()->view); + } + + return $this->view; + } + + /** + * Set the view + * + * @param View $view + * + * @return $this + */ + public function setView(View $view) + { + $this->view = $view; + return $this; + } + + public function getChildren(): NavigationRenderer + { + return new static($this->current()->getChildren(), $this->skipOuterElement); + } + + public function hasChildren(): bool + { + return $this->current()->hasChildren(); + } + + public function current(): NavigationItem + { + return $this->iterator->current(); + } + + public function key(): int + { + return $this->iterator->key(); + } + + public function next(): void + { + $this->iterator->next(); + } + + public function rewind(): void + { + $this->iterator->rewind(); + if (! $this->skipOuterElement) { + $this->content[] = $this->beginMarkup(); + } + } + + public function valid(): bool + { + $valid = $this->iterator->valid(); + if (! $this->skipOuterElement && !$valid) { + $this->content[] = $this->endMarkup(); + } + + return $valid; + } + + /** + * Return the opening markup for the navigation + * + * @return string + */ + public function beginMarkup() + { + $content = array(); + $content[] = sprintf( + '<%s%s role="navigation">', + $this->getElementTag(), + $this->getCssClass() !== null ? ' class="' . $this->getCssClass() . '"' : '' + ); + if (($heading = $this->getHeading()) !== null) { + $content[] = sprintf( + '%2$s', + static::HEADING_RANK, + $this->view()->escape($heading) + ); + } + $content[] = $this->beginChildrenMarkup(); + return join("\n", $content); + } + + /** + * Return the closing markup for the navigation + * + * @return string + */ + public function endMarkup() + { + $content = array(); + $content[] = $this->endChildrenMarkup(); + $content[] = 'getElementTag() . '>'; + return join("\n", $content); + } + + /** + * Return the opening markup for multiple navigation items + * + * @param int $level + * + * @return string + */ + public function beginChildrenMarkup($level = 1) + { + $cssClass = array(static::CSS_CLASS_NAV); + if ($this->navigation->getLayout() === Navigation::LAYOUT_TABS) { + $cssClass[] = static::CSS_CLASS_NAV_TABS; + } elseif ($this->navigation->getLayout() === Navigation::LAYOUT_DROPDOWN) { + $cssClass[] = static::CSS_CLASS_NAV_DROPDOWN; + } + + $cssClass[] = 'nav-level-' . $level; + + return '
      '; + } + + /** + * Return the closing markup for multiple navigation items + * + * @return string + */ + public function endChildrenMarkup() + { + return '
    '; + } + + /** + * Return the opening markup for the given navigation item + * + * @param NavigationItem $item + * + * @return string + */ + public function beginItemMarkup(NavigationItem $item) + { + $cssClasses = array(static::CSS_CLASS_ITEM); + + if ($item->hasChildren() && $item->getChildren()->getLayout() === Navigation::LAYOUT_DROPDOWN) { + $cssClasses[] = static::CSS_CLASS_DROPDOWN; + $item + ->setAttribute('class', static::CSS_CLASS_DROPDOWN_TOGGLE) + ->setIcon(static::DROPDOWN_TOGGLE_ICON) + ->setUrl('#'); + } + + if ($item->getActive()) { + $cssClasses[] = static::CSS_CLASS_ACTIVE; + } + + if ($item->getSelected()) { + $cssClasses[] = static::CSS_CLASS_SELECTED; + } + + if ($cssClass = $item->getCssClass()) { + $cssClasses[] = $cssClass; + } + + $content = sprintf( + '
  • ', + join(' ', $cssClasses) + ); + return $content; + } + + /** + * Return the closing markup for a navigation item + * + * @return string + */ + public function endItemMarkup() + { + return '
  • '; + } + + /** + * {@inheritdoc} + */ + public function render() + { + foreach ($this as $item) { + /** @var NavigationItem $item */ + if ($item->shouldRender()) { + $content = $item->render(); + $this->content[] = $this->beginItemMarkup($item); + $this->content[] = $content; + $this->content[] = $this->endItemMarkup(); + } + } + + return join("\n", $this->content); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return IcingaException::describe($e); + } + } +} diff --git a/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php b/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php new file mode 100644 index 0000000..4495b73 --- /dev/null +++ b/library/Icinga/Web/Navigation/Renderer/NavigationRendererInterface.php @@ -0,0 +1,142 @@ +content = array(); + parent::__construct( + new NavigationRenderer($navigation, true), + RecursiveIteratorIterator::SELF_FIRST + ); + } + + /** + * Set whether to use the standard navigation item renderer + * + * @param bool $state + * + * @return $this + */ + public function setUseStandardItemRenderer($state = true) + { + $this->useStandardRenderer = (bool) $state; + return $this; + } + + /** + * Return whether to use the standard navigation item renderer + * + * @return bool + */ + public function getUseStandardItemRenderer() + { + return $this->useStandardRenderer; + } + + /** + * {@inheritdoc} + */ + public function setElementTag($tag) + { + $this->getInnerIterator()->setElementTag($tag); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getElementTag() + { + return $this->getInnerIterator()->getElementTag(); + } + + /** + * {@inheritdoc} + */ + public function setCssClass($class) + { + $this->getInnerIterator()->setCssClass($class); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCssClass() + { + return $this->getInnerIterator()->getCssClass(); + } + + /** + * {@inheritdoc} + */ + public function setHeading($heading) + { + $this->getInnerIterator()->setHeading($heading); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getHeading() + { + return $this->getInnerIterator()->getHeading(); + } + + public function beginIteration(): void + { + $this->content[] = $this->getInnerIterator()->beginMarkup(); + } + + public function endIteration(): void + { + $this->content[] = $this->getInnerIterator()->endMarkup(); + } + + public function beginChildren(): void + { + $this->content[] = $this->getInnerIterator()->beginChildrenMarkup($this->getDepth() + 1); + } + + public function endChildren(): void + { + $this->content[] = $this->getInnerIterator()->endChildrenMarkup(); + $this->content[] = $this->getInnerIterator()->endItemMarkup(); + } + + /** + * {@inheritdoc} + */ + public function render() + { + foreach ($this as $item) { + /** @var NavigationItem $item */ + if ($item->shouldRender()) { + if ($this->getDepth() > 0) { + $item->setIcon(null); + } + if ($this->getUseStandardItemRenderer()) { + $renderer = new NavigationItemRenderer(); + $content = $renderer->render($item); + } else { + $content = $item->render(); + } + $this->content[] = $this->getInnerIterator()->beginItemMarkup($item); + + $this->content[] = $content; + + if (! $item->hasChildren()) { + $this->content[] = $this->getInnerIterator()->endItemMarkup(); + } + } + } + + return join("\n", $this->content); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return IcingaException::describe($e); + } + } +} diff --git a/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php b/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php new file mode 100644 index 0000000..2916f4e --- /dev/null +++ b/library/Icinga/Web/Navigation/Renderer/SummaryNavigationItemRenderer.php @@ -0,0 +1,72 @@ + 0, + self::STATE_PENDING => 1, + self::STATE_UNKNOWN => 2, + self::STATE_WARNING => 3, + self::STATE_CRITICAL => 4, + ); + + /** + * Severity to state map + * + * @var array + */ + protected static $severityStateMap = array( + self::STATE_OK, + self::STATE_PENDING, + self::STATE_UNKNOWN, + self::STATE_WARNING, + self::STATE_CRITICAL + ); + + /** + * {@inheritdoc} + */ + public function getCount() + { + if ($this->count === null) { + $countMap = array_fill(0, 5, 0); + $maxSeverity = 0; + $titles = array(); + foreach ($this->getItem()->getChildren() as $child) { + $renderer = $child->getRenderer(); + if ($renderer instanceof BadgeNavigationItemRenderer) { + $count = $renderer->getCount(); + if ($count) { + $severity = static::$stateSeverityMap[$renderer->getState()]; + $countMap[$severity] += $count; + $titles[] = $renderer->getTitle(); + $maxSeverity = max($maxSeverity, $severity); + } + } + } + $this->count = $countMap[$maxSeverity]; + $this->state = static::$severityStateMap[$maxSeverity]; + $this->title = implode('. ', $titles); + } + + return $this->count; + } +} diff --git a/library/Icinga/Web/Notification.php b/library/Icinga/Web/Notification.php new file mode 100644 index 0000000..6f33a32 --- /dev/null +++ b/library/Icinga/Web/Notification.php @@ -0,0 +1,220 @@ + + * [getUser()]->notify('some message', Notification::INFO); + * + */ +class Notification +{ + /** + * Notification type info + * + * @var string + */ + const INFO = 'info'; + + /** + * Notification type error + * + * @var string + */ + const ERROR = 'error'; + + /** + * Notification type success + * + * @var string + */ + const SUCCESS = 'success'; + + /** + * Notification type warning + * + * @var string + */ + const WARNING = 'warning'; + + /** + * Name of the session key for notification messages + * + * @var string + */ + const SESSION_KEY = 'session'; + + /** + * Singleton instance + * + * @var self + */ + protected static $instance; + + /** + * Whether the platform is CLI + * + * @var bool + */ + protected $isCli = false; + + /** + * Notification messages + * + * @var array + */ + protected $messages = array(); + + /** + * Session + * + * @var Session + */ + protected $session; + + /** + * Create the notification instance + */ + final private function __construct() + { + if (Platform::isCli()) { + $this->isCli = true; + return; + } + + $this->session = Session::getSession(); + $messages = $this->session->get(self::SESSION_KEY); + if (is_array($messages)) { + $this->messages = $messages; + $this->session->delete(self::SESSION_KEY); + $this->session->write(); + } + } + + /** + * Get the Notification instance + * + * @return Notification + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Add info notification + * + * @param string $msg + */ + public static function info($msg) + { + self::getInstance()->addMessage($msg, self::INFO); + } + + /** + * Add error notification + * + * @param string $msg + */ + public static function error($msg) + { + self::getInstance()->addMessage($msg, self::ERROR); + } + + /** + * Add success notification + * + * @param string $msg + */ + public static function success($msg) + { + self::getInstance()->addMessage($msg, self::SUCCESS); + } + + /** + * Add warning notification + * + * @param string $msg + */ + public static function warning($msg) + { + self::getInstance()->addMessage($msg, self::WARNING); + } + + /** + * Add a notification message + * + * @param string $message + * @param string $type + */ + protected function addMessage($message, $type = self::INFO) + { + if ($this->isCli) { + $msg = sprintf('[%s] %s', $type, $message); + switch ($type) { + case self::INFO: + case self::SUCCESS: + Logger::info($msg); + break; + case self::ERROR: + Logger::error($msg); + break; + case self::WARNING: + Logger::warning($msg); + break; + } + } else { + $this->messages[] = (object) array( + 'type' => $type, + 'message' => $message, + ); + } + } + + /** + * Pop the notification messages + * + * @return array + */ + public function popMessages() + { + $messages = $this->messages; + $this->messages = array(); + return $messages; + } + + /** + * Get whether notification messages have been added + * + * @return bool + */ + public function hasMessages() + { + return ! empty($this->messages); + } + + /** + * Destroy the notification instance + */ + final public function __destruct() + { + if ($this->isCli) { + return; + } + if ($this->hasMessages() && $this->session->get('messages') !== $this->messages) { + $this->session->set(self::SESSION_KEY, $this->messages); + $this->session->write(); + } + } +} diff --git a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php new file mode 100644 index 0000000..6f103e5 --- /dev/null +++ b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php @@ -0,0 +1,84 @@ +setQuery($query); + } + + /** + * Set the query to paginate + * + * @param QueryInterface $query + * + * @return $this + */ + public function setQuery(QueryInterface $query) + { + $this->query = $query; + return $this; + } + + /** + * Return the query being paginated + * + * @return QueryInterface + */ + public function getQuery() + { + return $this->query; + } + + /** + * Fetch and return the rows in the given range of the query result + * + * @param int $offset Page offset + * @param int $itemCountPerPage Number of items per page + * + * @return array + */ + public function getItems($offset, $itemCountPerPage) + { + return $this->query->limit($itemCountPerPage, $offset)->fetchAll(); + } + + /** + * Return the total number of items in the query result + * + * @return int + */ + public function count(): int + { + if ($this->count === null) { + $this->count = $this->query->count(); + } + + return $this->count; + } +} diff --git a/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php b/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php new file mode 100644 index 0000000..d9b2ed9 --- /dev/null +++ b/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorder.php @@ -0,0 +1,78 @@ +getPageRange(); + } + + $pageNumber = $paginator->getCurrentPageNumber(); + $pageCount = count($paginator); + $range = array(); + + if ($pageCount < 10) { + // Show all pages if we have less than 10. + + for ($i = 1; $i < 10; $i++) { + if ($i > $pageCount) { + break; + } + $range[$i] = $i; + } + } else { + // More than 10 pages: + + foreach (array(1, 2) as $i) { + $range[$i] = $i; + } + if ($pageNumber < 6) { + // We are on page 1-5 from + for ($i = 1; $i <= 7; $i++) { + $range[$i] = $i; + } + } else { + // Current page > 5 + $range[] = '...'; + + // Less than 5 pages left + if (($pageCount - $pageNumber) < 5) { + $start = 5 - ($pageCount - $pageNumber); + } else { + $start = 1; + } + + for ($i = $pageNumber - $start; $i < ($pageNumber + (4 - $start)); $i++) { + if ($i > $pageCount) { + break; + } + $range[$i] = $i; + } + } + if ($pageNumber < ($pageCount - 2)) { + $range[] = '...'; + } + + foreach (array($pageCount - 1, $pageCount) as $i) { + $range[$i] = $i; + } + } + if (empty($range)) { + $range[] = 1; + } + return $range; + } +} diff --git a/library/Icinga/Web/RememberMe.php b/library/Icinga/Web/RememberMe.php new file mode 100644 index 0000000..1002396 --- /dev/null +++ b/library/Icinga/Web/RememberMe.php @@ -0,0 +1,363 @@ +hasDb()) { + return false; + } + + try { + (new AesCrypt())->getMethod(); + } catch (RuntimeException $_) { + return false; + } + + return true; + } + + /** + * Get whether the remember cookie is set + * + * @return bool + */ + public static function hasCookie() + { + return isset($_COOKIE[static::COOKIE]); + } + + /** + * Remove the database entry if exists and unset the remember me cookie from PHP's `$_COOKIE` superglobal + * + * @return Cookie The invalidation cookie which has to be sent to client in oder to remove the remember me cookie + */ + public static function forget() + { + if (self::hasCookie()) { + $data = explode('|', $_COOKIE[static::COOKIE]); + $iv = base64_decode(array_pop($data)); + (new self())->remove(bin2hex($iv)); + } + + unset($_COOKIE[static::COOKIE]); + + return (new Cookie(static::COOKIE)) + ->setHttpOnly(true) + ->forgetMe(); + } + + /** + * Create the remember me component from the remember me cookie + * + * @return static + */ + public static function fromCookie() + { + $data = explode('|', $_COOKIE[static::COOKIE]); + $iv = base64_decode(array_pop($data)); + + $select = (new Select()) + ->from(static::TABLE) + ->columns('*') + ->where(['random_iv = ?' => bin2hex($iv)]); + + $rememberMe = new static(); + $rs = $rememberMe->getDb()->select($select)->fetch(); + + if (! $rs) { + throw new RuntimeException(sprintf( + "No database entry found for IV '%s'", + bin2hex($iv) + )); + } + + $rememberMe->aesCrypt = (new AesCrypt()) + ->setKey(hex2bin($rs->passphrase)) + ->setIV($iv); + + if (count($data) > 1) { + $rememberMe->aesCrypt->setTag( + base64_decode(array_pop($data)) + ); + } elseif ($rememberMe->aesCrypt->isAuthenticatedEncryptionRequired()) { + throw new RuntimeException( + "The given decryption method needs a tag, but is not specified. " + . "You have probably updated the PHP version." + ); + } + + $rememberMe->username = $rs->username; + $rememberMe->encryptedPassword = $data[0]; + + return $rememberMe; + } + + /** + * Create the remember me component from the given username and password + * + * @param string $username + * @param string $password + * + * @return static + */ + public static function fromCredentials($username, $password) + { + $aesCrypt = new AesCrypt(); + $rememberMe = new static(); + $rememberMe->encryptedPassword = $aesCrypt->encrypt($password); + $rememberMe->username = $username; + $rememberMe->aesCrypt = $aesCrypt; + + return $rememberMe; + } + + /** + * Remove expired remember me information from the database + */ + public static function removeExpired() + { + $rememberMe = new static(); + if (! $rememberMe->hasDb()) { + return; + } + + $rememberMe->getDb()->delete(static::TABLE, [ + 'expires_at < NOW()' + ]); + } + + /** + * Get the remember me cookie + * + * @return Cookie + */ + public function getCookie() + { + $values = [ + $this->encryptedPassword, + base64_encode($this->aesCrypt->getIV()), + ]; + + if ($this->aesCrypt->isAuthenticatedEncryptionRequired()) { + array_splice($values, 1, 0, base64_encode($this->aesCrypt->getTag())); + } + + return (new Cookie(static::COOKIE)) + ->setExpire($this->getExpiresAt()) + ->setHttpOnly(true) + ->setValue(implode('|', $values)); + } + + /** + * Get the timestamp when the cookie expires + * + * Defaults to now plus 30 days, if not set via {@link setExpiresAt()}. + * + * @return int + */ + public function getExpiresAt() + { + if ($this->expiresAt === null) { + $this->expiresAt = time() + 60 * 60 * 24 * 30; + } + + return $this->expiresAt; + } + + /** + * Set the timestamp when the cookie expires + * + * @param int $expiresAt + * + * @return $this + */ + public function setExpiresAt($expiresAt) + { + $this->expiresAt = $expiresAt; + + return $this; + } + + /** + * Authenticate via the remember me cookie + * + * @return bool + * + * @throws \Icinga\Exception\AuthenticationException + */ + public function authenticate() + { + $auth = Auth::getInstance(); + $authChain = $auth->getAuthChain(); + $authChain->setSkipExternalBackends(true); + $user = new User($this->username); + if (! $user->hasDomain()) { + $user->setDomain(Config::app()->get('authentication', 'default_domain')); + } + + $authenticated = $authChain->authenticate( + $user, + $this->aesCrypt->decrypt($this->encryptedPassword) + ); + + if ($authenticated) { + $auth->setAuthenticated($user); + } + + return $authenticated; + } + + /** + * Persist the remember me information into the database + * + * To remove any previous stored information, set the iv + * + * @param string|null $iv To remove a specific iv record from the database + * + * @return $this + */ + public function persist($iv = null) + { + if ($iv) { + $this->remove(bin2hex($iv)); + } + + $this->getDb()->insert(static::TABLE, [ + 'username' => $this->username, + 'passphrase' => bin2hex($this->aesCrypt->getKey()), + 'random_iv' => bin2hex($this->aesCrypt->getIV()), + 'http_user_agent' => (new UserAgent)->getAgent(), + 'expires_at' => date('Y-m-d H:i:s', $this->getExpiresAt()), + 'ctime' => new Expression('NOW()'), + 'mtime' => new Expression('NOW()') + ]); + + return $this; + } + + /** + * Remove remember me information from the database on the basis of iv + * + * @param string $iv + * + * @return $this + */ + public function remove($iv) + { + $this->getDb()->delete(static::TABLE, [ + 'random_iv = ?' => $iv + ]); + + return $this; + } + + /** + * Create renewed remember me cookie + * + * @return static New remember me cookie which has to be sent to the client + */ + public function renew() + { + return static::fromCredentials( + $this->username, + $this->aesCrypt->decrypt($this->encryptedPassword) + ); + } + + /** + * Get all users using remember me cookie + * + * @return array Array of users + */ + public static function getAllUser() + { + $rememberMe = new static(); + if (! $rememberMe->hasDb()) { + return []; + } + + $select = (new Select()) + ->from(static::TABLE) + ->columns('username') + ->groupBy('username'); + + return $rememberMe->getDb()->select($select)->fetchAll(); + } + + /** + * Get all remember me entries from the database of the given user. + * + * @param $username + * + * @return array Array of database entries + */ + public static function getAllByUsername($username) + { + $rememberMe = new static(); + if (! $rememberMe->hasDb()) { + return []; + } + + $select = (new Select()) + ->from(static::TABLE) + ->columns(['http_user_agent', 'random_iv']) + ->where(['username = ?' => $username]); + + return $rememberMe->getDb()->select($select)->fetchAll(); + } + + /** + * Get the AesCrypt instance + * + * @return AesCrypt + */ + public function getAesCrypt() + { + return $this->aesCrypt; + } +} diff --git a/library/Icinga/Web/RememberMeUserDevicesList.php b/library/Icinga/Web/RememberMeUserDevicesList.php new file mode 100644 index 0000000..66609de --- /dev/null +++ b/library/Icinga/Web/RememberMeUserDevicesList.php @@ -0,0 +1,144 @@ + 'common-table', + 'data-base-target' => '_self' + ]; + + /** + * @var array + */ + protected $devicesList; + + /** + * @var string + */ + protected $username; + + /** + * @var string + */ + protected $url; + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @param string $url + * + * @return $this + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * @param string $username + * + * @return $this + */ + public function setUsername($username) + { + $this->username = $username; + + return $this; + } + + /** + * @return array List of devices. Each device contains user agent and fingerprint string + */ + public function getDevicesList() + { + return $this->devicesList; + } + + /** + * @param $devicesList + * + * @return $this + */ + public function setDevicesList($devicesList) + { + $this->devicesList = $devicesList; + + return $this; + } + + protected function assemble() + { + $thead = Html::tag('thead'); + $theadRow = Html::tag('tr') + ->add(Html::tag( + 'th', + sprintf(t('List of devices and browsers %s is currently logged in:'), $this->getUsername()) + )); + + $thead->add($theadRow); + + $head = Html::tag('tr') + ->add(Html::tag('th', t('OS'))) + ->add(Html::tag('th', t('Browser'))) + ->add(Html::tag('th', t('Fingerprint'))); + + $thead->add($head); + $tbody = Html::tag('tbody'); + + if (empty($this->getDevicesList())) { + $tbody->add(Html::tag('td', t('No device found'))); + } else { + foreach ($this->getDevicesList() as $device) { + $agent = new UserAgent($device); + $element = Html::tag('tr') + ->add(Html::tag('td', $agent->getOs())) + ->add(Html::tag('td', $agent->getBrowser())) + ->add(Html::tag('td', $device->random_iv)); + + $link = (new Link( + new Icon('trash'), + iplWebUrl::fromPath($this->getUrl()) + ->addParams( + [ + 'name' => $this->getUsername(), + 'fingerprint' => $device->random_iv, + ] + ) + )); + + $element->add(Html::tag('td', $link)); + $tbody->add($element); + } + } + + $this->add($thead); + $this->add($tbody); + } +} diff --git a/library/Icinga/Web/RememberMeUserList.php b/library/Icinga/Web/RememberMeUserList.php new file mode 100644 index 0000000..bb95dc9 --- /dev/null +++ b/library/Icinga/Web/RememberMeUserList.php @@ -0,0 +1,106 @@ + 'common-table table-row-selectable', + 'data-base-target' => '_next', + ]; + + /** + * @var array + */ + protected $users; + + /** + * @var string + */ + protected $url; + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @param string $url + * + * @return $this + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * @return array + */ + public function getUsers() + { + return $this->users; + } + + /** + * @param array $users + * + * @return $this + */ + public function setUsers($users) + { + $this->users = $users; + + return $this; + } + + protected function assemble() + { + $thead = Html::tag('thead'); + $theadRow = Html::tag('tr') + ->add(Html::tag( + 'th', + t('List of users who stay logged in') + )); + + $thead->add($theadRow); + $tbody = Html::tag('tbody'); + + if (empty($this->getUsers())) { + $tbody->add(Html::tag('td', t('No user found'))); + } else { + foreach ($this->getUsers() as $user) { + $element = Html::tag('tr'); + $link = new Link( + $user->username, + iplWebUrl::fromPath($this->getUrl())->addParams(['name' => $user->username]), + ['title' => sprintf(t('Device list of %s'), $user->username)] + ); + + $element->add(Html::tag('td', $link)); + $tbody->add($element); + } + } + + $this->add($thead); + $this->add($tbody); + } +} diff --git a/library/Icinga/Web/Request.php b/library/Icinga/Web/Request.php new file mode 100644 index 0000000..064ce63 --- /dev/null +++ b/library/Icinga/Web/Request.php @@ -0,0 +1,142 @@ +response === null) { + $this->response = Icinga::app()->getResponse(); + } + + return $this->response; + } + + /** + * Get the request URL + * + * @return Url + */ + public function getUrl() + { + if ($this->url === null) { + $this->url = Url::fromRequest($this); + } + return $this->url; + } + + /** + * Get the user if authenticated + * + * @return User|null + */ + public function getUser() + { + return $this->user; + } + + /** + * Set the authenticated user + * + * @param User $user + * + * @return $this + */ + public function setUser(User $user) + { + $this->user = $user; + return $this; + } + + /** + * Get whether the request seems to be an API request + * + * @return bool + */ + public function isApiRequest() + { + return $this->getHeader('Accept') === 'application/json'; + } + + /** + * Makes an ID unique to this request, to prevent id collisions in different containers + * + * Call this whenever an ID might show up multiple times in different containers. This function is useful + * for ensuring unique ids on sites, even if we combine the HTML of different requests into one site, + * while still being able to reference elements uniquely in the same request. + * + * @param string $id + * + * @return string The id suffixed w/ an identifier unique to this request + */ + public function protectId($id) + { + return $id . '-' . Window::getInstance()->getContainerId(); + } + + public function getPost($key = null, $default = null) + { + if ($key === null && $this->extractMediaType($this->getHeader('Content-Type')) === 'application/json') { + return Json::decode(file_get_contents('php://input'), true); + } + + return parent::getPost($key, $default); + } + + /** + * Extract and return the media type from the given header value + * + * @param string $headerValue + * + * @return string + */ + protected function extractMediaType($headerValue) + { + // Pretty basic and does not care about parameters + $parts = explode(';', $headerValue, 2); + return strtolower(trim($parts[0])); + } +} diff --git a/library/Icinga/Web/Response.php b/library/Icinga/Web/Response.php new file mode 100644 index 0000000..df0b842 --- /dev/null +++ b/library/Icinga/Web/Response.php @@ -0,0 +1,429 @@ +autoRefreshInterval; + } + + /** + * Set the auto-refresh interval + * + * @param int $autoRefreshInterval + * + * @return $this + */ + public function setAutoRefreshInterval($autoRefreshInterval) + { + $this->autoRefreshInterval = $autoRefreshInterval; + return $this; + } + + /** + * Get the set of cookies which are to be sent to the client + * + * @return CookieSet + */ + public function getCookies() + { + if ($this->cookies === null) { + $this->cookies = new CookieSet(); + } + return $this->cookies; + } + + /** + * Get the cookie with the given name from the set of cookies which are to be sent to the client + * + * @param string $name The name of the cookie + * + * @return Cookie|null The cookie with the given name or null if the cookie does not exist + */ + public function getCookie($name) + { + return $this->getCookies()->get($name); + } + + /** + * Set the given cookie for sending it to the client + * + * @param Cookie $cookie The cookie to send to the client + * + * @return $this + */ + public function setCookie(Cookie $cookie) + { + $this->getCookies()->add($cookie); + return $this; + } + + /** + * Get the redirect URL + * + * @return Url|null + */ + protected function getRedirectUrl() + { + return $this->redirectUrl; + } + + /** + * Set the redirect URL + * + * Unlike {@link setRedirect()} this method only sets a redirect URL on the response for later usage. + * {@link prepare()} will take care of the correct redirect handling and HTTP headers on XHR and "normal" browser + * requests. + * + * @param string|Url $redirectUrl + * + * @return $this + */ + protected function setRedirectUrl($redirectUrl) + { + if (! $redirectUrl instanceof Url) { + $redirectUrl = Url::fromPath((string) $redirectUrl); + } + $redirectUrl->getParams()->setSeparator('&'); + $this->redirectUrl = $redirectUrl; + return $this; + } + + /** + * Get an array of all header values for the given name + * + * @param string $name The name of the header + * @param bool $lastOnly If this is true, the last value will be returned as a string + * + * @return null|array|string + */ + public function getHeader($name, $lastOnly = false) + { + $result = ($lastOnly ? null : array()); + $headers = $this->getHeaders(); + foreach ($headers as $header) { + if ($header['name'] === $name) { + if ($lastOnly) { + $result = $header['value']; + } else { + $result[] = $header['value']; + } + } + } + + return $result; + } + + /** + * Get the request + * + * @return Request + */ + public function getRequest() + { + if ($this->request === null) { + $this->request = Icinga::app()->getRequest(); + } + return $this->request; + } + + /** + * Get whether to instruct the client to reload the window + * + * @return bool + */ + public function isWindowReloaded() + { + return $this->reloadWindow; + } + + /** + * Set whether to instruct the client to reload the window + * + * @param bool $reloadWindow + * + * @return $this + */ + public function setReloadWindow($reloadWindow) + { + $this->reloadWindow = $reloadWindow; + + return $this; + } + + /** + * Get whether to instruct client side script code to reload CSS + * + * @return bool + */ + public function isReloadCss() + { + return $this->reloadCss; + } + + /** + * Set whether to instruct client side script code to reload CSS + * + * @param bool $reloadCss + * + * @return $this + */ + public function setReloadCss($reloadCss) + { + $this->reloadCss = $reloadCss; + return $this; + } + + /** + * Get whether to send the rerender layout header on XHR + * + * @return bool + */ + public function getRerenderLayout() + { + return $this->rerenderLayout; + } + + /** + * Get whether to send the rerender layout header on XHR + * + * @param bool $rerenderLayout + * + * @return $this + */ + public function setRerenderLayout($rerenderLayout = true) + { + $this->rerenderLayout = (bool) $rerenderLayout; + return $this; + } + + /** + * Get whether to send the current window ID to the client + * + * @return bool + */ + public function getOverrideWindowId() + { + return $this->overrideWindowId; + } + + /** + * Set whether to send the current window ID to the client + * + * @param bool $overrideWindowId + * + * @return $this + */ + public function setOverrideWindowId($overrideWindowId = true) + { + $this->overrideWindowId = $overrideWindowId; + return $this; + } + + /** + * Entry point for HTTP responses in JSON format + * + * @return JsonResponse + */ + public function json() + { + $response = new JsonResponse(); + $response->copyMetaDataFrom($this); + return $response; + } + + /** + * Prepare the request before sending + */ + protected function prepare() + { + $redirectUrl = $this->getRedirectUrl(); + if ($this->getRequest()->isXmlHttpRequest()) { + if ($redirectUrl !== null) { + if ($this->getRequest()->isGet() && Icinga::app()->getViewRenderer()->view->compact) { + $redirectUrl->getParams()->set('showCompact', true); + } + + $this->setHeader('X-Icinga-Redirect', rawurlencode($redirectUrl->getAbsoluteUrl()), true); + if ($this->getRerenderLayout()) { + $this->setHeader('X-Icinga-Rerender-Layout', 'yes', true); + } + } + if ($this->getOverrideWindowId()) { + $this->setHeader('X-Icinga-WindowId', Window::getInstance()->getId(), true); + } + if ($this->getRerenderLayout()) { + $this->setHeader('X-Icinga-Container', 'layout', true); + } + if ($this->isWindowReloaded()) { + $this->setHeader('X-Icinga-Reload-Window', 'yes', true); + } + if ($this->isReloadCss()) { + $this->setHeader('X-Icinga-Reload-Css', 'now', true); + } + if (($autoRefreshInterval = $this->getAutoRefreshInterval()) !== null) { + $this->setHeader('X-Icinga-Refresh', $autoRefreshInterval, true); + } + + $notifications = Notification::getInstance(); + if ($notifications->hasMessages()) { + $notificationList = array(); + foreach ($notifications->popMessages() as $m) { + $notificationList[] = rawurlencode($m->type . ' ' . $m->message); + } + $this->setHeader('X-Icinga-Notification', implode('&', $notificationList), true); + } + } else { + if ($redirectUrl !== null) { + $this->setRedirect($redirectUrl->getAbsoluteUrl()); + } + } + + if (! $this->getHeader('Content-Type', true)) { + $this->setHeader('Content-Type', static::DEFAULT_CONTENT_TYPE); + } + } + + /** + * Redirect to the given URL and exit immediately + * + * @param string|Url $url + */ + public function redirectAndExit($url) + { + $this->setRedirectUrl($url); + + $session = Session::getSession(); + if ($session->hasChanged()) { + $session->write(); + } + + $this->sendHeaders(); + exit; + } + + /** + * Send the cookies to the client + */ + public function sendCookies() + { + foreach ($this->getCookies() as $cookie) { + /** @var Cookie $cookie */ + setcookie( + $cookie->getName(), + $cookie->getValue() ?? '', + $cookie->getExpire() ?? 0, + $cookie->getPath(), + $cookie->getDomain() ?? '', + $cookie->isSecure(), + $cookie->isHttpOnly() ?? true + ); + } + } + + /** + * {@inheritdoc} + */ + public function sendHeaders() + { + $this->prepare(); + if (! $this->getRequest()->isApiRequest()) { + $this->sendCookies(); + } + return parent::sendHeaders(); + } + + /** + * Copies non-body-related response data from $response + * + * @param Response $response + * + * @return $this + */ + protected function copyMetaDataFrom(self $response) + { + $this->_headers = $response->_headers; + $this->_headersRaw = $response->_headersRaw; + $this->_httpResponseCode = $response->_httpResponseCode; + $this->headersSentThrowsException = $response->headersSentThrowsException; + return $this; + } +} diff --git a/library/Icinga/Web/Response/JsonResponse.php b/library/Icinga/Web/Response/JsonResponse.php new file mode 100644 index 0000000..f914f2c --- /dev/null +++ b/library/Icinga/Web/Response/JsonResponse.php @@ -0,0 +1,239 @@ +encodingOptions; + } + + /** + * Set the JSON encoding options + * + * @param int $encodingOptions + * + * @return $this + */ + public function setEncodingOptions($encodingOptions) + { + $this->encodingOptions = (int) $encodingOptions; + return $this; + } + + /** + * Get whether to automatically sanitize invalid UTF-8 (if any) + * + * @return bool + */ + public function getAutoSanitize() + { + return $this->autoSanitize; + } + + /** + * Set whether to automatically sanitize invalid UTF-8 (if any) + * + * @param bool $autoSanitize + * + * @return $this + */ + public function setAutoSanitize($autoSanitize = true) + { + $this->autoSanitize = $autoSanitize; + + return $this; + } + + /** + * Get the error message if the API call failed due to a server error + * + * @return string|null + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * Set the error message if the API call failed due to a server error + * + * @param string $errorMessage + * + * @return $this + */ + public function setErrorMessage($errorMessage) + { + $this->errorMessage = (string) $errorMessage; + $this->status = static::STATUS_ERROR; + return $this; + } + + /** + * Get the fail data for rejected API calls + * + * @return array|null + */ + public function getFailData() + { + return (! is_array($this->failData) || empty($this->failData)) ? null : $this->failData; + } + + /** + * Set the fail data for rejected API calls + * + * @param array $failData + * + * @return $this + */ + public function setFailData(array $failData) + { + $this->failData = $failData; + $this->status = static::STATUS_FAIL; + return $this; + } + + /** + * Get the data for successful API requests + * + * @return array|null + */ + public function getSuccessData() + { + return (! is_array($this->successData) || empty($this->successData)) ? null : $this->successData; + } + + /** + * Set the data for successful API requests + * + * @param array $successData + * + * @return $this + */ + public function setSuccessData(array $successData = null) + { + $this->successData = $successData; + $this->status = static::STATUS_SUCCESS; + return $this; + } + + /** + * {@inheritdoc} + */ + public function outputBody() + { + $body = array( + 'status' => $this->status + ); + switch ($this->status) { + /** @noinspection PhpMissingBreakStatementInspection */ + case static::STATUS_ERROR: + $body['message'] = $this->getErrorMessage(); + // Fallthrough + case static::STATUS_FAIL: + $failData = $this->getFailData(); + if ($failData !== null || $this->status === static::STATUS_FAIL) { + $body['data'] = $failData; + } + break; + case static::STATUS_SUCCESS: + $body['data'] = $this->getSuccessData(); + break; + } + echo $this->getAutoSanitize() + ? Json::sanitize($body, $this->getEncodingOptions()) + : Json::encode($body, $this->getEncodingOptions()); + } + + /** + * {@inheritdoc} + */ + public function sendResponse() + { + Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer')->setNoRender(true); + parent::sendResponse(); + exit; + } +} diff --git a/library/Icinga/Web/Session.php b/library/Icinga/Web/Session.php new file mode 100644 index 0000000..40df89f --- /dev/null +++ b/library/Icinga/Web/Session.php @@ -0,0 +1,54 @@ +sessionName); + + $cookie = new Cookie('bogus'); + session_set_cookie_params( + 0, + $cookie->getPath(), + $cookie->getDomain(), + $cookie->isSecure(), + true + ); + + session_start(array( + 'use_cookies' => true, + 'use_only_cookies' => true, + 'use_trans_sid' => false + )); + } +} diff --git a/library/Icinga/Web/Session/PhpSession.php b/library/Icinga/Web/Session/PhpSession.php new file mode 100644 index 0000000..36dd84e --- /dev/null +++ b/library/Icinga/Web/Session/PhpSession.php @@ -0,0 +1,256 @@ + false, + 'use_cookies' => true, + 'cookie_httponly' => true, + 'use_only_cookies' => true + ); + + if (version_compare(PHP_VERSION, '7.1.0') < 0) { + $defaultCookieOptions['hash_function'] = true; + $defaultCookieOptions['hash_bits_per_character'] = 5; + } else { + $defaultCookieOptions['sid_bits_per_character'] = 5; + } + + if ($options !== null) { + $options = array_merge($defaultCookieOptions, $options); + } else { + $options = $defaultCookieOptions; + } + + if (array_key_exists('test_session_name', $options)) { + $this->sessionName = $options['test_session_name']; + unset($options['test_session_name']); + } + + foreach ($options as $sessionVar => $value) { + if (ini_set("session." . $sessionVar, $value) === false) { + Logger::warning( + 'Could not set php.ini setting %s = %s. This might affect your sessions behaviour.', + $sessionVar, + $value + ); + } + } + + $sessionSavePath = session_save_path() ?: sys_get_temp_dir(); + if (session_module_name() === 'files' && !is_writable($sessionSavePath)) { + throw new ConfigurationError("Can't save session, path '$sessionSavePath' is not writable."); + } + + if ($this->exists()) { + // We do not want to start a new session here if there is not any + $this->read(); + } + } + + /** + * Open a PHP session + */ + protected function open() + { + session_name($this->sessionName); + + if ($this->hasBeenTouched) { + $cacheLimiter = ini_get('session.cache_limiter'); + ini_set('session.use_cookies', false); + ini_set('session.use_only_cookies', false); + ini_set('session.cache_limiter', null); + } + + $cookie = new Cookie('bogus'); + session_set_cookie_params( + 0, + $cookie->getPath(), + $cookie->getDomain(), + $cookie->isSecure(), + true + ); + + session_start(); + + if ($this->hasBeenTouched) { + ini_set('session.use_cookies', true); + ini_set('session.use_only_cookies', true); + /** @noinspection PhpUndefinedVariableInspection */ + ini_set('session.cache_limiter', $cacheLimiter); + } + } + + /** + * Read all values written to the underling session and make them accessible. + */ + public function read() + { + $this->clear(); + $this->open(); + + foreach ($_SESSION as $key => $value) { + if (strpos($key, self::NAMESPACE_PREFIX) === 0) { + $namespace = new SessionNamespace(); + $namespace->setAll($value); + $this->namespaces[substr($key, strlen(self::NAMESPACE_PREFIX))] = $namespace; + } else { + $this->set($key, $value); + } + } + + session_write_close(); + $this->hasBeenTouched = true; + } + + /** + * Write all values of this session object to the underlying session implementation + */ + public function write() + { + $this->open(); + + foreach ($this->removed as $key) { + unset($_SESSION[$key]); + } + foreach ($this->values as $key => $value) { + $_SESSION[$key] = $value; + } + foreach ($this->removedNamespaces as $identifier) { + unset($_SESSION[self::NAMESPACE_PREFIX . $identifier]); + } + foreach ($this->namespaces as $identifier => $namespace) { + $_SESSION[self::NAMESPACE_PREFIX . $identifier] = $namespace->getAll(); + } + + session_write_close(); + $this->hasBeenTouched = true; + } + + /** + * Delete the current session, causing all session information to be lost + */ + public function purge() + { + $this->open(); + $_SESSION = array(); + $this->clear(); + session_destroy(); + $this->clearCookies(); + session_write_close(); + $this->hasBeenTouched = true; + } + + /** + * Remove session cookies + */ + protected function clearCookies() + { + if (ini_get('session.use_cookies')) { + Logger::debug('Clear session cookie'); + $params = session_get_cookie_params(); + setcookie( + session_name(), + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); + } + } + + /** + * @see Session::getId() + */ + public function getId() + { + if (($id = session_id()) === '') { + // Make sure we actually get a id + $this->open(); + session_write_close(); + $this->hasBeenTouched = true; + $id = session_id(); + } + + return $id; + } + + /** + * Assign a new sessionId to the currently active session + */ + public function refreshId() + { + $this->open(); + if ($this->exists()) { + session_regenerate_id(); + } + session_write_close(); + $this->hasBeenTouched = true; + } + + /** + * @see Session::exists() + */ + public function exists() + { + return isset($_COOKIE[$this->sessionName]); + } +} diff --git a/library/Icinga/Web/Session/Session.php b/library/Icinga/Web/Session/Session.php new file mode 100644 index 0000000..e73e9b4 --- /dev/null +++ b/library/Icinga/Web/Session/Session.php @@ -0,0 +1,126 @@ +namespaces[$identifier])) { + if (in_array($identifier, $this->removedNamespaces, true)) { + unset($this->removedNamespaces[array_search($identifier, $this->removedNamespaces, true)]); + } + + $this->namespaces[$identifier] = new SessionNamespace(); + } + + return $this->namespaces[$identifier]; + } + + /** + * Return whether the given session namespace exists + * + * @param string $identifier The namespace's identifier to check + * + * @return bool + */ + public function hasNamespace($identifier) + { + return isset($this->namespaces[$identifier]); + } + + /** + * Remove the given session namespace + * + * @param string $identifier The identifier of the namespace to remove + */ + public function removeNamespace($identifier) + { + unset($this->namespaces[$identifier]); + $this->removedNamespaces[] = $identifier; + } + + /** + * Return whether the session has changed + * + * @return bool + */ + public function hasChanged() + { + return parent::hasChanged() || false === empty($this->namespaces) || false === empty($this->removedNamespaces); + } + + /** + * Clear all values and namespaces from the session cache + */ + public function clear() + { + parent::clear(); + $this->namespaces = array(); + $this->removedNamespaces = array(); + } +} diff --git a/library/Icinga/Web/Session/SessionNamespace.php b/library/Icinga/Web/Session/SessionNamespace.php new file mode 100644 index 0000000..1c9c13f --- /dev/null +++ b/library/Icinga/Web/Session/SessionNamespace.php @@ -0,0 +1,201 @@ +getAll()); + } + + /** + * Set a session value by property access + * + * @param string $key The value's name + * @param mixed $value The value + */ + public function __set($key, $value) + { + $this->set($key, $value); + } + + /** + * Return a session value by property access + * + * @param string $key The value's name + * + * @return mixed The value + * @throws Exception When the given value-name is not found + */ + public function __get($key) + { + if (!array_key_exists($key, $this->values)) { + throw new IcingaException( + 'Cannot access non-existent session value "%s"', + $key + ); + } + + return $this->get($key); + } + + /** + * Return whether the given session value is set + * + * @param string $key The value's name + * @return bool + */ + public function __isset($key) + { + return isset($this->values[$key]); + } + + /** + * Unset the given session value + * + * @param string $key The value's name + */ + public function __unset($key) + { + $this->delete($key); + } + + /** + * Setter for session values + * + * @param string $key Name of value + * @param mixed $value Value to set + * + * @return $this + */ + public function set($key, $value) + { + $this->values[$key] = $value; + + if (in_array($key, $this->removed, true)) { + unset($this->removed[array_search($key, $this->removed, true)]); + } + + return $this; + } + + public function setByRef($key, &$value) + { + $this->values[$key] = & $value; + + if (in_array($key, $this->removed, true)) { + unset($this->removed[array_search($key, $this->removed, true)]); + } + + return $this; + } + + /** + * Getter for session values + * + * @param string $key Name of the value to return + * @param mixed $default Default value to return + * + * @return mixed + */ + public function get($key, $default = null) + { + return isset($this->values[$key]) ? $this->values[$key] : $default; + } + + public function & getByRef($key, $default = null) + { + $value = $default; + if (isset($this->values[$key])) { + $value = & $this->values[$key]; + } + + return $value; + } + + /** + * Delete the given value from the session + * + * @param string $key The value's name + */ + public function delete($key) + { + $this->removed[] = $key; + unset($this->values[$key]); + } + + /** + * Getter for all session values + * + * @return array + */ + public function getAll() + { + return $this->values; + } + + /** + * Put an array into the session + * + * @param array $values Values to set + * @param bool $overwrite Overwrite existing values + */ + public function setAll(array $values, $overwrite = false) + { + foreach ($values as $key => $value) { + if ($this->get($key, $value) !== $value && !$overwrite) { + continue; + } + $this->set($key, $value); + } + } + + /** + * Return whether the session namespace has been changed + * + * @return bool + */ + public function hasChanged() + { + return false === empty($this->values) || false === empty($this->removed); + } + + /** + * Clear all values from the session namespace + */ + public function clear() + { + $this->values = array(); + $this->removed = array(); + } +} diff --git a/library/Icinga/Web/StyleSheet.php b/library/Icinga/Web/StyleSheet.php new file mode 100644 index 0000000..9ca6d9a --- /dev/null +++ b/library/Icinga/Web/StyleSheet.php @@ -0,0 +1,341 @@ +app = $app; + $this->lessCompiler = new LessCompiler(); + $this->pubPath = $app->getBaseDir('public'); + $this->collect(); + } + + /** + * Collect Web 2 and module LESS files and add them to the LESS compiler + */ + protected function collect() + { + foreach ($this->app->getLibraries() as $library) { + foreach ($library->getCssAssets() as $lessFile) { + if (substr($lessFile, -4) === '.css') { + $this->cssFiles[] = $lessFile; + } else { + $this->lessCompiler->addLessFile($lessFile); + } + } + } + + foreach (self::$lessFiles as $lessFile) { + $this->lessCompiler->addLessFile($this->pubPath . '/' . $lessFile); + } + + $mm = $this->app->getModuleManager(); + + foreach ($mm->getLoadedModules() as $moduleName => $module) { + if ($module->hasCss()) { + foreach ($module->getCssFiles() as $lessFilePath) { + $this->lessCompiler->addModuleLessFile($moduleName, $lessFilePath); + } + } + } + + $themingConfig = $this->app->getConfig()->getSection('themes'); + $defaultTheme = $themingConfig->get('default'); + $theme = null; + if ($defaultTheme !== null && $defaultTheme !== self::DEFAULT_THEME) { + $theme = $defaultTheme; + } + + if (! (bool) $themingConfig->get('disabled', false)) { + $auth = Auth::getInstance(); + if ($auth->isAuthenticated()) { + $userTheme = $auth->getUser()->getPreferences()->getValue('icingaweb', 'theme'); + if ($userTheme !== null) { + $theme = $userTheme; + } + } + } + + if ($themePath = self::getThemeFile($theme)) { + if ($this->app->isCli() || is_file($themePath) && is_readable($themePath)) { + $this->lessCompiler->setTheme($themePath); + } else { + $themePath = null; + Logger::warning(sprintf( + 'Theme "%s" set by user "%s" has not been found.', + $theme, + ($user = Auth::getInstance()->getUser()) !== null ? $user->getUsername() : 'anonymous' + )); + } + } + + if (! $themePath || in_array($theme, self::THEME_WHITELIST, true)) { + $this->lessCompiler->addLessFile($this->pubPath . '/css/icinga/login-orbs.less'); + } + + $mode = 'none'; + if ($user = Auth::getInstance()->getUser()) { + $file = $themePath !== null ? @file_get_contents($themePath) : false; + if (! $file || strpos($file, self::LIGHT_MODE_IDENTIFIER) !== false) { + $mode = $user->getPreferences()->getValue('icingaweb', 'theme_mode', self::DEFAULT_MODE); + } + } + + $this->lessCompiler->setThemeMode($this->pubPath . '/css/modes/'. $mode . '.less'); + } + + /** + * Get all collected files + * + * @return string[] + */ + protected function getFiles(): array + { + return array_merge($this->cssFiles, $this->lessCompiler->getLessFiles()); + } + + /** + * Get the stylesheet for PDF export + * + * @return $this + */ + public static function forPdf() + { + $styleSheet = new self(); + $styleSheet->lessCompiler->setTheme(null); + $styleSheet->lessCompiler->setThemeMode($styleSheet->pubPath . '/css/modes/none.less'); + $styleSheet->lessCompiler->addLessFile($styleSheet->pubPath . '/css/pdf/pdfprint.less'); + // TODO(el): Caching + return $styleSheet; + } + + /** + * Render the stylesheet + * + * @param bool $minified Whether to compress the stylesheet + * + * @return string CSS + */ + public function render($minified = false) + { + if ($minified) { + $this->lessCompiler->compress(); + } + + $css = ''; + foreach ($this->cssFiles as $cssFile) { + $css .= file_get_contents($cssFile); + } + + return $css . $this->lessCompiler->render(); + } + + /** + * Send the stylesheet to the client + * + * Does not cache the stylesheet if the HTTP header Cache-Control or Pragma is set to no-cache. + * + * @param bool $minified Whether to compress the stylesheet + */ + public static function send($minified = false) + { + $styleSheet = new self(); + + $request = $styleSheet->app->getRequest(); + $response = $styleSheet->app->getResponse(); + $response->setHeader('Cache-Control', 'private,no-cache,must-revalidate', true); + + $noCache = $request->getHeader('Cache-Control') === 'no-cache' || $request->getHeader('Pragma') === 'no-cache'; + + $collectedFiles = $styleSheet->getFiles(); + if (! $noCache && FileCache::etagMatchesFiles($collectedFiles)) { + $response + ->setHttpResponseCode(304) + ->sendHeaders(); + return; + } + + $etag = FileCache::etagForFiles($collectedFiles); + + $response->setHeader('ETag', $etag, true) + ->setHeader('Content-Type', 'text/css', true); + + $cacheFile = 'icinga-' . $etag . ($minified ? '.min' : '') . '.css'; + $cache = FileCache::instance(); + + if (! $noCache && $cache->has($cacheFile)) { + $response->setBody($cache->get($cacheFile)); + } else { + $css = $styleSheet->render($minified); + $response->setBody($css); + $cache->store($cacheFile, $css); + } + + $response->sendResponse(); + } + + /** + * Render the stylesheet + * + * @return string + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + Logger::error($e); + return IcingaException::describe($e); + } + } + + /** + * Get the path to the current LESS theme file + * + * @param $theme + * + * @return string|null Return null if self::DEFAULT_THEME is set as theme, path otherwise + */ + public static function getThemeFile($theme) + { + $app = Icinga::app(); + + if ($theme && $theme !== self::DEFAULT_THEME) { + if (Hook::has('ThemeLoader')) { + try { + $path = Hook::first('ThemeLoader')->getThemeFile($theme); + } catch (Exception $e) { + Logger::error('Failed to call ThemeLoader hook: %s', $e); + $path = null; + } + + if ($path !== null) { + return $path; + } + } + + if (($pos = strpos($theme, '/')) !== false) { + $moduleName = substr($theme, 0, $pos); + $theme = substr($theme, $pos + 1); + if ($app->getModuleManager()->hasLoaded($moduleName)) { + $module = $app->getModuleManager()->getModule($moduleName); + + return $module->getCssDir() . '/themes/' . $theme . '.less'; + } + } else { + return $app->getBaseDir('public') . '/css/themes/' . $theme . '.less'; + } + } + + return null; + } +} diff --git a/library/Icinga/Web/Url.php b/library/Icinga/Web/Url.php new file mode 100644 index 0000000..f4fcba3 --- /dev/null +++ b/library/Icinga/Web/Url.php @@ -0,0 +1,806 @@ +params = UrlParams::fromQueryString(''); // TODO: ::create() + } + + /** + * Create a new Url class representing the current request + * + * If $params are given, those will be added to the request's parameters + * and overwrite any existing parameters + * + * @param UrlParams|array $params Parameters that should additionally be considered for the url + * @param Zend_Request $request A request to use instead of the default one + * + * @return static + */ + public static function fromRequest($params = array(), $request = null) + { + if ($request === null) { + $request = static::getRequest(); + } + + $url = new static(); + $url->setPath(ltrim($request->getPathInfo(), '/')); + + // $urlParams = UrlParams::fromQueryString($request->getQuery()); + if (isset($_SERVER['QUERY_STRING'])) { + $urlParams = UrlParams::fromQueryString($_SERVER['QUERY_STRING']); + } else { + $urlParams = UrlParams::fromQueryString(''); + foreach ($request->getQuery() as $k => $v) { + $urlParams->set($k, $v); + } + } + + foreach ($params as $k => $v) { + $urlParams->set($k, $v); + } + $url->setParams($urlParams); + $url->setBasePath($request->getBaseUrl()); + return $url; + } + + /** + * Return a request object that should be used for determining the URL + * + * @return Zend_Abstract_Request + */ + protected static function getRequest() + { + $app = Icinga::app(); + if ($app->isCli()) { + throw new ProgrammingError( + 'Url::fromRequest and Url::fromPath are currently not supported for CLI operations' + ); + } else { + return $app->getRequest(); + } + } + + /** + * Create a new Url class representing the given url + * + * If $params are given, those will be added to the urls parameters + * and overwrite any existing parameters + * + * @param string $url The string representation of the url to parse + * @param array $params An array of parameters that should additionally be considered for the url + * @param Zend_Request $request A request to use instead of the default one + * + * @return static + */ + public static function fromPath($url, array $params = array(), $request = null) + { + if ($request === null) { + $request = static::getRequest(); + } + + if (! is_string($url)) { + throw new ProgrammingError( + 'url %s is not a string', + var_export($url, true) + ); + } + + $urlObject = new static(); + + if ($url === '#') { + $urlObject->setPath($url); + return $urlObject; + } + + $urlParts = parse_url($url); + if (isset($urlParts['scheme']) && ( + $urlParts['scheme'] !== $request->getScheme() + || (isset($urlParts['host']) && $urlParts['host'] !== $request->getServer('SERVER_NAME')) + || (isset($urlParts['port']) && $urlParts['port'] != $request->getServer('SERVER_PORT'))) + ) { + $urlObject->setIsExternal(); + } + + if (isset($urlParts['path'])) { + $urlPath = $urlParts['path']; + if ($urlPath && $urlPath[0] === '/') { + if ($urlObject->isExternal() || isset($urlParts['user'])) { + $urlPath = ltrim($urlPath, '/'); + } else { + $requestBaseUrl = $request->getBaseUrl(); + if ($requestBaseUrl && $requestBaseUrl !== '/' && strpos($urlPath, $requestBaseUrl) === 0) { + $urlPath = ltrim(substr($urlPath, strlen($requestBaseUrl)), '/'); + $urlObject->setBasePath($requestBaseUrl); + } + } + } elseif (! $urlObject->isExternal()) { + $urlObject->setBasePath($request->getBaseUrl()); + } + + $urlObject->setPath($urlPath); + } elseif (! $urlObject->isExternal()) { + $urlObject->setBasePath($request->getBaseUrl()); + } + + // TODO: This has been used by former filter implementation, remove it: + if (isset($urlParts['query'])) { + $params = UrlParams::fromQueryString($urlParts['query'])->mergeValues($params); + } + if (isset($urlParts['fragment'])) { + $urlObject->setAnchor($urlParts['fragment']); + } + + if (isset($urlParts['user']) || $urlObject->isExternal()) { + if (isset($urlParts['user'])) { + $urlObject->setUsername($urlParts['user']); + } + if (isset($urlParts['host'])) { + $urlObject->setHost($urlParts['host']); + } + if (isset($urlParts['port'])) { + $urlObject->setPort($urlParts['port']); + } + if (isset($urlParts['scheme'])) { + $urlObject->setScheme($urlParts['scheme']); + } + if (isset($urlParts['pass'])) { + $urlObject->setPassword($urlParts['pass']); + } + } + + $urlObject->setParams($params); + return $urlObject; + } + + /** + * Create a new filter that needs to fullfill the base filter and the optional filter (if it exists) + * + * @param string $url The url to apply the new filter to + * @param Filter $filter The base filter + * @param Filter $optional The optional filter + * + * @return static The altered URL containing the new filter + * @throws ProgrammingError + */ + public static function urlAddFilterOptional($url, $filter, $optional) + { + $url = static::fromPath($url); + $f = $filter; + if (isset($optional)) { + $f = Filter::matchAll($filter, $optional); + } + return $url->setQueryString($f->toQueryString()); + } + + /** + * Add the given filter to the current filter of the URL + * + * @param Filter $and + * + * @return $this + */ + public function addFilter($and) + { + $this->setQueryString( + Filter::fromQueryString($this->getQueryString()) + ->andFilter($and) + ->toQueryString() + ); + return $this; + } + + /** + * Set the basePath for this url + * + * @param string $basePath New basePath of this url + * + * @return $this + */ + public function setBasePath($basePath) + { + $this->basePath = rtrim($basePath, '/ '); + return $this; + } + + /** + * Return the basePath set for this url + * + * @return string + */ + public function getBasePath() + { + return $this->basePath; + } + + /** + * Set the host for this url + * + * @param string $host New host of this Url + * + * @return $this + */ + public function setHost($host) + { + $this->host = $host; + return $this; + } + + /** + * Return the host set for this url + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Set the port for this url + * + * @param string $port New port of this url + * + * @return $this + */ + public function setPort($port) + { + $this->port = $port; + return $this; + } + + /** + * Return the port set for this url + * + * @return string + */ + public function getPort() + { + return $this->port; + } + + /** + * Set the scheme for this url + * + * @param string $scheme The scheme used for this url + * + * @return $this + */ + public function setScheme($scheme) + { + $this->scheme = $scheme; + return $this; + } + + /** + * Return the scheme set for this url + * + * @return string + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * Set the relative path of this url, without query parameters + * + * @param string $path The path to set + * + * @return $this + */ + public function setPath($path) + { + $this->path = $path; + return $this; + } + + /** + * Return the relative path of this url, without query parameters + * + * If you want the relative path with query parameters use getRelativeUrl + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Set whether this url points to an external resource + * + * @param bool $state + * + * @return $this + */ + public function setIsExternal($state = true) + { + $this->external = (bool) $state; + return $this; + } + + /** + * Return whether this url points to an external resource + * + * @return bool + */ + public function isExternal() + { + return $this->external; + } + + /** + * Set the username passed with this url + * + * @param string $username The username to set + * + * @return $this + */ + public function setUsername($username) + { + $this->username = $username; + return $this; + } + + /** + * Return the username passed with this url + * + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * Set the username passed with this url + * + * @param string $password The password to set + * + * @return $this + */ + public function setPassword($password) + { + $this->password = $password; + return $this; + } + + /** + * Return the password passed with this url + * + * @return string + */ + public function getPassword() + { + return $this->password; + } + + /** + * Return the relative url + * + * @return string + */ + public function getRelativeUrl($separator = '&') + { + $path = $this->buildPathQueryAndFragment($separator); + if ($path && $path[0] === '/') { + return ''; + } + + return $path; + } + + /** + * Return this url's path with its query parameters and fragment as string + * + * @return string + */ + protected function buildPathQueryAndFragment($querySeparator) + { + $anchor = $this->getAnchor(); + if ($anchor) { + $anchor = '#' . $anchor; + } + + $query = $this->getQueryString($querySeparator); + if ($query) { + $query = '?' . $query; + } + + return $this->getPath() . $query . $anchor; + } + + public function setQueryString($queryString) + { + $this->params = UrlParams::fromQueryString($queryString); + return $this; + } + + public function getQueryString($separator = null) + { + return $this->params->toString($separator); + } + + /** + * Return the absolute url with query parameters as a string + * + * @return string + */ + public function getAbsoluteUrl($separator = '&') + { + $path = $this->buildPathQueryAndFragment($separator); + if ($path && ($path === '#' || $path[0] === '/')) { + return $path; + } + + $basePath = $this->getBasePath(); + if (! $basePath) { + $basePath = '/'; + } + + if ($this->getUsername() || $this->isExternal()) { + $urlString = ''; + if ($this->getScheme()) { + $urlString .= $this->getScheme() . '://'; + } + if ($this->getPassword()) { + $urlString .= $this->getUsername() . ':' . $this->getPassword() . '@'; + } elseif ($this->getUsername()) { + $urlString .= $this->getUsername() . '@'; + } + if ($this->getHost()) { + $urlString .= $this->getHost(); + } + if ($this->getPort()) { + $urlString .= ':' . $this->getPort(); + } + + return $urlString . $basePath . ($basePath !== '/' && $path ? '/' : '') . $path; + } else { + return $basePath . ($basePath !== '/' && $path ? '/' : '') . $path; + } + } + + /** + * Add a set of parameters to the query part if the keys don't exist yet + * + * @param array $params The parameters to add + * + * @return $this + */ + public function addParams(array $params) + { + foreach ($params as $k => $v) { + $this->params->add($k, $v); + } + + return $this; + } + + /** + * Set and overwrite the given params if one if the same key already exists + * + * @param array $params The parameters to set + * + * @return $this + */ + public function overwriteParams(array $params) + { + foreach ($params as $k => $v) { + $this->params->set($k, $v); + } + + return $this; + } + + /** + * Overwrite the parameters used in the query part + * + * @param UrlParams|array $params The new parameters to use for the query part + * + * @return $this + */ + public function setParams($params) + { + if ($params instanceof UrlParams) { + $this->params = $params; + } elseif (is_array($params)) { + $urlParams = UrlParams::fromQueryString(''); + foreach ($params as $k => $v) { + $urlParams->set($k, $v); + } + $this->params = $urlParams; + } else { + throw new ProgrammingError( + 'Url params needs to be either an array or an UrlParams instance' + ); + } + return $this; + } + + /** + * Return all parameters that will be used in the query part + * + * @return UrlParams An instance of UrlParam containing all parameters + */ + public function getParams() + { + return $this->params; + } + + /** + * Return true if a urls' query parameter exists, otherwise false + * + * @param string $param The url parameter name to check + * + * @return bool + */ + public function hasParam($param) + { + return $this->params->has($param); + } + + /** + * Return a url's query parameter if it exists, otherwise $default + * + * @param string $param A query parameter name to return if existing + * @param mixed $default A value to return when the parameter doesn't exist + * + * @return mixed + */ + public function getParam($param, $default = null) + { + return $this->params->get($param, $default); + } + + /** + * Set a single parameter, overwriting any existing one with the same name + * + * @param string $param The query parameter name + * @param array|string $value An array or string to set as the parameter value + * + * @return $this + */ + public function setParam($param, $value = true) + { + $this->params->set($param, $value); + return $this; + } + + /** + * Set the url anchor-part + * + * @param string $anchor The site's anchor string without the '#' + * + * @return $this + */ + public function setAnchor($anchor) + { + $this->anchor = $anchor; + return $this; + } + + /** + * Return the url anchor-part + * + * @return string The site's anchor string without the '#' + */ + public function getAnchor() + { + return $this->anchor; + } + + /** + * Remove provided key (if string) or keys (if array of string) from the query parameter array + * + * @param string|array $keyOrArrayOfKeys An array of strings or a string representing the key(s) + * of the parameters to be removed + * @return $this + */ + public function remove($keyOrArrayOfKeys) + { + $this->params->remove($keyOrArrayOfKeys); + return $this; + } + + /** + * Shift a query parameter from this URL if it exists, otherwise $default + * + * @param string $param Parameter name + * @param mixed $default Default value in case $param does not exist + * + * @return mixed + */ + public function shift($param, $default = null) + { + return $this->params->shift($param, $default); + } + + /** + * Whether the given URL matches this URL object + * + * This does an exact match, parameters MUST be in the same order + * + * @param Url|string $url the URL to compare against + * + * @return bool whether the URL matches + */ + public function matches($url) + { + if (! $url instanceof static) { + $url = static::fromPath($url); + } + return (string) $url === (string) $this; + } + + /** + * Return a copy of this url without the parameter given + * + * The argument can be either a single query parameter name or an array of parameter names to + * remove from the query list + * + * @param string|array $keyOrArrayOfKeys A single string or an array containing parameter names + * + * @return static + */ + public function getUrlWithout($keyOrArrayOfKeys) + { + return $this->without($keyOrArrayOfKeys); + } + + public function without($keyOrArrayOfKeys) + { + $url = clone($this); + $url->remove($keyOrArrayOfKeys); + return $url; + } + + /** + * Return a copy of this url with the given parameter(s) + * + * The argument can be either a single query parameter name or an array of parameter names to + * remove from the query list + * + * @param string|array $param A single string or an array containing parameter names + * @param array $values an optional values array + * + * @return static + */ + public function with($param, $values = null) + { + $url = clone($this); + $url->params->mergeValues($param, $values); + return $url; + } + + /** + * Return a copy of this url with only the given parameter(s) + * + * The argument can be either a single query parameter name or + * an array of parameter names to keep on on the query + * + * @param string|array $keyOrArrayOfKeys + * + * @return static + */ + public function onlyWith($keyOrArrayOfKeys) + { + if (! is_array($keyOrArrayOfKeys)) { + $keyOrArrayOfKeys = [$keyOrArrayOfKeys]; + } + + $url = clone $this; + foreach ($url->getParams()->toArray(false) as $param => $value) { + if (is_int($param)) { + $param = $value; + } + + if (! in_array($param, $keyOrArrayOfKeys, true)) { + $url->remove($param); + } + } + + return $url; + } + + public function __clone() + { + $this->params = clone $this->params; + } + + /** + * Alias for @see Url::getAbsoluteUrl() + * + * @return string + */ + public function __toString() + { + return htmlspecialchars($this->getAbsoluteUrl(), ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true); + } +} diff --git a/library/Icinga/Web/UrlParams.php b/library/Icinga/Web/UrlParams.php new file mode 100644 index 0000000..6da1f30 --- /dev/null +++ b/library/Icinga/Web/UrlParams.php @@ -0,0 +1,433 @@ +index); + } + + public function setSeparator($separator) + { + $this->separator = $separator; + return $this; + } + + /** + * Get the given parameter + * + * Returns the last URL param if defined multiple times, $default if not + * given at all + * + * @param string $param The parameter you're interested in + * @param string $default An optional default value + * + * @return mixed + */ + public function get($param, $default = null) + { + if (! $this->has($param)) { + return $default; + } + + return rawurldecode($this->params[ end($this->index[$param]) ][ 1 ]); + } + + /** + * Require a parameter + * + * @param string $name Name of the parameter + * @param bool $strict Whether the parameter's value must not be the empty string + * + * @return mixed + * + * @throws MissingParameterException If the parameter was not given + */ + public function getRequired($name, $strict = true) + { + if ($this->has($name)) { + $value = $this->get($name); + if (! $strict || strlen($value) > 0) { + return $value; + } + } + $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name); + $e->setParameter($name); + throw $e; + } + + /** + * Get all instances of the given parameter + * + * Returns an array containing all values defined for a given parameter, + * $default if none. + * + * @param string $param The parameter you're interested in + * @param string $default An optional default value + * + * @return mixed + */ + public function getValues($param, $default = array()) + { + if (! $this->has($param)) { + return $default; + } + + $ret = array(); + foreach ($this->index[$param] as $key) { + $ret[] = rawurldecode($this->params[$key][1]); + } + return $ret; + } + + /** + * Whether the given parameter exists + * + * Returns true if such a parameter has been defined, false otherwise. + * + * @param string $param The parameter you're interested in + * + * @return boolean + */ + public function has($param) + { + return array_key_exists($param, $this->index); + } + + /** + * Get and remove the given parameter + * + * Returns the last URL param if defined multiple times, $default if not + * given at all. The parameter will be removed from this object. + * + * @param string $param The parameter you're interested in + * @param string $default An optional default value + * + * @return mixed + */ + public function shift($param = null, $default = null) + { + if ($param === null) { + if (empty($this->params)) { + return $default; + } + $ret = array_shift($this->params); + $ret[0] = rawurldecode($ret[0]); + $ret[1] = rawurldecode($ret[1]); + } else { + if (! $this->has($param)) { + return $default; + } + $key = reset($this->index[$param]); + $ret = rawurldecode($this->params[$key][1]); + unset($this->params[$key]); + } + + $this->reIndexAll(); + return $ret; + } + + /** + * Require and remove a parameter + * + * @param string $name Name of the parameter + * @param bool $strict Whether the parameter's value must not be the empty string + * + * @return mixed + * + * @throws MissingParameterException If the parameter was not given + */ + public function shiftRequired($name, $strict = true) + { + if ($this->has($name)) { + $value = $this->get($name); + if (! $strict || strlen($value) > 0) { + $this->shift($name); + return $value; + } + } + $e = new MissingParameterException(t('Required parameter \'%s\' missing'), $name); + $e->setParameter($name); + throw $e; + } + + public function addEncoded($param, $value = true) + { + $this->params[] = array($param, $this->cleanupValue($value)); + $this->indexLastOne(); + return $this; + } + + protected function urlEncode($value) + { + return rawurlencode($value instanceof Url ? $value->getAbsoluteUrl() : (string) $value); + } + + /** + * Add the given parameter with the given value + * + * This will add the given parameter, regardless of whether it already + * exists. + * + * @param string $param The parameter you're interested in + * @param string $value The value to be stored + * + * @return $this + */ + public function add($param, $value = true) + { + return $this->addEncoded($this->urlEncode($param), $this->urlEncode($value)); + } + + /** + * Adds a list of parameters + * + * This may be used with either a list of values for a single parameter or + * with a list of parameter / value pairs. + * + * @param string $param Parameter name or param/value list + * @param string $value The value to be stored + * + * @return $this + */ + public function addValues($param, $values = null) + { + if ($values === null && is_array($param)) { + foreach ($param as $k => $v) { + $this->add($k, $v); + } + } else { + foreach ($values as $value) { + $this->add($param, $value); + } + } + + return $this; + } + + protected function clearValues() + { + $this->params = array(); + $this->index = array(); + } + + public function mergeValues($param, $values = null) + { + if ($values === null && is_array($param)) { + foreach ($param as $k => $v) { + $this->set($k, $v); + } + } else { + if (! is_array($values)) { + $values = array($values); + } + foreach ($values as $value) { + $this->set($param, $value); + } + } + + return $this; + } + + public function setValues($param, $values = null) + { + $this->clearValues(); + return $this->addValues($param, $values); + } + + /** + * Add the given parameter with the given value in front of all other values + * + * This will add the given parameter in front of all others, regardless of + * whether it already exists. + * + * @param string $param The parameter you're interested in + * @param string $value The value to be stored + * + * @return $this + */ + public function unshift($param, $value) + { + array_unshift($this->params, array($this->urlEncode($param), $this->urlEncode($value))); + $this->reIndexAll(); + return $this; + } + + /** + * Set the given parameter with the given value + * + * This will set the given parameter, and override eventually existing ones. + * + * @param string $param The parameter you want to set + * @param string $value The value to be stored + * + * @return $this + */ + public function set($param, $value) + { + if (! $this->has($param)) { + return $this->add($param, $value); + } + + while (count($this->index[$param]) > 1) { + $remove = array_pop($this->index[$param]); + unset($this->params[$remove]); + } + + $this->params[$this->index[$param][0]] = array( + $this->urlEncode($param), + $this->urlEncode($this->cleanupValue($value)) + ); + $this->reIndexAll(); + + return $this; + } + + public function remove($param) + { + $changed = false; + + if (! is_array($param)) { + $param = array($param); + } + + foreach ($param as $p) { + if ($this->has($p)) { + foreach ($this->index[$p] as $key) { + unset($this->params[$key]); + } + $changed = true; + } + } + + if ($changed) { + $this->reIndexAll(); + } + + return $this; + } + + public function without($param) + { + $params = clone $this; + return $params->remove($param); + } + + // TODO: push, pop? + + protected function indexLastOne() + { + end($this->params); + $key = key($this->params); + $param = $this->params[$key][0]; + $this->addParamToIndex($param, $key); + } + + protected function addParamToIndex($param, $key) + { + if (! $this->has($param)) { + $this->index[$param] = array(); + } + $this->index[$param][] = $key; + } + + protected function reIndexAll() + { + $this->index = array(); + $this->params = array_values($this->params); + foreach ($this->params as $key => & $param) { + $this->addParamToIndex($param[0], $key); + } + } + + protected function cleanupValue($value) + { + return is_bool($value) ? $value : (string) $value; + } + + protected function parseQueryString($queryString) + { + $parts = preg_split('~&~', $queryString, -1, PREG_SPLIT_NO_EMPTY); + foreach ($parts as $part) { + $this->parseQueryStringPart($part); + } + } + + protected function parseQueryStringPart($part) + { + if (strpos($part, '=') === false) { + $this->addEncoded($part, true); + } else { + list($key, $val) = preg_split('/=/', $part, 2); + $this->addEncoded($key, $val); + } + } + + /** + * Return the parameters of this url as sequenced or associative array + * + * @param bool $sequenced + * + * @return array + */ + public function toArray($sequenced = true) + { + if ($sequenced) { + return $this->params; + } + + $params = array(); + foreach ($this->params as $param) { + if ($param[1] === true) { + $params[] = $param[0]; + } else { + $params[$param[0]] = $param[1]; + } + } + + return $params; + } + + public function toString($separator = null) + { + if ($separator === null) { + $separator = $this->separator; + } + $parts = array(); + foreach ($this->params as $p) { + if ($p[1] === true) { + $parts[] = $p[0]; + } else { + $parts[] = $p[0] . '=' . $p[1]; + } + } + return implode($separator, $parts); + } + + public function __toString() + { + return $this->toString(); + } + + public static function fromQueryString($queryString = null) + { + if ($queryString === null) { + $queryString = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : ''; + } + $params = new static(); + $params->parseQueryString($queryString); + + return $params; + } +} diff --git a/library/Icinga/Web/UserAgent.php b/library/Icinga/Web/UserAgent.php new file mode 100644 index 0000000..71c1a8b --- /dev/null +++ b/library/Icinga/Web/UserAgent.php @@ -0,0 +1,86 @@ +agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null; + + if ($agent) { + $this->agent = $agent->http_user_agent; + } + } + + /** + * Return $_SERVER['HTTP_USER_AGENT'] output string of given or current device + * + * @return string + */ + public function getAgent() + { + return $this->agent; + } + + /** + * Get Browser name + * + * @return string Browser name or unknown if not found + */ + public function getBrowser() + { + // key => regex value + $browsers = [ + "Internet Explorer" => "/MSIE(.*)/i", + "Seamonkey" => "/Seamonkey(.*)/i", + "MS Edge" => "/Edg(.*)/i", + "Opera" => "/Opera(.*)/i", + "Opera Browser" => "/OPR(.*)/i", + "Chromium" => "/Chromium(.*)/i", + "Firefox" => "/Firefox(.*)/i", + "Google Chrome" => "/Chrome(.*)/i", + "Safari" => "/Safari(.*)/i" + ]; + //TODO find a way to return also the version of the browser + foreach ($browsers as $browser => $regex) { + if (preg_match($regex, $this->agent)) { + return $browser; + } + } + + return 'unknown'; + } + + /** + * Get Operating system information + * + * @return string os information + */ + public function getOs() + { + // get string before the first appearance of ')' + $device = strstr($this->agent, ')', true); + if (! $device) { + return 'unknown'; + } + + // return string after the first appearance of '(' + return substr($device, strpos($device, '(') + 1); + } +} diff --git a/library/Icinga/Web/View.php b/library/Icinga/Web/View.php new file mode 100644 index 0000000..ec16940 --- /dev/null +++ b/library/Icinga/Web/View.php @@ -0,0 +1,254 @@ +getLibraryDir('Icinga/Web/View/Helper'); + + parent::__construct($config); + } + + /** + * Initialize the view + * + * @see Zend_View_Abstract::init + */ + public function init() + { + $this->loadGlobalHelpers(); + } + + /** + * Escape the given value top be safely used in view scripts + * + * @param string $value The output to be escaped + * @return string + */ + public function escape($value) + { + return htmlspecialchars($value ?? '', ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, self::CHARSET, true); + } + + /** + * Whether a specific helper (closure) has been registered + * + * @param string $name The desired function name + * @return boolean + */ + public function hasHelperFunction($name) + { + return array_key_exists($name, $this->helperFunctions); + } + + /** + * Add a new helper function + * + * @param string $name The desired function name + * @param Closure $function An anonymous function + * @return $this + */ + public function addHelperFunction($name, Closure $function) + { + if ($this->hasHelperFunction($name)) { + throw new ProgrammingError( + 'Cannot assign the same helper function twice: "%s"', + $name + ); + } + + $this->helperFunctions[$name] = $function; + return $this; + } + + /** + * Set or overwrite a helper function + * + * @param string $name + * @param Closure $function + * + * @return $this + */ + public function setHelperFunction($name, Closure $function) + { + $this->helperFunctions[$name] = $function; + return $this; + } + + /** + * Drop a helper function + * + * @param string $name + * + * @return $this + */ + public function dropHelperFunction($name) + { + unset($this->helperFunctions[$name]); + return $this; + } + + /** + * Call a helper function + * + * @param string $name The desired function name + * @param Array $args Function arguments + * @return mixed + */ + public function callHelperFunction($name, $args) + { + return call_user_func_array( + $this->helperFunctions[$name], + $args + ); + } + + /** + * Load helpers + */ + private function loadGlobalHelpers() + { + $pattern = dirname(__FILE__) . '/View/helpers/*.php'; + $files = glob($pattern); + foreach ($files as $file) { + require_once $file; + } + } + + /** + * Get the authentication manager + * + * @return Auth + */ + public function Auth() + { + if ($this->auth === null) { + $this->auth = Auth::getInstance(); + } + return $this->auth; + } + + /** + * Whether the current user has the given permission + * + * @param string $permission Name of the permission + * + * @return bool + */ + public function hasPermission($permission) + { + return $this->Auth()->hasPermission($permission); + } + + /** + * Use to include the view script in a scope that only allows public + * members. + * + * @return mixed + * + * @see Zend_View_Abstract::run + */ + protected function _run() + { + foreach ($this->getVars() as $k => $v) { + // Exporting global variables to view scripts: + $$k = $v; + } + + include func_get_arg(0); + } + + /** + * Accesses a helper object from within a script + * + * @param string $name + * @param array $args + * + * @return string + */ + public function __call($name, $args) + { + if ($this->hasHelperFunction($name)) { + return $this->callHelperFunction($name, $args); + } else { + return parent::__call($name, $args); + } + } +} diff --git a/library/Icinga/Web/View/AppHealth.php b/library/Icinga/Web/View/AppHealth.php new file mode 100644 index 0000000..c66ca05 --- /dev/null +++ b/library/Icinga/Web/View/AppHealth.php @@ -0,0 +1,89 @@ + ['app-health', 'common-table', 'table-row-selectable']]; + + /** @var Traversable */ + protected $data; + + public function __construct(Traversable $data) + { + $this->data = $data; + + $this->setBaseTarget('_next'); + } + + protected function assemble() + { + foreach ($this->data as $row) { + $this->add(Table::tr([ + Table::th(HtmlElement::create('span', ['class' => [ + 'ball', + 'ball-size-xl', + $this->getStateClass($row->state) + ]])), + Table::td([ + new HtmlElement('header', null, FormattedString::create( + t('%s by %s is %s', ' by is '), + $row->url + ? new Link(HtmlElement::create('span', null, $row->name), $row->url) + : HtmlElement::create('span', null, $row->name), + HtmlElement::create('span', null, $row->module), + HtmlElement::create('span', null, $this->getStateText($row->state)) + )), + HtmlElement::create('section', null, $row->message) + ]) + ])); + } + } + + protected function getStateClass($state) + { + if ($state === null) { + $state = HealthHook::STATE_UNKNOWN; + } + + switch ($state) { + case HealthHook::STATE_OK: + return 'state-ok'; + case HealthHook::STATE_WARNING: + return 'state-warning'; + case HealthHook::STATE_CRITICAL: + return 'state-critical'; + case HealthHook::STATE_UNKNOWN: + return 'state-unknown'; + } + } + + protected function getStateText($state) + { + if ($state === null) { + $state = t('UNKNOWN'); + } + + switch ($state) { + case HealthHook::STATE_OK: + return t('OK'); + case HealthHook::STATE_WARNING: + return t('WARNING'); + case HealthHook::STATE_CRITICAL: + return t('CRITICAL'); + case HealthHook::STATE_UNKNOWN: + return t('UNKNOWN'); + } + } +} diff --git a/library/Icinga/Web/View/Helper/IcingaCheckbox.php b/library/Icinga/Web/View/Helper/IcingaCheckbox.php new file mode 100644 index 0000000..07cf01f --- /dev/null +++ b/library/Icinga/Web/View/Helper/IcingaCheckbox.php @@ -0,0 +1,30 @@ +view->protectId('icingaCheckbox_' . $name); + } + + $attribs['class'] = (isset($attribs['class']) ? $attribs['class'] . ' ' : '') . 'sr-only'; + $html = parent::formCheckbox($name, $value, $attribs, $checkedOptions); + + $class = 'toggle-switch'; + if (isset($attribs['disabled'])) { + $class .= ' disabled'; + } + + return $html + . ''; + } +} diff --git a/library/Icinga/Web/View/PrivilegeAudit.php b/library/Icinga/Web/View/PrivilegeAudit.php new file mode 100644 index 0000000..fcb4083 --- /dev/null +++ b/library/Icinga/Web/View/PrivilegeAudit.php @@ -0,0 +1,622 @@ + 'privilege-audit']; + + /** @var Role[] */ + protected $roles; + + public function __construct(array $roles) + { + $this->roles = $roles; + $this->setBaseTarget('_next'); + } + + protected function auditPermission($permission) + { + $grantedBy = []; + $refusedBy = []; + foreach ($this->roles as $role) { + if ($permission === self::UNRESTRICTED_PERMISSION) { + if ($role->isUnrestricted()) { + $grantedBy[] = $role->getName(); + } + } elseif ($role->denies($permission)) { + $refusedBy[] = $role->getName(); + } elseif ($role->grants($permission, false, false)) { + $grantedBy[] = $role->getName(); + } + } + + $header = new HtmlElement('summary'); + if (! empty($refusedBy)) { + $header->add([ + new Icon('times-circle', ['class' => 'refused']), + count($refusedBy) > 2 + ? sprintf( + tp( + 'Refused by %s and %s as well as one other', + 'Refused by %s and %s as well as %d others', + count($refusedBy) - 2 + ), + $refusedBy[0], + $refusedBy[1], + count($refusedBy) - 2 + ) + : sprintf( + tp('Refused by %s', 'Refused by %s and %s', count($refusedBy)), + ...$refusedBy + ) + ]); + } elseif (! empty($grantedBy)) { + $header->add([ + new Icon('check-circle', ['class' => 'granted']), + count($grantedBy) > 2 + ? sprintf( + tp( + 'Granted by %s and %s as well as one other', + 'Granted by %s and %s as well as %d others', + count($grantedBy) - 2 + ), + $grantedBy[0], + $grantedBy[1], + count($grantedBy) - 2 + ) + : sprintf( + tp('Granted by %s', 'Granted by %s and %s', count($grantedBy)), + ...$grantedBy + ) + ]); + } else { + $header->add([new Icon('minus-circle'), t('Not granted or refused by any role')]); + } + + $vClass = null; + $rolePaths = []; + foreach (array_reverse($this->roles) as $role) { + if (! in_array($role->getName(), $refusedBy, true) && ! in_array($role->getName(), $grantedBy, true)) { + continue; + } + + /** @var Role[] $rolesReversed */ + $rolesReversed = []; + + do { + array_unshift($rolesReversed, $role); + } while (($role = $role->getParent()) !== null); + + $path = new HtmlElement('ol'); + + $class = null; + $setInitiator = false; + foreach ($rolesReversed as $role) { + $granted = false; + $refused = false; + $icon = new Icon('minus-circle'); + if ($permission === self::UNRESTRICTED_PERMISSION) { + if ($role->isUnrestricted()) { + $granted = true; + $icon = new Icon('check-circle', ['class' => 'granted']); + } + } elseif ($role->denies($permission, true)) { + $refused = true; + $icon = new Icon('times-circle', ['class' => 'refused']); + } elseif ($role->grants($permission, true, false)) { + $granted = true; + $icon = new Icon('check-circle', ['class' => 'granted']); + } + + $connector = null; + if ($role->getParent() !== null) { + $connector = HtmlElement::create('li', ['class' => ['connector', $class]]); + if ($setInitiator) { + $setInitiator = false; + $connector->getAttributes()->add('class', 'initiator'); + } + + $path->prependHtml($connector); + } + + $path->prependHtml(new HtmlElement('li', Attributes::create([ + 'class' => ['role', $class], + 'title' => $role->getName() + ]), new Link([$icon, $role->getName()], Url::fromPath('role/edit', ['role' => $role->getName()])))); + + if ($refused) { + $setInitiator = $class !== 'refused'; + $class = 'refused'; + } elseif ($granted) { + $setInitiator = $class === null; + $class = $class ?: 'granted'; + } + } + + if ($vClass === null || $vClass === 'granted') { + $vClass = $class; + } + + array_unshift($rolePaths, $path->prepend([ + empty($rolePaths) ? null : HtmlElement::create('li', ['class' => ['vertical-line', $vClass]]), + new HtmlElement('li', Attributes::create(['class' => [ + 'connector', + $class, + $setInitiator ? 'initiator' : null + ]])) + ])); + } + + if (empty($rolePaths)) { + return [ + empty($refusedBy) ? (empty($grantedBy) ? null : true) : false, + new HtmlElement( + 'div', + Attributes::create(['class' => 'inheritance-paths']), + $header->setTag('div') + ) + ]; + } + + return [ + empty($refusedBy) ? (empty($grantedBy) ? null : true) : false, + HtmlElement::create('details', [ + 'class' => ['collapsible', 'inheritance-paths'], + 'data-no-persistence' => true, + 'open' => getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf' + ], [ + $header->addAttributes(['class' => 'collapsible-control']), + $rolePaths + ]) + ]; + } + + protected function auditRestriction($restriction) + { + $restrictedBy = []; + $restrictions = []; + foreach ($this->roles as $role) { + if ($role->isUnrestricted()) { + $restrictedBy = []; + $restrictions = []; + break; + } + + foreach ($this->collectRestrictions($role, $restriction) as $role => $roleRestriction) { + $restrictedBy[] = $role; + $restrictions[] = $roleRestriction; + } + } + + $header = new HtmlElement('summary'); + if (! empty($restrictedBy)) { + $header->add([ + new Icon('filter', ['class' => 'restricted']), + count($restrictedBy) > 2 + ? sprintf( + tp( + 'Restricted by %s and %s as well as one other', + 'Restricted by %s and %s as well as %d others', + count($restrictedBy) - 2 + ), + $restrictedBy[0]->getName(), + $restrictedBy[1]->getName(), + count($restrictedBy) - 2 + ) + : sprintf( + tp('Restricted by %s', 'Restricted by %s and %s', count($restrictedBy)), + ...array_map(function ($role) { + return $role->getName(); + }, $restrictedBy) + ) + ]); + } else { + $header->add([new Icon('filter'), t('Not restricted by any role')]); + } + + $roles = []; + if (! empty($restrictions) && count($restrictions) > 1) { + list($combinedRestrictions, $combinedLinks) = $this->createRestrictionLinks($restriction, $restrictions); + $roles[] = HtmlElement::create('li', null, [ + new HtmlElement( + 'div', + Attributes::create(['class' => 'flex-overflow']), + HtmlElement::create('span', [ + 'class' => 'role', + 'title' => t('All roles combined') + ], join(' | ', array_map(function ($role) { + return $role->getName(); + }, $restrictedBy))), + HtmlElement::create('code', ['class' => 'restriction'], $combinedRestrictions) + ), + $combinedLinks ? new HtmlElement( + 'div', + Attributes::create(['class' => 'previews']), + HtmlElement::create('em', null, t('Previews:')), + $combinedLinks + ) : null + ]); + } + + foreach ($restrictedBy as $role) { + list($roleRestriction, $restrictionLinks) = $this->createRestrictionLinks( + $restriction, + [$role->getRestrictions($restriction)] + ); + + $roles[] = HtmlElement::create('li', null, [ + new HtmlElement( + 'div', + Attributes::create(['class' => 'flex-overflow']), + new Link($role->getName(), Url::fromPath('role/edit', ['role' => $role->getName()]), [ + 'class' => 'role', + 'title' => $role->getName() + ]), + HtmlElement::create('code', ['class' => 'restriction'], $roleRestriction) + ), + $restrictionLinks ? new HtmlElement( + 'div', + Attributes::create(['class' => 'previews']), + HtmlElement::create('em', null, t('Previews:')), + $restrictionLinks + ) : null + ]); + } + + if (empty($roles)) { + return [ + ! empty($restrictedBy), + new HtmlElement( + 'div', + Attributes::create(['class' => 'restrictions']), + $header->setTag('div') + ) + ]; + } + + return [ + ! empty($restrictedBy), + new HtmlElement( + 'details', + Attributes::create([ + 'class' => ['collapsible', 'restrictions'], + 'data-no-persistence' => true, + 'open' => getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf' + ]), + $header->addAttributes(['class' => 'collapsible-control']), + new HtmlElement('ul', null, ...$roles) + ) + ]; + } + + protected function assemble() + { + list($permissions, $restrictions) = RoleForm::collectProvidedPrivileges(); + list($wildcardState, $wildcardAudit) = $this->auditPermission('*'); + list($unrestrictedState, $unrestrictedAudit) = $this->auditPermission(self::UNRESTRICTED_PERMISSION); + + $this->addHtml(new HtmlElement( + 'li', + null, + new HtmlElement( + 'details', + Attributes::create([ + 'class' => ['collapsible', 'privilege-section'], + 'open' => ($wildcardState || $unrestrictedState) && getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf' + ]), + new HtmlElement( + 'summary', + Attributes::create(['class' => [ + 'collapsible-control', // Helps JS, improves performance a bit + ]]), + new HtmlElement('span', null, Text::create(t('Administrative Privileges'))), + HtmlElement::create( + 'span', + ['class' => 'audit-preview'], + $wildcardState || $unrestrictedState + ? new Icon('check-circle', ['class' => 'granted']) + : null + ), + new Icon('angles-down', ['class' => 'collapse-icon']), + new Icon('angles-left', ['class' => 'expand-icon']) + ), + new HtmlElement( + 'ol', + Attributes::create(['class' => 'privilege-list']), + new HtmlElement( + 'li', + null, + HtmlElement::create('p', ['class' => 'privilege-label'], t('Administrative Access')), + HtmlElement::create('div', ['class' => 'spacer']), + $wildcardAudit + ), + new HtmlElement( + 'li', + null, + HtmlElement::create('p', ['class' => 'privilege-label'], t('Unrestricted Access')), + HtmlElement::create('div', ['class' => 'spacer']), + $unrestrictedAudit + ) + ) + ) + )); + + $privilegeSources = array_unique(array_merge(array_keys($permissions), array_keys($restrictions))); + foreach ($privilegeSources as $source) { + $anythingGranted = false; + $anythingRefused = false; + $anythingRestricted = false; + + $permissionList = new HtmlElement('ol', Attributes::create(['class' => 'privilege-list'])); + foreach (isset($permissions[$source]) ? $permissions[$source] : [] as $permission => $metaData) { + list($permissionState, $permissionAudit) = $this->auditPermission($permission); + if ($permissionState !== null) { + if ($permissionState) { + $anythingGranted = true; + } else { + $anythingRefused = true; + } + } + + $permissionList->addHtml(new HtmlElement( + 'li', + null, + HtmlElement::create( + 'p', + ['class' => 'privilege-label'], + isset($metaData['label']) + ? $metaData['label'] + : array_map(function ($segment) { + return $segment[0] === '/' ? [ + // Adds a zero-width char after each slash to help browsers break onto newlines + new HtmlString('/​'), + HtmlElement::create('span', ['class' => 'no-wrap'], substr($segment, 1)) + ] : HtmlElement::create('em', null, $segment); + }, preg_split( + '~(/[^/]+)~', + $permission, + -1, + PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY + )) + ), + new HtmlElement('div', Attributes::create(['class' => 'spacer'])), + $permissionAudit + )); + } + + $restrictionList = new HtmlElement('ol', Attributes::create(['class' => 'privilege-list'])); + foreach (isset($restrictions[$source]) ? $restrictions[$source] : [] as $restriction => $metaData) { + list($restrictionState, $restrictionAudit) = $this->auditRestriction($restriction); + if ($restrictionState) { + $anythingRestricted = true; + } + + $restrictionList->addHtml(new HtmlElement( + 'li', + null, + HtmlElement::create( + 'p', + ['class' => 'privilege-label'], + isset($metaData['label']) + ? $metaData['label'] + : array_map(function ($segment) { + return $segment[0] === '/' ? [ + // Adds a zero-width char after each slash to help browsers break onto newlines + new HtmlString('/​'), + HtmlElement::create('span', ['class' => 'no-wrap'], substr($segment, 1)) + ] : HtmlElement::create('em', null, $segment); + }, preg_split( + '~(/[^/]+)~', + $restriction, + -1, + PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY + )) + ), + new HtmlElement('div', Attributes::create(['class' => 'spacer'])), + $restrictionAudit + )); + } + + if ($source === 'application') { + $label = 'Icinga Web 2'; + } else { + $label = [$source, ' ', HtmlElement::create('em', null, t('Module'))]; + } + + $this->addHtml(new HtmlElement( + 'li', + null, + HtmlElement::create('details', [ + 'class' => ['collapsible', 'privilege-section'], + 'open' => ($anythingGranted || $anythingRefused || $anythingRestricted) + && getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf' + ], [ + new HtmlElement( + 'summary', + Attributes::create(['class' => [ + 'collapsible-control', // Helps JS, improves performance a bit + ]]), + HtmlElement::create('span', null, $label), + HtmlElement::create('span', ['class' => 'audit-preview'], [ + $anythingGranted ? new Icon('check-circle', ['class' => 'granted']) : null, + $anythingRefused ? new Icon('times-circle', ['class' => 'refused']) : null, + $anythingRestricted ? new Icon('filter', ['class' => 'restricted']) : null + ]), + new Icon('angles-down', ['class' => 'collapse-icon']), + new Icon('angles-left', ['class' => 'expand-icon']) + ), + $permissionList->isEmpty() ? null : [ + HtmlElement::create('h4', null, t('Permissions')), + $permissionList + ], + $restrictionList->isEmpty() ? null : [ + HtmlElement::create('h4', null, t('Restrictions')), + $restrictionList + ] + ]) + )); + } + } + + private function collectRestrictions(Role $role, $restrictionName) + { + do { + $restriction = $role->getRestrictions($restrictionName); + if ($restriction) { + yield $role => $restriction; + } + } while (($role = $role->getParent()) !== null); + } + + private function createRestrictionLinks($restrictionName, array $restrictions) + { + // TODO: Remove this hardcoded mess. Do this based on the restriction's meta data + switch ($restrictionName) { + case 'icingadb/filter/objects': + $filterString = join('|', $restrictions); + $list = new HtmlElement( + 'ul', + Attributes::create(['class' => 'links']), + new HtmlElement('li', null, new Link( + 'icingadb/hosts', + Url::fromPath('icingadb/hosts')->setQueryString($filterString) + )), + new HtmlElement('li', null, new Link( + 'icingadb/services', + Url::fromPath('icingadb/services')->setQueryString($filterString) + )), + new HtmlElement('li', null, new Link( + 'icingadb/hostgroups', + Url::fromPath('icingadb/hostgroups')->setQueryString($filterString) + )), + new HtmlElement('li', null, new Link( + 'icingadb/servicegroups', + Url::fromPath('icingadb/servicegroups')->setQueryString($filterString) + )) + ); + + break; + case 'icingadb/filter/hosts': + $filterString = join('|', $restrictions); + $list = new HtmlElement( + 'ul', + Attributes::create(['class' => 'links']), + new HtmlElement('li', null, new Link( + 'icingadb/hosts', + Url::fromPath('icingadb/hosts')->setQueryString($filterString) + )), + new HtmlElement('li', null, new Link( + 'icingadb/services', + Url::fromPath('icingadb/services')->setQueryString($filterString) + )) + ); + + break; + case 'icingadb/filter/services': + $filterString = join('|', $restrictions); + $list = new HtmlElement( + 'ul', + Attributes::create(['class' => 'links']), + new HtmlElement('li', null, new Link( + 'icingadb/services', + Url::fromPath('icingadb/services')->setQueryString($filterString) + )) + ); + + break; + case 'monitoring/filter/objects': + $filterString = join('|', $restrictions); + $list = new HtmlElement( + 'ul', + Attributes::create(['class' => 'links']), + new HtmlElement('li', null, new Link( + 'monitoring/list/hosts', + Url::fromPath('monitoring/list/hosts')->setQueryString($filterString) + )), + new HtmlElement('li', null, new Link( + 'monitoring/list/services', + Url::fromPath('monitoring/list/services')->setQueryString($filterString) + )), + new HtmlElement('li', null, new Link( + 'monitoring/list/hostgroups', + Url::fromPath('monitoring/list/hostgroups')->setQueryString($filterString) + )), + new HtmlElement('li', null, new Link( + 'monitoring/list/servicegroups', + Url::fromPath('monitoring/list/servicegroups')->setQueryString($filterString) + )) + ); + + break; + case 'application/share/users': + $filter = Filter::any(); + foreach ($restrictions as $roleRestriction) { + $userNames = StringHelper::trimSplit($roleRestriction); + foreach ($userNames as $userName) { + $filter->add(Filter::equal('user_name', $userName)); + } + } + + $filterString = QueryString::render($filter); + $list = new HtmlElement( + 'ul', + Attributes::create(['class' => 'links']), + new HtmlElement('li', null, new Link( + 'user/list', + Url::fromPath('user/list')->setQueryString($filterString) + )) + ); + + break; + case 'application/share/groups': + $filter = Filter::any(); + foreach ($restrictions as $roleRestriction) { + $groupNames = StringHelper::trimSplit($roleRestriction); + foreach ($groupNames as $groupName) { + $filter->add(Filter::equal('group_name', $groupName)); + } + } + + $filterString = QueryString::render($filter); + $list = new HtmlElement( + 'ul', + Attributes::create(['class' => 'links']), + new HtmlElement('li', null, new Link( + 'group/list', + Url::fromPath('group/list')->setQueryString($filterString) + )) + ); + + break; + default: + $filterString = join(', ', $restrictions); + $list = null; + } + + return [$filterString, $list]; + } +} diff --git a/library/Icinga/Web/View/helpers/format.php b/library/Icinga/Web/View/helpers/format.php new file mode 100644 index 0000000..4008583 --- /dev/null +++ b/library/Icinga/Web/View/helpers/format.php @@ -0,0 +1,72 @@ +addHelperFunction('format', function () { + return Format::getInstance(); +}); + +$this->addHelperFunction('formatDate', function ($date) { + if (! $date) { + return ''; + } + return DateFormatter::formatDate($date); +}); + +$this->addHelperFunction('formatDateTime', function ($dateTime) { + if (! $dateTime) { + return ''; + } + return DateFormatter::formatDateTime($dateTime); +}); + +$this->addHelperFunction('formatDuration', function ($seconds) { + if (! $seconds) { + return ''; + } + return DateFormatter::formatDuration($seconds); +}); + +$this->addHelperFunction('formatTime', function ($time) { + if (! $time) { + return ''; + } + return DateFormatter::formatTime($time); +}); + +$this->addHelperFunction('timeAgo', function ($time, $timeOnly = false, $requireTime = false) { + if (! $time) { + return ''; + } + return sprintf( + '%s', + DateFormatter::formatDateTime($time), + DateFormatter::timeAgo($time, $timeOnly, $requireTime) + ); +}); + +$this->addHelperFunction('timeSince', function ($time, $timeOnly = false, $requireTime = false) { + if (! $time) { + return ''; + } + return sprintf( + '%s', + DateFormatter::formatDateTime($time), + DateFormatter::timeSince($time, $timeOnly, $requireTime) + ); +}); + +$this->addHelperFunction('timeUntil', function ($time, $timeOnly = false, $requireTime = false) { + if (! $time) { + return ''; + } + return sprintf( + '%s', + DateFormatter::formatDateTime($time), + DateFormatter::timeUntil($time, $timeOnly, $requireTime) + ); +}); diff --git a/library/Icinga/Web/View/helpers/generic.php b/library/Icinga/Web/View/helpers/generic.php new file mode 100644 index 0000000..bfd3f86 --- /dev/null +++ b/library/Icinga/Web/View/helpers/generic.php @@ -0,0 +1,15 @@ +addHelperFunction('auth', function () { + return Auth::getInstance(); +}); + +$this->addHelperFunction('widget', function ($name, $options = null) { + return Widget::create($name, $options); +}); diff --git a/library/Icinga/Web/View/helpers/string.php b/library/Icinga/Web/View/helpers/string.php new file mode 100644 index 0000000..b3f667b --- /dev/null +++ b/library/Icinga/Web/View/helpers/string.php @@ -0,0 +1,36 @@ +addHelperFunction('ellipsis', function ($string, $maxLength, $ellipsis = '...') { + return StringHelper::ellipsis($string, $maxLength, $ellipsis); +}); + +$this->addHelperFunction('nl2br', function ($string) { + return nl2br(str_replace(array('\r\n', '\r', '\n'), '
    ', $string), false); +}); + +$this->addHelperFunction('markdown', function ($content, $containerAttribs = null) { + if (! isset($containerAttribs['class'])) { + $containerAttribs['class'] = 'markdown'; + } else { + $containerAttribs['class'] .= ' markdown'; + } + + return 'propertiesToString($containerAttribs) . '>' . Markdown::text($content) . ''; +}); + +$this->addHelperFunction('markdownLine', function ($content, $containerAttribs = null) { + if (! isset($containerAttribs['class'])) { + $containerAttribs['class'] = 'markdown inline'; + } else { + $containerAttribs['class'] .= ' markdown inline'; + } + + return 'propertiesToString($containerAttribs) . '>' . + Markdown::line($content) . ''; +}); diff --git a/library/Icinga/Web/View/helpers/url.php b/library/Icinga/Web/View/helpers/url.php new file mode 100644 index 0000000..277c237 --- /dev/null +++ b/library/Icinga/Web/View/helpers/url.php @@ -0,0 +1,158 @@ +addHelperFunction('href', function ($path = null, $params = null) use ($view) { + return $view->url($path, $params); +}); + +$this->addHelperFunction('url', function ($path = null, $params = null) { + if ($path === null) { + $url = Url::fromRequest(); + } elseif ($path instanceof Url) { + $url = $path; + } else { + $url = Url::fromPath($path); + } + + if ($params !== null) { + if ($url === $path) { + $url = clone $url; + } + + $url->overwriteParams($params); + } + + return $url; +}); + +$this->addHelperFunction( + 'qlink', + function ($title, $url, $params = null, $properties = null, $escape = true) use ($view) { + $icon = ''; + if ($properties) { + if (array_key_exists('title', $properties) && !array_key_exists('aria-label', $properties)) { + $properties['aria-label'] = $properties['title']; + } + + if (array_key_exists('icon', $properties)) { + $icon = $view->icon($properties['icon']); + unset($properties['icon']); + } + + if (array_key_exists('img', $properties)) { + $icon = $view->img($properties['img']); + unset($properties['img']); + } + } + + return sprintf( + '%s', + $view->url($url, $params), + $view->propertiesToString($properties), + $icon . ($escape ? $view->escape($title) : $title) + ); + } +); + +$this->addHelperFunction('img', function ($url, $params = null, array $properties = array()) use ($view) { + if (! array_key_exists('alt', $properties)) { + $properties['alt'] = ''; + } + + $ariaHidden = array_key_exists('aria-hidden', $properties) ? $properties['aria-hidden'] : null; + if (array_key_exists('title', $properties)) { + if (! array_key_exists('aria-label', $properties) && $ariaHidden !== 'true') { + $properties['aria-label'] = $properties['title']; + } + } elseif ($ariaHidden === null) { + $properties['aria-hidden'] = 'true'; + } + + return sprintf( + '', + $view->escape($view->url($url, $params)->getAbsoluteUrl()), + $view->propertiesToString($properties) + ); +}); + +$this->addHelperFunction('icon', function ($img, $title = null, array $properties = array()) use ($view) { + if (strpos($img, '.') !== false) { + if (array_key_exists('class', $properties)) { + $properties['class'] .= ' icon'; + } else { + $properties['class'] = 'icon'; + } + if (strpos($img, '/') === false) { + return $view->img('img/icons/' . $img, null, $properties); + } else { + return $view->img($img, null, $properties); + } + } + + $ariaHidden = array_key_exists('aria-hidden', $properties) ? $properties['aria-hidden'] : null; + if ($title !== null) { + $properties['role'] = 'img'; + $properties['title'] = $title; + + if (! array_key_exists('aria-label', $properties) && $ariaHidden !== 'true') { + $properties['aria-label'] = $title; + } + } elseif ($ariaHidden === null) { + $properties['aria-hidden'] = 'true'; + } + + if (isset($properties['class'])) { + $properties['class'] .= ' icon-' . $img; + } else { + $properties['class'] = 'icon-' . $img; + } + + return sprintf('', $view->propertiesToString($properties)); +}); + +$this->addHelperFunction('propertiesToString', function ($properties) use ($view) { + if (empty($properties)) { + return ''; + } + $attributes = array(); + + foreach ($properties as $key => $val) { + if ($key === 'style' && is_array($val)) { + if (empty($val)) { + continue; + } + $parts = array(); + foreach ($val as $k => $v) { + $parts[] = "$k: $v"; + } + $val = implode('; ', $parts); + continue; + } + + $attributes[] = $view->attributeToString($key, $val); + } + return ' ' . implode(' ', $attributes); +}); + +$this->addHelperFunction('attributeToString', function ($key, $value) use ($view) { + // TODO: Doublecheck this! + if (! preg_match('~^[a-zA-Z0-9-]+$~', $key)) { + throw new ProgrammingError( + 'Trying to set an invalid HTML attribute name: %s', + $key + ); + } + + return sprintf( + '%s="%s"', + $key, + $view->escape($value) + ); +}); diff --git a/library/Icinga/Web/Widget.php b/library/Icinga/Web/Widget.php new file mode 100644 index 0000000..242138b --- /dev/null +++ b/library/Icinga/Web/Widget.php @@ -0,0 +1,48 @@ + + * $tabs = Widget::create('tabs'); + * + * + * @copyright Copyright (c) 2013 Icinga-Web Team + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +class Widget +{ + /** + * Create a new widget + * + * @param string $name Widget name + * @param array $options Widget constructor options + * + * @return Icinga\Web\Widget\AbstractWidget + */ + public static function create($name, $options = array(), $module_name = null) + { + $class = 'Icinga\\Web\\Widget\\' . ucfirst($name); + + if (! class_exists($class)) { + throw new ProgrammingError( + 'There is no such widget: %s', + $name + ); + } + + $widget = new $class($options, $module_name); + return $widget; + } +} diff --git a/library/Icinga/Web/Widget/AbstractWidget.php b/library/Icinga/Web/Widget/AbstractWidget.php new file mode 100644 index 0000000..a3f5a35 --- /dev/null +++ b/library/Icinga/Web/Widget/AbstractWidget.php @@ -0,0 +1,120 @@ + + * @author Icinga-Web Team + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + */ +abstract class AbstractWidget +{ + /** + * If you are going to access the current view with the view() function, + * its instance is stored here for performance reasons. + * + * @var Zend_View_Abstract + */ + protected static $view; + + // TODO: Should we kick this? + protected $properties = array(); + + /** + * Getter for widget properties + * + * @param string $key The option you're interested in + * + * @throws ProgrammingError for unknown property name + * + * @return mixed + */ + public function __get($key) + { + if (array_key_exists($key, $this->properties)) { + return $this->properties[$key]; + } + + throw new ProgrammingError( + 'Trying to get invalid "%s" property for %s', + $key, + get_class($this) + ); + } + + /** + * Setter for widget properties + * + * @param string $key The option you want to set + * @param string $val The new value going to be assigned to this option + * + * @throws ProgrammingError for unknown property name + * + * @return mixed + */ + public function __set($key, $val) + { + if (array_key_exists($key, $this->properties)) { + $this->properties[$key] = $val; + return; + } + + throw new ProgrammingError( + 'Trying to set invalid "%s" property in %s. Allowed are: %s', + $key, + get_class($this), + empty($this->properties) + ? 'none' + : implode(', ', array_keys($this->properties)) + ); + } + + abstract public function render(); + + /** + * Access the current view + * + * Will instantiate a new one if none exists + * // TODO: App->getView + * + * @return Zend_View_Abstract + */ + protected function view() + { + if (self::$view === null) { + self::$view = Icinga::app()->getViewRenderer()->view; + } + + return self::$view; + } + + /** + * Cast this widget to a string. Will call your render() function + * + * @return string + */ + public function __toString() + { + try { + $html = $this->render($this->view()); + } catch (Exception $e) { + return htmlspecialchars($e->getMessage()); + } + return (string) $html; + } +} diff --git a/library/Icinga/Web/Widget/Announcements.php b/library/Icinga/Web/Widget/Announcements.php new file mode 100644 index 0000000..5944eb4 --- /dev/null +++ b/library/Icinga/Web/Widget/Announcements.php @@ -0,0 +1,55 @@ +getEtag(); + $cookie = new AnnouncementCookie(); + if ($cookie->getEtag() !== $etag) { + $cookie->setEtag($etag); + $cookie->setNextActive($repo->findNextActive()); + Icinga::app()->getResponse()->setCookie($cookie); + } + $acked = array(); + foreach ($cookie->getAcknowledged() as $hash) { + $acked[] = Filter::expression('hash', '!=', $hash); + } + $acked = Filter::matchAll($acked); + $announcements = $repo->findActive(); + $announcements->applyFilter($acked); + if ($announcements->hasResult()) { + $html = '
      '; + foreach ($announcements as $announcement) { + $ackForm = new AcknowledgeAnnouncementForm(); + $ackForm->populate(array('hash' => $announcement->hash)); + $html .= '
    • ' + . Markdown::text($announcement->message) + . '
      ' + . $ackForm + . '
    • '; + } + $html .= '
    '; + return $html; + } + // Force container update on XHR + return '
    '; + } +} diff --git a/library/Icinga/Web/Widget/ApplicationStateMessages.php b/library/Icinga/Web/Widget/ApplicationStateMessages.php new file mode 100644 index 0000000..e8f5e72 --- /dev/null +++ b/library/Icinga/Web/Widget/ApplicationStateMessages.php @@ -0,0 +1,74 @@ +getAcknowledgedMessages()); + $messages = ApplicationStateHook::getAllMessages(); + + $active = array_diff_key($messages, $acked); + + return $active; + } + + public function render() + { + $enabled = Auth::getInstance() + ->getUser() + ->getPreferences() + ->getValue('icingaweb', 'show_application_state_messages', 'system'); + + if ($enabled === 'system') { + $enabled = Config::app()->get('global', 'show_application_state_messages', true); + } + + if (! (bool) $enabled) { + return '
    '; + } + + $active = $this->getMessages(); + + if (empty($active)) { + // Force container update on XHR + return '
    '; + } + + $html = '
    '; + + reset($active); + + $id = key($active); + $spec = current($active); + $message = array_pop($spec); // We don't use state and timestamp here + + + $ackForm = new AcknowledgeApplicationStateMessageForm(); + $ackForm->populate(['id' => $id]); + + $html .= '
    '; + $html .= Markdown::text($message); + $html .= '
    '; + + $html .= $ackForm; + + $html .= '
    '; + + return $html; + } +} diff --git a/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php new file mode 100644 index 0000000..ed1e377 --- /dev/null +++ b/library/Icinga/Web/Widget/Chart/HistoryColorGrid.php @@ -0,0 +1,373 @@ +setColor($color); + if (isset($start)) { + $this->start = $this->tsToDateStr($start); + } + if (isset($end)) { + $this->end = $this->tsToDateStr($end); + } + } + + /** + * Set the displayed data-set + * + * @param $events array The history events to display as an array of arrays: + * value: The value to display + * caption: The caption on mouse-over + * url: The url to open on click. + */ + public function setData(array $events) + { + $this->data = $events; + $start = time(); + $end = time(); + foreach ($this->data as $entry) { + $entry['value'] = intval($entry['value']); + } + foreach ($this->data as $date => $entry) { + $time = strtotime($date); + if ($entry['value'] > $this->maxValue) { + $this->maxValue = $entry['value']; + } + if ($time > $end) { + $end = $time; + } + if ($time < $start) { + $start = $time; + } + } + if (!isset($this->start)) { + $this->start = $this->tsToDateStr($start); + } + if (!isset($this->end)) { + $this->end = $this->tsToDateStr($end); + } + } + + /** + * Set the used color. + * + * @param $color + */ + public function setColor($color) + { + $this->color = $color; + } + + /** + * Set the used opacity + * + * @param $opacity + */ + public function setOpacity($opacity) + { + $this->opacity = $opacity; + } + + /** + * Calculate the color to display for the given value. + * + * @param $value integer + * + * @return string The color-string to use for this entry. + */ + private function calculateColor($value) + { + $saturation = $value / $this->maxValue; + return Color::changeSaturation($this->color, $saturation); + } + + /** + * Render the html to display the given $day + * + * @param $day string The day to display YYYY-MM-DD + * + * @return string The rendered html + */ + private function renderDay($day) + { + if (array_key_exists($day, $this->data) && $this->data[$day]['value'] > 0) { + $entry = $this->data[$day]; + return ''; + } else { + return ''; + } + } + + /** + * Render the grid with an horizontal alignment. + * + * @param array $grid The values returned from the createGrid function + * + * @return string The rendered html + */ + private function renderHorizontal($grid) + { + $weeks = $grid['weeks']; + $months = $grid['months']; + $years = $grid['years']; + $html = ''; + $html .= ''; + $old = -1; + foreach ($months as $week => $month) { + if ($old !== $month) { + $old = $month; + $txt = $this->monthName($month, $years[$week]); + } else { + $txt = ''; + } + $html .= ''; + } + $html .= ''; + for ($i = 0; $i < 7; $i++) { + $html .= $this->renderWeekdayHorizontal($i, $weeks); + } + $html .= '
    ' . $txt . '
    '; + return $html; + } + + /** + * @param $grid + * + * @return string + */ + private function renderVertical($grid) + { + $years = $grid['years']; + $weeks = $grid['weeks']; + $months = $grid['months']; + $html = ''; + $html .= ''; + for ($i = 0; $i < 7; $i++) { + $html .= '"; + } + $html .= ''; + $old = -1; + foreach ($weeks as $index => $week) { + for ($i = 0; $i < 7; $i++) { + if (array_key_exists($i, $week)) { + $html .= ''; + } else { + $html .= ''; + } + } + if ($old !== $months[$index]) { + $old = $months[$index]; + $txt = $this->monthName($old, $years[$index]); + } else { + $txt = ''; + } + $html .= ''; + } + $html .= '
    ' . $this->weekdayName($this->weekStartMonday ? $i + 1 : $i) . "
    ' . $this->renderDay($week[$i]) . '' . $txt . '
    '; + return $html; + } + + /** + * Render the row for the given weekday. + * + * @param integer $weekday The day to render (0-6) + * @param array $weeks The weeks + * + * @return string The formatted table-row + */ + private function renderWeekdayHorizontal($weekday, &$weeks) + { + $html = '' + . $this->weekdayName($this->weekStartMonday ? $weekday + 1 : $weekday) + . ''; + foreach ($weeks as $week) { + if (array_key_exists($weekday, $week)) { + $html .= '' . $this->renderDay($week[$weekday]) . ''; + } else { + $html .= ''; + } + } + $html .= ''; + return $html; + } + + + + /** + * @return array + */ + private function createGrid() + { + $weeks = array(array()); + $week = 0; + $months = array(); + $years = array(); + $start = strtotime($this->start); + $year = intval(date('Y', $start)); + $month = intval(date('n', $start)); + $day = intval(date('j', $start)); + $weekday = intval(date('w', $start)); + if ($this->weekStartMonday) { + // 0 => monday, 6 => sunday + $weekday = $weekday === 0 ? 6 : $weekday - 1; + } + + $date = $this->toDateStr($day, $month, $year); + $weeks[0][$weekday] = $date; + $years[0] = $year; + $months[0] = $month; + while ($date !== $this->end) { + $day++; + $weekday++; + if ($weekday > 6) { + $weekday = 0; + $weeks[] = array(); + // PRESENT => The last day of week determines the month + if ($this->weekFlow === self::CAL_GROW_INTO_PRESENT) { + $months[$week] = $month; + $years[$week] = $year; + } + $week++; + } + if ($day > date('t', mktime(0, 0, 0, $month, 1, $year))) { + $month++; + if ($month > 12) { + $year++; + $month = 1; + } + $day = 1; + } + if ($weekday === 0) { + // PAST => The first day of each week determines the month + if ($this->weekFlow === self::CAL_GROW_INTO_PAST) { + $months[$week] = $month; + $years[$week] = $year; + } + } + $date = $this->toDateStr($day, $month, $year); + $weeks[$week][$weekday] = $date; + }; + $years[$week] = $year; + $months[$week] = $month; + if ($this->weekFlow == self::CAL_GROW_INTO_PAST) { + return array( + 'weeks' => array_reverse($weeks), + 'months' => array_reverse($months), + 'years' => array_reverse($years) + ); + } + return array( + 'weeks' => $weeks, + 'months' => $months, + 'years' => $years + ); + } + + /** + * Get the localized month-name for the given month + * + * @param integer $month The month-number + * + * @return string The + */ + private function monthName($month, $year) + { + // TODO: find a way to render years without messing up the layout + $dt = new DateTime($year . '-' . $month . '-01'); + return $dt->format('M'); + } + + /** + * @param $weekday + * + * @return string + */ + private function weekdayName($weekday) + { + $sun = new DateTime('last Sunday'); + $interval = new DateInterval('P' . $weekday . 'D'); + $sun->add($interval); + return substr($sun->format('D'), 0, 2); + } + + /** + * + * + * @param $timestamp + * + * @return bool|string + */ + private function tsToDateStr($timestamp) + { + return date('Y-m-d', $timestamp); + } + + /** + * @param $day + * @param $mon + * @param $year + * + * @return string + */ + private function toDateStr($day, $mon, $year) + { + $day = $day > 9 ? (string)$day : '0' . (string)$day; + $mon = $mon > 9 ? (string)$mon : '0' . (string)$mon; + return $year . '-' . $mon . '-' . $day; + } + + /** + * @return string + */ + public function render() + { + if (empty($this->data)) { + return '
    No entries
    '; + } + $grid = $this->createGrid(); + if ($this->orientation === self::ORIENTATION_HORIZONTAL) { + return $this->renderHorizontal($grid); + } + return $this->renderVertical($grid); + } +} diff --git a/library/Icinga/Web/Widget/Chart/InlinePie.php b/library/Icinga/Web/Widget/Chart/InlinePie.php new file mode 100644 index 0000000..21b4ca4 --- /dev/null +++ b/library/Icinga/Web/Widget/Chart/InlinePie.php @@ -0,0 +1,257 @@ +{svg}'; + + /** + * The colors used to display the slices of this pie-chart. + * + * @var array + */ + private $colors = array('#049BAF', '#ffaa44', '#ff5566', '#ddccdd'); + + /** + * The title of the chart + * + * @var string + */ + private $title; + + /** + * @var int + */ + private $size = 16; + + /** + * The data displayed by the pie-chart + * + * @var array + */ + private $data; + + /** + * @var + */ + private $class = ''; + + /** + * Set the data to be displayed. + * + * @param $data array + * + * @return $this + */ + public function setData(array $data) + { + $this->data = $data; + + return $this; + } + + /** + * Set the size of the inline pie + * + * @param int $size Sets both, the height and width + * + * @return $this + */ + public function setSize($size = null) + { + $this->size = $size; + + return $this; + } + + /** + * Set the class to define the + * + * @param $class + * + * @return $this + */ + public function setSparklineClass($class) + { + $this->class = $class; + + return $this; + } + + /** + * Set the colors used by the slices of the pie chart. + * + * @param array $colors + * + * @return $this + */ + public function setColors(array $colors = null) + { + $this->colors = $colors; + + return $this; + } + + /** + * Set the title of the displayed Data + * + * @param string $title + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $this->view()->escape($title); + + return $this; + } + + /** + * Create a new InlinePie + * + * @param array $data The data displayed by the slices + * @param string $title The title of this Pie + * @param array $colors An array of RGB-Color values to use + */ + public function __construct(array $data, $title, $colors = null) + { + $this->setTitle($title); + + if (array_key_exists('data', $data)) { + $this->data = $data['data']; + if (array_key_exists('colors', $data)) { + $this->colors = $data['colors']; + } + } else { + $this->setData($data); + } + + if (isset($colors)) { + $this->setColors($colors); + } else { + $this->setColors($this->colors); + } + } + + /** + * Renders this widget via the given view and returns the + * HTML as a string + * + * @return string + */ + public function render() + { + $pie = new PieChart(); + $pie->alignTopLeft(); + $pie->disableLegend(); + $pie->drawPie([ + 'data' => $this->data, + 'colors' => $this->colors + ]); + + if ($this->view()->layout()->getLayout() === 'pdf') { + try { + $png = $pie->toPng($this->size, $this->size); + return ''; + } catch (IcingaException $_) { + return ''; + } + } + + $pie->title = $this->title; + $pie->description = $this->title; + + $template = $this->template; + $template = str_replace('{class}', $this->class, $template); + $template = str_replace('{svg}', $pie->render(), $template); + + return $template; + } + + public static function createFromStateSummary(stdClass $states, $title, array $colors) + { + $handledUnhandledStates = []; + foreach ($states as $key => $value) { + if (StringHelper::endsWith($key, '_handled') || StringHelper::endsWith($key, '_unhandled')) { + $handledUnhandledStates[$key] = $value; + } + } + + $chart = new self(array_values($handledUnhandledStates), $title, $colors); + + return $chart + ->setSize(50) + ->setTitle('') + ->setSparklineClass('sparkline-multi'); + } +} diff --git a/library/Icinga/Web/Widget/Dashboard.php b/library/Icinga/Web/Widget/Dashboard.php new file mode 100644 index 0000000..5a8796d --- /dev/null +++ b/library/Icinga/Web/Widget/Dashboard.php @@ -0,0 +1,475 @@ +getTabs()->activate($name); + } + + /** + * Load Pane items provided by all enabled modules + * + * @return $this + */ + public function load() + { + $navigation = new Navigation(); + $navigation->load('dashboard-pane'); + + $panes = array(); + foreach ($navigation as $dashboardPane) { + /** @var DashboardPane $dashboardPane */ + $pane = new Pane($dashboardPane->getLabel()); + foreach ($dashboardPane->getChildren() as $dashlet) { + $pane->addDashlet($dashlet->getLabel(), $dashlet->getUrl()); + } + + $panes[] = $pane; + } + + $this->mergePanes($panes); + $this->loadUserDashboards($navigation); + return $this; + } + + /** + * Create and return a Config object for this dashboard + * + * @return Config + */ + public function getConfig() + { + $output = array(); + foreach ($this->panes as $pane) { + if ($pane->isUserWidget()) { + $output[$pane->getName()] = $pane->toArray(); + } + foreach ($pane->getDashlets() as $dashlet) { + if ($dashlet->isUserWidget()) { + $output[$pane->getName() . '.' . $dashlet->getName()] = $dashlet->toArray(); + } + } + } + + return DashboardConfig::fromArray($output)->setConfigFile($this->getConfigFile())->setUser($this->user); + } + + /** + * Load user dashboards from all config files that match the username + */ + protected function loadUserDashboards(Navigation $navigation) + { + foreach (DashboardConfig::listConfigFilesForUser($this->user) as $file) { + $this->loadUserDashboardsFromFile($file, $navigation); + } + } + + /** + * Load user dashboards from the given config file + * + * @param string $file + * + * @return bool + */ + protected function loadUserDashboardsFromFile($file, Navigation $dashboardNavigation) + { + try { + $config = Config::fromIni($file); + } catch (NotReadableError $e) { + return false; + } + + if (! count($config)) { + return false; + } + $panes = array(); + $dashlets = array(); + foreach ($config as $key => $part) { + if (strpos($key, '.') === false) { + $dashboardPane = $dashboardNavigation->getItem($key); + if ($dashboardPane !== null) { + $key = $dashboardPane->getLabel(); + } + if ($this->hasPane($key)) { + $panes[$key] = $this->getPane($key); + } else { + $panes[$key] = new Pane($key); + $panes[$key]->setTitle($part->title); + } + $panes[$key]->setUserWidget(); + if ((bool) $part->get('disabled', false) === true) { + $panes[$key]->setDisabled(); + } + } else { + list($paneName, $dashletName) = explode('.', $key, 2); + $dashboardPane = $dashboardNavigation->getItem($paneName); + if ($dashboardPane !== null) { + $paneName = $dashboardPane->getLabel(); + $dashletItem = $dashboardPane->getChildren()->getItem($dashletName); + if ($dashletItem !== null) { + $dashletName = $dashletItem->getLabel(); + } + } + $part->pane = $paneName; + $part->dashlet = $dashletName; + $dashlets[] = $part; + } + } + foreach ($dashlets as $dashletData) { + $pane = null; + + if (array_key_exists($dashletData->pane, $panes) === true) { + $pane = $panes[$dashletData->pane]; + } elseif (array_key_exists($dashletData->pane, $this->panes) === true) { + $pane = $this->panes[$dashletData->pane]; + } else { + continue; + } + $dashlet = new DashboardDashlet( + $dashletData->title, + $dashletData->url, + $pane + ); + $dashlet->setName($dashletData->dashlet); + + if ((bool) $dashletData->get('disabled', false) === true) { + $dashlet->setDisabled(true); + } + + $dashlet->setUserWidget(); + $pane->addDashlet($dashlet); + } + + $this->mergePanes($panes); + + return true; + } + + /** + * Merge panes with existing panes + * + * @param array $panes + * + * @return $this + */ + public function mergePanes(array $panes) + { + /** @var $pane Pane */ + foreach ($panes as $pane) { + if ($this->hasPane($pane->getName()) === true) { + /** @var $current Pane */ + $current = $this->panes[$pane->getName()]; + $current->addDashlets($pane->getDashlets()); + } else { + $this->panes[$pane->getName()] = $pane; + } + } + + return $this; + } + + /** + * Return the tab object used to navigate through this dashboard + * + * @return Tabs + */ + public function getTabs() + { + $url = Url::fromPath('dashboard')->getUrlWithout($this->tabParam); + if ($this->tabs === null) { + $this->tabs = new Tabs(); + + foreach ($this->panes as $key => $pane) { + if ($pane->getDisabled()) { + continue; + } + $this->tabs->add( + $key, + array( + 'title' => sprintf( + t('Show %s', 'dashboard.pane.tooltip'), + $pane->getTitle() + ), + 'label' => $pane->getTitle(), + 'url' => clone($url), + 'urlParams' => array($this->tabParam => $key) + ) + ); + } + } + return $this->tabs; + } + + /** + * Return all panes of this dashboard + * + * @return array + */ + public function getPanes() + { + return $this->panes; + } + + + /** + * Creates a new empty pane with the given title + * + * @param string $title + * + * @return $this + */ + public function createPane($title) + { + $pane = new Pane($title); + $pane->setTitle($title); + $this->addPane($pane); + + return $this; + } + + /** + * Checks if the current dashboard has any panes + * + * @return bool + */ + public function hasPanes() + { + return ! empty($this->panes); + } + + /** + * Check if a panel exist + * + * @param string $pane + * @return bool + */ + public function hasPane($pane) + { + return $pane && array_key_exists($pane, $this->panes); + } + + /** + * Add a pane object to this dashboard + * + * @param Pane $pane The pane to add + * + * @return $this + */ + public function addPane(Pane $pane) + { + $this->panes[$pane->getName()] = $pane; + return $this; + } + + public function removePane($title) + { + if ($this->hasPane($title) === true) { + $pane = $this->getPane($title); + if ($pane->isUserWidget() === true) { + unset($this->panes[$pane->getName()]); + } else { + $pane->setDisabled(); + $pane->setUserWidget(); + } + } else { + throw new ProgrammingError('Pane not found: ' . $title); + } + } + + /** + * Return the pane with the provided name + * + * @param string $name The name of the pane to return + * + * @return Pane The pane or null if no pane with the given name exists + * @throws ProgrammingError + */ + public function getPane($name) + { + if (! array_key_exists($name, $this->panes)) { + throw new ProgrammingError( + 'Trying to retrieve invalid dashboard pane "%s"', + $name + ); + } + return $this->panes[$name]; + } + + /** + * Return an array with pane name=>title format used for comboboxes + * + * @return array + */ + public function getPaneKeyTitleArray() + { + $list = array(); + foreach ($this->panes as $name => $pane) { + $list[$name] = $pane->getTitle(); + } + return $list; + } + + /** + * @see Icinga\Web\Widget::render + */ + public function render() + { + if (empty($this->panes)) { + return ''; + } + + return $this->determineActivePane()->render(); + } + + /** + * Activates the default pane of this dashboard and returns its name + * + * @return mixed + */ + private function setDefaultPane() + { + $active = null; + + foreach ($this->panes as $key => $pane) { + if ($pane->getDisabled() === false) { + $active = $key; + break; + } + } + + if ($active !== null) { + $this->activate($active); + } + return $active; + } + + /** + * @see determineActivePane() + */ + public function getActivePane() + { + return $this->determineActivePane(); + } + + /** + * Determine the active pane either by the selected tab or the current request + * + * @throws \Icinga\Exception\ConfigurationError + * @throws \Icinga\Exception\ProgrammingError + * + * @return Pane The currently active pane + */ + public function determineActivePane() + { + $active = $this->getTabs()->getActiveName(); + if (! $active) { + if ($active = Url::fromRequest()->getParam($this->tabParam)) { + if ($this->hasPane($active)) { + $this->activate($active); + } else { + throw new ProgrammingError( + 'Try to get an inexistent pane.' + ); + } + } else { + $active = $this->setDefaultPane(); + } + } + + if (isset($this->panes[$active])) { + return $this->panes[$active]; + } + + throw new ConfigurationError('Could not determine active pane'); + } + + /** + * Setter for user object + * + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * Getter for user object + * + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** + * Get config file + * + * @return string + */ + public function getConfigFile() + { + if ($this->user === null) { + throw new ProgrammingError('Can\'t load dashboards. User is not set'); + } + return Config::resolvePath('dashboards/' . strtolower($this->user->getUsername()) . '/dashboard.ini'); + } +} diff --git a/library/Icinga/Web/Widget/Dashboard/Dashlet.php b/library/Icinga/Web/Widget/Dashboard/Dashlet.php new file mode 100644 index 0000000..2ba26df --- /dev/null +++ b/library/Icinga/Web/Widget/Dashboard/Dashlet.php @@ -0,0 +1,315 @@ + +

    {TITLE}

    +

    {PROGRESS_LABEL}...

    + + +EOD; + + /** + * The template string used for rendering this widget in case of an error + * + * @var string + */ + private $errorTemplate = <<<'EOD' + +
    +

    {TITLE}

    +

    {ERROR_MESSAGE}

    +
    +EOD; + + /** + * Create a new dashlet displaying the given url in the provided pane + * + * @param string $title The title to use for this dashlet + * @param Url|string $url The url this dashlet uses for displaying information + * @param Pane $pane The pane this Dashlet will be added to + */ + public function __construct($title, $url, Pane $pane) + { + $this->name = $title; + $this->title = $title; + $this->pane = $pane; + $this->url = $url; + } + + public function setName($name) + { + $this->name = $name; + return $this; + } + + public function getName() + { + return $this->name; + } + + /** + * Retrieve the dashlets title + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * @param string $title + */ + public function setTitle($title) + { + $this->title = $title; + } + + /** + * Retrieve the dashlets url + * + * @return Url|null + */ + public function getUrl() + { + if ($this->url !== null && ! $this->url instanceof Url) { + $this->url = Url::fromPath($this->url); + } + return $this->url; + } + + /** + * Set the dashlets URL + * + * @param string|Url $url The url to use, either as an Url object or as a path + * + * @return $this + */ + public function setUrl($url) + { + $this->url = $url; + return $this; + } + + /** + * Set the disabled property + * + * @param boolean $disabled + */ + public function setDisabled($disabled) + { + $this->disabled = $disabled; + } + + /** + * Get the disabled property + * + * @return boolean + */ + public function getDisabled() + { + return $this->disabled; + } + + /** + * Set the progress label to use + * + * @param string $label + * + * @return $this + */ + public function setProgressLabel($label) + { + $this->progressLabel = $label; + return $this; + } + + /** + * Return the progress label to use + * + * @return string + */ + public function getProgressLabe() + { + if ($this->progressLabel === null) { + return $this->view()->translate('Loading'); + } + + return $this->progressLabel; + } + + /** + * Return this dashlet's structure as array + * + * @return array + */ + public function toArray() + { + $array = array( + 'url' => $this->getUrl()->getRelativeUrl(), + 'title' => $this->getTitle() + ); + if ($this->getDisabled() === true) { + $array['disabled'] = 1; + } + return $array; + } + + /** + * @see Widget::render() + */ + public function render() + { + if ($this->disabled === true) { + return ''; + } + + $view = $this->view(); + + if (! $this->url) { + $searchTokens = array( + '{TOOLTIP}', + '{TITLE}', + '{ERROR_MESSAGE}' + ); + + $replaceTokens = array( + sprintf($view->translate('Show %s', 'dashboard.dashlet.tooltip'), $view->escape($this->getTitle())), + $view->escape($this->getTitle()), + $view->escape( + sprintf($view->translate('Cannot create dashboard dashlet "%s" without valid URL'), $this->title) + ) + ); + + return str_replace($searchTokens, $replaceTokens, $this->errorTemplate); + } + + $url = $this->getUrl(); + $url->setParam('showCompact', true); + $iframeUrl = clone $url; + $iframeUrl->setParam('isIframe'); + + $searchTokens = array( + '{URL}', + '{IFRAME_URL}', + '{FULL_URL}', + '{TOOLTIP}', + '{TITLE}', + '{TITLE_PREFIX}', + '{PROGRESS_LABEL}' + ); + + $replaceTokens = array( + $url, + $iframeUrl, + $url->getUrlWithout(['showCompact', 'limit', 'view']), + sprintf($view->translate('Show %s', 'dashboard.dashlet.tooltip'), $view->escape($this->getTitle())), + $view->escape($this->getTitle()), + $view->translate('Dashlet') . ': ', + $this->getProgressLabe() + ); + + return str_replace($searchTokens, $replaceTokens, $this->template); + } + + /** + * Create a @see Dashlet instance from the given Zend config, using the provided title + * + * @param $title The title for this dashlet + * @param ConfigObject $config The configuration defining url, parameters, height, width, etc. + * @param Pane $pane The pane this dashlet belongs to + * + * @return Dashlet A newly created Dashlet for use in the Dashboard + */ + public static function fromIni($title, ConfigObject $config, Pane $pane) + { + $height = null; + $width = null; + $url = $config->get('url'); + $parameters = $config->toArray(); + unset($parameters['url']); // otherwise there's an url = parameter in the Url + + $cmp = new Dashlet($title, Url::fromPath($url, $parameters), $pane); + return $cmp; + } + + /** + * @param \Icinga\Web\Widget\Dashboard\Pane $pane + */ + public function setPane(Pane $pane) + { + $this->pane = $pane; + } + + /** + * @return \Icinga\Web\Widget\Dashboard\Pane + */ + public function getPane() + { + return $this->pane; + } +} diff --git a/library/Icinga/Web/Widget/Dashboard/Pane.php b/library/Icinga/Web/Widget/Dashboard/Pane.php new file mode 100644 index 0000000..c8b14c5 --- /dev/null +++ b/library/Icinga/Web/Widget/Dashboard/Pane.php @@ -0,0 +1,335 @@ +name = $name; + $this->title = $name; + } + + /** + * Set the name of this pane + * + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Returns the name of this pane + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the title of this pane + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Overwrite the title of this pane + * + * @param string $title The new title to use for this pane + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + return $this; + } + + /** + * Return true if a dashlet with the given title exists in this pane + * + * @param string $title The title of the dashlet to check for existence + * + * @return bool + */ + public function hasDashlet($title) + { + return array_key_exists($title, $this->dashlets); + } + + /** + * Checks if the current pane has any dashlets + * + * @return bool + */ + public function hasDashlets() + { + return ! empty($this->dashlets); + } + + /** + * Return a dashlet with the given name if existing + * + * @param string $title The title of the dashlet to return + * + * @return Dashlet The dashlet with the given title + * @throws ProgrammingError If the dashlet doesn't exist + */ + public function getDashlet($title) + { + if ($this->hasDashlet($title)) { + return $this->dashlets[$title]; + } + throw new ProgrammingError( + 'Trying to access invalid dashlet: %s', + $title + ); + } + + /** + * Removes the dashlet with the given title if it exists in this pane + * + * @param string $title The pane + * @return Pane $this + */ + public function removeDashlet($title) + { + if ($this->hasDashlet($title)) { + $dashlet = $this->getDashlet($title); + if ($dashlet->isUserWidget() === true) { + unset($this->dashlets[$title]); + } else { + $dashlet->setDisabled(true); + $dashlet->setUserWidget(); + } + } else { + throw new ProgrammingError('Dashlet does not exist: ' . $title); + } + return $this; + } + + /** + * Removes all or a given list of dashlets from this pane + * + * @param array $dashlets Optional list of dashlet titles + * @return Pane $this + */ + public function removeDashlets(array $dashlets = null) + { + if ($dashlets === null) { + $this->dashlets = array(); + } else { + foreach ($dashlets as $dashlet) { + $this->removeDashlet($dashlet); + } + } + return $this; + } + + /** + * Return all dashlets added at this pane + * + * @return array + */ + public function getDashlets() + { + return $this->dashlets; + } + + /** + * @see Widget::render + */ + public function render() + { + $dashlets = array_filter( + $this->dashlets, + function ($e) { + return ! $e->getDisabled(); + } + ); + return implode("\n", $dashlets) . "\n"; + } + + /** + * Create, add and return a new dashlet + * + * @param string $title + * @param string $url + * + * @return Dashlet + */ + public function createDashlet($title, $url = null) + { + $dashlet = new Dashlet($title, $url, $this); + $this->addDashlet($dashlet); + return $dashlet; + } + + /** + * Add a dashlet to this pane, optionally creating it if $dashlet is a string + * + * @param string|Dashlet $dashlet The dashlet object or title + * (if a new dashlet will be created) + * @param string|null $url An Url to be used when dashlet is a string + * + * @return $this + * @throws \Icinga\Exception\ConfigurationError + */ + public function addDashlet($dashlet, $url = null) + { + if ($dashlet instanceof Dashlet) { + $this->dashlets[$dashlet->getName()] = $dashlet; + } elseif (is_string($dashlet) && $url !== null) { + $this->createDashlet($dashlet, $url); + } else { + throw new ConfigurationError('Invalid dashlet added: %s', $dashlet); + } + return $this; + } + + /** + * Add new dashlets to existing dashlets + * + * @param array $dashlets + * @return $this + */ + public function addDashlets(array $dashlets) + { + /* @var $dashlet Dashlet */ + foreach ($dashlets as $dashlet) { + if (array_key_exists($dashlet->getName(), $this->dashlets)) { + if (preg_match('/_(\d+)$/', $dashlet->getName(), $m)) { + $name = preg_replace('/_\d+$/', $m[1]++, $dashlet->getName()); + } else { + $name = $dashlet->getName() . '_2'; + } + $this->dashlets[$name] = $dashlet; + } else { + $this->dashlets[$dashlet->getName()] = $dashlet; + } + } + + return $this; + } + + /** + * Add a dashlet to the current pane + * + * @param $title + * @param $url + * @return Dashlet + * + * @see addDashlet() + */ + public function add($title, $url = null) + { + $this->addDashlet($title, $url); + + return $this->dashlets[$title]; + } + + /** + * Return the this pane's structure as array + * + * @return array + */ + public function toArray() + { + $pane = array( + 'title' => $this->getTitle(), + ); + + if ($this->getDisabled() === true) { + $pane['disabled'] = 1; + } + + return $pane; + } + + /** + * Create a new pane with the title $title from the given configuration + * + * @param $title The title for this pane + * @param ConfigObject $config The configuration to use for setup + * + * @return Pane + */ + public static function fromIni($title, ConfigObject $config) + { + $pane = new Pane($title); + if ($config->get('title', false)) { + $pane->setTitle($config->get('title')); + } + return $pane; + } + + /** + * Setter for disabled + * + * @param boolean $disabled + */ + public function setDisabled($disabled = true) + { + $this->disabled = (bool) $disabled; + } + + /** + * Getter for disabled + * + * @return boolean + */ + public function getDisabled() + { + return $this->disabled; + } +} diff --git a/library/Icinga/Web/Widget/Dashboard/UserWidget.php b/library/Icinga/Web/Widget/Dashboard/UserWidget.php new file mode 100644 index 0000000..164d58b --- /dev/null +++ b/library/Icinga/Web/Widget/Dashboard/UserWidget.php @@ -0,0 +1,36 @@ +userWidget = (bool) $userWidget; + } + + /** + * Getter for user widget flag + * + * @return boolean + */ + public function isUserWidget() + { + return $this->userWidget; + } +} diff --git a/library/Icinga/Web/Widget/FilterEditor.php b/library/Icinga/Web/Widget/FilterEditor.php new file mode 100644 index 0000000..f1efd9a --- /dev/null +++ b/library/Icinga/Web/Widget/FilterEditor.php @@ -0,0 +1,811 @@ +setFilter($props['filter']); + } + if (array_key_exists('query', $props)) { + $this->setQuery($props['query']); + } + } + + public function setFilter(Filter $filter) + { + $this->filter = $filter; + return $this; + } + + public function getFilter() + { + if ($this->filter === null) { + $this->filter = Filter::fromQueryString((string) $this->url()->getParams()); + } + return $this->filter; + } + + /** + * Set columns to search in + * + * @param array $searchColumns + * + * @return $this + */ + public function setSearchColumns(array $searchColumns = null) + { + $this->searchColumns = $searchColumns; + return $this; + } + + public function setUrl($url) + { + $this->url = $url; + return $this; + } + + protected function url() + { + if ($this->url === null) { + $this->url = Url::fromRequest(); + } + return $this->url; + } + + protected function preservedUrl() + { + if ($this->preservedUrl === null) { + $this->preservedUrl = $this->url()->with($this->preservedParams); + } + return $this->preservedUrl; + } + + /** + * Set the query to filter + * + * @param Filterable $query + * + * @return $this + */ + public function setQuery(Filterable $query) + { + $this->query = $query; + return $this; + } + + public function ignoreParams() + { + $this->ignoreParams = func_get_args(); + return $this; + } + + public function preserveParams() + { + $this->preserveParams = func_get_args(); + return $this; + } + + /** + * Get whether the filter control is visible + * + * @return bool + */ + public function isVisible() + { + return $this->visible; + } + + /** + * Set whether the filter control is visible + * + * @param bool $visible + * + * @return $this + */ + public function setVisible($visible) + { + $this->visible = (bool) $visible; + + return $this; + } + + protected function redirectNow($url) + { + $response = Icinga::app()->getFrontController()->getResponse(); + $response->redirectAndExit($url); + } + + protected function mergeRootExpression($filter, $column, $sign, $expression) + { + $found = false; + if ($filter->isChain() && $filter->getOperatorName() === 'AND') { + foreach ($filter->filters() as $f) { + if ($f->isExpression() + && $f->getColumn() === $column + && $f->getSign() === $sign + ) { + $f->setExpression($expression); + $found = true; + break; + } + } + } elseif ($filter->isExpression()) { + if ($filter->getColumn() === $column && $filter->getSign() === $sign) { + $filter->setExpression($expression); + $found = true; + } + } + if (! $found) { + $filter = $filter->andFilter( + Filter::expression($column, $sign, $expression) + ); + } + return $filter; + } + + protected function resetSearchColumns(Filter &$filter) + { + if ($filter->isChain()) { + $filters = &$filter->filters(); + if (!($empty = empty($filters))) { + foreach ($filters as $k => &$f) { + if (false === $this->resetSearchColumns($f)) { + unset($filters[$k]); + } + } + } + return $empty || !empty($filters); + } + return $filter->isExpression() ? !( + in_array($filter->getColumn(), $this->searchColumns) + && + $filter->getSign() === '=' + ) : true; + } + + public function handleRequest($request) + { + $this->setUrl($request->getUrl()->without($this->ignoreParams)); + $params = $this->url()->getParams(); + + $preserve = array(); + foreach ($this->preserveParams as $key) { + if (null !== ($value = $params->shift($key))) { + $preserve[$key] = $value; + } + } + $this->preservedParams = $preserve; + + $add = $params->shift('addFilter'); + $remove = $params->shift('removeFilter'); + $strip = $params->shift('stripFilter'); + $modify = $params->shift('modifyFilter'); + + + + $search = null; + if ($request->isPost()) { + $search = $request->getPost('q'); + } + + if ($search === null) { + $search = $params->shift('q'); + } + + $filter = $this->getFilter(); + + if ($search !== null) { + if (strpos($search, '=') !== false) { + list($k, $v) = preg_split('/=/', $search); + $filter = $this->mergeRootExpression($filter, trim($k), '=', ltrim($v)); + } else { + if ($this->searchColumns === null && $this->query instanceof FilterColumns) { + $this->searchColumns = $this->query->getSearchColumns($search); + } + + if (! empty($this->searchColumns)) { + if (! $this->resetSearchColumns($filter)) { + $filter = Filter::matchAll(); + } + $filters = array(); + $search = trim($search); + foreach ($this->searchColumns as $searchColumn) { + $filters[] = Filter::expression($searchColumn, '=', "*$search*"); + } + $filter = $filter->andFilter(new FilterOr($filters)); + } else { + Notification::error(mt('monitoring', 'Cannot search here')); + return $this; + } + } + + $url = Url::fromRequest()->onlyWith($this->preserveParams); + $urlParams = $url->getParams(); + $url->setQueryString($filter->toQueryString()); + foreach ($urlParams->toArray(false) as $key => $value) { + $url->getParams()->addEncoded($key, $value); + } + + $this->redirectNow($url); + } + + if ($remove) { + $redirect = $this->url(); + if ($filter->getById($remove)->isRootNode()) { + $redirect->setQueryString(''); + } else { + $filter->removeId($remove); + $redirect->setQueryString($filter->toQueryString())->getParams()->add('modifyFilter'); + } + $this->redirectNow($redirect->addParams($preserve)); + } + + if ($strip) { + $redirect = $this->url(); + $subId = $strip . '-1'; + if ($filter->getId() === $strip) { + $filter = $filter->getById($strip . '-1'); + } else { + $filter->replaceById($strip, $filter->getById($strip . '-1')); + } + $redirect->setQueryString($filter->toQueryString())->getParams()->add('modifyFilter'); + $this->redirectNow($redirect->addParams($preserve)); + } + + + if ($modify) { + if ($request->isPost()) { + if ($request->get('cancel') === 'Cancel') { + $this->redirectNow($this->preservedUrl()->without('modifyFilter')); + } + if ($request->get('formUID') === 'FilterEditor') { + $filter = $this->applyChanges($request->getPost()); + $url = $this->url()->setQueryString($filter->toQueryString())->addParams($preserve); + $url->getParams()->add('modifyFilter'); + + $addFilter = $request->get('add_filter'); + if ($addFilter !== null) { + $url->setParam('addFilter', $addFilter); + } + + $removeFilter = $request->get('remove_filter'); + if ($removeFilter !== null) { + $url->setParam('removeFilter', $removeFilter); + } + + $this->redirectNow($url); + } + } + $this->url()->getParams()->add('modifyFilter'); + } + + if ($add) { + $this->addFilterToId($add); + } + + if ($this->query !== null && $request->isGet()) { + $this->query->applyFilter($this->getFilter()); + } + + return $this; + } + + protected function select($name, $list, $selected, $attributes = null) + { + $view = $this->view(); + if ($attributes === null) { + $attributes = ''; + } else { + $attributes = $view->propertiesToString($attributes); + } + $html = sprintf( + '' . "\n\n"; + return $html; + } + + protected function addFilterToId($id) + { + $this->addTo = $id; + return $this; + } + + protected function removeIndex($idx) + { + $this->selectedIdx = $idx; + return $this; + } + + protected function removeLink(Filter $filter) + { + return "'; + } + + protected function addLink(Filter $filter) + { + return "'; + } + + protected function stripLink(Filter $filter) + { + return $this->view()->qlink( + '', + $this->preservedUrl()->with('stripFilter', $filter->getId()), + null, + array( + 'icon' => 'minus', + 'title' => t('Strip this filter') + ) + ); + } + + protected function cancelLink() + { + return $this->view()->qlink( + '', + $this->preservedUrl()->without('addFilter'), + null, + array( + 'icon' => 'cancel', + 'title' => t('Cancel this operation') + ) + ); + } + + protected function renderFilter($filter, $level = 0) + { + if ($level === 0 && $filter->isChain() && $filter->isEmpty()) { + return '
    • ' . $this->renderNewFilter() . '
    '; + } + + if ($filter instanceof FilterChain) { + return $this->renderFilterChain($filter, $level); + } elseif ($filter instanceof FilterExpression) { + return $this->renderFilterExpression($filter); + } else { + throw new ProgrammingError('Got a Filter being neither expression nor chain'); + } + } + + protected function renderFilterChain(FilterChain $filter, $level) + { + $html = ' ' + . $this->selectOperator($filter) + . $this->removeLink($filter) + . ($filter->count() === 1 ? $this->stripLink($filter) : '') + . $this->addLink($filter); + + if ($filter->isEmpty() && ! $this->addTo) { + return $html; + } + + $parts = array(); + foreach ($filter->filters() as $f) { + $parts[] = '
  • ' . $this->renderFilter($f, $level + 1) . '
  • '; + } + + if ($this->addTo && $this->addTo == $filter->getId()) { + $parts[] = '
  • ' . $this->renderNewFilter() .$this->cancelLink(). '
  • '; + } + + $class = $level === 0 ? ' class="datafilter"' : ''; + $html .= sprintf( + "\n%s\n", + $class, + implode("", $parts) + ); + return $html; + } + + protected function renderFilterExpression(FilterExpression $filter) + { + if ($this->addTo && $this->addTo === $filter->getId()) { + return + preg_replace( + '/ class="autosubmit"/', + ' class="autofocus"', + $this->selectOperator() + ) + . '
    • ' + . $this->selectColumn($filter) + . $this->selectSign($filter) + . $this->text($filter) + . $this->removeLink($filter) + . $this->addLink($filter) + . '
    • ' + . $this->renderNewFilter() .$this->cancelLink() + . '
    ' + ; + } else { + return $this->selectColumn($filter) + . $this->selectSign($filter) + . $this->text($filter) + . $this->removeLink($filter) + . $this->addLink($filter) + ; + } + } + + protected function text(Filter $filter = null) + { + $value = $filter === null ? '' : $filter->getExpression(); + if (is_array($value)) { + $value = '(' . implode('|', $value) . ')'; + } + return sprintf( + '', + $this->elementId('value', $filter), + $this->view()->escape($value) + ); + } + + protected function renderNewFilter() + { + $html = $this->selectColumn() + . $this->selectSign() + . $this->text(); + + return preg_replace( + '/ class="autosubmit"/', + '', + $html + ); + } + + protected function arrayForSelect($array, $flip = false) + { + $res = array(); + foreach ($array as $k => $v) { + if (is_int($k)) { + $res[$v] = ucwords(str_replace('_', ' ', $v)); + } elseif ($flip) { + $res[$v] = $k; + } else { + $res[$k] = $v; + } + } + // sort($res); + return $res; + } + + protected function elementId($prefix, Filter $filter = null) + { + if ($filter === null) { + return $prefix . '_new_' . ($this->addTo ?: '0'); + } else { + return $prefix . '_' . $filter->getId(); + } + } + + protected function selectOperator(Filter $filter = null) + { + $ops = array( + 'AND' => 'AND', + 'OR' => 'OR', + 'NOT' => 'NOT' + ); + + return $this->select( + $this->elementId('operator', $filter), + $ops, + $filter === null ? null : $filter->getOperatorName(), + array('style' => 'width: 5em') + ); + } + + protected function selectSign(Filter $filter = null) + { + $signs = array( + '=' => '=', + '!=' => '!=', + '>' => '>', + '<' => '<', + '>=' => '>=', + '<=' => '<=', + ); + + return $this->select( + $this->elementId('sign', $filter), + $signs, + $filter === null ? null : $filter->getSign(), + array('style' => 'width: 4em') + ); + } + + public function setColumns(array $columns = null) + { + $this->cachedColumnSelect = $columns ? $this->arrayForSelect($columns) : null; + return $this; + } + + protected function selectColumn(Filter $filter = null) + { + $active = $filter === null ? null : $filter->getColumn(); + + if ($this->cachedColumnSelect === null && $this->query === null) { + return sprintf( + '', + $this->elementId('column', $filter), + $this->view()->escape($active) // Escape attribute? + ); + } + + if ($this->cachedColumnSelect === null && $this->query instanceof FilterColumns) { + $this->cachedColumnSelect = $this->arrayForSelect($this->query->getFilterColumns(), true); + asort($this->cachedColumnSelect); + } elseif ($this->cachedColumnSelect === null) { + throw new ProgrammingError('No columns set nor does the query provide any'); + } + + $cols = $this->cachedColumnSelect; + if ($active && !isset($cols[$active])) { + $cols[$active] = str_replace('_', ' ', ucfirst(ltrim($active, '_'))); + } + + return $this->select($this->elementId('column', $filter), $cols, $active); + } + + protected function applyChanges($changes) + { + $filter = $this->filter; + $pairs = array(); + $addTo = null; + $add = array(); + foreach ($changes as $k => $v) { + if (preg_match('/^(column|value|sign|operator)((?:_new)?)_([\d-]+)$/', $k, $m)) { + if ($m[2] === '_new') { + if ($addTo !== null && $addTo !== $m[3]) { + throw new \Exception('F...U'); + } + $addTo = $m[3]; + $add[$m[1]] = $v; + } else { + $pairs[$m[3]][$m[1]] = $v; + } + } + } + + $operators = array(); + foreach ($pairs as $id => $fs) { + if (array_key_exists('operator', $fs)) { + $operators[$id] = $fs['operator']; + } else { + $f = $filter->getById($id); + $f->setColumn($fs['column']); + if ($f->getSign() !== $fs['sign']) { + if ($f->isRootNode()) { + $filter = $f->setSign($fs['sign']); + } else { + $filter->replaceById($id, $f->setSign($fs['sign'])); + } + } + $f->setExpression($fs['value']); + } + } + + krsort($operators, SORT_NATURAL); + foreach ($operators as $id => $operator) { + $f = $filter->getById($id); + if ($f->getOperatorName() !== $operator) { + if ($f->isRootNode()) { + $filter = $f->setOperatorName($operator); + } else { + $filter->replaceById($id, $f->setOperatorName($operator)); + } + } + } + + if ($addTo !== null) { + if ($addTo === '0') { + $filter = Filter::expression($add['column'], $add['sign'], $add['value']); + } else { + $parent = $filter->getById($addTo); + $f = Filter::expression($add['column'], $add['sign'], $add['value']); + if (isset($add['operator'])) { + switch ($add['operator']) { + case 'AND': + if ($parent->isExpression()) { + if ($parent->isRootNode()) { + $filter = Filter::matchAll(clone $parent, $f); + } else { + $filter = $filter->replaceById($addTo, Filter::matchAll(clone $parent, $f)); + } + } else { + $parent->addFilter(Filter::matchAll($f)); + } + break; + case 'OR': + if ($parent->isExpression()) { + if ($parent->isRootNode()) { + $filter = Filter::matchAny(clone $parent, $f); + } else { + $filter = $filter->replaceById($addTo, Filter::matchAny(clone $parent, $f)); + } + } else { + $parent->addFilter(Filter::matchAny($f)); + } + break; + case 'NOT': + if ($parent->isExpression()) { + if ($parent->isRootNode()) { + $filter = Filter::not(Filter::matchAll($parent, $f)); + } else { + $filter = $filter->replaceById($addTo, Filter::not(Filter::matchAll($parent, $f))); + } + } else { + $parent->addFilter(Filter::not($f)); + } + break; + } + } else { + $parent->addFilter($f); + } + } + } + + return $filter; + } + + public function renderSearch() + { + $preservedUrl = $this->preservedUrl(); + + $html = ' '; + + if ($this->filter->isEmpty()) { + $title = t('Filter this list'); + } else { + $title = t('Modify this filter'); + if (! $this->filter->isEmpty()) { + $title .= ': ' . $this->view()->escape($this->filter); + } + } + + return $html + . '' + . '' + . ''; + } + + public function render() + { + if (! $this->visible) { + return ''; + } + if (! $this->preservedUrl()->getParam('modifyFilter')) { + return '
    ' + . $this->renderSearch() + . $this->view()->escape($this->shorten($this->filter, 50)) + . '
    '; + } + return '
    ' + . $this->renderSearch() + . '
    ' + . '' + . '
    • ' + . $this->renderFilter($this->filter) + . '
    ' + . '
    ' + . '' + . '' + . '
    ' + . '' + . '
    ' + . '
    '; + } + + protected function shorten($string, $length) + { + if (strlen($string) > $length) { + return substr($string, 0, $length) . '...'; + } + return $string; + } + + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return 'ERROR in FilterEditor: ' . $e->getMessage(); + } + } +} diff --git a/library/Icinga/Web/Widget/FilterWidget.php b/library/Icinga/Web/Widget/FilterWidget.php new file mode 100644 index 0000000..11b1ecb --- /dev/null +++ b/library/Icinga/Web/Widget/FilterWidget.php @@ -0,0 +1,122 @@ +filter = $filter; + } + + protected function renderFilter($filter, $level = 0) + { + $html = ''; + $url = Url::fromRequest(); + if ($filter instanceof FilterChain) { + if ($level === 0) { + $op = '
  • )' . $filter->getOperatorName() . ' ('; + } else { + $op = '
  • ) ' . $filter->getOperatorName() . ' ( '; + } + $parts = array(); + foreach ($filter->filters() as $f) { + $parts[] = $this->renderFilter($f, $level + 1); + } + if (empty($parts)) { + return $html; + } + if ($level === 0) { + $html .= '
    • ( ' . implode($op, $parts) . ' )
    '; + } else { + $html .= '
    • ( ' . implode($op, $parts) . ' )
    '; + } + return $html; + } elseif ($filter instanceof FilterExpression) { + $u = $url->without($filter->getColumn()); + } else { + $u = $url . '--'; + } + $html .= '' . $filter . ' '; + return $html; + } + + public function render() + { + $url = Url::fromRequest(); + $view = $this->view(); + $html = '
    '; + + + // $html .= $this->renderFilter($this->filter); + + $editorUrl = clone $url; + $editorUrl->setParam('modifyFilter', true); + if ($this->filter->isEmpty()) { + $title = t('Filter this list'); + $txt = $view->icon('plus'); + $remove = ''; + } else { + $txt = t('Filtered'); + $title = t('Modify this filter'); + $remove = ' ' + . $view->icon('cancel') + . ''; + } + $filter = $this->filter->isEmpty() ? '' : ': ' . $this->filter; + $html .= ($filter ? '

    ' : ' ') + . '' + . $txt + . '' + . $this->shorten($filter, 72) + . $remove + . ($filter ? '

    ' : ''); + + return $html; + } + + protected function shorten($string, $length) + { + if (strlen($string) > $length) { + return substr($string, 0, $length) . '...'; + } + return $string; + } +} diff --git a/library/Icinga/Web/Widget/Limiter.php b/library/Icinga/Web/Widget/Limiter.php new file mode 100644 index 0000000..d127aca --- /dev/null +++ b/library/Icinga/Web/Widget/Limiter.php @@ -0,0 +1,54 @@ +defaultLimit; + } + + /** + * Set the default limit + * + * @param int $defaultLimit + * + * @return $this + */ + public function setDefaultLimit($defaultLimit) + { + $this->defaultLimit = (int) $defaultLimit; + return $this; + } + + /** + * {@inheritdoc} + */ + public function render() + { + $control = new LimiterControlForm(); + $control + ->setDefaultLimit($this->defaultLimit) + ->handleRequest(); + return (string)$control; + } +} diff --git a/library/Icinga/Web/Widget/Paginator.php b/library/Icinga/Web/Widget/Paginator.php new file mode 100644 index 0000000..5f3ef04 --- /dev/null +++ b/library/Icinga/Web/Widget/Paginator.php @@ -0,0 +1,167 @@ +query = $query; + return $this; + } + + /** + * Set the view script to use + * + * @param string|array $script + * + * @return $this + */ + public function setViewScript($script) + { + $this->viewScript = $script; + return $this; + } + + /** + * Render this paginator + */ + public function render() + { + if ($this->query === null) { + throw new ProgrammingError('Need a query to create the paginator widget for'); + } + + $itemCountPerPage = $this->query->getLimit(); + if (! $itemCountPerPage) { + return ''; // No pagination required + } + + $totalItemCount = count($this->query); + $pageCount = (int) ceil($totalItemCount / $itemCountPerPage); + $currentPage = $this->query->hasOffset() ? ($this->query->getOffset() / $itemCountPerPage) + 1 : 1; + $pagesInRange = $this->getPages($pageCount, $currentPage); + $variables = array( + 'totalItemCount' => $totalItemCount, + 'pageCount' => $pageCount, + 'itemCountPerPage' => $itemCountPerPage, + 'first' => 1, + 'current' => $currentPage, + 'last' => $pageCount, + 'pagesInRange' => $pagesInRange, + 'firstPageInRange' => min($pagesInRange), + 'lastPageInRange' => max($pagesInRange) + ); + + if ($currentPage > 1) { + $variables['previous'] = $currentPage - 1; + } + + if ($currentPage < $pageCount) { + $variables['next'] = $currentPage + 1; + } + + if (is_array($this->viewScript)) { + if ($this->viewScript[1] !== null) { + return $this->view()->partial($this->viewScript[0], $this->viewScript[1], $variables); + } + + return $this->view()->partial($this->viewScript[0], $variables); + } + + return $this->view()->partial($this->viewScript, $variables); + } + + /** + * Returns an array of "local" pages given the page count and current page number + * + * @return array + */ + protected function getPages($pageCount, $currentPage) + { + $range = array(); + + if ($pageCount < 10) { + // Show all pages if we have less than 10 + for ($i = 1; $i < 10; $i++) { + if ($i > $pageCount) { + break; + } + + $range[$i] = $i; + } + } else { + // More than 10 pages: + foreach (array(1, 2) as $i) { + $range[$i] = $i; + } + + if ($currentPage < 6) { + // We are on page 1-5 from + for ($i = 1; $i <= 7; $i++) { + $range[$i] = $i; + } + } else { + // Current page > 5 + $range[] = '...'; + + if (($pageCount - $currentPage) < 5) { + // Less than 5 pages left + $start = 5 - ($pageCount - $currentPage); + } else { + $start = 1; + } + + for ($i = $currentPage - $start; $i < ($currentPage + (4 - $start)); $i++) { + if ($i > $pageCount) { + break; + } + + $range[$i] = $i; + } + } + + if ($currentPage < ($pageCount - 2)) { + $range[] = '...'; + } + + foreach (array($pageCount - 1, $pageCount) as $i) { + $range[$i] = $i; + } + } + + if (empty($range)) { + $range[] = 1; + } + + return $range; + } +} diff --git a/library/Icinga/Web/Widget/SearchDashboard.php b/library/Icinga/Web/Widget/SearchDashboard.php new file mode 100644 index 0000000..1ce4c46 --- /dev/null +++ b/library/Icinga/Web/Widget/SearchDashboard.php @@ -0,0 +1,111 @@ +tabs === null) { + $this->tabs = new Tabs(); + $this->tabs->add( + 'search', + array( + 'title' => t('Show Search', 'dashboard.pane.tooltip'), + 'label' => t('Search'), + 'url' => Url::fromRequest() + ) + ); + } + return $this->tabs; + } + + /** + * Load all available search dashlets from modules + * + * @param string $searchString + * + * @return $this + */ + public function search($searchString = '') + { + $pane = $this->createPane(self::SEARCH_PANE)->getPane(self::SEARCH_PANE)->setTitle(t('Search')); + $this->activate(self::SEARCH_PANE); + + $manager = Icinga::app()->getModuleManager(); + $searchUrls = array(); + + foreach ($manager->getLoadedModules() as $module) { + if ($this->getUser()->can($manager::MODULE_PERMISSION_NS . $module->getName())) { + $moduleSearchUrls = $module->getSearchUrls(); + if (! empty($moduleSearchUrls)) { + if ($searchString === '') { + $pane->add(t('Ready to search'), 'search/hint'); + return $this; + } + $searchUrls = array_merge($searchUrls, $moduleSearchUrls); + } + } + } + + usort($searchUrls, array($this, 'compareSearchUrls')); + + foreach (array_reverse($searchUrls) as $searchUrl) { + $pane->createDashlet( + $searchUrl->title . ': ' . $searchString, + Url::fromPath($searchUrl->url, array('q' => $searchString)) + )->setProgressLabel(t('Searching')); + } + + return $this; + } + + /** + * Renders the output + * + * @return string + * + * @throws Zend_Controller_Action_Exception + */ + public function render() + { + if (! $this->getPane(self::SEARCH_PANE)->hasDashlets()) { + throw new Zend_Controller_Action_Exception(t('Page not found'), 404); + } + return parent::render(); + } + + /** + * Compare search URLs based on their priority + * + * @param object $a + * @param object $b + * + * @return int + */ + private function compareSearchUrls($a, $b) + { + if ($a->priority === $b->priority) { + return 0; + } + return ($a->priority < $b->priority) ? -1 : 1; + } +} diff --git a/library/Icinga/Web/Widget/SingleValueSearchControl.php b/library/Icinga/Web/Widget/SingleValueSearchControl.php new file mode 100644 index 0000000..470518c --- /dev/null +++ b/library/Icinga/Web/Widget/SingleValueSearchControl.php @@ -0,0 +1,200 @@ + 'icinga-controls inline']; + + /** @var string */ + protected $searchParameter = self::DEFAULT_SEARCH_PARAMETER; + + /** @var string */ + protected $inputLabel; + + /** @var string */ + protected $submitLabel; + + /** @var Url */ + protected $suggestionUrl; + + /** @var array */ + protected $metaDataNames; + + /** + * Set the search parameter to use + * + * @param string $name + * @return $this + */ + public function setSearchParameter($name) + { + $this->searchParameter = $name; + + return $this; + } + + /** + * Set the input's label + * + * @param string $label + * + * @return $this + */ + public function setInputLabel($label) + { + $this->inputLabel = $label; + + return $this; + } + + /** + * Set the submit button's label + * + * @param string $label + * + * @return $this + */ + public function setSubmitLabel($label) + { + $this->submitLabel = $label; + + return $this; + } + + /** + * Set the suggestion url + * + * @param Url $url + * + * @return $this + */ + public function setSuggestionUrl(Url $url) + { + $this->suggestionUrl = $url; + + return $this; + } + + /** + * Set names for which hidden meta data elements should be created + * + * @param string ...$names + * + * @return $this + */ + public function setMetaDataNames(...$names) + { + $this->metaDataNames = $names; + + return $this; + } + + protected function assemble() + { + $suggestionsId = Icinga::app()->getRequest()->protectId('single-value-suggestions'); + + $this->addElement( + 'text', + $this->searchParameter, + [ + 'required' => true, + 'minlength' => 1, + 'autocomplete' => 'off', + 'class' => 'search', + 'data-enrichment-type' => 'completion', + 'data-term-suggestions' => '#' . $suggestionsId, + 'data-suggest-url' => $this->suggestionUrl, + 'placeholder' => $this->inputLabel + ] + ); + + if (! empty($this->metaDataNames)) { + $fieldset = new HtmlElement('fieldset'); + foreach ($this->metaDataNames as $name) { + $hiddenElement = $this->createElement('hidden', $this->searchParameter . '-' . $name); + $this->registerElement($hiddenElement); + $fieldset->addHtml($hiddenElement); + } + + $this->getElement($this->searchParameter)->prependWrapper($fieldset); + } + + $this->addElement( + 'submit', + 'btn_sumit', + [ + 'label' => $this->submitLabel, + 'class' => 'btn-primary' + ] + ); + + $this->add(HtmlElement::create('div', [ + 'id' => $suggestionsId, + 'class' => 'search-suggestions' + ])); + } + + /** + * Create a list of search suggestions based on the given groups + * + * @param array $groups + * + * @return HtmlElement + */ + public static function createSuggestions(array $groups) + { + $ul = new HtmlElement('ul'); + foreach ($groups as list($name, $entries)) { + if ($name) { + if ($entries === false) { + $ul->addHtml(HtmlElement::create('li', ['class' => 'failure-message'], [ + HtmlElement::create('em', null, t('Can\'t search:')), + $name + ])); + continue; + } elseif (empty($entries)) { + $ul->addHtml(HtmlElement::create('li', ['class' => 'failure-message'], [ + HtmlElement::create('em', null, t('No results:')), + $name + ])); + continue; + } else { + $ul->addHtml( + HtmlElement::create('li', ['class' => Suggestions::SUGGESTION_TITLE_CLASS], $name) + ); + } + } + + $index = 0; + foreach ($entries as list($label, $metaData)) { + $attributes = [ + 'value' => $label, + 'type' => 'button', + 'tabindex' => -1 + ]; + foreach ($metaData as $key => $value) { + $attributes['data-' . $key] = $value; + } + + $liAtrs = ['class' => $index === 0 ? 'default' : null]; + $ul->addHtml(new HtmlElement('li', Attributes::create($liAtrs), new InputElement(null, $attributes))); + $index++; + } + } + + return $ul; + } +} diff --git a/library/Icinga/Web/Widget/SortBox.php b/library/Icinga/Web/Widget/SortBox.php new file mode 100644 index 0000000..72b6f58 --- /dev/null +++ b/library/Icinga/Web/Widget/SortBox.php @@ -0,0 +1,260 @@ +name = $name; + $this->sortFields = $sortFields; + $this->sortDefaults = $sortDefaults; + } + + /** + * Create a SortBox + * + * @param string $name The name for the SortBox + * @param array $sortFields An array containing the columns and their labels to be displayed in the SortBox + * @param array $sortDefaults An array containing default sort directions for specific columns + * + * @return SortBox + */ + public static function create($name, array $sortFields, array $sortDefaults = null) + { + return new static($name, $sortFields, $sortDefaults); + } + + /** + * Set the request to fetch sort rules from + * + * @param Request $request + * + * @return $this + */ + public function setRequest($request) + { + $this->request = $request; + return $this; + } + + /** + * Set the query to apply sort rules on + * + * @param Sortable $query + * + * @return $this + */ + public function setQuery(Sortable $query) + { + $this->query = $query; + return $this; + } + + /** + * Return the default sort rule for the query + * + * @param string $column An optional column + * + * @return array An array of two values: $column, $direction + */ + protected function getSortDefaults($column = null) + { + $direction = null; + if (! empty($this->sortDefaults) && ($column === null || isset($this->sortDefaults[$column]))) { + if ($column === null) { + reset($this->sortDefaults); + $column = key($this->sortDefaults); + } + + $direction = $this->sortDefaults[$column]; + } elseif ($this->query !== null && $this->query instanceof SortRules) { + $sortRules = $this->query->getSortRules(); + if ($column === null) { + $column = key($sortRules); + } + + if ($column !== null && isset($sortRules[$column]['order'])) { + $direction = strtoupper($sortRules[$column]['order']) === Sortable::SORT_DESC ? 'desc' : 'asc'; + } + } elseif ($column === null) { + reset($this->sortFields); + $column = key($this->sortFields); + } + + return array($column, $direction); + } + + /** + * Apply the sort rules from the given or current request on the query + * + * @param Request $request + * + * @return $this + */ + public function handleRequest(Request $request = null) + { + if ($this->query !== null) { + if ($request === null) { + $request = Icinga::app()->getRequest(); + } + + if (! ($sort = $request->getParam('sort'))) { + list($sort, $dir) = $this->getSortDefaults(); + } else { + list($_, $dir) = $this->getSortDefaults($sort); + } + + $this->query->order($sort, $request->getParam('dir', $dir)); + } + + return $this; + } + + /** + * Render this SortBox as HTML + * + * @return string + */ + public function render() + { + $columnForm = new Form(); + $columnForm->setTokenDisabled(); + $columnForm->setName($this->name . '-column'); + $columnForm->setAttrib('class', 'icinga-controls inline'); + $columnForm->addElement( + 'select', + 'sort', + array( + 'autosubmit' => true, + 'label' => $this->view()->translate('Sort by'), + 'multiOptions' => $this->sortFields, + 'decorators' => array( + array('ViewHelper'), + array('Label') + ) + ) + ); + + $column = null; + if ($this->request) { + $url = $this->request->getUrl(); + if ($url->hasParam('sort')) { + $column = $url->getParam('sort'); + + if ($url->hasParam('dir')) { + $direction = $url->getParam('dir'); + } else { + list($_, $direction) = $this->getSortDefaults($column); + } + } elseif ($url->hasParam('dir')) { + $direction = $url->getParam('dir'); + list($column, $_) = $this->getSortDefaults(); + } + } + + if ($column === null) { + list($column, $direction) = $this->getSortDefaults(); + } + + // TODO(el): ToggleButton :) + $toggle = array('asc' => 'sort-name-down', 'desc' => 'sort-name-up'); + unset($toggle[isset($direction) ? strtolower($direction) : 'asc']); + $newDirection = key($toggle); + $icon = current($toggle); + + $orderForm = new Form(); + $orderForm->setTokenDisabled(); + $orderForm->setName($this->name . '-order'); + $orderForm->setAttrib('class', 'inline sort-direction-control'); + $orderForm->addElement( + 'hidden', + 'dir' + ); + $orderForm->addElement( + 'button', + 'btn_submit', + array( + 'ignore' => true, + 'type' => 'submit', + 'label' => $this->view()->icon($icon), + 'decorators' => array('ViewHelper'), + 'escape' => false, + 'class' => 'link-button spinner', + 'value' => 'submit', + 'title' => t('Change sort direction'), + ) + ); + + + $columnForm->populate(array('sort' => $column)); + $orderForm->populate(array('dir' => $newDirection)); + return '
    ' . $columnForm . $orderForm . '
    '; + } +} diff --git a/library/Icinga/Web/Widget/Tab.php b/library/Icinga/Web/Widget/Tab.php new file mode 100644 index 0000000..d2d3f32 --- /dev/null +++ b/library/Icinga/Web/Widget/Tab.php @@ -0,0 +1,323 @@ +icon = $icon; + } + + /** + * Set's an icon class that will be used in an tag if no icon image is set + * + * @param string $iconCls The CSS class of the icon to use + */ + public function setIconCls($iconCls) + { + $this->iconCls = $iconCls; + } + + /** + * @param mixed $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } + + /** + * Set the tab label + * + * @param string $label + */ + public function setLabel($label) + { + $this->label = $label; + } + + /** + * Get the tab label + * + * @return string + */ + public function getLabel() + { + if (! $this->label) { + return $this->title; + } + + return $this->label; + } + + /** + * @param mixed $title + */ + public function setTitle($title) + { + $this->title = $title; + } + + /** + * Set the Url this tab points to + * + * @param string $url The Url to use for this tab + */ + public function setUrl($url) + { + if (is_string($url)) { + $url = Url::fromPath($url); + } + $this->url = $url; + } + + /** + * Get the tab's target URL + * + * @return Url + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the parameters to be set for this tabs Url + * + * @param array $url The Url parameters to set + */ + public function setUrlParams(array $urlParams) + { + $this->urlParams = $urlParams; + } + + /** + * Set additional a tag attributes + * + * @param array $tagParams + */ + public function setTagParams(array $tagParams) + { + $this->tagParams = $tagParams; + } + + public function setTargetBlank($value = true) + { + $this->targetBlank = $value; + } + + public function setBaseTarget($value) + { + $this->baseTarget = $value; + } + + /** + * Create a new Tab with the given properties + * + * Allowed properties are all properties for which a setter exists + * + * @param array $properties An array of properties + */ + public function __construct(array $properties = array()) + { + foreach ($properties as $name => $value) { + $setter = 'set' . ucfirst($name); + if (method_exists($this, $setter)) { + $this->$setter($value); + } + } + } + + /** + * Set this tab active (default) or inactive + * + * This is usually done through the tabs container widget, therefore it + * is not a good idea to directly call this function + * + * @param bool $active Whether the tab should be active + * + * @return $this + */ + public function setActive($active = true) + { + $this->active = (bool) $active; + return $this; + } + + /** + * @see Widget::render() + */ + public function render() + { + $view = $this->view(); + $classes = array(); + if ($this->active) { + $classes[] = 'active'; + } + + $caption = $view->escape($this->getLabel()); + $tagParams = $this->tagParams; + if ($this->targetBlank) { + // add warning to links that open in new tabs to improve accessibility, as recommended by WCAG20 G201 + $caption .= ' opens in new window '; + $tagParams['target'] ='_blank'; + } + + if ($this->title) { + if ($tagParams !== null) { + $tagParams['title'] = $this->title; + $tagParams['aria-label'] = $this->title; + } else { + $tagParams = array( + 'title' => $this->title, + 'aria-label' => $this->title + ); + } + } + + if ($this->baseTarget !== null) { + $tagParams['data-base-target'] = $this->baseTarget; + } + + if ($this->icon !== null) { + if (strpos($this->icon, '.') === false) { + $caption = $view->icon($this->icon) . $caption; + } else { + $caption = $view->img($this->icon, null, array('class' => 'icon')) . $caption; + } + } + + if ($this->url !== null) { + $this->url->overwriteParams($this->urlParams); + + if ($tagParams !== null) { + $params = $view->propertiesToString($tagParams); + } else { + $params = ''; + } + + $tab = sprintf( + '%s', + $this->view()->escape($this->url->getAbsoluteUrl()), + $params, + $caption + ); + } else { + $tab = $caption; + } + + $class = empty($classes) ? '' : sprintf(' class="%s"', implode(' ', $classes)); + return '
  • ' . $tab . "
  • \n"; + } +} diff --git a/library/Icinga/Web/Widget/Tabextension/DashboardAction.php b/library/Icinga/Web/Widget/Tabextension/DashboardAction.php new file mode 100644 index 0000000..a3e6c43 --- /dev/null +++ b/library/Icinga/Web/Widget/Tabextension/DashboardAction.php @@ -0,0 +1,35 @@ +addAsDropdown( + 'dashboard', + array( + 'icon' => 'dashboard', + 'label' => t('Add To Dashboard'), + 'url' => Url::fromPath('dashboard/new-dashlet'), + 'urlParams' => array( + 'url' => rawurlencode(Url::fromRequest()->getRelativeUrl()) + ) + ) + ); + } +} diff --git a/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php b/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php new file mode 100644 index 0000000..fc7412a --- /dev/null +++ b/library/Icinga/Web/Widget/Tabextension/DashboardSettings.php @@ -0,0 +1,39 @@ +addAsDropdown( + 'dashboard_add', + array( + 'icon' => 'dashboard', + 'label' => t('Add Dashlet'), + 'url' => Url::fromPath('dashboard/new-dashlet') + ) + ); + + $tabs->addAsDropdown( + 'dashboard_settings', + array( + 'icon' => 'dashboard', + 'label' => t('Settings'), + 'url' => Url::fromPath('dashboard/settings') + ) + ); + } +} diff --git a/library/Icinga/Web/Widget/Tabextension/MenuAction.php b/library/Icinga/Web/Widget/Tabextension/MenuAction.php new file mode 100644 index 0000000..d713892 --- /dev/null +++ b/library/Icinga/Web/Widget/Tabextension/MenuAction.php @@ -0,0 +1,35 @@ +addAsDropdown( + 'menu-entry', + array( + 'icon' => 'menu', + 'label' => t('Add To Menu'), + 'url' => Url::fromPath('navigation/add'), + 'urlParams' => array( + 'url' => rawurlencode(Url::fromRequest()->getRelativeUrl()) + ) + ) + ); + } +} diff --git a/library/Icinga/Web/Widget/Tabextension/OutputFormat.php b/library/Icinga/Web/Widget/Tabextension/OutputFormat.php new file mode 100644 index 0000000..d5d83af --- /dev/null +++ b/library/Icinga/Web/Widget/Tabextension/OutputFormat.php @@ -0,0 +1,114 @@ +not show. + */ + public function __construct(array $disabled = array()) + { + foreach ($this->getSupportedTypes() as $type => $tabConfig) { + if (!in_array($type, $disabled)) { + $tabConfig['url'] = Url::fromRequest(); + $tab = new Tab($tabConfig); + $tab->setTargetBlank(); + $this->tabs[] = $tab; + } + } + } + + /** + * Applies the format selectio to the provided tabset + * + * @param Tabs $tabs The tabs object to extend with + * + * @see Tabextension::apply() + */ + public function apply(Tabs $tabs) + { + foreach ($this->tabs as $tab) { + $tabs->addAsDropdown($tab->getName(), $tab); + } + } + + /** + * Return an array containing the tab definitions for all supported types + * + * Using array_keys on this array or isset allows to check whether a + * requested type is supported + * + * @return array + */ + public function getSupportedTypes() + { + $supportedTypes = array(); + + $pdfexport = Hook::has('Pdfexport'); + + if ($pdfexport || Platform::extensionLoaded('gd')) { + $supportedTypes[self::TYPE_PDF] = array( + 'name' => 'pdf', + 'label' => 'PDF', + 'icon' => 'file-pdf', + 'urlParams' => array('format' => 'pdf'), + ); + } + + $supportedTypes[self::TYPE_CSV] = array( + 'name' => 'csv', + 'label' => 'CSV', + 'icon' => 'file-excel', + 'urlParams' => array('format' => 'csv') + ); + + if (Platform::extensionLoaded('json')) { + $supportedTypes[self::TYPE_JSON] = array( + 'name' => 'json', + 'label' => 'JSON', + 'icon' => 'doc-text', + 'urlParams' => array('format' => 'json') + ); + } + + return $supportedTypes; + } +} diff --git a/library/Icinga/Web/Widget/Tabextension/Tabextension.php b/library/Icinga/Web/Widget/Tabextension/Tabextension.php new file mode 100644 index 0000000..ea49c4b --- /dev/null +++ b/library/Icinga/Web/Widget/Tabextension/Tabextension.php @@ -0,0 +1,25 @@ + + {TABS} + {DROPDOWN} + {REFRESH} + {CLOSE} + +EOT; + + /** + * Template used for the tabs dropdown + * + * @var string + */ + private $dropdownTpl = <<< 'EOT' + +EOT; + + /** + * Template used for the close-button + * + * @var string + */ + private $closeTpl = <<< 'EOT' +
  • + + + +
  • +EOT; + + /** + * Template used for the refresh icon + * + * @var string + */ + private $refreshTpl = <<< 'EOT' +
  • + + + +
  • +EOT; + + /** + * This is where single tabs added to this container will be stored + * + * @var array + */ + private $tabs = array(); + + /** + * The name of the currently activated tab + * + * @var string + */ + private $active; + + /** + * Array of tab names which should be displayed in a dropdown + * + * @var array + */ + private $dropdownTabs = array(); + + /** + * Whether only the close-button should by rendered for this tab + * + * @var bool + */ + private $closeButtonOnly = false; + + /** + * Whether the tabs should contain a close-button + * + * @var bool + */ + private $closeTab = true; + + /** + * Set whether the current tab is closable + */ + public function hideCloseButton() + { + $this->closeTab = false; + } + + /** + * Activate the tab with the given name + * + * If another tab is currently active it will be deactivated + * + * @param string $name Name of the tab going to be activated + * + * @return $this + * + * @throws HttpNotFoundException When the tab w/ the given name does not exist + * + */ + public function activate($name) + { + if (! $this->has($name)) { + throw new HttpNotFoundException('Can\'t activate tab %s. Tab does not exist', $name); + } + + if ($this->active !== null) { + $this->tabs[$this->active]->setActive(false); + } + $this->get($name)->setActive(); + $this->active = $name; + + return $this; + } + + /** + * Return the name of the active tab + * + * @return string + */ + public function getActiveName() + { + return $this->active; + } + + /** + * Set the CSS class name(s) for the <ul> element + * + * @param string $name CSS class name(s) + * + * @return $this + */ + public function setClass($name) + { + $this->tab_class = $name; + return $this; + } + + /** + * Whether the given tab name exists + * + * @param string $name Tab name + * + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->tabs); + } + + /** + * Whether the given tab name exists + * + * @param string $name The tab you're interested in + * + * @return Tab + * + * @throws ProgrammingError When the given tab name doesn't exist + */ + public function get($name) + { + if (!$this->has($name)) { + return null; + } + return $this->tabs[$name]; + } + + /** + * Add a new tab + * + * A unique tab name is required, the Tab itself can either be an array + * with tab properties or an instance of an existing Tab + * + * @param string $name The new tab name + * @param array|Tab $tab The tab itself of its properties + * + * @return $this + * + * @throws ProgrammingError When the tab name already exists + */ + public function add($name, $tab) + { + if ($this->has($name)) { + throw new ProgrammingError( + 'Cannot add a tab named "%s" twice"', + $name + ); + } + return $this->set($name, $tab); + } + + /** + * Set a tab + * + * A unique tab name is required, will be replaced in case it already + * exists. The tab can either be an array with tab properties or an instance + * of an existing Tab + * + * @param string $name The new tab name + * @param array|Tab $tab The tab itself of its properties + * + * @return $this + */ + public function set($name, $tab) + { + if ($tab instanceof Tab) { + $this->tabs[$name] = $tab; + } else { + $this->tabs[$name] = new Tab($tab + array('name' => $name)); + } + return $this; + } + + /** + * Remove a tab + * + * @param string $name + * + * @return $this + */ + public function remove($name) + { + if ($this->has($name)) { + unset($this->tabs[$name]); + if (($dropdownIndex = array_search($name, $this->dropdownTabs, true)) !== false) { + array_splice($this->dropdownTabs, $dropdownIndex, 1); + } + } + + return $this; + } + + /** + * Add a tab to the dropdown on the right side of the tab-bar. + * + * @param $name + * @param $tab + */ + public function addAsDropdown($name, $tab) + { + $this->set($name, $tab); + $this->dropdownTabs[] = $name; + $this->dropdownTabs = array_unique($this->dropdownTabs); + } + + /** + * Render the dropdown area with its tabs and return the resulting HTML + * + * @return mixed|string + */ + private function renderDropdownTabs() + { + if (empty($this->dropdownTabs)) { + return ''; + } + $tabs = ''; + foreach ($this->dropdownTabs as $tabname) { + $tab = $this->get($tabname); + if ($tab === null) { + continue; + } + $tabs .= $tab; + } + return str_replace(array('{TABS}', '{TITLE}'), array($tabs, t('Dropdown menu')), $this->dropdownTpl); + } + + /** + * Render all tabs, except the ones in dropdown area and return the resulting HTML + * + * @return string + */ + private function renderTabs() + { + $tabs = ''; + foreach ($this->tabs as $name => $tab) { + // ignore tabs added to dropdown + if (in_array($name, $this->dropdownTabs)) { + continue; + } + $tabs .= $tab; + } + return $tabs; + } + + private function renderCloseTab() + { + return str_replace('{TITLE}', t('Close container'), $this->closeTpl); + } + + private function renderRefreshTab() + { + $url = Url::fromRequest(); + $tab = $this->get($this->getActiveName()); + + if ($tab !== null) { + $label = $this->view()->escape( + $tab->getLabel() + ); + } + + if (! empty($label)) { + $caption = $label; + } else { + $caption = t('Content'); + } + + $label = sprintf(t('Refresh the %s'), $caption); + $title = $label; + + $tpl = str_replace( + array( + '{URL}', + '{TITLE}', + '{LABEL}' + ), + array( + $this->view()->escape($url->getAbsoluteUrl()), + $title, + $label + ), + $this->refreshTpl + ); + + return $tpl; + } + + /** + * Render to HTML + * + * @see Widget::render + */ + public function render() + { + if (empty($this->tabs) || true === $this->closeButtonOnly) { + $tabs = ''; + $drop = ''; + } else { + $tabs = $this->renderTabs(); + $drop = $this->renderDropdownTabs(); + } + $close = $this->closeTab ? $this->renderCloseTab() : ''; + $refresh = $this->renderRefreshTab(); + + return str_replace( + array( + '{TABS}', + '{DROPDOWN}', + '{REFRESH}', + '{CLOSE}' + ), + array( + $tabs, + $drop, + $refresh, + $close + ), + $this->baseTpl + ); + } + + public function __toString() + { + try { + $html = $this->render(Icinga::app()->getViewRenderer()->view); + } catch (Exception $e) { + return htmlspecialchars($e->getMessage()); + } + return $html; + } + + /** + * Return the number of tabs + * + * @return int + * + * @see Countable + */ + public function count(): int + { + return count($this->tabs); + } + + /** + * Return all tabs contained in this tab panel + * + * @return array + */ + public function getTabs() + { + return $this->tabs; + } + + /** + * Whether to hide all elements except of the close button + * + * @param bool $value + * @return Tabs fluent interface + */ + public function showOnlyCloseButton($value = true) + { + $this->closeButtonOnly = $value; + return $this; + } + + /** + * Apply a Tabextension on this tabs object + * + * @param Tabextension $tabextension + * + * @return $this + */ + public function extend(Tabextension $tabextension) + { + $tabextension->apply($this); + return $this; + } +} diff --git a/library/Icinga/Web/Widget/Widget.php b/library/Icinga/Web/Widget/Widget.php new file mode 100644 index 0000000..879858a --- /dev/null +++ b/library/Icinga/Web/Widget/Widget.php @@ -0,0 +1,24 @@ +id = $parts[0]; + $this->containerId = $id; + } else { + $this->id = $id; + } + } + + /** + * Get whether the window's ID is undefined + * + * @return bool + */ + public function isUndefined() + { + return $this->id === self::UNDEFINED; + } + + /** + * Get the window's ID + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Get the container's ID + * + * @return string + */ + public function getContainerId() + { + return $this->containerId ?: $this->id; + } + + /** + * Return a window-aware session by using the given prefix + * + * @param string $prefix The prefix to use + * @param bool $reset Whether to reset any existing session-data + * + * @return SessionNamespace + */ + public function getSessionNamespace($prefix, $reset = false) + { + $session = Session::getSession(); + + $identifier = $prefix . '_' . $this->getId(); + if ($reset && $session->hasNamespace($identifier)) { + $session->removeNamespace($identifier); + } + + $namespace = $session->getNamespace($identifier); + $nsUndef = $prefix . '_' . self::UNDEFINED; + + if (! $reset && ! $this->isUndefined() && $session->hasNamespace($nsUndef)) { + // We may not have any window-id on the very first request. Now we add + // all values from the namespace, that has been created in this case, + // to the new one and remove it afterwards. + foreach ($session->getNamespace($nsUndef) as $name => $value) { + $namespace->set($name, $value); + } + + $session->removeNamespace($nsUndef); + } + + return $namespace; + } + + /** + * Generate a random string + * + * @return string + */ + public static function generateId() + { + $letters = 'abcefghijklmnopqrstuvwxyz'; + return substr(str_shuffle($letters), 0, 12); + } + + /** + * @return Window + */ + public static function getInstance() + { + if (! isset(static::$window)) { + $id = Icinga::app()->getRequest()->getHeader('X-Icinga-WindowId'); + if (empty($id) || $id === static::UNDEFINED) { + Icinga::app()->getResponse()->setOverrideWindowId(); + $id = static::generateId(); + } + + static::$window = new Window($id); + } + + return static::$window; + } +} diff --git a/library/Icinga/Web/Wizard.php b/library/Icinga/Web/Wizard.php new file mode 100644 index 0000000..6ba90bc --- /dev/null +++ b/library/Icinga/Web/Wizard.php @@ -0,0 +1,719 @@ +init(); + } + + /** + * Run additional initialization routines + * + * Should be implemented by subclasses to add pages to the wizard. + */ + protected function init() + { + } + + /** + * Return this wizard's parent or null in case it has none + * + * @return Wizard|null + */ + public function getParent() + { + return $this->parent; + } + + /** + * Set this wizard's parent + * + * @param Wizard $wizard The parent wizard + * + * @return $this + */ + public function setParent(Wizard $wizard) + { + $this->parent = $wizard; + return $this; + } + + /** + * Return the pages being part of this wizard + * + * In case this is a nested wizard a flattened array of all contained pages is returned. + * + * @return array + */ + public function getPages() + { + $pages = array(); + foreach ($this->pages as $page) { + if ($page instanceof self) { + $pages = array_merge($pages, $page->getPages()); + } else { + $pages[] = $page; + } + } + + return $pages; + } + + /** + * Return the page with the given name + * + * Note that it's also possible to retrieve a nested wizard's page by using this method. + * + * @param string $name The name of the page to return + * + * @return null|Form The page or null in case there is no page with the given name + */ + public function getPage($name) + { + foreach ($this->getPages() as $page) { + if ($name === $page->getName()) { + return $page; + } + } + } + + /** + * Add a new page or wizard to this wizard + * + * @param Form|Wizard $page The page or wizard to add to the wizard + * + * @return $this + */ + public function addPage($page) + { + if (! $page instanceof Form && ! $page instanceof self) { + throw InvalidArgumentException( + 'The $page argument must be an instance of Icinga\Web\Form ' + . 'or Icinga\Web\Wizard but is of type: ' . get_class($page) + ); + } elseif ($page instanceof self) { + $page->setParent($this); + } + + $this->pages[] = $page; + return $this; + } + + /** + * Add multiple pages or wizards to this wizard + * + * @param array $pages The pages or wizards to add to the wizard + * + * @return $this + */ + public function addPages(array $pages) + { + foreach ($pages as $page) { + $this->addPage($page); + } + + return $this; + } + + /** + * Assert that this wizard has any pages + * + * @throws LogicException In case this wizard has no pages + */ + protected function assertHasPages() + { + $pages = $this->getPages(); + if (count($pages) < 2) { + throw new LogicException("Although Chuck Norris can advance a wizard with less than two pages, you can't."); + } + } + + /** + * Return the current page of this wizard + * + * @return Form + * + * @throws LogicException In case the name of the current page currently being set is invalid + */ + public function getCurrentPage() + { + if ($this->parent) { + return $this->parent->getCurrentPage(); + } + + if ($this->currentPage === null) { + $this->assertHasPages(); + $pages = $this->getPages(); + $this->currentPage = $this->getSession()->get('current_page', $pages[0]->getName()); + } + + if (($page = $this->getPage($this->currentPage)) === null) { + throw new LogicException(sprintf('No page found with name "%s"', $this->currentPage)); + } + + return $page; + } + + /** + * Set the current page of this wizard + * + * @param Form $page The page to set as current page + * + * @return $this + */ + public function setCurrentPage(Form $page) + { + $this->currentPage = $page->getName(); + $this->getSession()->set('current_page', $this->currentPage); + return $this; + } + + /** + * Setup the given page that is either going to be displayed or validated + * + * Implement this method in a subclass to populate default values and/or other data required to process the form. + * + * @param Form $page The page to setup + * @param Request $request The current request + */ + public function setupPage(Form $page, Request $request) + { + } + + /** + * Process the given request using this wizard + * + * Validate the request data using the current page, update the wizard's + * position and redirect to the page's redirect url upon success. + * + * @param Request $request The request to be processed + * + * @return Request The request supposed to be processed + */ + public function handleRequest(Request $request = null) + { + $page = $this->getCurrentPage(); + + if (($wizard = $this->findWizard($page)) !== null) { + return $wizard->handleRequest($request); + } + + if ($request === null) { + $request = $page->getRequest(); + } + + $this->setupPage($page, $request); + $requestData = $this->getRequestData($page, $request); + if ($page->wasSent($requestData)) { + if (($requestedPage = $this->getRequestedPage($requestData)) !== null) { + $isValid = false; + $direction = $this->getDirection($request); + if ($direction === static::FORWARD && $page->isValid($requestData)) { + $isValid = true; + if ($this->isLastPage($page)) { + $this->setIsFinished(); + } + } elseif ($direction === static::BACKWARD) { + $page->populate($requestData); + $isValid = true; + } + + if ($isValid) { + $pageData = & $this->getPageData(); + $pageData[$page->getName()] = ConfigForm::transformEmptyValuesToNull($page->getValues()); + $this->setCurrentPage($this->getNewPage($requestedPage, $page)); + $page->getResponse()->redirectAndExit($page->getRedirectUrl()); + } + } elseif ($page->getValidatePartial()) { + $page->isValidPartial($requestData); + } else { + $page->populate($requestData); + } + } elseif (($pageData = $this->getPageData($page->getName())) !== null) { + $page->populate($pageData); + } + + return $request; + } + + /** + * Return the wizard for the given page or null if its not part of a wizard + * + * @param Form $page The page to return its wizard for + * + * @return Wizard|null + */ + protected function findWizard(Form $page) + { + foreach ($this->getWizards() as $wizard) { + if ($wizard->getPage($page->getName()) === $page) { + return $wizard; + } + } + } + + /** + * Return this wizard's child wizards + * + * @return array + */ + protected function getWizards() + { + $wizards = array(); + foreach ($this->pages as $pageOrWizard) { + if ($pageOrWizard instanceof self) { + $wizards[] = $pageOrWizard; + } + } + + return $wizards; + } + + /** + * Return the request data based on given form's request method + * + * @param Form $page The page to fetch the data for + * @param Request $request The request to fetch the data from + * + * @return array + */ + protected function getRequestData(Form $page, Request $request) + { + if (strtolower($request->getMethod()) === $page->getMethod()) { + return $request->{'get' . ($request->isPost() ? 'Post' : 'Query')}(); + } + + return array(); + } + + /** + * Return the name of the requested page + * + * @param array $requestData The request's data + * + * @return null|string The name of the requested page or null in case no page has been requested + */ + protected function getRequestedPage(array $requestData) + { + if ($this->parent) { + return $this->parent->getRequestedPage($requestData); + } + + if (isset($requestData[static::BTN_NEXT])) { + return $requestData[static::BTN_NEXT]; + } elseif (isset($requestData[static::BTN_PREV])) { + return $requestData[static::BTN_PREV]; + } + } + + /** + * Return the direction of this wizard using the given request + * + * @param Request $request The request to use + * + * @return int The direction @see Wizard::FORWARD @see Wizard::BACKWARD @see Wizard::NO_CHANGE + */ + protected function getDirection(Request $request = null) + { + if ($this->parent) { + return $this->parent->getDirection($request); + } + + $currentPage = $this->getCurrentPage(); + + if ($request === null) { + $request = $currentPage->getRequest(); + } + + $requestData = $this->getRequestData($currentPage, $request); + if (isset($requestData[static::BTN_NEXT])) { + return static::FORWARD; + } elseif (isset($requestData[static::BTN_PREV])) { + return static::BACKWARD; + } + + return static::NO_CHANGE; + } + + /** + * Return the new page to set as current page + * + * Permission is checked by verifying that the requested page or its previous page has page data available. + * The requested page is automatically permitted without any checks if the origin page is its previous + * page or one that occurs later in order. + * + * @param string $requestedPage The name of the requested page + * @param Form $originPage The origin page + * + * @return Form The new page + * + * @throws InvalidArgumentException In case the requested page does not exist or is not permitted yet + */ + protected function getNewPage($requestedPage, Form $originPage) + { + if ($this->parent) { + return $this->parent->getNewPage($requestedPage, $originPage); + } + + if (($page = $this->getPage($requestedPage)) !== null) { + $permitted = true; + + $pages = $this->getPages(); + if (! $this->hasPageData($requestedPage) && ($index = array_search($page, $pages, true)) > 0) { + $previousPage = $pages[$index - 1]; + if ($originPage === null || ($previousPage->getName() !== $originPage->getName() + && array_search($originPage, $pages, true) < $index)) { + $permitted = $this->hasPageData($previousPage->getName()); + } + } + + if ($permitted) { + return $page; + } + } + + throw new InvalidArgumentException( + sprintf('"%s" is either an unknown page or one you are not permitted to view', $requestedPage) + ); + } + + /** + * Return the next or previous page based on the given one + * + * @param Form $page The page to skip + * + * @return Form + */ + protected function skipPage(Form $page) + { + if ($this->parent) { + return $this->parent->skipPage($page); + } + + if ($this->hasPageData($page->getName())) { + $pageData = & $this->getPageData(); + unset($pageData[$page->getName()]); + } + + $pages = $this->getPages(); + if ($this->getDirection() === static::FORWARD) { + $nextPage = $pages[array_search($page, $pages, true) + 1]; + $newPage = $this->getNewPage($nextPage->getName(), $page); + } else { // $this->getDirection() === static::BACKWARD + $previousPage = $pages[array_search($page, $pages, true) - 1]; + $newPage = $this->getNewPage($previousPage->getName(), $page); + } + + return $newPage; + } + + /** + * Return whether the given page is this wizard's last page + * + * @param Form $page The page to check + * + * @return bool + */ + protected function isLastPage(Form $page) + { + if ($this->parent) { + return $this->parent->isLastPage($page); + } + + $pages = $this->getPages(); + return $page->getName() === end($pages)->getName(); + } + + /** + * Return whether all of this wizard's pages were visited by the user + * + * The base implementation just verifies that the very last page has page data available. + * + * @return bool + */ + public function isComplete() + { + $pages = $this->getPages(); + return $this->hasPageData($pages[count($pages) - 1]->getName()); + } + + /** + * Set whether this wizard has been completed + * + * @param bool $state Whether this wizard has been completed + * + * @return $this + */ + public function setIsFinished($state = true) + { + $this->getSession()->set('isFinished', $state); + return $this; + } + + /** + * Return whether this wizard has been completed + * + * @return bool + */ + public function isFinished() + { + return $this->getSession()->get('isFinished', false); + } + + /** + * Return the overall page data or one for a particular page + * + * Note that this method returns by reference so in order to update the + * returned array set this method's return value also by reference. + * + * @param string $pageName The page for which to return the data + * + * @return array + */ + public function & getPageData($pageName = null) + { + $session = $this->getSession(); + + if (false === isset($session->page_data)) { + $session->page_data = array(); + } + + $pageData = & $session->getByRef('page_data'); + if ($pageName !== null) { + $data = null; + if (isset($pageData[$pageName])) { + $data = & $pageData[$pageName]; + } + + return $data; + } + + return $pageData; + } + + /** + * Return whether there is any data for the given page + * + * @param string $pageName The name of the page to check + * + * @return bool + */ + public function hasPageData($pageName) + { + return $this->getPageData($pageName) !== null; + } + + /** + * Return a session to be used by this wizard + * + * @return SessionNamespace + */ + public function getSession() + { + if ($this->parent) { + return $this->parent->getSession(); + } + + return Session::getSession()->getNamespace(get_class($this)); + } + + /** + * Clear the session being used by this wizard + */ + public function clearSession() + { + $this->getSession()->clear(); + } + + /** + * Add buttons to the given page based on its position in the page-chain + * + * @param Form $page The page to add the buttons to + */ + protected function addButtons(Form $page) + { + $pages = $this->getPages(); + $index = array_search($page, $pages, true); + if ($index === 0) { + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'class' => 'control-button btn-primary', + 'type' => 'submit', + 'value' => $pages[1]->getName(), + 'label' => t('Next'), + 'decorators' => array('ViewHelper', 'Spinner') + ) + ); + } elseif ($index < count($pages) - 1) { + $page->addElement( + 'button', + static::BTN_PREV, + array( + 'class' => 'control-button', + 'type' => 'submit', + 'value' => $pages[$index - 1]->getName(), + 'label' => t('Back'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'class' => 'control-button btn-primary', + 'type' => 'submit', + 'value' => $pages[$index + 1]->getName(), + 'label' => t('Next'), + 'decorators' => array('ViewHelper') + ) + ); + } else { + $page->addElement( + 'button', + static::BTN_PREV, + array( + 'class' => 'control-button', + 'type' => 'submit', + 'value' => $pages[$index - 1]->getName(), + 'label' => t('Back'), + 'decorators' => array('ViewHelper'), + 'formnovalidate' => 'formnovalidate' + ) + ); + $page->addElement( + 'button', + static::BTN_NEXT, + array( + 'class' => 'control-button btn-primary', + 'type' => 'submit', + 'value' => $page->getName(), + 'label' => t('Finish'), + 'decorators' => array('ViewHelper') + ) + ); + } + + $page->setAttrib('data-progress-element', static::PROGRESS_ELEMENT); + $page->addElement( + 'note', + static::PROGRESS_ELEMENT, + array( + 'order' => 99, // Ensures that it's shown on the right even if a sub-class adds another button + 'decorators' => array( + 'ViewHelper', + array('Spinner', array('id' => static::PROGRESS_ELEMENT)) + ) + ) + ); + + $page->addDisplayGroup( + array(static::BTN_PREV, static::BTN_NEXT, static::PROGRESS_ELEMENT), + 'buttons', + array( + 'decorators' => array( + 'FormElements', + new ElementDoubler(array( + 'double' => static::BTN_NEXT, + 'condition' => static::BTN_PREV, + 'placement' => ElementDoubler::PREPEND, + 'attributes' => array('tabindex' => -1, 'class' => 'double') + )), + array('HtmlTag', array('tag' => 'div', 'class' => 'buttons')) + ) + ) + ); + } + + /** + * Return the current page of this wizard with appropriate buttons being added + * + * @return Form + */ + public function getForm() + { + $form = $this->getCurrentPage(); + $form->create(); // Make sure that buttons are displayed at the very bottom + $this->addButtons($form); + return $form; + } + + /** + * Return the current page of this wizard rendered as HTML + * + * @return string + */ + public function __toString() + { + return (string) $this->getForm(); + } +} diff --git a/library/vendor/HTMLPurifier.autoload.php b/library/vendor/HTMLPurifier.autoload.php new file mode 100644 index 0000000..7a69113 --- /dev/null +++ b/library/vendor/HTMLPurifier.autoload.php @@ -0,0 +1,25 @@ +config = HTMLPurifier_Config::create($config); + $this->strategy = new HTMLPurifier_Strategy_Core(); + } + + /** + * Adds a filter to process the output. First come first serve + * + * @param HTMLPurifier_Filter $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 string $html String of HTML to purify + * @param HTMLPurifier_Config $config 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 string 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 string[] $array_of_html Array of html snippets + * @param HTMLPurifier_Config $config Optional config object for this operation. + * See HTMLPurifier::purify() for more details. + * + * @return string[] Array of purified HTML + */ + public function purifyArray($array_of_html, $config = null) + { + $context_array = array(); + $array = array(); + foreach($array_of_html as $key=>$value){ + if (is_array($value)) { + $array[$key] = $this->purifyArray($value, $config); + } else { + $array[$key] = $this->purify($value, $config); + } + $context_array[$key] = $this->context; + } + $this->context = $context_array; + return $array; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + */ + 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; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + * @note Backwards compatibility, see instance() + */ + public static function getInstance($prototype = null) + { + return HTMLPurifier::instance($prototype); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Arborize.php b/library/vendor/HTMLPurifier/Arborize.php new file mode 100644 index 0000000..d2e9d22 --- /dev/null +++ b/library/vendor/HTMLPurifier/Arborize.php @@ -0,0 +1,71 @@ +getHTMLDefinition(); + $parent = new HTMLPurifier_Token_Start($definition->info_parent); + $stack = array($parent->toNode()); + foreach ($tokens as $token) { + $token->skip = null; // [MUT] + $token->carryover = null; // [MUT] + if ($token instanceof HTMLPurifier_Token_End) { + $token->start = null; // [MUT] + $r = array_pop($stack); + //assert($r->name === $token->name); + //assert(empty($token->attr)); + $r->endCol = $token->col; + $r->endLine = $token->line; + $r->endArmor = $token->armor; + continue; + } + $node = $token->toNode(); + $stack[count($stack)-1]->children[] = $node; + if ($token instanceof HTMLPurifier_Token_Start) { + $stack[] = $node; + } + } + //assert(count($stack) == 1); + return $stack[0]; + } + + public static function flatten($node, $config, $context) { + $level = 0; + $nodes = array($level => new HTMLPurifier_Queue(array($node))); + $closingTokens = array(); + $tokens = array(); + do { + while (!$nodes[$level]->isEmpty()) { + $node = $nodes[$level]->shift(); // FIFO + list($start, $end) = $node->toTokenPair(); + if ($level > 0) { + $tokens[] = $start; + } + if ($end !== NULL) { + $closingTokens[$level][] = $end; + } + if ($node instanceof HTMLPurifier_Node_Element) { + $level++; + $nodes[$level] = new HTMLPurifier_Queue(); + foreach ($node->children as $childNode) { + $nodes[$level]->push($childNode); + } + } + } + $level--; + if ($level && isset($closingTokens[$level])) { + while ($token = array_pop($closingTokens[$level])) { + $tokens[] = $token; + } + } + } while ($level > 0); + return $tokens; + } +} diff --git a/library/vendor/HTMLPurifier/AttrCollections.php b/library/vendor/HTMLPurifier/AttrCollections.php new file mode 100644 index 0000000..c7b17cf --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrCollections.php @@ -0,0 +1,148 @@ +doConstruct($attr_types, $modules); + } + + public function doConstruct($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 array &$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 array &$attr Reference to attribute array + * @param HTMLPurifier_AttrTypes $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/library/vendor/HTMLPurifier/AttrDef.php b/library/vendor/HTMLPurifier/AttrDef.php new file mode 100644 index 0000000..739646f --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef.php @@ -0,0 +1,144 @@ + 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 String construction info + * @return HTMLPurifier_AttrDef 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! + * @param string $string a CSS colour definition + * @return string + */ + protected function mungeRgb($string) + { + $p = '\s*(\d+(\.\d+)?([%]?))\s*'; + + if (preg_match('/(rgba|hsla)\(/', $string)) { + return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string); + } + + return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string); + } + + /** + * Parses a possibly escaped CSS string and returns the "pure" + * version of it. + */ + protected function expandCSSEscape($string) + { + // flexibly parse it + $ret = ''; + for ($i = 0, $c = strlen($string); $i < $c; $i++) { + if ($string[$i] === '\\') { + $i++; + if ($i >= $c) { + $ret .= '\\'; + break; + } + if (ctype_xdigit($string[$i])) { + $code = $string[$i]; + for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { + if (!ctype_xdigit($string[$i])) { + break; + } + $code .= $string[$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; + } + $ret .= $char; + if ($i < $c && trim($string[$i]) !== '') { + $i--; + } + continue; + } + if ($string[$i] === "\n") { + continue; + } + } + $ret .= $string[$i]; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/CSS.php b/library/vendor/HTMLPurifier/AttrDef/CSS.php new file mode 100644 index 0000000..ad2cb90 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS.php @@ -0,0 +1,136 @@ +parseCDATA($css); + + $definition = $config->getCSSDefinition(); + $allow_duplicates = $config->get("CSS.AllowDuplicates"); + + + // According to the CSS2.1 spec, the places where a + // non-delimiting semicolon can appear are in strings + // escape sequences. So here is some dumb hack to + // handle quotes. + $len = strlen($css); + $accum = ""; + $declarations = array(); + $quoted = false; + for ($i = 0; $i < $len; $i++) { + $c = strcspn($css, ";'\"", $i); + $accum .= substr($css, $i, $c); + $i += $c; + if ($i == $len) break; + $d = $css[$i]; + if ($quoted) { + $accum .= $d; + if ($d == $quoted) { + $quoted = false; + } + } else { + if ($d == ";") { + $declarations[] = $accum; + $accum = ""; + } else { + $accum .= $d; + $quoted = $d; + } + } + } + if ($accum != "") $declarations[] = $accum; + + $propvalues = array(); + $new_declarations = ''; + + /** + * 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; + } + if ($allow_duplicates) { + $new_declarations .= "$property:$result;"; + } else { + $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. + + foreach ($propvalues as $prop => $value) { + $new_declarations .= "$prop:$value;"; + } + + return $new_declarations ? $new_declarations : false; + + } + +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/CSS/AlphaValue.php b/library/vendor/HTMLPurifier/AttrDef/CSS/AlphaValue.php new file mode 100644 index 0000000..af2b83d --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/AlphaValue.php @@ -0,0 +1,34 @@ + 1.0) { + $result = '1'; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/CSS/Background.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Background.php new file mode 100644 index 0000000..28c4988 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Background.php @@ -0,0 +1,113 @@ +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']; + $this->info['background-size'] = $def->info['background-size']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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(' ', $string); // bits to process + + $caught = array(); + $caught['color'] = false; + $caught['image'] = false; + $caught['repeat'] = false; + $caught['attachment'] = false; + $caught['position'] = false; + $caught['size'] = false; + + $i = 0; // number of catches + + 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/library/vendor/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php b/library/vendor/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php new file mode 100644 index 0000000..4580ef5 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php @@ -0,0 +1,157 @@ + | | left | center | right + ] + [ + | | 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 +{ + + /** + * @type HTMLPurifier_AttrDef_CSS_Length + */ + protected $length; + + /** + * @type HTMLPurifier_AttrDef_CSS_Percentage + */ + protected $percentage; + + public function __construct() + { + $this->length = new HTMLPurifier_AttrDef_CSS_Length(); + $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage(); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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['ch'] = false; // center (first word) + $keywords['cv'] = false; // center (second word) + $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]; + if ($status == 'c') { + if ($i == 0) { + $status = 'ch'; + } else { + $status = 'cv'; + } + } + $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 ($keywords['ch']) { + $ret[] = $keywords['ch']; + $keywords['cv'] = false; // prevent re-use: center = center center + } elseif (count($measures)) { + $ret[] = array_shift($measures); + } + + if ($keywords['v']) { + $ret[] = $keywords['v']; + } elseif ($keywords['cv']) { + $ret[] = $keywords['cv']; + } elseif (count($measures)) { + $ret[] = array_shift($measures); + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/CSS/Border.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Border.php new file mode 100644 index 0000000..16243ba --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Border.php @@ -0,0 +1,56 @@ +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']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/Color.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Color.php new file mode 100644 index 0000000..d7287a0 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Color.php @@ -0,0 +1,161 @@ +alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + + /** + * @param string $color + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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 (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) { + $length = strlen($color); + if (strpos($color, ')') !== $length - 1) { + return false; + } + + // get used function : rgb, rgba, hsl or hsla + $function = $matches[1]; + + $parameters_size = 3; + $alpha_channel = false; + if (substr($function, -1) === 'a') { + $parameters_size = 4; + $alpha_channel = true; + } + + /* + * Allowed types for values : + * parameter_position => [type => max_value] + */ + $allowed_types = array( + 1 => array('percentage' => 100, 'integer' => 255), + 2 => array('percentage' => 100, 'integer' => 255), + 3 => array('percentage' => 100, 'integer' => 255), + ); + $allow_different_types = false; + + if (strpos($function, 'hsl') !== false) { + $allowed_types = array( + 1 => array('integer' => 360), + 2 => array('percentage' => 100), + 3 => array('percentage' => 100), + ); + $allow_different_types = true; + } + + $values = trim(str_replace($function, '', $color), ' ()'); + + $parts = explode(',', $values); + if (count($parts) !== $parameters_size) { + return false; + } + + $type = false; + $new_parts = array(); + $i = 0; + + foreach ($parts as $part) { + $i++; + $part = trim($part); + + if ($part === '') { + return false; + } + + // different check for alpha channel + if ($alpha_channel === true && $i === count($parts)) { + $result = $this->alpha->validate($part, $config, $context); + + if ($result === false) { + return false; + } + + $new_parts[] = (string)$result; + continue; + } + + if (substr($part, -1) === '%') { + $current_type = 'percentage'; + } else { + $current_type = 'integer'; + } + + if (!array_key_exists($current_type, $allowed_types[$i])) { + return false; + } + + if (!$type) { + $type = $current_type; + } + + if ($allow_different_types === false && $type != $current_type) { + return false; + } + + $max_value = $allowed_types[$i][$current_type]; + + if ($current_type == 'integer') { + // Return value between range 0 -> $max_value + $new_parts[] = (int)max(min($part, $max_value), 0); + } elseif ($current_type == 'percentage') { + $new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%'; + } + } + + $new_values = implode(',', $new_parts); + + $color = $function . '(' . $new_values . ')'; + } 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/library/vendor/HTMLPurifier/AttrDef/CSS/Composite.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Composite.php new file mode 100644 index 0000000..9c17505 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Composite.php @@ -0,0 +1,48 @@ +defs = $defs; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php b/library/vendor/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php new file mode 100644 index 0000000..9d77cc9 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php @@ -0,0 +1,44 @@ +def = $def; + $this->element = $element; + } + + /** + * Checks if CurrentToken is set and equal to $this->element + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/Filter.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Filter.php new file mode 100644 index 0000000..bde4c33 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Filter.php @@ -0,0 +1,77 @@ +intValidator = new HTMLPurifier_AttrDef_Integer(); + } + + /** + * @param string $value + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/Font.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Font.php new file mode 100644 index 0000000..579b97e --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Font.php @@ -0,0 +1,176 @@ +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']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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) { + case 0: // attempting to catch font-style, font-variant or font-weight + 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; + } + case 1: // attempting to catch font-size and perhaps line-height + $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; + case 2: // attempting to catch font-family + $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/library/vendor/HTMLPurifier/AttrDef/CSS/FontFamily.php b/library/vendor/HTMLPurifier/AttrDef/CSS/FontFamily.php new file mode 100644 index 0000000..74e24c8 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/FontFamily.php @@ -0,0 +1,219 @@ +mask = '_- '; + for ($c = 'a'; $c <= 'z'; $c++) { + $this->mask .= $c; + } + for ($c = 'A'; $c <= 'Z'; $c++) { + $this->mask .= $c; + } + for ($c = '0'; $c <= '9'; $c++) { + $this->mask .= $c; + } // cast-y, but should be fine + // special bytes used by UTF-8 + for ($i = 0x80; $i <= 0xFF; $i++) { + // We don't bother excluding invalid bytes in this range, + // because the our restriction of well-formed UTF-8 will + // prevent these from ever occurring. + $this->mask .= chr($i); + } + + /* + PHP's internal strcspn implementation is + O(length of string * length of mask), making it inefficient + for large masks. However, it's still faster than + preg_match 8) + for (p = s1;;) { + spanp = s2; + do { + if (*spanp == c || p == s1_end) { + return p - s1; + } + } while (spanp++ < (s2_end - 1)); + c = *++p; + } + */ + // possible optimization: invert the mask. + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $generic_names = array( + 'serif' => true, + 'sans-serif' => true, + 'monospace' => true, + 'fantasy' => true, + 'cursive' => true + ); + $allowed_fonts = $config->get('CSS.AllowedFonts'); + + // 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])) { + if ($allowed_fonts === null || isset($allowed_fonts[$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); + } + + $font = $this->expandCSSEscape($font); + + // $font is a pure representation of the font name + + if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { + continue; + } + + if (ctype_alnum($font) && $font !== '') { + // very simple font, allow it in unharmed + $final .= $font . ', '; + continue; + } + + // bugger out on whitespace. form feed (0C) really + // shouldn't show up regardless + $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font); + + // Here, there are various classes of characters which need + // to be treated differently: + // - Alphanumeric characters are essentially safe. We + // handled these above. + // - Spaces require quoting, though most parsers will do + // the right thing if there aren't any characters that + // can be misinterpreted + // - Dashes rarely occur, but they fairly unproblematic + // for parsing/rendering purposes. + // The above characters cover the majority of Western font + // names. + // - Arbitrary Unicode characters not in ASCII. Because + // most parsers give little thought to Unicode, treatment + // of these codepoints is basically uniform, even for + // punctuation-like codepoints. These characters can + // show up in non-Western pages and are supported by most + // major browsers, for example: "MS 明朝" is a + // legitimate font-name + // . See + // the CSS3 spec for more examples: + // + // You can see live samples of these on the Internet: + // + // However, most of these fonts have ASCII equivalents: + // for example, 'MS Mincho', and it's considered + // professional to use ASCII font names instead of + // Unicode font names. Thanks Takeshi Terada for + // providing this information. + // The following characters, to my knowledge, have not been + // used to name font names. + // - Single quote. While theoretically you might find a + // font name that has a single quote in its name (serving + // as an apostrophe, e.g. Dave's Scribble), I haven't + // been able to find any actual examples of this. + // Internet Explorer's cssText translation (which I + // believe is invoked by innerHTML) normalizes any + // quoting to single quotes, and fails to escape single + // quotes. (Note that this is not IE's behavior for all + // CSS properties, just some sort of special casing for + // font-family). So a single quote *cannot* be used + // safely in the font-family context if there will be an + // innerHTML/cssText translation. Note that Firefox 3.x + // does this too. + // - Double quote. In IE, these get normalized to + // single-quotes, no matter what the encoding. (Fun + // fact, in IE8, the 'content' CSS property gained + // support, where they special cased to preserve encoded + // double quotes, but still translate unadorned double + // quotes into single quotes.) So, because their + // fixpoint behavior is identical to single quotes, they + // cannot be allowed either. Firefox 3.x displays + // single-quote style behavior. + // - Backslashes are reduced by one (so \\ -> \) every + // iteration, so they cannot be used safely. This shows + // up in IE7, IE8 and FF3 + // - Semicolons, commas and backticks are handled properly. + // - The rest of the ASCII punctuation is handled properly. + // We haven't checked what browsers do to unadorned + // versions, but this is not important as long as the + // browser doesn't /remove/ surrounding quotes (as IE does + // for HTML). + // + // With these results in hand, we conclude that there are + // various levels of safety: + // - Paranoid: alphanumeric, spaces and dashes(?) + // - International: Paranoid + non-ASCII Unicode + // - Edgy: Everything except quotes, backslashes + // - NoJS: Standards compliance, e.g. sod IE. Note that + // with some judicious character escaping (since certain + // types of escaping doesn't work) this is theoretically + // OK as long as innerHTML/cssText is not called. + // We believe that international is a reasonable default + // (that we will implement now), and once we do more + // extensive research, we may feel comfortable with dropping + // it down to edgy. + + // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of + // str(c)spn assumes that the string was already well formed + // Unicode (which of course it is). + if (strspn($font, $this->mask) !== strlen($font)) { + continue; + } + + // Historical: + // In the absence of innerHTML/cssText, these ugly + // transforms don't pose a security risk (as \\ and \" + // might--these escapes are not supported by most browsers). + // We could try to be clever and use single-quote wrapping + // when there is a double quote present, but I have choosen + // not to implement that. (NOTE: you can reduce the amount + // of escapes by one depending on what quoting style you use) + // $font = str_replace('\\', '\\5C ', $font); + // $font = str_replace('"', '\\22 ', $font); + // $font = str_replace("'", '\\27 ', $font); + + // font possibly with spaces, requires quoting + $final .= "'$font', "; + } + $final = rtrim($final, ', '); + if ($final === '') { + return false; + } + return $final; + } + +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/CSS/Ident.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Ident.php new file mode 100644 index 0000000..973002c --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Ident.php @@ -0,0 +1,32 @@ +def = $def; + $this->allow = $allow; + } + + /** + * Intercepts and removes !important if necessary + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/Length.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Length.php new file mode 100644 index 0000000..f12453a --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Length.php @@ -0,0 +1,77 @@ +min = $min !== null ? HTMLPurifier_Length::make($min) : null; + $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/ListStyle.php b/library/vendor/HTMLPurifier/AttrDef/CSS/ListStyle.php new file mode 100644 index 0000000..e74d426 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/ListStyle.php @@ -0,0 +1,112 @@ +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']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/Multiple.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Multiple.php new file mode 100644 index 0000000..e707f87 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Multiple.php @@ -0,0 +1,71 @@ +single = $single; + $this->max = $max; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->mungeRgb($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/library/vendor/HTMLPurifier/AttrDef/CSS/Number.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Number.php new file mode 100644 index 0000000..ef49d20 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Number.php @@ -0,0 +1,90 @@ +non_negative = $non_negative; + } + + /** + * @param string $number + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string|bool + * @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; + } + + // Remove leading zeros until positive number or a zero stays left + if (ltrim($left, '0') != '') { + $left = ltrim($left, '0'); + } else { + $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/library/vendor/HTMLPurifier/AttrDef/CSS/Percentage.php b/library/vendor/HTMLPurifier/AttrDef/CSS/Percentage.php new file mode 100644 index 0000000..f0f25c5 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/Percentage.php @@ -0,0 +1,54 @@ +number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/library/vendor/HTMLPurifier/AttrDef/CSS/TextDecoration.php new file mode 100644 index 0000000..5fd4b7f --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/TextDecoration.php @@ -0,0 +1,46 @@ + 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/library/vendor/HTMLPurifier/AttrDef/CSS/URI.php b/library/vendor/HTMLPurifier/AttrDef/CSS/URI.php new file mode 100644 index 0000000..6617aca --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/CSS/URI.php @@ -0,0 +1,77 @@ +parseCDATA($uri_string); + if (strpos($uri_string, 'url(') !== 0) { + return false; + } + $uri_string = substr($uri_string, 4); + if (strlen($uri_string) == 0) { + return false; + } + $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); + } + + $uri = $this->expandCSSEscape($uri); + + $result = parent::validate($uri, $config, $context); + + if ($result === false) { + return false; + } + + // extra sanity check; should have been done by URI + $result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result); + + // suspicious characters are ()'; we're going to percent encode + // them for safety. + $result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result); + + // there's an extra bug where ampersands lose their escaping on + // an innerHTML cycle, so a very unlucky query parameter could + // then change the meaning of the URL. Unfortunately, there's + // not much we can do about that... + return "url(\"$result\")"; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/Clone.php b/library/vendor/HTMLPurifier/AttrDef/Clone.php new file mode 100644 index 0000000..6698a00 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/Clone.php @@ -0,0 +1,44 @@ +clone = $clone; + } + + /** + * @param string $v + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($v, $config, $context) + { + return $this->clone->validate($v, $config, $context); + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef + */ + public function make($string) + { + return clone $this->clone; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/Enum.php b/library/vendor/HTMLPurifier/AttrDef/Enum.php new file mode 100644 index 0000000..8abda7f --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/Enum.php @@ -0,0 +1,73 @@ +valid_values = array_flip($valid_values); + $this->case_sensitive = $case_sensitive; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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 $string In form of comma-delimited list of case-insensitive + * valid values. Example: "foo,bar,baz". Prepend "s:" to make + * case sensitive + * @return HTMLPurifier_AttrDef_Enum + */ + 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/library/vendor/HTMLPurifier/AttrDef/HTML/Bool.php b/library/vendor/HTMLPurifier/AttrDef/HTML/Bool.php new file mode 100644 index 0000000..be3bbc8 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/Bool.php @@ -0,0 +1,48 @@ +name = $name; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + return $this->name; + } + + /** + * @param string $string Name of attribute + * @return HTMLPurifier_AttrDef_HTML_Bool + */ + public function make($string) + { + return new HTMLPurifier_AttrDef_HTML_Bool($string); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/HTML/Class.php b/library/vendor/HTMLPurifier/AttrDef/HTML/Class.php new file mode 100644 index 0000000..d501348 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/Class.php @@ -0,0 +1,48 @@ +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); + } + } + + /** + * @param array $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrDef/HTML/Color.php b/library/vendor/HTMLPurifier/AttrDef/HTML/Color.php new file mode 100644 index 0000000..946ebb7 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/Color.php @@ -0,0 +1,51 @@ +get('Core.ColorKeywords'); + } + + $string = trim($string); + + if (empty($string)) { + return false; + } + $lower = strtolower($string); + if (isset($colors[$lower])) { + return $colors[$lower]; + } + 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/library/vendor/HTMLPurifier/AttrDef/HTML/ContentEditable.php b/library/vendor/HTMLPurifier/AttrDef/HTML/ContentEditable.php new file mode 100644 index 0000000..5b03d3e --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/ContentEditable.php @@ -0,0 +1,16 @@ +get('HTML.Trusted')) { + $allowed = array('', 'true', 'false'); + } + + $enum = new HTMLPurifier_AttrDef_Enum($allowed); + + return $enum->validate($string, $config, $context); + } +} diff --git a/library/vendor/HTMLPurifier/AttrDef/HTML/FrameTarget.php b/library/vendor/HTMLPurifier/AttrDef/HTML/FrameTarget.php new file mode 100644 index 0000000..d79ba12 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/FrameTarget.php @@ -0,0 +1,38 @@ +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/library/vendor/HTMLPurifier/AttrDef/HTML/ID.php b/library/vendor/HTMLPurifier/AttrDef/HTML/ID.php new file mode 100644 index 0000000..4ba4561 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/ID.php @@ -0,0 +1,113 @@ +selector = $selector; + } + + /** + * @param string $id + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($id, $config, $context) + { + if (!$this->selector && !$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->selector) { + $id_accumulator =& $context->get('IDAccumulator'); + if (isset($id_accumulator->ids[$id])) { + return false; + } + } + + // we purposely avoid using regex, hopefully this is faster + + if ($config->get('Attr.ID.HTML5') === true) { + if (preg_match('/[\t\n\x0b\x0c ]/', $id)) { + return false; + } + } else { + if (ctype_alpha($id)) { + // OK + } else { + if (!ctype_alpha(@$id[0])) { + return false; + } + // primitive style of regexps, I suppose + $trim = trim( + $id, + 'A..Za..z0..9:-._' + ); + if ($trim !== '') { + return false; + } + } + } + + $regexp = $config->get('Attr.IDBlacklistRegexp'); + if ($regexp && preg_match($regexp, $id)) { + return false; + } + + if (!$this->selector) { + $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 $id; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/HTML/Length.php b/library/vendor/HTMLPurifier/AttrDef/HTML/Length.php new file mode 100644 index 0000000..1c4006f --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/Length.php @@ -0,0 +1,56 @@ + 100) { + return '100%'; + } + return ((string)$points) . '%'; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/library/vendor/HTMLPurifier/AttrDef/HTML/LinkTypes.php new file mode 100644 index 0000000..63fa04c --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/LinkTypes.php @@ -0,0 +1,72 @@ + 'AllowedRel', + 'rev' => 'AllowedRev' + ); + if (!isset($configLookup[$name])) { + trigger_error( + 'Unrecognized attribute name for link ' . + 'relationship.', + E_USER_ERROR + ); + return; + } + $this->name = $configLookup[$name]; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/HTML/MultiLength.php b/library/vendor/HTMLPurifier/AttrDef/HTML/MultiLength.php new file mode 100644 index 0000000..bbb20f2 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/MultiLength.php @@ -0,0 +1,60 @@ +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. + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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. + * @param array $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function filter($tokens, $config, $context) + { + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/HTML/Pixels.php b/library/vendor/HTMLPurifier/AttrDef/HTML/Pixels.php new file mode 100644 index 0000000..a1d019e --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/HTML/Pixels.php @@ -0,0 +1,76 @@ +max = $max; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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 + // 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; + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef + */ + 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/library/vendor/HTMLPurifier/AttrDef/Integer.php b/library/vendor/HTMLPurifier/AttrDef/Integer.php new file mode 100644 index 0000000..400e707 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/Integer.php @@ -0,0 +1,91 @@ +negative = $negative; + $this->zero = $zero; + $this->positive = $positive; + } + + /** + * @param string $integer + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/Lang.php b/library/vendor/HTMLPurifier/AttrDef/Lang.php new file mode 100644 index 0000000..2a55cea --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/Lang.php @@ -0,0 +1,86 @@ + 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/library/vendor/HTMLPurifier/AttrDef/Switch.php b/library/vendor/HTMLPurifier/AttrDef/Switch.php new file mode 100644 index 0000000..c7eb319 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/Switch.php @@ -0,0 +1,53 @@ +tag = $tag; + $this->withTag = $with_tag; + $this->withoutTag = $without_tag; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/Text.php b/library/vendor/HTMLPurifier/AttrDef/Text.php new file mode 100644 index 0000000..4553a4e --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/Text.php @@ -0,0 +1,21 @@ +parseCDATA($string); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/URI.php b/library/vendor/HTMLPurifier/AttrDef/URI.php new file mode 100644 index 0000000..c1cd897 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/URI.php @@ -0,0 +1,111 @@ +parser = new HTMLPurifier_URIParser(); + $this->embedsResource = (bool)$embeds_resource; + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef_URI + */ + public function make($string) + { + $embeds = ($string === 'embedded'); + return new HTMLPurifier_AttrDef_URI($embeds); + } + + /** + * @param string $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + 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/library/vendor/HTMLPurifier/AttrDef/URI/Email.php b/library/vendor/HTMLPurifier/AttrDef/URI/Email.php new file mode 100644 index 0000000..daf32b7 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/URI/Email.php @@ -0,0 +1,20 @@ +" + // 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/library/vendor/HTMLPurifier/AttrDef/URI/Host.php b/library/vendor/HTMLPurifier/AttrDef/URI/Host.php new file mode 100644 index 0000000..1beeaa5 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/URI/Host.php @@ -0,0 +1,142 @@ +ipv4 = new HTMLPurifier_AttrDef_URI_IPv4(); + $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6(); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $length = strlen($string); + // empty hostname is OK; it's usually semantically equivalent: + // the default host as defined by a URI scheme is used: + // + // If the URI scheme defines a default for host, then that + // default applies when the host subcomponent is undefined + // or when the registered name is empty (zero length). + 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 doesn't match I18N domain names, but we don't have proper IRI support, + // so force users to insert Punycode. + + // There is not a good sense in which underscores should be + // allowed, since it's technically not! (And if you go as + // far to allow everything as specified by the DNS spec... + // well, that's literally everything, modulo some space limits + // for the components and the overall name (which, by the way, + // we are NOT checking!). So we (arbitrarily) decide this: + // let's allow underscores wherever we would have allowed + // hyphens, if they are enabled. This is a pretty good match + // for browser behavior, for example, a large number of browsers + // cannot handle foo_.example.com, but foo_bar.example.com is + // fairly well supported. + $underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : ''; + + // Based off of RFC 1738, but amended so that + // as per RFC 3696, the top label need only not be all numeric. + // The productions describing this are: + $a = '[a-z]'; // alpha + $an = '[a-z0-9]'; // alphanum + $and = "[a-z0-9-$underscore]"; // alphanum | "-" + // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + $domainlabel = "$an(?:$and*$an)?"; + // AMENDED as per RFC 3696 + // toplabel = alphanum | alphanum *( alphanum | "-" ) alphanum + // side condition: not all numeric + $toplabel = "$an(?:$and*$an)?"; + // hostname = *( domainlabel "." ) toplabel [ "." ] + if (preg_match("/^(?:$domainlabel\.)*($toplabel)\.?$/i", $string, $matches)) { + if (!ctype_digit($matches[1])) { + return $string; + } + } + + // PHP 5.3 and later support this functionality natively + if (function_exists('idn_to_ascii')) { + if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) { + $string = idn_to_ascii($string, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + } else { + $string = idn_to_ascii($string); + } + + // If we have Net_IDNA2 support, we can support IRIs by + // punycoding them. (This is the most portable thing to do, + // since otherwise we have to assume browsers support + } elseif ($config->get('Core.EnableIDNA')) { + $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true)); + // we need to encode each period separately + $parts = explode('.', $string); + try { + $new_parts = array(); + foreach ($parts as $part) { + $encodable = false; + for ($i = 0, $c = strlen($part); $i < $c; $i++) { + if (ord($part[$i]) > 0x7a) { + $encodable = true; + break; + } + } + if (!$encodable) { + $new_parts[] = $part; + } else { + $new_parts[] = $idna->encode($part); + } + } + $string = implode('.', $new_parts); + } catch (Exception $e) { + // XXX error reporting + } + } + // Try again + if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { + return $string; + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrDef/URI/IPv4.php b/library/vendor/HTMLPurifier/AttrDef/URI/IPv4.php new file mode 100644 index 0000000..30ac16c --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/URI/IPv4.php @@ -0,0 +1,45 @@ +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/library/vendor/HTMLPurifier/AttrDef/URI/IPv6.php b/library/vendor/HTMLPurifier/AttrDef/URI/IPv6.php new file mode 100644 index 0000000..f243793 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrDef/URI/IPv6.php @@ -0,0 +1,89 @@ +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/library/vendor/HTMLPurifier/AttrTransform.php b/library/vendor/HTMLPurifier/AttrTransform.php new file mode 100644 index 0000000..b428331 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform.php @@ -0,0 +1,60 @@ +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/library/vendor/HTMLPurifier/AttrTransform/BdoDir.php b/library/vendor/HTMLPurifier/AttrTransform/BdoDir.php new file mode 100644 index 0000000..d66c04a --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/BdoDir.php @@ -0,0 +1,27 @@ +get('Attr.DefaultTextDir'); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrTransform/BgColor.php b/library/vendor/HTMLPurifier/AttrTransform/BgColor.php new file mode 100644 index 0000000..0f51fd2 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/BgColor.php @@ -0,0 +1,28 @@ +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/library/vendor/HTMLPurifier/AttrTransform/BoolToCSS.php b/library/vendor/HTMLPurifier/AttrTransform/BoolToCSS.php new file mode 100644 index 0000000..f25cd01 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/BoolToCSS.php @@ -0,0 +1,47 @@ +attr = $attr; + $this->css = $css; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrTransform/Border.php b/library/vendor/HTMLPurifier/AttrTransform/Border.php new file mode 100644 index 0000000..057dc01 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/Border.php @@ -0,0 +1,26 @@ +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/library/vendor/HTMLPurifier/AttrTransform/EnumToCSS.php b/library/vendor/HTMLPurifier/AttrTransform/EnumToCSS.php new file mode 100644 index 0000000..7ccd0e3 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/EnumToCSS.php @@ -0,0 +1,68 @@ +attr = $attr; + $this->enumToCSS = $enum_to_css; + $this->caseSensitive = (bool)$case_sensitive; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrTransform/ImgRequired.php b/library/vendor/HTMLPurifier/AttrTransform/ImgRequired.php new file mode 100644 index 0000000..235ebb3 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/ImgRequired.php @@ -0,0 +1,47 @@ +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/library/vendor/HTMLPurifier/AttrTransform/ImgSpace.php b/library/vendor/HTMLPurifier/AttrTransform/ImgSpace.php new file mode 100644 index 0000000..350b335 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/ImgSpace.php @@ -0,0 +1,61 @@ + array('left', 'right'), + 'vspace' => array('top', 'bottom') + ); + + /** + * @param string $attr + */ + public function __construct($attr) + { + $this->attr = $attr; + if (!isset($this->css[$attr])) { + trigger_error(htmlspecialchars($attr) . ' is not valid space attribute'); + } + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrTransform/Input.php b/library/vendor/HTMLPurifier/AttrTransform/Input.php new file mode 100644 index 0000000..3ab47ed --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/Input.php @@ -0,0 +1,56 @@ +pixels = new HTMLPurifier_AttrDef_HTML_Pixels(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrTransform/Lang.php b/library/vendor/HTMLPurifier/AttrTransform/Lang.php new file mode 100644 index 0000000..5b0aff0 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/Lang.php @@ -0,0 +1,31 @@ +name = $name; + $this->cssName = $css_name ? $css_name : $name; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrTransform/Name.php b/library/vendor/HTMLPurifier/AttrTransform/Name.php new file mode 100644 index 0000000..63cce68 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/Name.php @@ -0,0 +1,33 @@ +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/library/vendor/HTMLPurifier/AttrTransform/NameSync.php b/library/vendor/HTMLPurifier/AttrTransform/NameSync.php new file mode 100644 index 0000000..5a1fdbb --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/NameSync.php @@ -0,0 +1,46 @@ +idDef = new HTMLPurifier_AttrDef_HTML_ID(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrTransform/Nofollow.php b/library/vendor/HTMLPurifier/AttrTransform/Nofollow.php new file mode 100644 index 0000000..1057ebe --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/Nofollow.php @@ -0,0 +1,52 @@ +parser = new HTMLPurifier_URIParser(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['href'])) { + return $attr; + } + + // XXX Kind of inefficient + $url = $this->parser->parse($attr['href']); + $scheme = $url->getSchemeObj($config, $context); + + if ($scheme->browsable && !$url->isLocal($config, $context)) { + if (isset($attr['rel'])) { + $rels = explode(' ', $attr['rel']); + if (!in_array('nofollow', $rels)) { + $rels[] = 'nofollow'; + } + $attr['rel'] = implode(' ', $rels); + } else { + $attr['rel'] = 'nofollow'; + } + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrTransform/SafeEmbed.php b/library/vendor/HTMLPurifier/AttrTransform/SafeEmbed.php new file mode 100644 index 0000000..231c81a --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/SafeEmbed.php @@ -0,0 +1,25 @@ +uri = new HTMLPurifier_AttrDef_URI(true); // embedded + $this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent')); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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 'allowFullScreen': + if ($config->get('HTML.FlashAllowFullScreen')) { + $attr['value'] = ($attr['value'] == 'true') ? 'true' : 'false'; + } else { + $attr['value'] = 'false'; + } + break; + case 'wmode': + $attr['value'] = $this->wmode->validate($attr['value'], $config, $context); + break; + case 'movie': + case 'src': + $attr['name'] = "movie"; + $attr['value'] = $this->uri->validate($attr['value'], $config, $context); + break; + case 'flashvars': + // we're going to allow arbitrary inputs to the SWF, on + // the reasoning that it could only hack the SWF, not us. + 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/library/vendor/HTMLPurifier/AttrTransform/ScriptRequired.php b/library/vendor/HTMLPurifier/AttrTransform/ScriptRequired.php new file mode 100644 index 0000000..b7057bb --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/ScriptRequired.php @@ -0,0 +1,23 @@ + + */ +class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrTransform/TargetBlank.php b/library/vendor/HTMLPurifier/AttrTransform/TargetBlank.php new file mode 100644 index 0000000..dd63ea8 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/TargetBlank.php @@ -0,0 +1,45 @@ +parser = new HTMLPurifier_URIParser(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['href'])) { + return $attr; + } + + // XXX Kind of inefficient + $url = $this->parser->parse($attr['href']); + $scheme = $url->getSchemeObj($config, $context); + + if ($scheme->browsable && !$url->isBenign($config, $context)) { + $attr['target'] = '_blank'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrTransform/TargetNoopener.php b/library/vendor/HTMLPurifier/AttrTransform/TargetNoopener.php new file mode 100644 index 0000000..1db3c6c --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTransform/TargetNoopener.php @@ -0,0 +1,37 @@ + + */ +class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + 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/library/vendor/HTMLPurifier/AttrTypes.php b/library/vendor/HTMLPurifier/AttrTypes.php new file mode 100644 index 0000000..e4429e8 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrTypes.php @@ -0,0 +1,97 @@ +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(); + $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right'); + $this->info['LAlign'] = self::makeEnum('top,bottom,left,right'); + $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget(); + $this->info['ContentEditable'] = new HTMLPurifier_AttrDef_HTML_ContentEditable(); + + // 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); + } + + private static function makeEnum($in) + { + return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in))); + } + + /** + * Retrieves a type + * @param string $type String type name + * @return HTMLPurifier_AttrDef 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 string $type String type name + * @param HTMLPurifier_AttrDef $impl Object AttrDef for type + */ + public function set($type, $impl) + { + $this->info[$type] = $impl; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/AttrValidator.php b/library/vendor/HTMLPurifier/AttrValidator.php new file mode 100644 index 0000000..f97dc93 --- /dev/null +++ b/library/vendor/HTMLPurifier/AttrValidator.php @@ -0,0 +1,178 @@ +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; + } + + // 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.

    to

    + 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/library/vendor/HTMLPurifier/Bootstrap.php b/library/vendor/HTMLPurifier/Bootstrap.php new file mode 100644 index 0000000..707122b --- /dev/null +++ b/library/vendor/HTMLPurifier/Bootstrap.php @@ -0,0 +1,124 @@ + +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 string $class Class to load + * @return bool + */ + public static function autoload($class) + { + $file = HTMLPurifier_Bootstrap::getPath($class); + if (!$file) { + return false; + } + // Technically speaking, it should be ok and more efficient to + // just do 'require', but Antonio Parraga reports that with + // Zend extensions such as Zend debugger and APC, this invariant + // may be broken. Since we have efficient alternatives, pay + // the cost here and avoid the bug. + require_once HTMLPURIFIER_PREFIX . '/' . $file; + return true; + } + + /** + * Returns the path for a specific class. + * @param string $class Class path to get + * @return string + */ + 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')) { + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + // prepend flag exists, no need for shenanigans + spl_autoload_register($autoload, true, true); + } else { + $buggy = version_compare(PHP_VERSION, '5.2.11', '<'); + $compat = version_compare(PHP_VERSION, '5.1.2', '<=') && + version_compare(PHP_VERSION, '5.1.0', '>='); + foreach ($funcs as $func) { + if ($buggy && 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/library/vendor/HTMLPurifier/CSSDefinition.php b/library/vendor/HTMLPurifier/CSSDefinition.php new file mode 100644 index 0000000..3f08b81 --- /dev/null +++ b/library/vendor/HTMLPurifier/CSSDefinition.php @@ -0,0 +1,549 @@ +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(); + + $this->info['background-size'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'auto', + 'cover', + 'contain', + 'initial', + 'inherit', + ) + ), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $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', 'initial', 'inherit')) + ) + ); + $trusted_min_wh = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(array('initial', 'inherit')) + ) + ); + $trusted_max_wh = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(array('none', 'initial', 'inherit')) + ) + ); + $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['min-width'] = + $this->info['min-height'] = + $max === null ? + $trusted_min_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('initial', 'inherit')) + ) + ), + // For everyone else: + $trusted_min_wh + ); + $this->info['max-width'] = + $this->info['max-height'] = + $max === null ? + $trusted_max_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('none', 'initial', 'inherit')) + ) + ), + // For everyone else: + $trusted_max_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); + + // These CSS properties don't work on many browsers, but we live + // in THE FUTURE! + $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum( + array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line') + ); + + if ($config->get('CSS.Proprietary')) { + $this->doSetupProprietary($config); + } + + if ($config->get('CSS.AllowTricky')) { + $this->doSetupTricky($config); + } + + if ($config->get('CSS.Trusted')) { + $this->doSetupTrusted($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); + } + + /** + * @param HTMLPurifier_Config $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(); + + // vendor specific prefixes of opacity + $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(); + + // more CSS3 + $this->info['page-break-after'] = + $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum( + array( + 'auto', + 'always', + 'avoid', + 'left', + 'right' + ) + ); + $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto', 'avoid')); + + $border_radius = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Percentage(true), // disallow negative + new HTMLPurifier_AttrDef_CSS_Length('0') // disallow negative + )); + + $this->info['border-top-left-radius'] = + $this->info['border-top-right-radius'] = + $this->info['border-bottom-right-radius'] = + $this->info['border-bottom-left-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 2); + // TODO: support SLASH syntax + $this->info['border-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 4); + + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTricky($config) + { + $this->info['display'] = new HTMLPurifier_AttrDef_Enum( + array( + 'inline', + 'block', + 'list-item', + 'run-in', + 'compact', + 'marker', + 'table', + 'inline-block', + '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')); + $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTrusted($config) + { + $this->info['position'] = new HTMLPurifier_AttrDef_Enum( + array('static', 'relative', 'absolute', 'fixed') + ); + $this->info['top'] = + $this->info['left'] = + $this->info['right'] = + $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Integer(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + } + + /** + * Performs extra config-based processing. Based off of + * HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_Config $config + * @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_properties = $config->get('CSS.AllowedProperties'); + if ($allowed_properties !== null) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_properties[$name])) { + unset($this->info[$name]); + } + unset($allowed_properties[$name]); + } + // emit errors + foreach ($allowed_properties 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); + } + } + + $forbidden_properties = $config->get('CSS.ForbiddenProperties'); + if ($forbidden_properties !== null) { + foreach ($this->info as $name => $d) { + if (isset($forbidden_properties[$name])) { + unset($this->info[$name]); + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ChildDef.php b/library/vendor/HTMLPurifier/ChildDef.php new file mode 100644 index 0000000..8eb17b8 --- /dev/null +++ b/library/vendor/HTMLPurifier/ChildDef.php @@ -0,0 +1,52 @@ +elements; + } + + /** + * Validates nodes according to definition and returns modification. + * + * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @param HTMLPurifier_Context $context HTMLPurifier_Context object + * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children + */ + abstract public function validateChildren($children, $config, $context); +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ChildDef/Chameleon.php b/library/vendor/HTMLPurifier/ChildDef/Chameleon.php new file mode 100644 index 0000000..7439be2 --- /dev/null +++ b/library/vendor/HTMLPurifier/ChildDef/Chameleon.php @@ -0,0 +1,67 @@ +inline = new HTMLPurifier_ChildDef_Optional($inline); + $this->block = new HTMLPurifier_ChildDef_Optional($block); + $this->elements = $this->block->elements; + } + + /** + * @param HTMLPurifier_Node[] $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function validateChildren($children, $config, $context) + { + if ($context->get('IsInline') === false) { + return $this->block->validateChildren( + $children, + $config, + $context + ); + } else { + return $this->inline->validateChildren( + $children, + $config, + $context + ); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ChildDef/Custom.php b/library/vendor/HTMLPurifier/ChildDef/Custom.php new file mode 100644 index 0000000..f515888 --- /dev/null +++ b/library/vendor/HTMLPurifier/ChildDef/Custom.php @@ -0,0 +1,102 @@ +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; + } + + /** + * @param HTMLPurifier_Node[] $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function validateChildren($children, $config, $context) + { + $list_of_children = ''; + $nesting = 0; // depth into the nest + foreach ($children as $node) { + if (!empty($node->is_whitespace)) { + continue; + } + $list_of_children .= $node->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/library/vendor/HTMLPurifier/ChildDef/Empty.php b/library/vendor/HTMLPurifier/ChildDef/Empty.php new file mode 100644 index 0000000..a8a6cbd --- /dev/null +++ b/library/vendor/HTMLPurifier/ChildDef/Empty.php @@ -0,0 +1,38 @@ + true, 'ul' => true, 'ol' => true); + + public $whitespace; + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + // Flag for subclasses + $this->whitespace = false; + + // if there are no tokens, delete parent node + if (empty($children)) { + return false; + } + + // if li is not allowed, delete parent node + if (!isset($config->getHTMLDefinition()->info['li'])) { + trigger_error("Cannot allow ul/ol without allowing li", E_USER_WARNING); + return false; + } + + // the new set of children + $result = array(); + + // a little sanity check to make sure it's not ALL whitespace + $all_whitespace = true; + + $current_li = null; + + foreach ($children as $node) { + if (!empty($node->is_whitespace)) { + $result[] = $node; + continue; + } + $all_whitespace = false; // phew, we're not talking about whitespace + + if ($node->name === 'li') { + // good + $current_li = $node; + $result[] = $node; + } else { + // we want to tuck this into the previous li + // Invariant: we expect the node to be ol/ul + // ToDo: Make this more robust in the case of not ol/ul + // by distinguishing between existing li and li created + // to handle non-list elements; non-list elements should + // not be appended to an existing li; only li created + // for non-list. This distinction is not currently made. + if ($current_li === null) { + $current_li = new HTMLPurifier_Node_Element('li'); + $result[] = $current_li; + } + $current_li->children[] = $node; + $current_li->empty = false; // XXX fascinating! Check for this error elsewhere ToDo + } + } + if (empty($result)) { + return false; + } + if ($all_whitespace) { + return false; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ChildDef/Optional.php b/library/vendor/HTMLPurifier/ChildDef/Optional.php new file mode 100644 index 0000000..b946806 --- /dev/null +++ b/library/vendor/HTMLPurifier/ChildDef/Optional.php @@ -0,0 +1,45 @@ +whitespace) { + return $children; + } else { + return array(); + } + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ChildDef/Required.php b/library/vendor/HTMLPurifier/ChildDef/Required.php new file mode 100644 index 0000000..0d1c8f5 --- /dev/null +++ b/library/vendor/HTMLPurifier/ChildDef/Required.php @@ -0,0 +1,118 @@ + $x) { + $elements[$i] = true; + if (empty($i)) { + unset($elements[$i]); + } // remove blank + } + } + $this->elements = $elements; + } + + /** + * @type bool + */ + public $allow_empty = false; + + /** + * @type string + */ + public $type = 'required'; + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + // Flag for subclasses + $this->whitespace = false; + + // if there are no tokens, delete parent node + if (empty($children)) { + return false; + } + + // the new set of children + $result = array(); + + // 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; + + $stack = array_reverse($children); + while (!empty($stack)) { + $node = array_pop($stack); + if (!empty($node->is_whitespace)) { + $result[] = $node; + continue; + } + $all_whitespace = false; // phew, we're not talking about whitespace + + if (!isset($this->elements[$node->name])) { + // special case text + // XXX One of these ought to be redundant or something + if ($pcdata_allowed && $node instanceof HTMLPurifier_Node_Text) { + $result[] = $node; + continue; + } + // spill the child contents in + // ToDo: Make configurable + if ($node instanceof HTMLPurifier_Node_Element) { + for ($i = count($node->children) - 1; $i >= 0; $i--) { + $stack[] = $node->children[$i]; + } + continue; + } + continue; + } + $result[] = $node; + } + if (empty($result)) { + return false; + } + if ($all_whitespace) { + $this->whitespace = true; + return false; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ChildDef/StrictBlockquote.php b/library/vendor/HTMLPurifier/ChildDef/StrictBlockquote.php new file mode 100644 index 0000000..3270a46 --- /dev/null +++ b/library/vendor/HTMLPurifier/ChildDef/StrictBlockquote.php @@ -0,0 +1,110 @@ +init($config); + return $this->fake_elements; + } + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + $this->init($config); + + // trick the parent class into thinking it allows more + $this->elements = $this->fake_elements; + $result = parent::validateChildren($children, $config, $context); + $this->elements = $this->real_elements; + + if ($result === false) { + return array(); + } + if ($result === true) { + $result = $children; + } + + $def = $config->getHTMLDefinition(); + $block_wrap_name = $def->info_block_wrapper; + $block_wrap = false; + $ret = array(); + + foreach ($result as $node) { + if ($block_wrap === false) { + if (($node instanceof HTMLPurifier_Node_Text && !$node->is_whitespace) || + ($node instanceof HTMLPurifier_Node_Element && !isset($this->elements[$node->name]))) { + $block_wrap = new HTMLPurifier_Node_Element($def->info_block_wrapper); + $ret[] = $block_wrap; + } + } else { + if ($node instanceof HTMLPurifier_Node_Element && isset($this->elements[$node->name])) { + $block_wrap = false; + + } + } + if ($block_wrap) { + $block_wrap->children[] = $node; + } else { + $ret[] = $node; + } + } + return $ret; + } + + /** + * @param HTMLPurifier_Config $config + */ + 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/library/vendor/HTMLPurifier/ChildDef/Table.php b/library/vendor/HTMLPurifier/ChildDef/Table.php new file mode 100644 index 0000000..67c7e95 --- /dev/null +++ b/library/vendor/HTMLPurifier/ChildDef/Table.php @@ -0,0 +1,224 @@ + true, + 'tbody' => true, + 'thead' => true, + 'tfoot' => true, + 'caption' => true, + 'colgroup' => true, + 'col' => true + ); + + public function __construct() + { + } + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + if (empty($children)) { + return false; + } + + // only one of these elements is allowed in a table + $caption = false; + $thead = false; + $tfoot = false; + + // whitespace + $initial_ws = array(); + $after_caption_ws = array(); + $after_thead_ws = array(); + $after_tfoot_ws = array(); + + // as many of these as you want + $cols = array(); + $content = array(); + + $tbody_mode = false; // if true, then we need to wrap any stray + // s with a . + + $ws_accum =& $initial_ws; + + foreach ($children as $node) { + if ($node instanceof HTMLPurifier_Node_Comment) { + $ws_accum[] = $node; + continue; + } + switch ($node->name) { + case 'tbody': + $tbody_mode = true; + // fall through + case 'tr': + $content[] = $node; + $ws_accum =& $content; + break; + case 'caption': + // there can only be one caption! + if ($caption !== false) break; + $caption = $node; + $ws_accum =& $after_caption_ws; + break; + case 'thead': + $tbody_mode = true; + // XXX This breaks rendering properties with + // Firefox, which never floats a to + // the top. Ever. (Our scheme will float the + // first to the top.) So maybe + // s that are not first should be + // turned into ? Very tricky, indeed. + if ($thead === false) { + $thead = $node; + $ws_accum =& $after_thead_ws; + } else { + // Oops, there's a second one! What + // should we do? Current behavior is to + // transmutate the first and last entries into + // tbody tags, and then put into content. + // Maybe a better idea is to *attach + // it* to the existing thead or tfoot? + // We don't do this, because Firefox + // doesn't float an extra tfoot to the + // bottom like it does for the first one. + $node->name = 'tbody'; + $content[] = $node; + $ws_accum =& $content; + } + break; + case 'tfoot': + // see above for some aveats + $tbody_mode = true; + if ($tfoot === false) { + $tfoot = $node; + $ws_accum =& $after_tfoot_ws; + } else { + $node->name = 'tbody'; + $content[] = $node; + $ws_accum =& $content; + } + break; + case 'colgroup': + case 'col': + $cols[] = $node; + $ws_accum =& $cols; + break; + case '#PCDATA': + // How is whitespace handled? We treat is as sticky to + // the *end* of the previous element. So all of the + // nonsense we have worked on is to keep things + // together. + if (!empty($node->is_whitespace)) { + $ws_accum[] = $node; + } + break; + } + } + + if (empty($content) && $thead === false && $tfoot === false) { + return false; + } + + $ret = $initial_ws; + if ($caption !== false) { + $ret[] = $caption; + $ret = array_merge($ret, $after_caption_ws); + } + if ($cols !== false) { + $ret = array_merge($ret, $cols); + } + if ($thead !== false) { + $ret[] = $thead; + $ret = array_merge($ret, $after_thead_ws); + } + if ($tfoot !== false) { + $ret[] = $tfoot; + $ret = array_merge($ret, $after_tfoot_ws); + } + + if ($tbody_mode) { + // we have to shuffle tr into tbody + $current_tr_tbody = null; + + foreach($content as $node) { + switch ($node->name) { + case 'tbody': + $current_tr_tbody = null; + $ret[] = $node; + break; + case 'tr': + if ($current_tr_tbody === null) { + $current_tr_tbody = new HTMLPurifier_Node_Element('tbody'); + $ret[] = $current_tr_tbody; + } + $current_tr_tbody->children[] = $node; + break; + case '#PCDATA': + //assert($node->is_whitespace); + if ($current_tr_tbody === null) { + $ret[] = $node; + } else { + $current_tr_tbody->children[] = $node; + } + break; + } + } + } else { + $ret = array_merge($ret, $content); + } + + return $ret; + + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Config.php b/library/vendor/HTMLPurifier/Config.php new file mode 100644 index 0000000..797d268 --- /dev/null +++ b/library/vendor/HTMLPurifier/Config.php @@ -0,0 +1,920 @@ +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 Schema object + * @return HTMLPurifier_Config Configured 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 HTMLPurifier_Config default object. + */ + public static function createDefault() + { + $definition = HTMLPurifier_ConfigSchema::instance(); + $config = new HTMLPurifier_Config($definition); + return $config; + } + + /** + * Retrieves a value from the configuration. + * + * @param string $key String key + * @param mixed $a + * + * @return mixed + */ + 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); + } + + /** + * Retrieves an array of directives to values from a given namespace + * + * @param string $namespace String namespace + * + * @return array + */ + 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 SHA-1 signature of a segment of the configuration object + * that uniquely identifies that particular configuration + * + * @param string $namespace Namespace to get serial for + * + * @return string + * @note Revision is handled specially and is removed from the batch + * before processing! + */ + public function getBatchSerial($namespace) + { + if (empty($this->serials[$namespace])) { + $batch = $this->getBatch($namespace); + unset($batch['DefinitionRev']); + $this->serials[$namespace] = sha1(serialize($batch)); + } + return $this->serials[$namespace]; + } + + /** + * Returns a SHA-1 signature for the entire configuration object + * that uniquely identifies that particular configuration + * + * @return string + */ + public function getSerial() + { + if (empty($this->serial)) { + $this->serial = sha1(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 string $key key + * @param mixed $value value + * @param mixed $a + */ + 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 + * + * @param array $lookup + * + * @return string + */ + private function _listify($lookup) + { + $list = array(); + foreach ($lookup as $name => $b) { + $list[] = $name; + } + return implode(', ', $list); + } + + /** + * Retrieves object reference to the HTML definition. + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawHTMLDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_HTMLDefinition|null + */ + public function getHTMLDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('HTML', $raw, $optimized); + } + + /** + * Retrieves object reference to the CSS definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawCSSDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_CSSDefinition|null + */ + public function getCSSDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('CSS', $raw, $optimized); + } + + /** + * Retrieves object reference to the URI definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawURIDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_URIDefinition|null + */ + public function getURIDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('URI', $raw, $optimized); + } + + /** + * Retrieves a definition + * + * @param string $type Type of definition: HTML, CSS, etc + * @param bool $raw Whether or not definition should be returned raw + * @param bool $optimized Only has an effect when $raw is true. Whether + * or not to return null if the result is already present in + * the cache. This is off by default for backwards + * compatibility reasons, but you need to do things this + * way in order to ensure that caching is done properly. + * Check out enduser-customize.html for more details. + * We probably won't ever change this default, as much as the + * maybe semantics is the "right thing to do." + * + * @throws HTMLPurifier_Exception + * @return HTMLPurifier_Definition|null + */ + public function getDefinition($type, $raw = false, $optimized = false) + { + if ($optimized && !$raw) { + throw new HTMLPurifier_Exception("Cannot set optimized = true when 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) { + // full definition + // --------------- + // check if definition is in memory + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + // check if the definition is setup + if ($def->setup) { + return $def; + } else { + $def->setup($this); + if ($def->optimized) { + $cache->add($def, $this); + } + return $def; + } + } + // check if definition is in cache + $def = $cache->get($this); + if ($def) { + // definition in cache, save to memory and return it + $this->definitions[$type] = $def; + return $def; + } + // initialize it + $def = $this->initDefinition($type); + // set it up + $this->lock = $type; + $def->setup($this); + $this->lock = null; + // save in cache + $cache->add($def, $this); + // return it + return $def; + } else { + // raw definition + // -------------- + // check preconditions + $def = null; + if ($optimized) { + 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" + ); + } + } + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + if ($def->setup && !$optimized) { + $extra = $this->chatty ? + " (try moving this code block earlier in your initialization)" : + ""; + throw new HTMLPurifier_Exception( + "Cannot retrieve raw definition after it has already been setup" . + $extra + ); + } + if ($def->optimized === null) { + $extra = $this->chatty ? " (try flushing your cache)" : ""; + throw new HTMLPurifier_Exception( + "Optimization status of definition is unknown" . $extra + ); + } + if ($def->optimized !== $optimized) { + $msg = $optimized ? "optimized" : "unoptimized"; + $extra = $this->chatty ? + " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" + : ""; + throw new HTMLPurifier_Exception( + "Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra + ); + } + } + // check if definition was in memory + if ($def) { + if ($def->setup) { + // invariant: $optimized === true (checked above) + return null; + } else { + return $def; + } + } + // if optimized, check if definition was in cache + // (because we do the memory check first, this formulation + // is prone to cache slamming, but I think + // guaranteeing that either /all/ of the raw + // setup code or /none/ of it is run is more important.) + if ($optimized) { + // This code path only gets run once; once we put + // something in $definitions (which is guaranteed by the + // trailing code), we always short-circuit above. + $def = $cache->get($this); + if ($def) { + // save the full definition for later, but don't + // return it yet + $this->definitions[$type] = $def; + return null; + } + } + // check invariants for creation + if (!$optimized) { + if (!is_null($this->get($type . '.DefinitionID'))) { + if ($this->chatty) { + $this->triggerError( + 'Due to a documentation error in previous version of HTML Purifier, your ' . + 'definitions are not being cached. If this is OK, you can remove the ' . + '%$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, ' . + 'modify your code to use maybeGetRawDefinition, and test if the returned ' . + 'value is null before making any edits (if it is null, that means that a ' . + 'cached version is available, and no raw operations are necessary). See ' . + '' . + 'Customize for more details', + E_USER_WARNING + ); + } else { + $this->triggerError( + "Useless DefinitionID declaration", + E_USER_WARNING + ); + } + } + } + // initialize it + $def = $this->initDefinition($type); + $def->optimized = $optimized; + return $def; + } + throw new HTMLPurifier_Exception("The impossible happened!"); + } + + /** + * Initialise definition + * + * @param string $type What type of definition to create + * + * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition + * @throws HTMLPurifier_Exception + */ + private function initDefinition($type) + { + // quick checks failed, let's create the object + if ($type == 'HTML') { + $def = new HTMLPurifier_HTMLDefinition(); + } elseif ($type == 'CSS') { + $def = new HTMLPurifier_CSSDefinition(); + } elseif ($type == 'URI') { + $def = new HTMLPurifier_URIDefinition(); + } else { + throw new HTMLPurifier_Exception( + "Definition of $type type not supported" + ); + } + $this->definitions[$type] = $def; + return $def; + } + + public function maybeGetRawDefinition($name) + { + return $this->getDefinition($name, true, true); + } + + /** + * @return HTMLPurifier_HTMLDefinition|null + */ + public function maybeGetRawHTMLDefinition() + { + return $this->getDefinition('HTML', true, true); + } + + /** + * @return HTMLPurifier_CSSDefinition|null + */ + public function maybeGetRawCSSDefinition() + { + return $this->getDefinition('CSS', true, true); + } + + /** + * @return HTMLPurifier_URIDefinition|null + */ + public function maybeGetRawURIDefinition() + { + return $this->getDefinition('URI', true, true); + } + + /** + * Loads configuration values from an array with the following structure: + * Namespace.Directive => Value + * + * @param array $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 => $value2) { + $this->set($namespace .'.'. $directive, $value2); + } + } + } + } + + /** + * 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 array $allowed List of allowed namespaces/directives + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + 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 $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return mixed + */ + 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. + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + */ + 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 + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + 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 && version_compare(PHP_VERSION, '7.4.0', '<') && 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 string $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 string|bool $error String error message, or false for no error + * + * @return bool + */ + 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; + $this->parser = null; + } + + /** + * Produces a nicely formatted error message by supplying the + * stack frame information OUTSIDE of HTMLPurifier_Config. + * + * @param string $msg An error message + * @param int $no An error number + */ + protected function triggerError($msg, $no) + { + // determine previous stack frame + $extra = ''; + if ($this->chatty) { + $trace = debug_backtrace(); + // zip(tail(trace), trace) -- but PHP is not Haskell har har + for ($i = 0, $c = count($trace); $i < $c - 1; $i++) { + // XXX this is not correct on some versions of HTML Purifier + if (isset($trace[$i + 1]['class']) && $trace[$i + 1]['class'] === 'HTMLPurifier_Config') { + continue; + } + $frame = $trace[$i]; + $extra = " invoked on line {$frame['line']} in file {$frame['file']}"; + break; + } + } + trigger_error($msg . $extra, $no); + } + + /** + * Returns a serialized form of the configuration object that can + * be reconstituted. + * + * @return string + */ + public function serialize() + { + $this->getDefinition('HTML'); + $this->getDefinition('CSS'); + $this->getDefinition('URI'); + return serialize($this); + } + +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema.php b/library/vendor/HTMLPurifier/ConfigSchema.php new file mode 100644 index 0000000..c3fe8cd --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema.php @@ -0,0 +1,176 @@ + 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. + * @type array + */ + public $info = array(); + + /** + * Application-wide singleton + * @type HTMLPurifier_ConfigSchema + */ + protected static $singleton; + + public function __construct() + { + $this->defaultPlist = new HTMLPurifier_PropertyList(); + } + + /** + * Unserializes the default ConfigSchema. + * @return HTMLPurifier_ConfigSchema + */ + public static function makeFromSerial() + { + $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'); + $r = unserialize($contents); + if (!$r) { + $hash = sha1($contents); + trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR); + } + return $r; + } + + /** + * Retrieves an instance of the application-wide configuration definition. + * @param HTMLPurifier_ConfigSchema $prototype + * @return HTMLPurifier_ConfigSchema + */ + 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 string $key Name of directive + * @param mixed $default Default value of directive + * @param string $type Allowed type of the directive. See + * HTMLPurifier_VarParser::$types for allowed values + * @param bool $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 string $key Name of Directive + * @param array $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 string $key Name of directive + * @param array $allowed Lookup array of allowed values + */ + public function addAllowedValues($key, $allowed) + { + $this->info[$key]->allowed = $allowed; + } + + /** + * Defines a directive alias for backwards compatibility + * @param string $key Directive that will be aliased + * @param string $new_key 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/library/vendor/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/library/vendor/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php new file mode 100644 index 0000000..d5906cd --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php @@ -0,0 +1,48 @@ +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/library/vendor/HTMLPurifier/ConfigSchema/Builder/Xml.php b/library/vendor/HTMLPurifier/ConfigSchema/Builder/Xml.php new file mode 100644 index 0000000..5fa56f7 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/Builder/Xml.php @@ -0,0 +1,144 @@ +startElement('div'); + + $purifier = HTMLPurifier::getInstance(); + $html = $purifier->purify($html); + $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $this->writeRaw($html); + + $this->endElement(); // div + } + + /** + * @param mixed $var + * @return string + */ + protected function export($var) + { + if ($var === array()) { + return 'array()'; + } + return var_export($var, true); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + */ + 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(); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + */ + 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/library/vendor/HTMLPurifier/ConfigSchema/Exception.php b/library/vendor/HTMLPurifier/ConfigSchema/Exception.php new file mode 100644 index 0000000..2671516 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/Exception.php @@ -0,0 +1,11 @@ + array(directive info) + * @type HTMLPurifier_ConfigSchema_Interchange_Directive[] + */ + public $directives = array(); + + /** + * Adds a directive array to $directives + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + * @throws HTMLPurifier_ConfigSchema_Exception + */ + 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/library/vendor/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/library/vendor/HTMLPurifier/ConfigSchema/Interchange/Directive.php new file mode 100644 index 0000000..127a39a --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/Interchange/Directive.php @@ -0,0 +1,89 @@ + true). + * Null if all values are allowed. + * @type array + */ + public $allowed; + + /** + * List of aliases for the directive. + * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). + * @type HTMLPurifier_ConfigSchema_Interchange_Id[] + */ + public $aliases = array(); + + /** + * Hash of value aliases, e.g. array('alt' => 'real'). Null if value + * aliasing is disabled (necessary for non-scalar types). + * @type array + */ + public $valueAliases; + + /** + * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. + * Null if the directive has always existed. + * @type string + */ + public $version; + + /** + * ID of directive that supercedes this old directive. + * Null if not deprecated. + * @type HTMLPurifier_ConfigSchema_Interchange_Id + */ + public $deprecatedUse; + + /** + * Version of HTML Purifier this directive was deprecated. Null if not + * deprecated. + * @type string + */ + public $deprecatedVersion; + + /** + * List of external projects this directive depends on, e.g. array('CSSTidy'). + * @type array + */ + public $external = array(); +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/Interchange/Id.php b/library/vendor/HTMLPurifier/ConfigSchema/Interchange/Id.php new file mode 100644 index 0000000..126f09d --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/Interchange/Id.php @@ -0,0 +1,58 @@ +key = $key; + } + + /** + * @return string + * @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; + } + + /** + * @return string + */ + public function getRootNamespace() + { + return substr($this->key, 0, strpos($this->key, ".")); + } + + /** + * @return string + */ + public function getDirective() + { + return substr($this->key, strpos($this->key, ".") + 1); + } + + /** + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + public static function make($id) + { + return new HTMLPurifier_ConfigSchema_Interchange_Id($id); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/library/vendor/HTMLPurifier/ConfigSchema/InterchangeBuilder.php new file mode 100644 index 0000000..655e6dd --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/InterchangeBuilder.php @@ -0,0 +1,226 @@ +varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); + } + + /** + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public static function buildFromDirectory($dir = null) + { + $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); + $interchange = new HTMLPurifier_ConfigSchema_Interchange(); + return $builder->buildDir($interchange, $dir); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + 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; + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $file + */ + 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 HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build + * @param HTMLPurifier_StringHash $hash source data + * @throws HTMLPurifier_ConfigSchema_Exception + */ + 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); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param HTMLPurifier_StringHash $hash + * @throws HTMLPurifier_ConfigSchema_Exception + */ + 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 + * @param string $contents + */ + protected function evalArray($contents) + { + return eval('return array(' . $contents . ');'); + } + + /** + * Converts an array list into a lookup array. + * @param array $array + * @return 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. + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_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 HTMLPurifier_StringHash $hash Hash 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/library/vendor/HTMLPurifier/ConfigSchema/Validator.php b/library/vendor/HTMLPurifier/ConfigSchema/Validator.php new file mode 100644 index 0000000..fb31277 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/Validator.php @@ -0,0 +1,248 @@ +parser = new HTMLPurifier_VarParser(); + } + + /** + * Validates a fully-formed interchange object. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @return bool + */ + 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. + * @param HTMLPurifier_ConfigSchema_Interchange_Id $id + */ + 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. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + 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. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + 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. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + 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. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + 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. + * @param $obj + * @param $member + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + protected function with($obj, $member) + { + return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); + } + + /** + * Emits an error, providing helpful context. + * @throws HTMLPurifier_ConfigSchema_Exception + */ + 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. + * @return string + */ + protected function getFormattedContext() + { + return implode(' in ', array_reverse($this->context)); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/library/vendor/HTMLPurifier/ConfigSchema/ValidatorAtom.php new file mode 100644 index 0000000..c9aa364 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/ValidatorAtom.php @@ -0,0 +1,130 @@ +context = $context; + $this->obj = $obj; + $this->member = $member; + $this->contents =& $obj->$member; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsString() + { + if (!is_string($this->contents)) { + $this->error('must be a string'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsBool() + { + if (!is_bool($this->contents)) { + $this->error('must be a boolean'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsArray() + { + if (!is_array($this->contents)) { + $this->error('must be an array'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotNull() + { + if ($this->contents === null) { + $this->error('must not be null'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertAlnum() + { + $this->assertIsString(); + if (!ctype_alnum($this->contents)) { + $this->error('must be alphanumeric'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotEmpty() + { + if (empty($this->contents)) { + $this->error('must not be empty'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsLookup() + { + $this->assertIsArray(); + foreach ($this->contents as $v) { + if ($v !== true) { + $this->error('must be a lookup array'); + } + } + return $this; + } + + /** + * @param string $msg + * @throws HTMLPurifier_ConfigSchema_Exception + */ + 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/library/vendor/HTMLPurifier/ConfigSchema/schema.ser b/library/vendor/HTMLPurifier/ConfigSchema/schema.ser new file mode 100644 index 0000000..a5426c7 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema.ser @@ -0,0 +1 @@ +O:25:"HTMLPurifier_ConfigSchema":3:{s:8:"defaults";a:127:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";i:1200;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:20:"URI.SafeIframeRegexp";N;}s:12:"defaultPlist";O:25:"HTMLPurifier_PropertyList":3:{s:7:"*data";a:127:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";i:1200;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:20:"URI.SafeIframeRegexp";N;}s:9:"*parent";N;s:8:"*cache";N;}s:4:"info";a:140:{s:19:"Attr.AllowedClasses";i:-8;s:24:"Attr.AllowedFrameTargets";i:8;s:15:"Attr.AllowedRel";i:8;s:15:"Attr.AllowedRev";i:8;s:18:"Attr.ClassUseCDATA";i:-7;s:20:"Attr.DefaultImageAlt";i:-1;s:24:"Attr.DefaultInvalidImage";i:1;s:27:"Attr.DefaultInvalidImageAlt";i:1;s:19:"Attr.DefaultTextDir";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:2:{s:3:"ltr";b:1;s:3:"rtl";b:1;}}s:13:"Attr.EnableID";i:7;s:17:"HTML.EnableAttrID";O:8:"stdClass":2:{s:3:"key";s:13:"Attr.EnableID";s:7:"isAlias";b:1;}s:21:"Attr.ForbiddenClasses";i:8;s:13:"Attr.ID.HTML5";i:-7;s:16:"Attr.IDBlacklist";i:9;s:22:"Attr.IDBlacklistRegexp";i:-1;s:13:"Attr.IDPrefix";i:1;s:18:"Attr.IDPrefixLocal";i:1;s:24:"AutoFormat.AutoParagraph";i:7;s:17:"AutoFormat.Custom";i:9;s:25:"AutoFormat.DisplayLinkURI";i:7;s:18:"AutoFormat.Linkify";i:7;s:33:"AutoFormat.PurifierLinkify.DocURL";i:1;s:37:"AutoFormatParam.PurifierLinkifyDocURL";O:8:"stdClass":2:{s:3:"key";s:33:"AutoFormat.PurifierLinkify.DocURL";s:7:"isAlias";b:1;}s:26:"AutoFormat.PurifierLinkify";i:7;s:32:"AutoFormat.RemoveEmpty.Predicate";i:10;s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";i:8;s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";i:7;s:22:"AutoFormat.RemoveEmpty";i:7;s:39:"AutoFormat.RemoveSpansWithoutAttributes";i:7;s:19:"CSS.AllowDuplicates";i:7;s:18:"CSS.AllowImportant";i:7;s:15:"CSS.AllowTricky";i:7;s:16:"CSS.AllowedFonts";i:-8;s:21:"CSS.AllowedProperties";i:-8;s:17:"CSS.DefinitionRev";i:5;s:23:"CSS.ForbiddenProperties";i:8;s:16:"CSS.MaxImgLength";i:-1;s:15:"CSS.Proprietary";i:7;s:11:"CSS.Trusted";i:7;s:20:"Cache.DefinitionImpl";i:-1;s:20:"Core.DefinitionCache";O:8:"stdClass":2:{s:3:"key";s:20:"Cache.DefinitionImpl";s:7:"isAlias";b:1;}s:20:"Cache.SerializerPath";i:-1;s:27:"Cache.SerializerPermissions";i:-5;s:22:"Core.AggressivelyFixLt";i:7;s:29:"Core.AggressivelyRemoveScript";i:7;s:28:"Core.AllowHostnameUnderscore";i:7;s:23:"Core.AllowParseManyTags";i:7;s:18:"Core.CollectErrors";i:7;s:18:"Core.ColorKeywords";i:10;s:30:"Core.ConvertDocumentToFragment";i:7;s:24:"Core.AcceptFullDocuments";O:8:"stdClass":2:{s:3:"key";s:30:"Core.ConvertDocumentToFragment";s:7:"isAlias";b:1;}s:36:"Core.DirectLexLineNumberSyncInterval";i:5;s:20:"Core.DisableExcludes";i:7;s:15:"Core.EnableIDNA";i:7;s:13:"Core.Encoding";i:2;s:26:"Core.EscapeInvalidChildren";i:7;s:22:"Core.EscapeInvalidTags";i:7;s:29:"Core.EscapeNonASCIICharacters";i:7;s:19:"Core.HiddenElements";i:8;s:13:"Core.Language";i:1;s:24:"Core.LegacyEntityDecoder";i:7;s:14:"Core.LexerImpl";i:-11;s:24:"Core.MaintainLineNumbers";i:-7;s:22:"Core.NormalizeNewlines";i:7;s:21:"Core.RemoveInvalidImg";i:7;s:33:"Core.RemoveProcessingInstructions";i:7;s:25:"Core.RemoveScriptContents";i:-7;s:13:"Filter.Custom";i:9;s:34:"Filter.ExtractStyleBlocks.Escaping";i:7;s:33:"Filter.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:38:"FilterParam.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:31:"Filter.ExtractStyleBlocks.Scope";i:-1;s:30:"Filter.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:35:"FilterParam.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:34:"Filter.ExtractStyleBlocks.TidyImpl";i:-11;s:38:"FilterParam.ExtractStyleBlocksTidyImpl";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.TidyImpl";s:7:"isAlias";b:1;}s:25:"Filter.ExtractStyleBlocks";i:7;s:14:"Filter.YouTube";i:7;s:12:"HTML.Allowed";i:-4;s:22:"HTML.AllowedAttributes";i:-8;s:20:"HTML.AllowedComments";i:8;s:26:"HTML.AllowedCommentsRegexp";i:-1;s:20:"HTML.AllowedElements";i:-8;s:19:"HTML.AllowedModules";i:-8;s:23:"HTML.Attr.Name.UseCDATA";i:7;s:17:"HTML.BlockWrapper";i:1;s:16:"HTML.CoreModules";i:8;s:18:"HTML.CustomDoctype";i:-1;s:17:"HTML.DefinitionID";i:-1;s:18:"HTML.DefinitionRev";i:5;s:12:"HTML.Doctype";O:8:"stdClass":3:{s:4:"type";i:1;s:10:"allow_null";b:1;s:7:"allowed";a:5:{s:22:"HTML 4.01 Transitional";b:1;s:16:"HTML 4.01 Strict";b:1;s:22:"XHTML 1.0 Transitional";b:1;s:16:"XHTML 1.0 Strict";b:1;s:9:"XHTML 1.1";b:1;}}s:25:"HTML.FlashAllowFullScreen";i:7;s:24:"HTML.ForbiddenAttributes";i:8;s:22:"HTML.ForbiddenElements";i:8;s:10:"HTML.Forms";i:7;s:17:"HTML.MaxImgLength";i:-5;s:13:"HTML.Nofollow";i:7;s:11:"HTML.Parent";i:1;s:16:"HTML.Proprietary";i:7;s:14:"HTML.SafeEmbed";i:7;s:15:"HTML.SafeIframe";i:7;s:15:"HTML.SafeObject";i:7;s:18:"HTML.SafeScripting";i:8;s:11:"HTML.Strict";i:7;s:16:"HTML.TargetBlank";i:7;s:19:"HTML.TargetNoopener";i:7;s:21:"HTML.TargetNoreferrer";i:7;s:12:"HTML.TidyAdd";i:8;s:14:"HTML.TidyLevel";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:4:{s:4:"none";b:1;s:5:"light";b:1;s:6:"medium";b:1;s:5:"heavy";b:1;}}s:15:"HTML.TidyRemove";i:8;s:12:"HTML.Trusted";i:7;s:10:"HTML.XHTML";i:7;s:10:"Core.XHTML";O:8:"stdClass":2:{s:3:"key";s:10:"HTML.XHTML";s:7:"isAlias";b:1;}s:28:"Output.CommentScriptContents";i:7;s:26:"Core.CommentScriptContents";O:8:"stdClass":2:{s:3:"key";s:28:"Output.CommentScriptContents";s:7:"isAlias";b:1;}s:19:"Output.FixInnerHTML";i:7;s:18:"Output.FlashCompat";i:7;s:14:"Output.Newline";i:-1;s:15:"Output.SortAttr";i:7;s:17:"Output.TidyFormat";i:7;s:15:"Core.TidyFormat";O:8:"stdClass":2:{s:3:"key";s:17:"Output.TidyFormat";s:7:"isAlias";b:1;}s:17:"Test.ForceNoIconv";i:7;s:18:"URI.AllowedSchemes";i:8;s:8:"URI.Base";i:-1;s:17:"URI.DefaultScheme";i:-1;s:16:"URI.DefinitionID";i:-1;s:17:"URI.DefinitionRev";i:5;s:11:"URI.Disable";i:7;s:15:"Attr.DisableURI";O:8:"stdClass":2:{s:3:"key";s:11:"URI.Disable";s:7:"isAlias";b:1;}s:19:"URI.DisableExternal";i:7;s:28:"URI.DisableExternalResources";i:7;s:20:"URI.DisableResources";i:7;s:8:"URI.Host";i:-1;s:17:"URI.HostBlacklist";i:9;s:16:"URI.MakeAbsolute";i:7;s:9:"URI.Munge";i:-1;s:18:"URI.MungeResources";i:7;s:18:"URI.MungeSecretKey";i:-1;s:26:"URI.OverrideAllowedSchemes";i:7;s:20:"URI.SafeIframeRegexp";i:-1;}} \ No newline at end of file diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt new file mode 100644 index 0000000..0517fed --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt new file mode 100644 index 0000000..249edd6 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt new file mode 100644 index 0000000..9a8fa6a --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt new file mode 100644 index 0000000..b017883 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt new file mode 100644 index 0000000..e774b82 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt new file mode 100644 index 0000000..533165e --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt new file mode 100644 index 0000000..9eb7e38 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt new file mode 100644 index 0000000..2f17bf4 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt new file mode 100644 index 0000000..52654b5 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt new file mode 100644 index 0000000..6440d21 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt new file mode 100644 index 0000000..f31d226 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt new file mode 100644 index 0000000..735d4b7 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt @@ -0,0 +1,10 @@ +Attr.ID.HTML5 +TYPE: bool/null +DEFAULT: null +VERSION: 4.8.0 +--DESCRIPTION-- +In HTML5, restrictions on the format of the id attribute have been significantly +relaxed, such that any string is valid so long as it contains no spaces and +is at least one character. In lieu of a general HTML5 compatibility flag, +set this configuration directive to true to use the relaxed rules. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt new file mode 100644 index 0000000..5f2b5e3 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt new file mode 100644 index 0000000..6f58245 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt new file mode 100644 index 0000000..cc49d43 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt new file mode 100644 index 0000000..2c5924a --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt new file mode 100644 index 0000000..d5caa1b --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt @@ -0,0 +1,31 @@ +AutoFormat.AutoParagraph +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

    + This directive turns on auto-paragraphing, where double newlines are + converted in to paragraphs whenever possible. Auto-paragraphing: +

    +
      +
    • Always applies to inline elements or text in the root node,
    • +
    • Applies to inline elements or text with double newlines in nodes + that allow paragraph tags,
    • +
    • Applies to double newlines in paragraph tags
    • +
    +

    + p tags must be allowed for this directive to take effect. + We do not use br tags for paragraphing, as that is + semantically incorrect. +

    +

    + 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 pre, so this should not be difficult.) To prevent + the paragraphing of inline text adjacent to block elements, wrap them + in div tags (the behavior is slightly different outside of + the root node.) +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt new file mode 100644 index 0000000..2a47648 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt @@ -0,0 +1,12 @@ +AutoFormat.Custom +TYPE: list +VERSION: 2.0.1 +DEFAULT: array() +--DESCRIPTION-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt new file mode 100644 index 0000000..663064a --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt @@ -0,0 +1,11 @@ +AutoFormat.DisplayLinkURI +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

    + This directive turns on the in-text display of URIs in <a> tags, and disables + those links. For example, example becomes + example (http://example.com). +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt new file mode 100644 index 0000000..3a48ba9 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt @@ -0,0 +1,12 @@ +AutoFormat.Linkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

    + This directive turns on linkification, auto-linking http, ftp and + https URLs. a tags with the href attribute + must be allowed. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt new file mode 100644 index 0000000..db58b13 --- /dev/null +++ b/library/vendor/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-- +

    + Location of configuration documentation to link to, let %s substitute + into the configuration's namespace and directive names sans the percent + sign. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt new file mode 100644 index 0000000..7996488 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt @@ -0,0 +1,12 @@ +AutoFormat.PurifierLinkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

    + Internal auto-formatter that converts configuration directives in + syntax %Namespace.Directive to links. a tags + with the href attribute must be allowed. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt new file mode 100644 index 0000000..6367fe2 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt @@ -0,0 +1,14 @@ +AutoFormat.RemoveEmpty.Predicate +TYPE: hash +VERSION: 4.7.0 +DEFAULT: array('colgroup' => array(), 'th' => array(), 'td' => array(), 'iframe' => array('src')) +--DESCRIPTION-- +

    + Given that an element has no contents, it will be removed by default, unless + this predicate dictates otherwise. The predicate can either be an associative + map from tag name to list of attributes that must be present for the element + to be considered preserved: thus, the default always preserves colgroup, + th and td, and also iframe if it + has a src. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt new file mode 100644 index 0000000..35c393b --- /dev/null +++ b/library/vendor/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-- +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt new file mode 100644 index 0000000..9228dee --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt @@ -0,0 +1,15 @@ +AutoFormat.RemoveEmpty.RemoveNbsp +TYPE: bool +VERSION: 4.0.0 +DEFAULT: false +--DESCRIPTION-- +

    + 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 %AutoFormat.RemoveEmpty is enabled. +

    +

    + See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements + that don't have this behavior applied to them. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt new file mode 100644 index 0000000..34657ba --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt @@ -0,0 +1,46 @@ +AutoFormat.RemoveEmpty +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

    + 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: +

    +
    • + Tags with no attributes and no content, and that are not empty + elements (remove <a></a> but not + <br />), and +
    • +
    • + Tags with no content, except for:
        +
      • The colgroup element, or
      • +
      • + Elements with the id or name attribute, + when those attributes are permitted on those elements. +
      • +
    • +
    +

    + 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. +

    +

    + 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. +

    +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt new file mode 100644 index 0000000..dde990a --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt @@ -0,0 +1,11 @@ +AutoFormat.RemoveSpansWithoutAttributes +TYPE: bool +VERSION: 4.0.1 +DEFAULT: false +--DESCRIPTION-- +

    + This directive causes span tags without any attributes + to be removed. It will also remove spans that had all attributes + removed during processing. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt new file mode 100644 index 0000000..4d054b1 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt @@ -0,0 +1,11 @@ +CSS.AllowDuplicates +TYPE: bool +DEFAULT: false +VERSION: 4.8.0 +--DESCRIPTION-- +

    + By default, HTML Purifier removes duplicate CSS properties, + like color:red; color:blue. If this is set to + true, duplicate properties are allowed. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt new file mode 100644 index 0000000..b324608 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt new file mode 100644 index 0000000..748be0e --- /dev/null +++ b/library/vendor/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, display:none; 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/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt new file mode 100644 index 0000000..3fd4654 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt @@ -0,0 +1,12 @@ +CSS.AllowedFonts +TYPE: lookup/null +VERSION: 4.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

    + Allows you to manually specify a set of allowed fonts. If + NULL, all fonts are allowed. This directive + affects generic names (serif, sans-serif, monospace, cursive, + fantasy) as well as specific font families. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt new file mode 100644 index 0000000..460112e --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt @@ -0,0 +1,18 @@ +CSS.AllowedProperties +TYPE: lookup/null +VERSION: 3.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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. +

    +

    + Warning: If another directive conflicts with the + elements here, that directive will win and override. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt new file mode 100644 index 0000000..5cb7dda --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt @@ -0,0 +1,11 @@ +CSS.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

    + Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt new file mode 100644 index 0000000..f1f5c5f --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt @@ -0,0 +1,13 @@ +CSS.ForbiddenProperties +TYPE: lookup +VERSION: 4.2.0 +DEFAULT: array() +--DESCRIPTION-- +

    + This is the logical inverse of %CSS.AllowedProperties, and it will + override that directive or any other directive. If possible, + %CSS.AllowedProperties is recommended over this directive, + because it can sometimes be difficult to tell whether or not you've + forbidden all of the CSS properties you truly would like to disallow. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt new file mode 100644 index 0000000..7a32914 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt @@ -0,0 +1,16 @@ +CSS.MaxImgLength +TYPE: string/null +DEFAULT: '1200px' +VERSION: 3.1.1 +--DESCRIPTION-- +

    + This parameter sets the maximum allowed length on img tags, + effectively the width and height 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). +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt new file mode 100644 index 0000000..148eedb --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt @@ -0,0 +1,10 @@ +CSS.Proprietary +TYPE: bool +VERSION: 3.0.0 +DEFAULT: false +--DESCRIPTION-- + +

    + Whether or not to allow safe, proprietary CSS values. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt new file mode 100644 index 0000000..e733a61 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt @@ -0,0 +1,9 @@ +CSS.Trusted +TYPE: bool +VERSION: 4.2.1 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user's CSS input is trusted or not. If the +input is trusted, a more expansive set of allowed properties. See +also %HTML.Trusted. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt new file mode 100644 index 0000000..c486724 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt new file mode 100644 index 0000000..5403650 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt @@ -0,0 +1,13 @@ +Cache.SerializerPath +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt new file mode 100644 index 0000000..2e0cc81 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt @@ -0,0 +1,16 @@ +Cache.SerializerPermissions +TYPE: int/null +VERSION: 4.3.0 +DEFAULT: 0755 +--DESCRIPTION-- + +

    + Directory permissions of the files and directories created inside + the DefinitionCache/Serializer or other custom serializer path. +

    +

    + In HTML Purifier 4.8.0, this also supports NULL, + which means that no chmod'ing or directory creation shall + occur. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt new file mode 100644 index 0000000..568cbf3 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt @@ -0,0 +1,18 @@ +Core.AggressivelyFixLt +TYPE: bool +VERSION: 2.1.0 +DEFAULT: true +--DESCRIPTION-- +

    + 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. +

    +

    + Notice: This directive's default turned from false to true + in HTML Purifier 3.2.0. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt new file mode 100644 index 0000000..b2b6ab1 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt @@ -0,0 +1,16 @@ +Core.AggressivelyRemoveScript +TYPE: bool +VERSION: 4.9.0 +DEFAULT: true +--DESCRIPTION-- +

    + This directive enables aggressive pre-filter removal of + script tags. This is not necessary for security, + but it can help work around a bug in libxml where embedded + HTML elements inside script sections cause the parser to + choke. To revert to pre-4.9.0 behavior, set this to false. + This directive has no effect if %Core.Trusted is true, + %Core.RemoveScriptContents is false, or %Core.HiddenElements + does not contain script. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt new file mode 100644 index 0000000..2c910cc --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt @@ -0,0 +1,16 @@ +Core.AllowHostnameUnderscore +TYPE: bool +VERSION: 4.6.0 +DEFAULT: false +--DESCRIPTION-- +

    + By RFC 1123, underscores are not permitted in host names. + (This is in contrast to the specification for DNS, RFC + 2181, which allows underscores.) + However, most browsers do the right thing when faced with + an underscore in the host name, and so some poorly written + websites are written with the expectation this should work. + Setting this parameter to true relaxes our allowed character + check so that underscores are permitted. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt new file mode 100644 index 0000000..06278f8 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt @@ -0,0 +1,12 @@ +Core.AllowParseManyTags +TYPE: bool +DEFAULT: false +VERSION: 4.10.1 +--DESCRIPTION-- +

    + This directive allows parsing of many nested tags. + If you set true, relaxes any hardcoded limit from the parser. + However, in that case it may cause a Dos attack. + Be careful when enabling it. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt new file mode 100644 index 0000000..d731791 --- /dev/null +++ b/library/vendor/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. Warning: +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/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt new file mode 100644 index 0000000..a75844c --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt @@ -0,0 +1,160 @@ +Core.ColorKeywords +TYPE: hash +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'aliceblue' => '#F0F8FF', + 'antiquewhite' => '#FAEBD7', + 'aqua' => '#00FFFF', + 'aquamarine' => '#7FFFD4', + 'azure' => '#F0FFFF', + 'beige' => '#F5F5DC', + 'bisque' => '#FFE4C4', + 'black' => '#000000', + 'blanchedalmond' => '#FFEBCD', + 'blue' => '#0000FF', + 'blueviolet' => '#8A2BE2', + 'brown' => '#A52A2A', + 'burlywood' => '#DEB887', + 'cadetblue' => '#5F9EA0', + 'chartreuse' => '#7FFF00', + 'chocolate' => '#D2691E', + 'coral' => '#FF7F50', + 'cornflowerblue' => '#6495ED', + 'cornsilk' => '#FFF8DC', + 'crimson' => '#DC143C', + 'cyan' => '#00FFFF', + 'darkblue' => '#00008B', + 'darkcyan' => '#008B8B', + 'darkgoldenrod' => '#B8860B', + 'darkgray' => '#A9A9A9', + 'darkgrey' => '#A9A9A9', + 'darkgreen' => '#006400', + 'darkkhaki' => '#BDB76B', + 'darkmagenta' => '#8B008B', + 'darkolivegreen' => '#556B2F', + 'darkorange' => '#FF8C00', + 'darkorchid' => '#9932CC', + 'darkred' => '#8B0000', + 'darksalmon' => '#E9967A', + 'darkseagreen' => '#8FBC8F', + 'darkslateblue' => '#483D8B', + 'darkslategray' => '#2F4F4F', + 'darkslategrey' => '#2F4F4F', + 'darkturquoise' => '#00CED1', + 'darkviolet' => '#9400D3', + 'deeppink' => '#FF1493', + 'deepskyblue' => '#00BFFF', + 'dimgray' => '#696969', + 'dimgrey' => '#696969', + 'dodgerblue' => '#1E90FF', + 'firebrick' => '#B22222', + 'floralwhite' => '#FFFAF0', + 'forestgreen' => '#228B22', + 'fuchsia' => '#FF00FF', + 'gainsboro' => '#DCDCDC', + 'ghostwhite' => '#F8F8FF', + 'gold' => '#FFD700', + 'goldenrod' => '#DAA520', + 'gray' => '#808080', + 'grey' => '#808080', + 'green' => '#008000', + 'greenyellow' => '#ADFF2F', + 'honeydew' => '#F0FFF0', + 'hotpink' => '#FF69B4', + 'indianred' => '#CD5C5C', + 'indigo' => '#4B0082', + 'ivory' => '#FFFFF0', + 'khaki' => '#F0E68C', + 'lavender' => '#E6E6FA', + 'lavenderblush' => '#FFF0F5', + 'lawngreen' => '#7CFC00', + 'lemonchiffon' => '#FFFACD', + 'lightblue' => '#ADD8E6', + 'lightcoral' => '#F08080', + 'lightcyan' => '#E0FFFF', + 'lightgoldenrodyellow' => '#FAFAD2', + 'lightgray' => '#D3D3D3', + 'lightgrey' => '#D3D3D3', + 'lightgreen' => '#90EE90', + 'lightpink' => '#FFB6C1', + 'lightsalmon' => '#FFA07A', + 'lightseagreen' => '#20B2AA', + 'lightskyblue' => '#87CEFA', + 'lightslategray' => '#778899', + 'lightslategrey' => '#778899', + 'lightsteelblue' => '#B0C4DE', + 'lightyellow' => '#FFFFE0', + 'lime' => '#00FF00', + 'limegreen' => '#32CD32', + 'linen' => '#FAF0E6', + 'magenta' => '#FF00FF', + 'maroon' => '#800000', + 'mediumaquamarine' => '#66CDAA', + 'mediumblue' => '#0000CD', + 'mediumorchid' => '#BA55D3', + 'mediumpurple' => '#9370DB', + 'mediumseagreen' => '#3CB371', + 'mediumslateblue' => '#7B68EE', + 'mediumspringgreen' => '#00FA9A', + 'mediumturquoise' => '#48D1CC', + 'mediumvioletred' => '#C71585', + 'midnightblue' => '#191970', + 'mintcream' => '#F5FFFA', + 'mistyrose' => '#FFE4E1', + 'moccasin' => '#FFE4B5', + 'navajowhite' => '#FFDEAD', + 'navy' => '#000080', + 'oldlace' => '#FDF5E6', + 'olive' => '#808000', + 'olivedrab' => '#6B8E23', + 'orange' => '#FFA500', + 'orangered' => '#FF4500', + 'orchid' => '#DA70D6', + 'palegoldenrod' => '#EEE8AA', + 'palegreen' => '#98FB98', + 'paleturquoise' => '#AFEEEE', + 'palevioletred' => '#DB7093', + 'papayawhip' => '#FFEFD5', + 'peachpuff' => '#FFDAB9', + 'peru' => '#CD853F', + 'pink' => '#FFC0CB', + 'plum' => '#DDA0DD', + 'powderblue' => '#B0E0E6', + 'purple' => '#800080', + 'rebeccapurple' => '#663399', + 'red' => '#FF0000', + 'rosybrown' => '#BC8F8F', + 'royalblue' => '#4169E1', + 'saddlebrown' => '#8B4513', + 'salmon' => '#FA8072', + 'sandybrown' => '#F4A460', + 'seagreen' => '#2E8B57', + 'seashell' => '#FFF5EE', + 'sienna' => '#A0522D', + 'silver' => '#C0C0C0', + 'skyblue' => '#87CEEB', + 'slateblue' => '#6A5ACD', + 'slategray' => '#708090', + 'slategrey' => '#708090', + 'snow' => '#FFFAFA', + 'springgreen' => '#00FF7F', + 'steelblue' => '#4682B4', + 'tan' => '#D2B48C', + 'teal' => '#008080', + 'thistle' => '#D8BFD8', + 'tomato' => '#FF6347', + 'turquoise' => '#40E0D0', + 'violet' => '#EE82EE', + 'wheat' => '#F5DEB3', + 'white' => '#FFFFFF', + 'whitesmoke' => '#F5F5F5', + 'yellow' => '#FFFF00', + 'yellowgreen' => '#9ACD32' +) +--DESCRIPTION-- + +Lookup array of color names to six digit hexadecimal number corresponding +to color, with preceding hash mark. Used when parsing colors. The lookup +is done in a case-insensitive manner. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt new file mode 100644 index 0000000..64b114f --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt new file mode 100644 index 0000000..36f16e0 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt @@ -0,0 +1,17 @@ +Core.DirectLexLineNumberSyncInterval +TYPE: int +VERSION: 2.0.0 +DEFAULT: 0 +--DESCRIPTION-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt new file mode 100644 index 0000000..1cd4c2c --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt @@ -0,0 +1,14 @@ +Core.DisableExcludes +TYPE: bool +DEFAULT: false +VERSION: 4.5.0 +--DESCRIPTION-- +

    + This directive disables SGML-style exclusions, e.g. the exclusion of + <object> in any descendant of a + <pre> tag. Disabling excludes will allow some + invalid documents to pass through HTML Purifier, but HTML Purifier + will also be less likely to accidentally remove large documents during + processing. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt new file mode 100644 index 0000000..ce243c3 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt @@ -0,0 +1,9 @@ +Core.EnableIDNA +TYPE: bool +DEFAULT: false +VERSION: 4.4.0 +--DESCRIPTION-- +Allows international domain names in URLs. This configuration option +requires the PEAR Net_IDNA2 module to be installed. It operates by +punycoding any internationalized host names for maximum portability. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt new file mode 100644 index 0000000..8bfb47c --- /dev/null +++ b/library/vendor/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 feature 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/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt new file mode 100644 index 0000000..a3881be --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt @@ -0,0 +1,12 @@ +Core.EscapeInvalidChildren +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +

    Warning: this configuration option is no longer does anything as of 4.6.0.

    + +

    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/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt new file mode 100644 index 0000000..a7a5b24 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt new file mode 100644 index 0000000..abb4999 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt new file mode 100644 index 0000000..915391e --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt @@ -0,0 +1,19 @@ +Core.HiddenElements +TYPE: lookup +--DEFAULT-- +array ( + 'script' => true, + 'style' => true, +) +--DESCRIPTION-- + +

    + 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 script 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 b + tag, which defines some presentational changes but does not hide its + contents. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.Language.txt new file mode 100644 index 0000000..233fca1 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt new file mode 100644 index 0000000..392b436 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt @@ -0,0 +1,36 @@ +Core.LegacyEntityDecoder +TYPE: bool +VERSION: 4.9.0 +DEFAULT: false +--DESCRIPTION-- +

    + Prior to HTML Purifier 4.9.0, entities were decoded by performing + a global search replace for all entities whose decoded versions + did not have special meanings under HTML, and replaced them with + their decoded versions. We would match all entities, even if they did + not have a trailing semicolon, but only if there weren't any trailing + alphanumeric characters. +

    + + + + + + +
    OriginalTextAttribute
    &yen;¥¥
    &yen¥¥
    &yena&yena&yena
    &yen=¥=¥=
    +

    + In HTML Purifier 4.9.0, we changed the behavior of entity parsing + to match entities that had missing trailing semicolons in less + cases, to more closely match HTML5 parsing behavior: +

    + + + + + + +
    OriginalTextAttribute
    &yen;¥¥
    &yen¥¥
    &yena¥a&yena
    &yen=¥=&yen=
    +

    + This flag reverts back to pre-HTML Purifier 4.9.0 behavior. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt new file mode 100644 index 0000000..8983e2c --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt @@ -0,0 +1,34 @@ +Core.LexerImpl +TYPE: mixed/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + This parameter determines what lexer implementation can be used. The + valid values are: +

    +
    +
    null
    +
    + Recommended, the lexer implementation will be auto-detected based on + your PHP-version and configuration. +
    +
    string lexer identifier
    +
    + 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. +
    +
    object lexer instance
    +
    + Super-advanced: you can specify your own, custom, implementation that + implements the interface defined by HTMLPurifier_Lexer. + I may remove this option simply because I don't expect anyone + to use it. +
    +
    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt new file mode 100644 index 0000000..eb841a7 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt @@ -0,0 +1,16 @@ +Core.MaintainLineNumbers +TYPE: bool/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt new file mode 100644 index 0000000..d77f536 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt @@ -0,0 +1,11 @@ +Core.NormalizeNewlines +TYPE: bool +VERSION: 4.2.0 +DEFAULT: true +--DESCRIPTION-- +

    + Whether or not to normalize newlines to the operating + system default. When false, HTML Purifier + will attempt to preserve mixed newline files. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt new file mode 100644 index 0000000..4070c2a --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt @@ -0,0 +1,12 @@ +Core.RemoveInvalidImg +TYPE: bool +DEFAULT: true +VERSION: 1.3.0 +--DESCRIPTION-- + +

    + This directive enables pre-emptive URI checking in img + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt new file mode 100644 index 0000000..3397d9f --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt @@ -0,0 +1,11 @@ +Core.RemoveProcessingInstructions +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +Instead of escaping processing instructions in the form <? ... +?>, remove it out-right. This may be useful if the HTML +you are validating contains XML processing instruction gunk, however, +it can also be user-unfriendly for people attempting to post PHP +snippets. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt new file mode 100644 index 0000000..a4cd966 --- /dev/null +++ b/library/vendor/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-- +

    + This directive enables HTML Purifier to remove not only script tags + but all of their contents. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt new file mode 100644 index 0000000..3db50ef --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt @@ -0,0 +1,11 @@ +Filter.Custom +TYPE: list +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

    + This directive can be used to add custom filters; it is nearly the + equivalent of the now deprecated HTMLPurifier->addFilter() + method. Specify an array of concrete implementations. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt new file mode 100644 index 0000000..16829bc --- /dev/null +++ b/library/vendor/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-- + +

    + Whether or not to escape the dangerous characters <, > and & + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt new file mode 100644 index 0000000..7f95f54 --- /dev/null +++ b/library/vendor/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-- + +

    + 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 #user-content and a user uses the + selector a:hover, the final selector will be + #user-content a:hover. +

    +

    + The comma shorthand may be used; consider the above example, with + #user-content, #user-content2, the final selector will + be #user-content a:hover, #user-content2 a:hover. +

    +

    + Warning: 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt new file mode 100644 index 0000000..6c231b2 --- /dev/null +++ b/library/vendor/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-- +

    + If left NULL, HTML Purifier will attempt to instantiate a csstidy + class to use for internal cleaning. This will usually be good enough. +

    +

    + However, for trusted user input, you can set this to false 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt new file mode 100644 index 0000000..078d087 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt @@ -0,0 +1,74 @@ +Filter.ExtractStyleBlocks +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +EXTERNAL: CSSTidy +--DESCRIPTION-- +

    + This directive turns on the style block extraction filter, which removes + style blocks from input HTML, cleans them up with CSSTidy, + and places them in the StyleBlocks context variable, for further + use by you, usually to be placed in an external stylesheet, or a + style block in the head of your document. +

    +

    + Sample usage: +

    +
    ';
    +?>
    +
    +
    +
    +  Filter.ExtractStyleBlocks
    +body {color:#F00;} 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 '';
    +    }
    +?>
    +
    +
    +  
    + +
    + + +]]>
    +

    + Warning: 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt new file mode 100644 index 0000000..321eaa2 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt @@ -0,0 +1,16 @@ +Filter.YouTube +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

    + Warning: Deprecated in favor of %HTML.SafeObject and + %Output.FlashCompat (turn both on to allow YouTube videos and other + Flash content). +

    +

    + This directive enables YouTube video embedding in HTML Purifier. Check + this document + on embedding videos for more information on what this filter does. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt new file mode 100644 index 0000000..0b2c106 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt @@ -0,0 +1,25 @@ +HTML.Allowed +TYPE: itext/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + This is a preferred convenience directive that combines + %HTML.AllowedElements and %HTML.AllowedAttributes. + Specify elements and attributes that are allowed using: + element1[attr1|attr2],element2.... For example, + if you would like to only allow paragraphs and links, specify + a[href],p. You can specify attributes that apply + to all elements using an asterisk, e.g. *[lang]. + You can also use newlines instead of commas to separate elements. +

    +

    + Warning: + All of the constraints on the component directives are still enforced. + The syntax is a subset of TinyMCE's valid_elements + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt new file mode 100644 index 0000000..fcf093f --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt @@ -0,0 +1,19 @@ +HTML.AllowedAttributes +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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). +

    +

    + Warning: If another directive conflicts with the + elements here, that 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt new file mode 100644 index 0000000..140e214 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt @@ -0,0 +1,10 @@ +HTML.AllowedComments +TYPE: lookup +VERSION: 4.4.0 +DEFAULT: array() +--DESCRIPTION-- +A whitelist which indicates what explicit comment bodies should be +allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp +(these directives are union'ed together, so a comment is considered +valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt new file mode 100644 index 0000000..f22e977 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt @@ -0,0 +1,15 @@ +HTML.AllowedCommentsRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +A regexp, which if it matches the body of a comment, indicates that +it should be allowed. Trailing and leading spaces are removed prior +to running this regular expression. +Warning: Make sure you specify +correct anchor metacharacters ^regex$, otherwise you may accept +comments that you did not mean to! In particular, the regex /foo|bar/ +is probably not sufficiently strict, since it also allows foobar. +See also %HTML.AllowedComments (these directives are union'ed together, +so a comment is considered valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt new file mode 100644 index 0000000..1d3fa79 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt @@ -0,0 +1,23 @@ +HTML.AllowedElements +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

    + If HTML Purifier's tag set is unsatisfactory for your needs, you can + overload it with your own list of tags to allow. If you change + this, you probably also want to change %HTML.AllowedAttributes; see + also %HTML.Allowed which lets you set allowed elements and + attributes at the same time. +

    +

    + If you attempt to allow an element that HTML Purifier does not know + about, HTML Purifier will raise an error. You will need to manually + tell HTML Purifier about this element by using the + advanced customization features. +

    +

    + Warning: If another directive conflicts with the + elements here, that directive will win and override. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt new file mode 100644 index 0000000..5a59a55 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt @@ -0,0 +1,20 @@ +HTML.AllowedModules +TYPE: lookup/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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. +

    +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt new file mode 100644 index 0000000..151fb7b --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt new file mode 100644 index 0000000..45ae469 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt @@ -0,0 +1,18 @@ +HTML.BlockWrapper +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'p' +--DESCRIPTION-- + +

    + 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. +

    +

    + Example: by default value, + <blockquote>Foo</blockquote> would become + <blockquote><p>Foo</p></blockquote>. + The <p> tags can be replaced with whatever you desire, + as long as it is a block level element. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt new file mode 100644 index 0000000..5246188 --- /dev/null +++ b/library/vendor/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-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt new file mode 100644 index 0000000..6ed70b5 --- /dev/null +++ b/library/vendor/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 their own document +type. This directive only applies when %HTML.Doctype is blank. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt new file mode 100644 index 0000000..103db75 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt @@ -0,0 +1,33 @@ +HTML.DefinitionID +TYPE: string/null +DEFAULT: NULL +VERSION: 2.0.0 +--DESCRIPTION-- + +

    + 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: +

    +
    +$config = HTMLPurifier_Config::createDefault();
    +$config->set('HTML', 'DefinitionID', '1');
    +$def = $config->getHTMLDefinition();
    +$def->addAttribute('a', 'tabindex', 'Number');
    +
    +

    + 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. +

    +

    + You must specify a value to this directive to use the + advanced API features. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt new file mode 100644 index 0000000..229ae02 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt @@ -0,0 +1,16 @@ +HTML.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt new file mode 100644 index 0000000..9dab497 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt new file mode 100644 index 0000000..7878dc0 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt @@ -0,0 +1,11 @@ +HTML.FlashAllowFullScreen +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

    + Whether or not to permit embedded Flash content from + %HTML.SafeObject to expand to the full screen. Corresponds to + the allowFullScreen parameter. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt new file mode 100644 index 0000000..57358f9 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt @@ -0,0 +1,21 @@ +HTML.ForbiddenAttributes +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

    + While this directive is similar to %HTML.AllowedAttributes, for + forwards-compatibility with XML, this attribute has a different syntax. Instead of + tag.attr, use tag@attr. To disallow href + attributes in a tags, set this directive to + a@href. You can also disallow an attribute globally with + attr or *@attr (either syntax is fine; the latter + is provided for consistency with %HTML.AllowedAttributes). +

    +

    + Warning: This directive complements %HTML.ForbiddenElements, + accordingly, check + out that directive for a discussion of why you + should think twice before using this directive. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt new file mode 100644 index 0000000..93a53e1 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt @@ -0,0 +1,20 @@ +HTML.ForbiddenElements +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

    + 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. +

    +

    + 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 img + with the expectation of preventing images on your site, you'll be in for + a nasty surprise when people start using the background-image + CSS property. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt new file mode 100644 index 0000000..4a432d8 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt @@ -0,0 +1,11 @@ +HTML.Forms +TYPE: bool +VERSION: 4.13.0 +DEFAULT: false +--DESCRIPTION-- +

    + Whether or not to permit form elements in the user input, regardless of + %HTML.Trusted value. Please be very careful when using this functionality, as + enabling forms in untrusted documents may allow for phishing attacks. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt new file mode 100644 index 0000000..e424c38 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt @@ -0,0 +1,14 @@ +HTML.MaxImgLength +TYPE: int/null +DEFAULT: 1200 +VERSION: 3.1.1 +--DESCRIPTION-- +

    + This directive controls the maximum number of pixels in the width and + height attributes in img 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). +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt new file mode 100644 index 0000000..700b309 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt @@ -0,0 +1,7 @@ +HTML.Nofollow +TYPE: bool +VERSION: 4.3.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, nofollow rel attributes are added to all outgoing links. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt new file mode 100644 index 0000000..62e8e16 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt @@ -0,0 +1,12 @@ +HTML.Parent +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'div' +--DESCRIPTION-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt new file mode 100644 index 0000000..dfb7204 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt @@ -0,0 +1,12 @@ +HTML.Proprietary +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

    + Whether or not to allow proprietary elements and attributes in your + documents, as per HTMLPurifier_HTMLModule_Proprietary. + Warning: This can cause your documents to stop + validating! +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt new file mode 100644 index 0000000..cdda09a --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt @@ -0,0 +1,13 @@ +HTML.SafeEmbed +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

    + 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 should + see if you can use %Output.FlashCompat with %HTML.SafeObject instead + first.

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt new file mode 100644 index 0000000..5eb6ec2 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt @@ -0,0 +1,13 @@ +HTML.SafeIframe +TYPE: bool +VERSION: 4.4.0 +DEFAULT: false +--DESCRIPTION-- +

    + Whether or not to permit iframe tags in untrusted documents. This + directive must be accompanied by a whitelist of permitted iframes, + such as %URI.SafeIframeRegexp, otherwise it will fatally error. + This directive has no effect on strict doctypes, as iframes are not + valid. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt new file mode 100644 index 0000000..ceb342e --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt @@ -0,0 +1,13 @@ +HTML.SafeObject +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

    + 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 should also enable + %Output.FlashCompat in order to generate Internet Explorer + compatibility code for your object tags. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt new file mode 100644 index 0000000..5ebc7a1 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt @@ -0,0 +1,10 @@ +HTML.SafeScripting +TYPE: lookup +VERSION: 4.5.0 +DEFAULT: array() +--DESCRIPTION-- +

    + Whether or not to permit script tags to external scripts in documents. + Inline scripting is not allowed, and the script must match an explicit whitelist. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt new file mode 100644 index 0000000..a8b1de5 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt new file mode 100644 index 0000000..587a167 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt @@ -0,0 +1,8 @@ +HTML.TargetBlank +TYPE: bool +VERSION: 4.4.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, target=blank attributes are added to all outgoing links. +(This includes links from an HTTPS version of a page to an HTTP version.) +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt new file mode 100644 index 0000000..dd514c0 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt @@ -0,0 +1,10 @@ +--# vim: et sw=4 sts=4 +HTML.TargetNoopener +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noopener rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt new file mode 100644 index 0000000..cb5a0b0 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt @@ -0,0 +1,9 @@ +HTML.TargetNoreferrer +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noreferrer rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt new file mode 100644 index 0000000..b4c271b --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt new file mode 100644 index 0000000..4186ccd --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt @@ -0,0 +1,24 @@ +HTML.TidyLevel +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'medium' +--DESCRIPTION-- + +

    General level of cleanliness the Tidy module should enforce. +There are four allowed values:

    +
    +
    none
    +
    No extra tidying should be done
    +
    light
    +
    Only fix elements that would be discarded otherwise due to + lack of support in doctype
    +
    medium
    +
    Enforce best practices
    +
    heavy
    +
    Transform all deprecated elements and attributes to standards + compliant equivalents
    +
    + +--ALLOWED-- +'none', 'light', 'medium', 'heavy' +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt new file mode 100644 index 0000000..996762b --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt new file mode 100644 index 0000000..1db9237 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt @@ -0,0 +1,9 @@ +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. +See also %CSS.Trusted. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt new file mode 100644 index 0000000..2a47e38 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt new file mode 100644 index 0000000..08921fd --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt new file mode 100644 index 0000000..d6f0d9f --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt @@ -0,0 +1,15 @@ +Output.FixInnerHTML +TYPE: bool +VERSION: 4.3.0 +DEFAULT: true +--DESCRIPTION-- +

    + If true, HTML Purifier will protect against Internet Explorer's + mishandling of the innerHTML attribute by appending + a space to any attribute that does not contain angled brackets, spaces + or quotes, but contains a backtick. This slightly changes the + semantics of any given attribute, so if this is unacceptable and + you do not use innerHTML on any of your pages, you can + turn this directive off. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt new file mode 100644 index 0000000..93398e8 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt @@ -0,0 +1,11 @@ +Output.FlashCompat +TYPE: bool +VERSION: 4.1.0 +DEFAULT: false +--DESCRIPTION-- +

    + If true, HTML Purifier will generate Internet Explorer compatibility + code for all object code. This is highly recommended if you enable + %HTML.SafeObject. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt new file mode 100644 index 0000000..79f8ad8 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt @@ -0,0 +1,13 @@ +Output.Newline +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt new file mode 100644 index 0000000..232b023 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt @@ -0,0 +1,14 @@ +Output.SortAttr +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

    + If true, HTML Purifier will sort attributes by name before writing them back + to the document, converting a tag like: <el b="" a="" c="" /> + to <el a="" b="" c="" />. 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt new file mode 100644 index 0000000..06bab00 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt @@ -0,0 +1,25 @@ +Output.TidyFormat +TYPE: bool +VERSION: 1.1.1 +DEFAULT: false +--DESCRIPTION-- +

    + Determines whether or not to run Tidy on the final output for pretty + formatting reasons, such as indentation and wrap. +

    +

    + 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. +

    +

    + 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). +

    +--ALIASES-- +Core.TidyFormat +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt new file mode 100644 index 0000000..071bc02 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt new file mode 100644 index 0000000..eb97307 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt @@ -0,0 +1,18 @@ +URI.AllowedSchemes +TYPE: lookup +--DEFAULT-- +array ( + 'http' => true, + 'https' => true, + 'mailto' => true, + 'ftp' => true, + 'nntp' => true, + 'news' => true, + 'tel' => 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. +There is also support for the data and file +URI schemes, but they are not enabled by default. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Base.txt new file mode 100644 index 0000000..876f068 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Base.txt @@ -0,0 +1,17 @@ +URI.Base +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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). + Warning: If set, the scheme on this URI + overrides the one specified by %URI.DefaultScheme. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt new file mode 100644 index 0000000..834bc08 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt @@ -0,0 +1,15 @@ +URI.DefaultScheme +TYPE: string/null +DEFAULT: 'http' +--DESCRIPTION-- + +

    + Defines through what scheme the output will be served, in order to + select the proper object validator when no scheme information is present. +

    + +

    + Starting with HTML Purifier 4.9.0, the default scheme can be null, in + which case we reject all URIs which do not have explicit schemes. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt new file mode 100644 index 0000000..f05312b --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt @@ -0,0 +1,11 @@ +URI.DefinitionID +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + Unique identifier for a custom-built URI definition. If you want + to add custom URIFilters, you must specify this value. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt new file mode 100644 index 0000000..80cfea9 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt @@ -0,0 +1,11 @@ +URI.DefinitionRev +TYPE: int +VERSION: 2.1.0 +DEFAULT: 1 +--DESCRIPTION-- + +

    + Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt new file mode 100644 index 0000000..71ce025 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt @@ -0,0 +1,14 @@ +URI.Disable +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- + +

    + 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). +

    + +--ALIASES-- +Attr.DisableURI +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt new file mode 100644 index 0000000..13c122c --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt new file mode 100644 index 0000000..abcc1ef --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt new file mode 100644 index 0000000..f891de4 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt @@ -0,0 +1,15 @@ +URI.DisableResources +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

    + 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. +

    +

    + Note: While this directive has been available since 1.3.0, + it didn't actually start doing anything until 4.2.0. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Host.txt new file mode 100644 index 0000000..ee83b12 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Host.txt @@ -0,0 +1,19 @@ +URI.Host +TYPE: string/null +VERSION: 1.2.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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. + Note: 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt new file mode 100644 index 0000000..0b6df76 --- /dev/null +++ b/library/vendor/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 moo.com will catch moo.com.example.com. +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt new file mode 100644 index 0000000..4214900 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt @@ -0,0 +1,13 @@ +URI.MakeAbsolute +TYPE: bool +VERSION: 2.1.0 +DEFAULT: false +--DESCRIPTION-- + +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt new file mode 100644 index 0000000..58c81dc --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -0,0 +1,83 @@ +URI.Munge +TYPE: string/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

    + 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 %s where + the url-encoded original URI should be inserted (sample: + http://www.google.com/url?q=%s). +

    +

    + Uses for this directive: +

    +
      +
    • + 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). Notice: + Many security experts believe that this form of protection does not deter spam-bots. +
    • +
    • + 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. +
    • +
    +

    + 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 meta tags. To revert to + previous behavior, please use %URI.MungeResources. +

    +

    + 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. +

    +

    + Starting with HTML Purifier 3.1.1, there is also these substitutions: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyDescriptionExample <a href="">
    %r1 - The URI embeds a resource
    (blank) - The URI is merely a link
    %nThe name of the tag this URI came froma
    %mThe name of the attribute this URI came fromhref
    %pThe name of the CSS property this URI came from, or blank if irrelevant
    +

    + 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. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt new file mode 100644 index 0000000..6fce0fd --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt @@ -0,0 +1,17 @@ +URI.MungeResources +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

    + If true, any URI munging directives like %URI.Munge + will also apply to embedded resources, such as <img src="">. + Be careful enabling this directive if you have a redirector script + that does not use the Location HTTP header; all of your images + and other embedded resources will break. +

    +

    + Warning: It is strongly advised you use this in conjunction + %URI.MungeSecretKey to mitigate the security risk of an open redirector. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt new file mode 100644 index 0000000..1e17c1d --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt @@ -0,0 +1,30 @@ +URI.MungeSecretKey +TYPE: string/null +VERSION: 3.1.1 +DEFAULT: NULL +--DESCRIPTION-- +

    + 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: +

    + +
    $checksum === hash_hmac("sha256", $url, $secret_key)
    + +

    + If the output is TRUE, the redirector script should accept the URI. +

    + +

    + 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. +

    + +

    + Remember this has no effect if %URI.Munge is not on. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt new file mode 100644 index 0000000..23331a4 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt new file mode 100644 index 0000000..7908483 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt @@ -0,0 +1,22 @@ +URI.SafeIframeRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +

    + A PCRE regular expression that will be matched against an iframe URI. This is + a relatively inflexible scheme, but works well enough for the most common + use-case of iframes: embedded video. This directive only has an effect if + %HTML.SafeIframe is enabled. Here are some example values: +

    +
      +
    • %^http://www.youtube.com/embed/% - Allow YouTube videos
    • +
    • %^http://player.vimeo.com/video/% - Allow Vimeo videos
    • +
    • %^http://(www.youtube.com/embed/|player.vimeo.com/video/)% - Allow both
    • +
    +

    + Note that this directive does not give you enough granularity to, say, disable + all autoplay videos. Pipe up on the HTML Purifier forums if this + is a capability you want. +

    +--# vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ConfigSchema/schema/info.ini b/library/vendor/HTMLPurifier/ConfigSchema/schema/info.ini new file mode 100644 index 0000000..5de4505 --- /dev/null +++ b/library/vendor/HTMLPurifier/ConfigSchema/schema/info.ini @@ -0,0 +1,3 @@ +name = "HTML Purifier" + +; vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ContentSets.php b/library/vendor/HTMLPurifier/ContentSets.php new file mode 100644 index 0000000..543e3f8 --- /dev/null +++ b/library/vendor/HTMLPurifier/ContentSets.php @@ -0,0 +1,170 @@ + true) indexed by name. + * @type array + * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets + */ + public $lookup = array(); + + /** + * Synchronized list of defined content sets (keys of info). + * @type array + */ + protected $keys = array(); + /** + * Synchronized list of defined content values (values of info). + * @type array + */ + 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 HTMLPurifier_HTMLModule[] $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) { + 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 HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + */ + public function generateChildDef(&$def, $module) + { + if (!empty($def->child)) { // already done! + return; + } + $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 HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + * @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 $string List of elements + * @return array Lookup array of elements + */ + protected function convertToLookup($string) + { + $array = explode('|', str_replace(' ', '', $string)); + $ret = array(); + foreach ($array as $k) { + $ret[$k] = true; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Context.php b/library/vendor/HTMLPurifier/Context.php new file mode 100644 index 0000000..00e509c --- /dev/null +++ b/library/vendor/HTMLPurifier/Context.php @@ -0,0 +1,95 @@ +_storage)) { + 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 string $name String name + * @param bool $ignore_error Boolean whether or not to ignore error + * @return mixed + */ + public function &get($name, $ignore_error = false) + { + if (!array_key_exists($name, $this->_storage)) { + 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]; + } + + /** + * Destroys a variable in the context. + * @param string $name String name + */ + public function destroy($name) + { + if (!array_key_exists($name, $this->_storage)) { + 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 string $name String name + * @return bool + */ + public function exists($name) + { + return array_key_exists($name, $this->_storage); + } + + /** + * Loads a series of variables from an associative array + * @param array $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/library/vendor/HTMLPurifier/Definition.php b/library/vendor/HTMLPurifier/Definition.php new file mode 100644 index 0000000..bc6d433 --- /dev/null +++ b/library/vendor/HTMLPurifier/Definition.php @@ -0,0 +1,55 @@ +setup) { + return; + } + $this->setup = true; + $this->doSetup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/DefinitionCache.php b/library/vendor/HTMLPurifier/DefinitionCache.php new file mode 100644 index 0000000..9aa8ff3 --- /dev/null +++ b/library/vendor/HTMLPurifier/DefinitionCache.php @@ -0,0 +1,129 @@ +type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @return string + */ + 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 string $key Key to test + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against + * @return bool + */ + 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 HTMLPurifier_Definition $def Definition object to check + * @return bool 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 + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function add($def, $config); + + /** + * Unconditionally saves a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function set($def, $config); + + /** + * Replace an object in the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function replace($def, $config); + + /** + * Retrieves a definition object from the cache + * @param HTMLPurifier_Config $config + */ + abstract public function get($config); + + /** + * Removes a definition object to the cache + * @param HTMLPurifier_Config $config + */ + abstract public function remove($config); + + /** + * Clears all objects from cache + * @param HTMLPurifier_Config $config + */ + abstract public function flush($config); + + /** + * Clears all expired (older version or revision) objects from cache + * @note Be careful implementing this method as flush. Flush must + * not interfere with other Definition types, and cleanup() + * should not be repeatedly called by userland code. + * @param HTMLPurifier_Config $config + */ + abstract public function cleanup($config); +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/DefinitionCache/Decorator.php b/library/vendor/HTMLPurifier/DefinitionCache/Decorator.php new file mode 100644 index 0000000..b57a51b --- /dev/null +++ b/library/vendor/HTMLPurifier/DefinitionCache/Decorator.php @@ -0,0 +1,112 @@ +copy(); + // reference is necessary for mocks in PHP 4 + $decorator->cache =& $cache; + $decorator->type = $cache->type; + return $decorator; + } + + /** + * Cross-compatible clone substitute + * @return HTMLPurifier_DefinitionCache_Decorator + */ + public function copy() + { + return new HTMLPurifier_DefinitionCache_Decorator(); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function add($def, $config) + { + return $this->cache->add($def, $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function set($def, $config) + { + return $this->cache->set($def, $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function replace($def, $config) + { + return $this->cache->replace($def, $config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function get($config) + { + return $this->cache->get($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function remove($config) + { + return $this->cache->remove($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function flush($config) + { + return $this->cache->flush($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function cleanup($config) + { + return $this->cache->cleanup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/library/vendor/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php new file mode 100644 index 0000000..4991777 --- /dev/null +++ b/library/vendor/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php @@ -0,0 +1,78 @@ +definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function set($def, $config) + { + $status = parent::set($def, $config); + if ($status) { + $this->definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function replace($def, $config) + { + $status = parent::replace($def, $config); + if ($status) { + $this->definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + 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/library/vendor/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/library/vendor/HTMLPurifier/DefinitionCache/Decorator/Template.php.in new file mode 100644 index 0000000..b1fec8d --- /dev/null +++ b/library/vendor/HTMLPurifier/DefinitionCache/Decorator/Template.php.in @@ -0,0 +1,82 @@ +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), $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return int|bool + */ + 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), $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return int|bool + */ + 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), $config); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool|HTMLPurifier_Config + */ + public function get($config) + { + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + return unserialize(file_get_contents($file)); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function remove($config) + { + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + return unlink($file); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function flush($config) + { + if (!$this->_prepareDir($config)) { + return false; + } + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + // Apparently, on some versions of PHP, readdir will return + // an empty string if you pass an invalid argument to readdir. + // So you need this test. See #49. + if (false === $dh) { + return false; + } + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) { + continue; + } + if ($filename[0] === '.') { + continue; + } + unlink($dir . '/' . $filename); + } + closedir($dh); + return true; + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function cleanup($config) + { + if (!$this->_prepareDir($config)) { + return false; + } + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + // See #49 (and above). + if (false === $dh) { + return false; + } + 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); + } + } + closedir($dh); + return true; + } + + /** + * Generates the file path to the serial file corresponding to + * the configuration and definition name + * @param HTMLPurifier_Config $config + * @return string + * @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 + * @param HTMLPurifier_Config $config + * @return string + * @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 + * @param HTMLPurifier_Config $config + * @return mixed|string + * @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 string $file File name to write to + * @param string $data Data to write into file + * @param HTMLPurifier_Config $config + * @return int|bool Number of bytes written if success, or false if failure. + */ + private function _write($file, $data, $config) + { + $result = file_put_contents($file, $data); + if ($result !== false) { + // set permissions of the new file (no execute) + $chmod = $config->get('Cache.SerializerPermissions'); + if ($chmod !== null) { + chmod($file, $chmod & 0666); + } + } + return $result; + } + + /** + * Prepares the directory that this type stores the serials in + * @param HTMLPurifier_Config $config + * @return bool True if successful + */ + private function _prepareDir($config) + { + $directory = $this->generateDirectoryPath($config); + $chmod = $config->get('Cache.SerializerPermissions'); + if ($chmod === null) { + if (!@mkdir($directory) && !is_dir($directory)) { + trigger_error( + 'Could not create directory ' . $directory . '', + E_USER_WARNING + ); + return false; + } + return true; + } + 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, $chmod)) { + return false; + } + if (!@mkdir($directory, $chmod) && !is_dir($directory)) { + trigger_error( + 'Could not create directory ' . $directory . '', + E_USER_WARNING + ); + return false; + } + if (!$this->_testPermissions($directory, $chmod)) { + return false; + } + } elseif (!$this->_testPermissions($directory, $chmod)) { + return false; + } + return true; + } + + /** + * Tests permissions on a directory and throws out friendly + * error messages and attempts to chmod it itself if possible + * @param string $dir Directory path + * @param int $chmod Permissions + * @return bool True if directory is writable + */ + private function _testPermissions($dir, $chmod) + { + // 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') && $chmod !== null) { + // POSIX system, we can give more specific advice + if (fileowner($dir) === posix_getuid()) { + // we can chmod it ourselves + $chmod = $chmod | 0700; + if (chmod($dir, $chmod)) { + return true; + } + } elseif (filegroup($dir) === posix_getgid()) { + $chmod = $chmod | 0070; + } else { + // PHP's probably running as nobody, so we'll + // need to give global permissions + $chmod = $chmod | 0777; + } + trigger_error( + 'Directory ' . $dir . ' not writable, ' . + 'please chmod to ' . decoct($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/library/vendor/HTMLPurifier/DefinitionCache/Serializer/README b/library/vendor/HTMLPurifier/DefinitionCache/Serializer/README new file mode 100644 index 0000000..2e35c1c --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/DefinitionCacheFactory.php b/library/vendor/HTMLPurifier/DefinitionCacheFactory.php new file mode 100644 index 0000000..fd1cc9b --- /dev/null +++ b/library/vendor/HTMLPurifier/DefinitionCacheFactory.php @@ -0,0 +1,106 @@ + array()); + + /** + * @type array + */ + protected $implementations = array(); + + /** + * @type HTMLPurifier_DefinitionCache_Decorator[] + */ + protected $decorators = array(); + + /** + * Initialize default decorators + */ + public function setup() + { + $this->addDecorator('Cleanup'); + } + + /** + * Retrieves an instance of global definition cache factory. + * @param HTMLPurifier_DefinitionCacheFactory $prototype + * @return HTMLPurifier_DefinitionCacheFactory + */ + 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 string $short Short name of cache object, for reference + * @param string $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 string $type Name of definitions handled by cache + * @param HTMLPurifier_Config $config Config instance + * @return mixed + */ + 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 HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator + */ + 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/library/vendor/HTMLPurifier/Doctype.php b/library/vendor/HTMLPurifier/Doctype.php new file mode 100644 index 0000000..4acd06e --- /dev/null +++ b/library/vendor/HTMLPurifier/Doctype.php @@ -0,0 +1,73 @@ +renderDoctype. + * If structure changes, please update that function. + */ +class HTMLPurifier_Doctype +{ + /** + * Full name of doctype + * @type string + */ + public $name; + + /** + * List of standard modules (string identifiers or literal objects) + * that this doctype uses + * @type array + */ + public $modules = array(); + + /** + * List of modules to use for tidying up code + * @type array + */ + public $tidyModules = array(); + + /** + * Is the language derived from XML (i.e. XHTML)? + * @type bool + */ + public $xml = true; + + /** + * List of aliases for this doctype + * @type array + */ + public $aliases = array(); + + /** + * Public DTD identifier + * @type string + */ + public $dtdPublic; + + /** + * System DTD identifier + * @type string + */ + 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/library/vendor/HTMLPurifier/DoctypeRegistry.php b/library/vendor/HTMLPurifier/DoctypeRegistry.php new file mode 100644 index 0000000..acc1d64 --- /dev/null +++ b/library/vendor/HTMLPurifier/DoctypeRegistry.php @@ -0,0 +1,142 @@ +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 string $doctype Name of doctype + * @return HTMLPurifier_Doctype 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). + * @param HTMLPurifier_Config $config + * @return HTMLPurifier_Doctype + */ + public function make($config) + { + return clone $this->get($this->getDoctypeFromConfig($config)); + } + + /** + * Retrieves the doctype from the configuration object + * @param HTMLPurifier_Config $config + * @return string + */ + 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/library/vendor/HTMLPurifier/ElementDef.php b/library/vendor/HTMLPurifier/ElementDef.php new file mode 100644 index 0000000..57cfd2b --- /dev/null +++ b/library/vendor/HTMLPurifier/ElementDef.php @@ -0,0 +1,216 @@ +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(); + + // XXX: Design note: currently, it's not possible to override + // previously defined AttrTransforms without messing around with + // the final generated config. This is by design; a previous version + // used an associated list of attr_transform, but it was extremely + // easy to accidentally override other attribute transforms by + // forgetting to specify an index (and just using 0.) While we + // could check this by checking the index number and complaining, + // there is a second problem which is that it is not at all easy to + // tell when something is getting overridden. Combine this with a + // codebase where this isn't really being used, and it's perfect for + // nuking. + + /** + * List of tags HTMLPurifier_AttrTransform to be done before validation. + * @type array + */ + public $attr_transform_pre = array(); + + /** + * List of tags HTMLPurifier_AttrTransform to be done after validation. + * @type array + */ + public $attr_transform_post = array(); + + /** + * HTMLPurifier_ChildDef of this tag. + * @type HTMLPurifier_ChildDef + */ + 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 + * @type string + */ + 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 + * @type string + */ + 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. + * @type bool + */ + public $descendants_are_inline = false; + + /** + * List of the names of required attributes this element has. + * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement() + * @type array + */ + public $required_attr = array(); + + /** + * Lookup table of tags excluded from all descendants of this tag. + * @type array + * @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. + * @type array + */ + public $autoclose = array(); + + /** + * If a foreign element is found in this element, test if it is + * allowed by this sub-element; if it is, instead of closing the + * current element, place it inside this element. + * @type string + */ + public $wrap; + + /** + * Whether or not this is a formatting element affected by the + * "Active Formatting Elements" algorithm. + * @type bool + */ + 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. + * @param HTMLPurifier_ElementDef $def + */ + 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->excludes, $def->excludes); + $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); + $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); + + if (!empty($def->content_model)) { + $this->content_model = + str_replace("#SUPER", (string)$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/library/vendor/HTMLPurifier/Encoder.php b/library/vendor/HTMLPurifier/Encoder.php new file mode 100644 index 0000000..d4791cc --- /dev/null +++ b/library/vendor/HTMLPurifier/Encoder.php @@ -0,0 +1,617 @@ += $c) { + $r .= self::unsafeIconv($in, $out, substr($text, $i)); + break; + } + // wibble the boundary + if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { + $chunk_size = $max_chunk_size; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { + $chunk_size = $max_chunk_size - 1; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { + $chunk_size = $max_chunk_size - 2; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { + $chunk_size = $max_chunk_size - 3; + } else { + return false; // rather confusing UTF-8... + } + $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths + $r .= self::unsafeIconv($in, $out, $chunk); + $i += $chunk_size; + } + return $r; + } else { + return false; + } + } else { + return false; + } + } + + /** + * 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. + * + * Specifically, it will permit: + * \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF} + * Source: https://www.w3.org/TR/REC-xml/#NT-Char + * Arguably this function should be modernized to the HTML5 set + * of allowed characters: + * https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream + * which simultaneously expand and restrict the set of allowed characters. + * + * @param string $str The string to clean + * @param bool $force_php + * @return string + * + * @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 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. + */ + 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. + 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) || + (0xE000 <= $mUcs4 && 0xFFFD >= $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 + * , + * 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; + } + + /** + * @return bool + */ + public static function iconvAvailable() + { + static $iconv = null; + if ($iconv === null) { + $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; + } + return $iconv; + } + + /** + * Convert a string to UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + 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 = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // unaffected by bugs, since UTF-8 support all characters + $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); + if ($str === false) { + // $encoding is not a valid encoding + 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, self::testEncodingSupportsASCII($encoding)); + return $str; + } elseif ($encoding === 'iso-8859-1' && function_exists('mb_convert_encoding')) { + $str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1'); + return $str; + } + $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); + if ($bug == self::ICONV_OK) { + trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); + } else { + trigger_error( + 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . + 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', + E_USER_ERROR + ); + } + } + + /** + * Converts a string from UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + * @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 ($escape = $config->get('Core.EscapeNonASCIICharacters')) { + $str = self::convertToASCIIDumbLossless($str); + } + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // Undo our previous fix in convertToUTF8, otherwise iconv will barf + $ascii_fix = self::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 = self::iconv('utf-8', $encoding . '//IGNORE', $str); + return $str; + } elseif ($encoding === 'iso-8859-1' && function_exists('mb_convert_encoding')) { + $str = mb_convert_encoding($str, 'ISO-8859-1', 'UTF-8'); + return $str; + } + trigger_error('Encoding not supported', E_USER_ERROR); + // You might be tempted to assume that the ASCII representation + // might be OK, however, this is *not* universally true over all + // encodings. So we take the conservative route here, rather + // than forcibly turn on %Core.EscapeNonASCIICharacters + } + + /** + * Lossless (character-wise) conversion of HTML to ASCII + * @param string $str UTF-8 string to be converted to ASCII + * @return string 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; + } + + /** No bugs detected in iconv. */ + const ICONV_OK = 0; + + /** Iconv truncates output if converting from UTF-8 to another + * character set with //IGNORE, and a non-encodable character is found */ + const ICONV_TRUNCATES = 1; + + /** Iconv does not support //IGNORE, making it unusable for + * transcoding purposes */ + const ICONV_UNUSABLE = 2; + + /** + * glibc iconv has a known bug where it doesn't handle the magic + * //IGNORE stanza correctly. In particular, rather than ignore + * characters, it will return an EILSEQ after consuming some number + * of characters, and expect you to restart iconv as if it were + * an E2BIG. Old versions of PHP did not respect the errno, and + * returned the fragment, so as a result you would see iconv + * mysteriously truncating output. We can work around this by + * manually chopping our input into segments of about 8000 + * characters, as long as PHP ignores the error code. If PHP starts + * paying attention to the error code, iconv becomes unusable. + * + * @return int Error code indicating severity of bug. + */ + public static function testIconvTruncateBug() + { + static $code = null; + if ($code === null) { + // better not use iconv, otherwise infinite loop! + $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); + if ($r === false) { + $code = self::ICONV_UNUSABLE; + } elseif (($c = strlen($r)) < 9000) { + $code = self::ICONV_TRUNCATES; + } elseif ($c > 9000) { + trigger_error( + 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . + 'include your iconv version as per phpversion()', + E_USER_ERROR + ); + } else { + $code = self::ICONV_OK; + } + } + return $code; + } + + /** + * 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) + { + // All calls to iconv here are unsafe, proof by case analysis: + // If ICONV_OK, no difference. + // If ICONV_TRUNCATE, all calls involve one character inputs, + // so bug is not triggered. + // If ICONV_UNUSABLE, this call is irrelevant + 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(); + if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) { + return false; + } + for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars + $c = chr($i); // UTF-8 char + $r = self::unsafeIconv('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 && self::unsafeIconv($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[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; + } + } + $encodings[$encoding] = $ret; + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/EntityLookup.php b/library/vendor/HTMLPurifier/EntityLookup.php new file mode 100644 index 0000000..f12ff13 --- /dev/null +++ b/library/vendor/HTMLPurifier/EntityLookup.php @@ -0,0 +1,48 @@ +table = unserialize(file_get_contents($file)); + } + + /** + * Retrieves sole instance of the object. + * @param bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with. + * @return HTMLPurifier_EntityLookup + */ + 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/library/vendor/HTMLPurifier/EntityLookup/entities.ser b/library/vendor/HTMLPurifier/EntityLookup/entities.ser new file mode 100644 index 0000000..e8b0812 --- /dev/null +++ b/library/vendor/HTMLPurifier/EntityLookup/entities.ser @@ -0,0 +1 @@ +a:253:{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:6:"there4";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:"♦";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:"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:4:"sup2";s:2:"²";s:4:"sup3";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:"sup1";s:2:"¹";s:4:"ordm";s:2:"º";s:5:"raquo";s:2:"»";s:6:"frac14";s:2:"¼";s:6:"frac12";s:2:"½";s:6:"frac34";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:"ÿ";} \ No newline at end of file diff --git a/library/vendor/HTMLPurifier/EntityParser.php b/library/vendor/HTMLPurifier/EntityParser.php new file mode 100644 index 0000000..3ef2d09 --- /dev/null +++ b/library/vendor/HTMLPurifier/EntityParser.php @@ -0,0 +1,285 @@ +_semiOptionalPrefixRegex = "/&()()()($semi_optional)/"; + + $this->_textEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + "($semi_optional)". + ')/'; + + $this->_attrEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + // don't match if trailing is equals or alphanumeric (URL + // like) + "($semi_optional)(?![=;A-Za-z0-9])". + ')/'; + + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * textual data in an HTML document (as opposed to attributes.) + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteTextEntities($string) + { + return preg_replace_callback( + $this->_textEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * attribute contents in documents. + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteAttrEntities($string) + { + return preg_replace_callback( + $this->_attrEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $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). + * @return string Replacement string. + */ + + protected function entityCallback($matches) + { + $entity = $matches[0]; + $hex_part = @$matches[1]; + $dec_part = @$matches[2]; + $named_part = empty($matches[3]) ? (empty($matches[4]) ? "" : $matches[4]) : $matches[3]; + if ($hex_part !== NULL && $hex_part !== "") { + return HTMLPurifier_Encoder::unichr(hexdec($hex_part)); + } elseif ($dec_part !== NULL && $dec_part !== "") { + return HTMLPurifier_Encoder::unichr((int) $dec_part); + } else { + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$named_part])) { + return $this->_entity_lookup->table[$named_part]; + } else { + // exact match didn't match anything, so test if + // any of the semicolon optional match the prefix. + // Test that this is an EXACT match is important to + // prevent infinite loop + if (!empty($matches[3])) { + return preg_replace_callback( + $this->_semiOptionalPrefixRegex, + array($this, 'entityCallback'), + $entity + ); + } + return $entity; + } + } + } + + // LEGACY CODE BELOW + + /** + * Callback regex string for parsing entities. + * @type string + */ + 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. + * @type array + */ + protected $_special_dec2str = + array( + 34 => '"', + 38 => '&', + 39 => "'", + 60 => '<', + 62 => '>' + ); + + /** + * Stripped entity names to decimal conversion table for special entities. + * @type array + */ + 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 String to have non-special entities parsed. + * @return string 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 array $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). + * @return string 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 String to have non-special entities parsed. + * @return string 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 array $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). + * @return string 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_dec2str[$this->_special_ent2dec[$matches[3]]] : + $entity; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/ErrorCollector.php b/library/vendor/HTMLPurifier/ErrorCollector.php new file mode 100644 index 0000000..d47e3f2 --- /dev/null +++ b/library/vendor/HTMLPurifier/ErrorCollector.php @@ -0,0 +1,244 @@ +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 int $severity Error severity, PHP error style (don't use E_USER_) + * @param string $msg Error message text + */ + 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 ... + // 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 + */ + public function getRaw() + { + return $this->errors; + } + + /** + * Default HTML formatting implementation for error messages + * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature + * @param array $errors Errors array to display; used for recursion. + * @return string + */ + 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 '

    ' . $this->locale->getMessage('ErrorCollector: No errors') . '

    '; + } else { + return '
    • ' . implode('
    • ', $ret) . '
    '; + } + + } + + 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 .= '
    '; + // W3C uses an icon to indicate the severity of the error. + $error = $this->locale->getErrorName($severity); + $string .= "$error "; + if (!is_null($line) && !is_null($col)) { + $string .= "Line $line, Column $col: "; + } else { + $string .= 'End of Document: '; + } + $string .= '' . $this->generator->escape($msg) . ' '; + $string .= '
    '; + // Here, have a marker for the character on the column appropriate. + // Be sure to clip extremely long lines. + //$string .= '
    ';
    +                //$string .= '';
    +                //$string .= '
    '; + $ret[] = $string; + } + foreach ($current->children as $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/library/vendor/HTMLPurifier/ErrorStruct.php b/library/vendor/HTMLPurifier/ErrorStruct.php new file mode 100644 index 0000000..cf869d3 --- /dev/null +++ b/library/vendor/HTMLPurifier/ErrorStruct.php @@ -0,0 +1,74 @@ +children[$type][$id])) { + $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); + $this->children[$type][$id]->type = $type; + } + return $this->children[$type][$id]; + } + + /** + * @param int $severity + * @param string $message + */ + public function addError($severity, $message) + { + $this->errors[] = array($severity, $message); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Exception.php b/library/vendor/HTMLPurifier/Exception.php new file mode 100644 index 0000000..be85b4c --- /dev/null +++ b/library/vendor/HTMLPurifier/Exception.php @@ -0,0 +1,12 @@ +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. + * @type string + */ + public $name; + + /** + * Pre-processor function, handles HTML before HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function preFilter($html, $config, $context) + { + return $html; + } + + /** + * Post-processor function, handles HTML after HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Filter/ExtractStyleBlocks.php b/library/vendor/HTMLPurifier/Filter/ExtractStyleBlocks.php new file mode 100644 index 0000000..66f70b0 --- /dev/null +++ b/library/vendor/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -0,0 +1,341 @@ + 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 +{ + /** + * @type string + */ + public $name = 'ExtractStyleBlocks'; + + /** + * @type array + */ + private $_styleMatches = array(); + + /** + * @type csstidy + */ + private $_tidy; + + /** + * @type HTMLPurifier_AttrDef_HTML_ID + */ + private $_id_attrdef; + + /** + * @type HTMLPurifier_AttrDef_CSS_Ident + */ + private $_class_attrdef; + + /** + * @type HTMLPurifier_AttrDef_Enum + */ + private $_enum_attrdef; + + public function __construct() + { + $this->_tidy = new csstidy(); + $this->_tidy->set_cfg('lowercase_s', false); + $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); + $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); + $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'first-child', + 'link', + 'visited', + 'active', + 'hover', + 'focus' + ) + ); + } + + /** + * Save the contents of CSS blocks to style matches + * @param array $matches preg_replace style $matches array + */ + protected function styleCallback($matches) + { + $this->_styleMatches[] = $matches[1]; + } + + /** + * Removes inline + // we must not grab foo 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/library/vendor/HTMLPurifier/Filter/YouTube.php b/library/vendor/HTMLPurifier/Filter/YouTube.php new file mode 100644 index 0000000..276d836 --- /dev/null +++ b/library/vendor/HTMLPurifier/Filter/YouTube.php @@ -0,0 +1,65 @@ +]+>.+?' . + '(?:http:)?//www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; + $pre_replace = '\1'; + return preg_replace($pre_regex, $pre_replace, $html); + } + + /** + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; + return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); + } + + /** + * @param $url + * @return string + */ + protected function armorUrl($url) + { + return str_replace('--', '--', $url); + } + + /** + * @param array $matches + * @return string + */ + protected function postFilterCallback($matches) + { + $url = $this->armorUrl($matches[1]); + return '' . + '' . + '' . + ''; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Generator.php b/library/vendor/HTMLPurifier/Generator.php new file mode 100644 index 0000000..eb56e2d --- /dev/null +++ b/library/vendor/HTMLPurifier/Generator.php @@ -0,0 +1,286 @@ + tags. + * @type bool + */ + private $_scriptFix = false; + + /** + * Cache of HTMLDefinition during HTML output to determine whether or + * not attributes should be minimized. + * @type HTMLPurifier_HTMLDefinition + */ + private $_def; + + /** + * Cache of %Output.SortAttr. + * @type bool + */ + private $_sortAttr; + + /** + * Cache of %Output.FlashCompat. + * @type bool + */ + private $_flashCompat; + + /** + * Cache of %Output.FixInnerHTML. + * @type bool + */ + private $_innerHTMLFix; + + /** + * Stack for keeping track of object information when outputting IE + * compatibility code. + * @type array + */ + private $_flashStack = array(); + + /** + * Configuration for the generator + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + public function __construct($config, $context) + { + $this->config = $config; + $this->_scriptFix = $config->get('Output.CommentScriptContents'); + $this->_innerHTMLFix = $config->get('Output.FixInnerHTML'); + $this->_sortAttr = $config->get('Output.SortAttr'); + $this->_flashCompat = $config->get('Output.FlashCompat'); + $this->_def = $config->getHTMLDefinition(); + $this->_xhtml = $this->_def->doctype->xml; + } + + /** + * Generates HTML from an array of tokens. + * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token + * @return string 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 + if ($this->config->get('Core.NormalizeNewlines')) { + $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 HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string 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); + if ($this->_flashCompat) { + if ($token->name == "object") { + $flash = new stdClass(); + $flash->attr = $token->attr; + $flash->param = array(); + $this->_flashStack[] = $flash; + } + } + return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_End) { + $_extra = ''; + if ($this->_flashCompat) { + if ($token->name == "object" && !empty($this->_flashStack)) { + // doesn't do anything for now + } + } + return $_extra . 'name . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) { + $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value']; + } + $attr = $this->generateAttributes($token->attr, $token->name); + return '<' . $token->name . ($attr ? ' ' : '') . $attr . + ( $this->_xhtml ? ' /': '' ) //
    v.
    + . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Text) { + return $this->escape($token->data, ENT_NOQUOTES); + + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + return ''; + } else { + return ''; + + } + } + + /** + * Special case processor for the contents of script tags + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string + * @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 + $data = preg_replace('#//\s*$#', '', $token->data); + return ''; + } + + /** + * Generates attribute declarations from attribute array. + * @note This does not include the leading or trailing space. + * @param array $assoc_array_of_attributes Attribute array + * @param string $element Name of element attributes are for, used to check + * attribute minimization. + * @return string Generated HTML fragment for insertion. + */ + public function generateAttributes($assoc_array_of_attributes, $element = '') + { + $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; + } + } + // Workaround for Internet Explorer innerHTML bug. + // Essentially, Internet Explorer, when calculating + // innerHTML, omits quotes if there are no instances of + // angled brackets, quotes or spaces. However, when parsing + // HTML (for example, when you assign to innerHTML), it + // treats backticks as quotes. Thus, + // `` + // becomes + // `` + // becomes + // + // Fortunately, all we need to do is trigger an appropriate + // quoting style, which we do by adding an extra space. + // This also is consistent with the W3C spec, which states + // that user agents may ignore leading or trailing + // whitespace (in fact, most don't, at least for attributes + // like alt, but an extra space at the end is barely + // noticeable). Still, we have a configuration knob for + // this, since this transformation is not necesary if you + // don't process user input with innerHTML or you don't plan + // on supporting Internet Explorer. + if ($this->_innerHTMLFix) { + if (strpos($value, '`') !== false) { + // check if correct quoting style would not already be + // triggered + if (strcspn($value, '"\' <>') === strlen($value)) { + // protect! + $value .= ' '; + } + } + } + $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 String data to escape for HTML. + * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is + * permissible for non-attribute output. + * @return string escaped data. + */ + public function escape($string, $quote = null) + { + // Workaround for APC bug on Mac Leopard reported by sidepodcast + // http://htmlpurifier.org/phorum/read.php?3,4823,4846 + if ($quote === null) { + $quote = ENT_COMPAT; + } + return htmlspecialchars($string, $quote, 'UTF-8'); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLDefinition.php b/library/vendor/HTMLPurifier/HTMLDefinition.php new file mode 100644 index 0000000..9b7b334 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLDefinition.php @@ -0,0 +1,493 @@ +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 + * @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 + * @param string $element_name + * @return HTMLPurifier_ElementDef + * @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. + * @return HTMLPurifier_HTMLModule + */ + public function getAnonymousModule() + { + if (!$this->_anonModule) { + $this->_anonModule = new HTMLPurifier_HTMLModule(); + $this->_anonModule->name = 'Anonymous'; + } + return $this->_anonModule; + } + + private $_anonModule = null; + + // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- + + /** + * @type string + */ + public $type = 'HTML'; + + /** + * @type HTMLPurifier_HTMLModuleManager + */ + public $manager; + + /** + * Performs low-cost, preliminary initialization. + */ + public function __construct() + { + $this->manager = new HTMLPurifier_HTMLModuleManager(); + } + + /** + * @param HTMLPurifier_Config $config + */ + 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 + * @param HTMLPurifier_Config $config + */ + 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. + * @param HTMLPurifier_Config $config + */ + 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) { + if ($this->info[$tag]->attr[$attr]->required) { + trigger_error( + "Required attribute '$attr' in element '$tag' " . + "was not allowed, which means '$tag' will not be allowed either", + E_USER_WARNING + ); + } + 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; + } elseif (isset($forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually + // $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 array $list String list to parse + * @return array + * @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/library/vendor/HTMLPurifier/HTMLModule.php b/library/vendor/HTMLPurifier/HTMLModule.php new file mode 100644 index 0000000..9dbb987 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule.php @@ -0,0 +1,285 @@ +info, since the object's data is only info, + * with extra behavior associated with it. + * @type array + */ + public $attr_collections = array(); + + /** + * Associative array of deprecated tag name to HTMLPurifier_TagTransform. + * @type array + */ + public $info_tag_transform = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed before validation. + * @type array + */ + public $info_attr_transform_pre = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed after validation. + * @type array + */ + 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. + * @type array + */ + 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! + * @type bool + */ + 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") + * + * @type bool + * @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 HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef subclass + */ + public function getChildDef($def) + { + return false; + } + + // -- Convenience ----------------------------------------------------- + + /** + * Convenience function that sets up a new element + * @param string $element Name of element to add + * @param string|bool $type What content set should element be registered to? + * Set as false to skip this step. + * @param string|HTMLPurifier_ChildDef $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @param array|string $attr_includes What attribute collections to register to + * element? + * @param array $attr What unique attributes does the element define? + * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters. + * @return HTMLPurifier_ElementDef 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 string $element Name of element to create + * @return HTMLPurifier_ElementDef 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 string $element Element to register + * @param string $type 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 string $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @return array + * @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 array $attr Reference to attr array to modify + * @param array $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 string $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 array array equivalent of list + */ + public function makeLookup($list) + { + $args = func_get_args(); + if (is_string($list)) { + $list = $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 HTMLPurifier_Config $config + */ + public function setup($config) + { + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/Bdo.php b/library/vendor/HTMLPurifier/HTMLModule/Bdo.php new file mode 100644 index 0000000..1e67c79 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Bdo.php @@ -0,0 +1,44 @@ + array('dir' => false) + ); + + /** + * @param HTMLPurifier_Config $config + */ + 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[] = new HTMLPurifier_AttrTransform_BdoDir(); + + $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl'; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/CommonAttributes.php b/library/vendor/HTMLPurifier/HTMLModule/CommonAttributes.php new file mode 100644 index 0000000..7220c14 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/CommonAttributes.php @@ -0,0 +1,32 @@ + array( + 0 => array('Style'), + // 'xml:space' => false, + 'class' => 'Class', + 'id' => 'ID', + 'title' => 'CDATA', + 'contenteditable' => 'ContentEditable', + ), + '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/library/vendor/HTMLPurifier/HTMLModule/Edit.php b/library/vendor/HTMLPurifier/HTMLModule/Edit.php new file mode 100644 index 0000000..a9042a3 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Edit.php @@ -0,0 +1,55 @@ + '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) + + /** + * @type bool + */ + public $defines_child_def = true; + + /** + * @param HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef_Chameleon + */ + 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/library/vendor/HTMLPurifier/HTMLModule/Forms.php b/library/vendor/HTMLPurifier/HTMLModule/Forms.php new file mode 100644 index 0000000..eb0edcf --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Forms.php @@ -0,0 +1,194 @@ + 'Form', + 'Inline' => 'Formctrl', + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + if ($config->get('HTML.Forms')) { + $this->safe = true; + } + + $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#embedded', + '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 + 'isindex', + 'iframe' // legacy items + ); + + // 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 . This one's a little complex + // because it maps to multiple elements. + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/Hypertext.php b/library/vendor/HTMLPurifier/HTMLModule/Hypertext.php new file mode 100644 index 0000000..72d7a31 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Hypertext.php @@ -0,0 +1,40 @@ +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/library/vendor/HTMLPurifier/HTMLModule/Iframe.php b/library/vendor/HTMLPurifier/HTMLModule/Iframe.php new file mode 100644 index 0000000..f7e7c91 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Iframe.php @@ -0,0 +1,51 @@ +get('HTML.SafeIframe')) { + $this->safe = true; + } + $this->addElement( + 'iframe', + 'Inline', + 'Flow', + 'Common', + array( + 'src' => 'URI#embedded', + 'width' => 'Length', + 'height' => 'Length', + 'name' => 'ID', + 'scrolling' => 'Enum#yes,no,auto', + 'frameborder' => 'Enum#0,1', + 'longdesc' => 'URI', + 'marginheight' => 'Pixels', + 'marginwidth' => 'Pixels', + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/Image.php b/library/vendor/HTMLPurifier/HTMLModule/Image.php new file mode 100644 index 0000000..0f5fdb3 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Image.php @@ -0,0 +1,49 @@ +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/library/vendor/HTMLPurifier/HTMLModule/Legacy.php b/library/vendor/HTMLPurifier/HTMLModule/Legacy.php new file mode 100644 index 0000000..86b5299 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Legacy.php @@ -0,0 +1,186 @@ +addElement( + 'basefont', + 'Inline', + 'Empty', + null, + 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'] = 'IAlign'; + $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'; + + // "safe" modifications to "unsafe" elements + // WARNING: If you want to add support for an unsafe, legacy + // attribute, make a new TrustedLegacy module with the trusted + // bit set appropriately + + $form = $this->addBlankElement('form'); + $form->content_model = 'Flow | #PCDATA'; + $form->content_model_type = 'optional'; + $form->attr['target'] = 'FrameTarget'; + + $input = $this->addBlankElement('input'); + $input->attr['align'] = 'IAlign'; + + $legend = $this->addBlankElement('legend'); + $legend->attr['align'] = 'LAlign'; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/List.php b/library/vendor/HTMLPurifier/HTMLModule/List.php new file mode 100644 index 0000000..7a20ff7 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/List.php @@ -0,0 +1,51 @@ + 'List'); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); + $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); + // XXX The wrap attribute is handled by MakeWellFormed. This is all + // quite unsatisfactory, because we generated this + // *specifically* for lists, and now a big chunk of the handling + // is done properly by the List ChildDef. So actually, we just + // want enough information to make autoclosing work properly, + // and then hand off the tricky stuff to the ChildDef. + $ol->wrap = 'li'; + $ul->wrap = 'li'; + $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/library/vendor/HTMLPurifier/HTMLModule/Name.php b/library/vendor/HTMLPurifier/HTMLModule/Name.php new file mode 100644 index 0000000..60c0545 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Name.php @@ -0,0 +1,26 @@ +addBlankElement($name); + $element->attr['name'] = 'CDATA'; + if (!$config->get('HTML.Attr.Name.UseCDATA')) { + $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync(); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/Nofollow.php b/library/vendor/HTMLPurifier/HTMLModule/Nofollow.php new file mode 100644 index 0000000..dc9410a --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Nofollow.php @@ -0,0 +1,25 @@ +addBlankElement('a'); + $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow(); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/library/vendor/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php new file mode 100644 index 0000000..da72225 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php @@ -0,0 +1,20 @@ + array( + 'lang' => 'LanguageCode', + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/Object.php b/library/vendor/HTMLPurifier/HTMLModule/Object.php new file mode 100644 index 0000000..2f9efc5 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Object.php @@ -0,0 +1,62 @@ + to cater to legacy browsers: this + * module does not allow this sort of behavior + */ +class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule +{ + /** + * @type string + */ + public $name = 'Object'; + + /** + * @type bool + */ + public $safe = false; + + /** + * @param HTMLPurifier_Config $config + */ + 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', + null, + array( + 'id' => 'ID', + 'name*' => 'Text', + 'type' => 'Text', + 'value' => 'Text', + 'valuetype' => 'Enum#data,ref,object' + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/Presentation.php b/library/vendor/HTMLPurifier/HTMLModule/Presentation.php new file mode 100644 index 0000000..6458ce9 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Presentation.php @@ -0,0 +1,42 @@ +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/library/vendor/HTMLPurifier/HTMLModule/Proprietary.php b/library/vendor/HTMLPurifier/HTMLModule/Proprietary.php new file mode 100644 index 0000000..5ee3c8e --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Proprietary.php @@ -0,0 +1,40 @@ +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/library/vendor/HTMLPurifier/HTMLModule/Ruby.php b/library/vendor/HTMLPurifier/HTMLModule/Ruby.php new file mode 100644 index 0000000..a0d4892 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/Ruby.php @@ -0,0 +1,36 @@ +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/library/vendor/HTMLPurifier/HTMLModule/SafeEmbed.php b/library/vendor/HTMLPurifier/HTMLModule/SafeEmbed.php new file mode 100644 index 0000000..04e6689 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/SafeEmbed.php @@ -0,0 +1,40 @@ +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', + 'flashvars' => 'Text', + 'wmode' => 'Enum#window,transparent,opaque', + 'name' => 'ID', + ) + ); + $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed(); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/HTMLModule/SafeObject.php b/library/vendor/HTMLPurifier/HTMLModule/SafeObject.php new file mode 100644 index 0000000..1297f80 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/SafeObject.php @@ -0,0 +1,62 @@ +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', + 'codebase' => new HTMLPurifier_AttrDef_Enum( + array( + 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0' + ) + ), + ) + ); + $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/library/vendor/HTMLPurifier/HTMLModule/SafeScripting.php b/library/vendor/HTMLPurifier/HTMLModule/SafeScripting.php new file mode 100644 index 0000000..aea7584 --- /dev/null +++ b/library/vendor/HTMLPurifier/HTMLModule/SafeScripting.php @@ -0,0 +1,40 @@ +get('HTML.SafeScripting'); + $script = $this->addElement( + 'script', + 'Inline', + 'Optional:', // Not `Empty` to not allow to autoclose the #i', '', $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('|(.*?)]*>(.*)|is', $html, $matches); + if ($result) { + // Make sure it's not in a comment + $comment_start = strrpos($matches[1], ''); + if ($comment_start === false || + ($comment_end !== false && $comment_end > $comment_start)) { + return $matches[2]; + } + } + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Lexer/DOMLex.php b/library/vendor/HTMLPurifier/Lexer/DOMLex.php new file mode 100644 index 0000000..ca5f25b --- /dev/null +++ b/library/vendor/HTMLPurifier/Lexer/DOMLex.php @@ -0,0 +1,338 @@ +factory = new HTMLPurifier_TokenFactory(); + } + + /** + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] + */ + 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", '<\\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 + + $options = 0; + if ($config->get('Core.AllowParseManyTags') && defined('LIBXML_PARSEHUGE')) { + $options |= LIBXML_PARSEHUGE; + } + + set_error_handler(array($this, 'muteErrorHandler')); + // loadHTML() fails on PHP 5.3 when second parameter is given + if ($options) { + $doc->loadHTML($html, $options); + } else { + $doc->loadHTML($html); + } + restore_error_handler(); + + $body = $doc->getElementsByTagName('html')->item(0)-> // + getElementsByTagName('body')->item(0); // + + $div = $body->getElementsByTagName('div')->item(0); //
    + $tokens = array(); + $this->tokenizeDOM($div, $tokens, $config); + // If the div has a sibling, that means we tripped across + // a premature
    tag. So remove the div we parsed, + // and then tokenize the rest of body. We can't tokenize + // the sibling directly as we'll lose the tags in that case. + if ($div->nextSibling) { + $body->removeChild($div); + $this->tokenizeDOM($body, $tokens, $config); + } + return $tokens; + } + + /** + * Iterative function that tokenizes a node, putting it into an accumulator. + * To iterate is human, to recurse divine - L. Peter Deutsch + * @param DOMNode $node DOMNode to be tokenized. + * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. + * @return HTMLPurifier_Token of node appended to previously passed tokens. + */ + protected function tokenizeDOM($node, &$tokens, $config) + { + $level = 0; + $nodes = array($level => new HTMLPurifier_Queue(array($node))); + $closingNodes = array(); + do { + while (!$nodes[$level]->isEmpty()) { + $node = $nodes[$level]->shift(); // FIFO + $collect = $level > 0 ? true : false; + $needEndingTag = $this->createStartNode($node, $tokens, $collect, $config); + if ($needEndingTag) { + $closingNodes[$level][] = $node; + } + if ($node->childNodes && $node->childNodes->length) { + $level++; + $nodes[$level] = new HTMLPurifier_Queue(); + foreach ($node->childNodes as $childNode) { + $nodes[$level]->push($childNode); + } + } + } + $level--; + if ($level && isset($closingNodes[$level])) { + while ($node = array_pop($closingNodes[$level])) { + $this->createEndNode($node, $tokens); + } + } + } while ($level > 0); + } + + /** + * Portably retrieve the tag name of a node; deals with older versions + * of libxml like 2.7.6 + * @param DOMNode $node + */ + protected function getTagName($node) + { + if (isset($node->tagName)) { + return $node->tagName; + } else if (isset($node->nodeName)) { + return $node->nodeName; + } else if (isset($node->localName)) { + return $node->localName; + } + return null; + } + + /** + * Portably retrieve the data of a node; deals with older versions + * of libxml like 2.7.6 + * @param DOMNode $node + */ + protected function getData($node) + { + if (isset($node->data)) { + return $node->data; + } else if (isset($node->nodeValue)) { + return $node->nodeValue; + } else if (isset($node->textContent)) { + return $node->textContent; + } + return null; + } + + + /** + * @param DOMNode $node DOMNode to be tokenized. + * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. + * @param bool $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. + * @return bool if the token needs an endtoken + * @todo data and tagName properties don't seem to exist in DOMNode? + */ + protected function createStartNode($node, &$tokens, $collect, $config) + { + // 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) { + $data = $this->getData($node); // Handle variable data property + if ($data !== null) { + $tokens[] = $this->factory->createText($data); + } + return false; + } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) { + // undo libxml's special treatment of )#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 "asdf" but not "asdf " + // 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->parseText( + substr( + $html, + $cursor, + $position_next_lt - $cursor + ), $config + ) + ); + 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->parseText( + substr( + $html, + $cursor + ), $config + ) + ); + 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
    , 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->parseText( + substr($html, $cursor), $config + ) + ); + 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 + * @param string $haystack + * @param string $needle + * @param int $offset + * @param int $length + * @return int + */ + 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 $string Inside of tag excluding name. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array 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 => $this->parseAttr($value, $config)); + } + + // 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 .= ' '; + + $old_cursor = -1; + while ($cursor < $size) { + if ($old_cursor >= $cursor) { + throw new Exception("Infinite loop detected"); + } + $old_cursor = $cursor; + + $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 += 1 + 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->parseAttr($value, $config); + $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/library/vendor/HTMLPurifier/Lexer/PH5P.php b/library/vendor/HTMLPurifier/Lexer/PH5P.php new file mode 100644 index 0000000..1564f28 --- /dev/null +++ b/library/vendor/HTMLPurifier/Lexer/PH5P.php @@ -0,0 +1,4788 @@ +normalize($html, $config, $context); + $new_html = $this->wrapHTML($new_html, $config, $context, false /* no div */); + 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)-> // + getElementsByTagName('body')->item(0) // + , + $tokens, $config + ); + return $tokens; + } +} + +/* + +Copyright 2007 Jeroen van der Meer + +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) + { + $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 (""), + 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' => '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' => '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 below. + 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 below. + 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('&' . rtrim($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'], (string)$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/library/vendor/HTMLPurifier/Node.php b/library/vendor/HTMLPurifier/Node.php new file mode 100644 index 0000000..3995fec --- /dev/null +++ b/library/vendor/HTMLPurifier/Node.php @@ -0,0 +1,49 @@ +data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); + } +} diff --git a/library/vendor/HTMLPurifier/Node/Element.php b/library/vendor/HTMLPurifier/Node/Element.php new file mode 100644 index 0000000..6cbf56d --- /dev/null +++ b/library/vendor/HTMLPurifier/Node/Element.php @@ -0,0 +1,59 @@ + form or the form, i.e. + * is it a pair of start/end tokens or an empty token. + * @bool + */ + public $empty = false; + + public $endCol = null, $endLine = null, $endArmor = array(); + + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { + $this->name = $name; + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toTokenPair() { + // XXX inefficiency here, normalization is not necessary + if ($this->empty) { + return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); + } else { + $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); + $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); + //$end->start = $start; + return array($start, $end); + } + } +} + diff --git a/library/vendor/HTMLPurifier/Node/Text.php b/library/vendor/HTMLPurifier/Node/Text.php new file mode 100644 index 0000000..aec9166 --- /dev/null +++ b/library/vendor/HTMLPurifier/Node/Text.php @@ -0,0 +1,54 @@ +data = $data; + $this->is_whitespace = $is_whitespace; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/PercentEncoder.php b/library/vendor/HTMLPurifier/PercentEncoder.php new file mode 100644 index 0000000..18c8bbb --- /dev/null +++ b/library/vendor/HTMLPurifier/PercentEncoder.php @@ -0,0 +1,111 @@ +preserve[$i] = true; + } + for ($i = 65; $i <= 90; $i++) { // upper-case + $this->preserve[$i] = true; + } + for ($i = 97; $i <= 122; $i++) { // lower-case + $this->preserve[$i] = true; + } + $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 String to be encoded + * @return string 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 String to normalize + * @return string + */ + 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/library/vendor/HTMLPurifier/Printer.php b/library/vendor/HTMLPurifier/Printer.php new file mode 100644 index 0000000..549e4ce --- /dev/null +++ b/library/vendor/HTMLPurifier/Printer.php @@ -0,0 +1,218 @@ +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 string $tag Tag name + * @param array $attr Attribute array + * @return string + */ + protected function start($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) + ); + } + + /** + * Returns an end tag + * @param string $tag Tag name + * @return string + */ + protected function end($tag) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_End($tag) + ); + } + + /** + * Prints a complete element with content inside + * @param string $tag Tag name + * @param string $contents Element contents + * @param array $attr Tag attributes + * @param bool $escape whether or not to escape contents + * @return string + */ + protected function element($tag, $contents, $attr = array(), $escape = true) + { + return $this->start($tag, $attr) . + ($escape ? $this->escape($contents) : $contents) . + $this->end($tag); + } + + /** + * @param string $tag + * @param array $attr + * @return string + */ + protected function elementEmpty($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Empty($tag, $attr) + ); + } + + /** + * @param string $text + * @return string + */ + protected function text($text) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Text($text) + ); + } + + /** + * Prints a simple key/value row in a table. + * @param string $name Key + * @param mixed $value Value + * @return string + */ + 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 String to escape + * @return string + */ + 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 string[] $array List of strings + * @param bool $polite Bool whether or not to add an end before the last + * @return string + */ + 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 object $obj Object to determine class of + * @param string $sec_prefix Further prefix to remove + * @return string + */ + 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/library/vendor/HTMLPurifier/Printer/CSSDefinition.php b/library/vendor/HTMLPurifier/Printer/CSSDefinition.php new file mode 100644 index 0000000..29505fe --- /dev/null +++ b/library/vendor/HTMLPurifier/Printer/CSSDefinition.php @@ -0,0 +1,44 @@ +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/library/vendor/HTMLPurifier/Printer/ConfigForm.css b/library/vendor/HTMLPurifier/Printer/ConfigForm.css new file mode 100644 index 0000000..3ff1a88 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/Printer/ConfigForm.js b/library/vendor/HTMLPurifier/Printer/ConfigForm.js new file mode 100644 index 0000000..cba00c9 --- /dev/null +++ b/library/vendor/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/library/vendor/HTMLPurifier/Printer/ConfigForm.php b/library/vendor/HTMLPurifier/Printer/ConfigForm.php new file mode 100644 index 0000000..33ae113 --- /dev/null +++ b/library/vendor/HTMLPurifier/Printer/ConfigForm.php @@ -0,0 +1,451 @@ +docURL = $doc_url; + $this->name = $name; + $this->compress = $compress; + // initialize sub-printers + $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); + $this->fields[HTMLPurifier_VarParser::C_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 HTMLPurifier_Config|array $config Configuration object of current form state, or an array + * where [0] has an HTML namespace and [1] is being rendered. + * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. + * @param bool $render_controls + * @return string + */ + 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 .= '[Reset]'; + $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 array $directives array of directives to values + * @return string + */ + 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 + * @type HTMLPurifier_Printer + */ + protected $obj; + + /** + * @param HTMLPurifier_Printer $obj Printer to decorate + */ + public function __construct($obj) + { + parent::__construct(); + $this->obj = $obj; + } + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + 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 +{ + /** + * @type int + */ + public $cols = 18; + + /** + * @type int + */ + public $rows = 5; + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + 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; + } + //TODO does this need a break? + case HTMLPurifier_VarParser::ALIST: + $value = implode(PHP_EOL, $value); + break; + case HTMLPurifier_VarParser::HASH: + $nvalue = ''; + foreach ($value as $i => $v) { + if (is_array($v)) { + // HACK + $v = implode(";", $v); + } + $nvalue .= "$i:$v" . PHP_EOL; + } + $value = $nvalue; + break; + default: + $value = ''; + } + } + if ($type === HTMLPurifier_VarParser::C_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 +{ + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + 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/library/vendor/HTMLPurifier/Printer/HTMLDefinition.php b/library/vendor/HTMLPurifier/Printer/HTMLDefinition.php new file mode 100644 index 0000000..ae86391 --- /dev/null +++ b/library/vendor/HTMLPurifier/Printer/HTMLDefinition.php @@ -0,0 +1,324 @@ +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 + * @return string + */ + 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 + * @return string + */ + 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), null, 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 + * @return string + */ + 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 + * @return string + */ + 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 HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element + * @return string + */ + 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', + 'Block: ' . + $this->escape($this->listifyTagLookup($def->block->elements)), + null, + 0 + ); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element( + 'td', + 'Inline: ' . + $this->escape($this->listifyTagLookup($def->inline->elements)), + null, + 0 + ); + + } elseif ($def->type == 'custom') { + + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $def->dtd_regex + ); + + } else { + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $this->escape($this->listifyTagLookup($elements)), + null, + 0 + ); + } + $ret .= $this->end('tr'); + return $ret; + } + + /** + * Listifies a tag lookup table. + * @param array $array Tag lookup array in form of array('tagname' => true) + * @return string + */ + 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 $array List of objects + * @return string + * @todo Also add information about internal state + */ + protected function listifyObjectList($array) + { + ksort($array); + $list = array(); + foreach ($array as $obj) { + $list[] = $this->getClass($obj, 'AttrTransform_'); + } + return $this->listify($list); + } + + /** + * Listifies a hash of attributes to AttrDef classes + * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) + * @return string + */ + protected function listifyAttr($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $obj) { + if ($obj === false) { + continue; + } + $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; + } + return $this->listify($list); + } + + /** + * Creates a heavy header row + * @param string $text + * @param int $num + * @return string + */ + 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/library/vendor/HTMLPurifier/PropertyList.php b/library/vendor/HTMLPurifier/PropertyList.php new file mode 100644 index 0000000..189348f --- /dev/null +++ b/library/vendor/HTMLPurifier/PropertyList.php @@ -0,0 +1,122 @@ +parent = $parent; + } + + /** + * Recursively retrieves the value for a key + * @param string $name + * @throws HTMLPurifier_Exception + */ + 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 + * @param string $name + * @param mixed $value + */ + public function set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * Returns true if a given key exists + * @param string $name + * @return bool + */ + 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. + * @param string $name + */ + 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 bool $force If true, ignores the cache and regenerates the array. + * @return 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. + * @return HTMLPurifier_PropertyList + */ + public function getParent() + { + return $this->parent; + } + + /** + * Sets the parent plist. + * @param HTMLPurifier_PropertyList $plist Parent plist + */ + public function setParent($plist) + { + $this->parent = $plist; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/PropertyListIterator.php b/library/vendor/HTMLPurifier/PropertyListIterator.php new file mode 100644 index 0000000..f68fc8c --- /dev/null +++ b/library/vendor/HTMLPurifier/PropertyListIterator.php @@ -0,0 +1,43 @@ +l = strlen($filter); + $this->filter = $filter; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + 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/library/vendor/HTMLPurifier/Queue.php b/library/vendor/HTMLPurifier/Queue.php new file mode 100644 index 0000000..f58db90 --- /dev/null +++ b/library/vendor/HTMLPurifier/Queue.php @@ -0,0 +1,56 @@ +input = $input; + $this->output = array(); + } + + /** + * Shifts an element off the front of the queue. + */ + public function shift() { + if (empty($this->output)) { + $this->output = array_reverse($this->input); + $this->input = array(); + } + if (empty($this->output)) { + return NULL; + } + return array_pop($this->output); + } + + /** + * Pushes an element onto the front of the queue. + */ + public function push($x) { + array_push($this->input, $x); + } + + /** + * Checks if it's empty. + */ + public function isEmpty() { + return empty($this->input) && empty($this->output); + } +} diff --git a/library/vendor/HTMLPurifier/SOURCE b/library/vendor/HTMLPurifier/SOURCE new file mode 100644 index 0000000..56f423d --- /dev/null +++ b/library/vendor/HTMLPurifier/SOURCE @@ -0,0 +1,10 @@ +GLOBIGNORE=$0; rm -rf * +rm ../HTMLPurifier*.php + +curl https://codeload.github.com/ezyang/htmlpurifier/tar.gz/v4.16.0 -o htmlpurifier-4.16.0.tar.gz +tar xzf htmlpurifier-4.16.0.tar.gz --strip-components 1 htmlpurifier-4.16.0/LICENSE +tar xzf htmlpurifier-4.16.0.tar.gz --strip-components 1 htmlpurifier-4.16.0/VERSION +tar xzf htmlpurifier-4.16.0.tar.gz -C ../ --strip-components 2 htmlpurifier-4.16.0/library/HTMLPurifier.php +tar xzf htmlpurifier-4.16.0.tar.gz -C ../ --strip-components 2 htmlpurifier-4.16.0/library/HTMLPurifier.autoload.php +tar xzf htmlpurifier-4.16.0.tar.gz --wildcards --strip-components 3 htmlpurifier-4.16.0/library/HTMLPurifier/* +rm htmlpurifier-4.16.0.tar.gz diff --git a/library/vendor/HTMLPurifier/Strategy.php b/library/vendor/HTMLPurifier/Strategy.php new file mode 100644 index 0000000..e1ff3b7 --- /dev/null +++ b/library/vendor/HTMLPurifier/Strategy.php @@ -0,0 +1,26 @@ +strategies as $strategy) { + $tokens = $strategy->execute($tokens, $config, $context); + } + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Strategy/Core.php b/library/vendor/HTMLPurifier/Strategy/Core.php new file mode 100644 index 0000000..4414c17 --- /dev/null +++ b/library/vendor/HTMLPurifier/Strategy/Core.php @@ -0,0 +1,17 @@ +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/library/vendor/HTMLPurifier/Strategy/FixNesting.php b/library/vendor/HTMLPurifier/Strategy/FixNesting.php new file mode 100644 index 0000000..6fa673d --- /dev/null +++ b/library/vendor/HTMLPurifier/Strategy/FixNesting.php @@ -0,0 +1,181 @@ +getHTMLDefinition(); + + $excludes_enabled = !$config->get('Core.DisableExcludes'); + + // 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 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($definition->info_parent_def->excludes); + + // variable that contains the start token while we are processing + // nodes. This enables error reporting to do its job + $node = $top_node; + // dummy token + list($token, $d) = $node->toTokenPair(); + $context->register('CurrentNode', $node); + $context->register('CurrentToken', $token); + + //####################################################################// + // Loop + + // We need to implement a post-order traversal iteratively, to + // avoid running into stack space limits. This is pretty tricky + // to reason about, so we just manually stack-ify the recursive + // variant: + // + // function f($node) { + // foreach ($node->children as $child) { + // f($child); + // } + // validate($node); + // } + // + // Thus, we will represent a stack frame as array($node, + // $is_inline, stack of children) + // e.g. array_reverse($node->children) - already processed + // children. + + $parent_def = $definition->info_parent_def; + $stack = array( + array($top_node, + $parent_def->descendants_are_inline, + $parent_def->excludes, // exclusions + 0) + ); + + while (!empty($stack)) { + list($node, $is_inline, $excludes, $ix) = array_pop($stack); + // recursive call + $go = false; + $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; + while (isset($node->children[$ix])) { + $child = $node->children[$ix++]; + if ($child instanceof HTMLPurifier_Node_Element) { + $go = true; + $stack[] = array($node, $is_inline, $excludes, $ix); + $stack[] = array($child, + // ToDo: I don't think it matters if it's def or + // child_def, but double check this... + $is_inline || $def->descendants_are_inline, + empty($def->excludes) ? $excludes + : array_merge($excludes, $def->excludes), + 0); + break; + } + }; + if ($go) continue; + list($token, $d) = $node->toTokenPair(); + // base case + if ($excludes_enabled && isset($excludes[$node->name])) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); + } else { + // XXX I suppose it would be slightly more efficient to + // avoid the allocation here and have children + // strategies handle it + $children = array(); + foreach ($node->children as $child) { + if (!$child->dead) $children[] = $child; + } + $result = $def->child->validateChildren($children, $config, $context); + if ($result === true) { + // nop + $node->children = $children; + } elseif ($result === false) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); + } else { + $node->children = $result; + if ($e) { + // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators + if (empty($result) && !empty($children)) { + $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); + } else if ($result != $children) { + $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); + } + } + } + } + } + + //####################################################################// + // Post-processing + + // remove context variables + $context->destroy('IsInline'); + $context->destroy('CurrentNode'); + $context->destroy('CurrentToken'); + + //####################################################################// + // Return + + return HTMLPurifier_Arborize::flatten($node, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/library/vendor/HTMLPurifier/Strategy/MakeWellFormed.php b/library/vendor/HTMLPurifier/Strategy/MakeWellFormed.php new file mode 100644 index 0000000..a6eb09e --- /dev/null +++ b/library/vendor/HTMLPurifier/Strategy/MakeWellFormed.php @@ -0,0 +1,659 @@ +getHTMLDefinition(); + + // local variables + $generator = new HTMLPurifier_Generator($config, $context); + $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); + // used for autoclose early abortion + $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config); + $e = $context->get('ErrorCollector', true); + $i = false; // injector index + list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens); + if ($token === NULL) { + return array(); + } + $reprocess = false; // whether or not to reprocess the same token + $stack = array(); + + // member variables + $this->stack =& $stack; + $this->tokens =& $tokens; + $this->token =& $token; + $this->zipper =& $zipper; + $this->config = $config; + $this->context = $context; + + // context variables + $context->register('CurrentNesting', $stack); + $context->register('InputZipper', $zipper); + $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 (!$injector) { + continue; + } + 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 reprocessing: + // 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 (;; + // only increment if we don't need to reprocess + $reprocess ? $reprocess = false : $token = $zipper->next($token)) { + + // check for a rewind + if (is_int($i)) { + // 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_offset = $this->injectors[$i]->getRewindOffset(); + if (is_int($rewind_offset)) { + for ($j = 0; $j < $rewind_offset; $j++) { + if (empty($zipper->front)) break; + $token = $zipper->prev($token); + // indicate that other injectors should not process this token, + // but we need to reprocess it. See Note [Injector skips] + unset($token->skip[$i]); + $token->rewind = $i; + if ($token instanceof HTMLPurifier_Token_Start) { + array_pop($this->stack); + } elseif ($token instanceof HTMLPurifier_Token_End) { + $this->stack[] = $token->start; + } + } + } + $i = false; + } + + // handle case of document end + if ($token === NULL) { + // kill processing if stack is empty + if (empty($this->stack)) { + break; + } + + // peek + $top_nesting = array_pop($this->stack); + $this->stack[] = $top_nesting; + + // send error [TagClosedSuppress] + 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 + $token = new HTMLPurifier_Token_End($top_nesting->name); + + // punt! + $reprocess = true; + continue; + } + + //echo '
    '; printZipper($zipper, $token);//printTokens($this->stack); + //flush(); + + // 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])) { + // See Note [Injector skips] + continue; + } + if ($token->rewind !== null && $token->rewind !== $i) { + continue; + } + // XXX fuckup + $r = $token; + $injector->handleText($r); + $token = $this->processToken($r, $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, + $token->line, + $token->col, + $token->armor + ); + $ok = true; + } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { + // claims to be empty but really is a start tag + // NB: this assignment is required + $old_token = $token; + $token = new HTMLPurifier_Token_End($token->name); + $token = $this->insertBefore( + new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor) + ); + // 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)) { + + // Performance note: you might think that it's rather + // inefficient, recalculating the autoclose information + // for every tag that a token closes (since when we + // do an autoclose, we push a new token into the + // stream and then /process/ that, before + // re-processing this token.) But this is + // necessary, because an injector can make an + // arbitrary transformations to the autoclosing + // tokens we introduce, so things may have changed + // in the meantime. Also, doing the inefficient thing is + // "easy" to reason about (for certain perverse definitions + // of "easy") + + $parent = array_pop($this->stack); + $this->stack[] = $parent; + + $parent_def = null; + $parent_elements = null; + $autoclose = false; + if (isset($definition->info[$parent->name])) { + $parent_def = $definition->info[$parent->name]; + $parent_elements = $parent_def->child->getAllowedElements($config); + $autoclose = !isset($parent_elements[$token->name]); + } + + if ($autoclose && $definition->info[$token->name]->wrap) { + // Check if an element can be wrapped by another + // element to make it valid in a context (for + // example,