Compare commits

...

39 Commits

Author SHA1 Message Date
5433811176 Use dedicated exceptions if unlinking failed for reasons other than the
file not existing
2025-03-25 14:57:29 +01:00
b9d82d672a Update years covered in license 2025-01-22 15:20:14 +01:00
9948ee5d3d Add clsas for MD_STD_STRINGS more obscure string operations 2025-01-16 14:21:04 +01:00
be437fcf5f Add function to get next strpos of any of a specified set of needles 2024-12-25 18:41:28 +01:00
fa2985463f Use more explicit type hints for improved static code analysis 2024-09-22 16:52:21 +02:00
bce4268a70 Merge branch 'master' of gitea:museum-digital/MD_STD 2024-09-05 14:25:14 +02:00
92c942aab3 Remove whitespaces in isbns before validating 2024-09-05 14:24:18 +02:00
6174c5454e Clean em dashes from ISBNs during sanitization 2024-08-14 16:41:55 +02:00
a57036789e Fix implicit array creation 2024-07-12 02:56:27 +02:00
fc727932ca Make function in MD_STD_HTML_TEST final 2024-07-12 02:54:26 +02:00
40d83ce5b0 Add class MD_STD_HTML_TEST for validating HTML outputs 2024-07-12 02:31:42 +02:00
cbc66c4140 Improve test coverage for MD_STD_SEC 2024-07-11 15:32:50 +02:00
11faeaa7e7 Improve test coverage 2024-07-11 14:53:05 +02:00
fb008e1b59 Add function for ensuring all input in an array is strings 2024-07-09 16:43:21 +02:00
cd46a3ec73 Add wrapper around file_put_contents 2024-06-24 16:53:01 +02:00
94dfa17290 Fix code smells 2024-06-11 21:57:56 +02:00
d4918dd893 Handle dates outside of strtotime()'s range in date_to_int, int_to_date
Close #9
2024-05-05 00:48:04 +02:00
fb1372d193 Use MD_STD::strtotime() over strtotime() 2024-05-04 01:19:52 +02:00
63ac1b296e Add functions for transferring dates to ints and vice versa 2024-05-03 17:42:32 +02:00
8c1050f40a Add wrapper around MD_STD::strtotime() that will throw an expected
exception
2024-01-30 01:21:51 +01:00
2bea372973 Use phpstan-specific comments for main sanitization + validation
functions
2024-01-14 22:18:48 +01:00
69e6850c16 Write prettier error message in MDFailedToCreateDirectory 2023-11-27 01:31:16 +01:00
8006695093 Throw a specific exception if MD_STD::mkdir fails
Close #8
2023-11-27 01:30:43 +01:00
db31822a3f Use empty() over === false to also return error in case of empty strings 2023-11-10 16:17:20 +01:00
0fb368b96d Extend MD_STD_IN::sanitize_url to automatically set protocol / scheme
names in lowercase
2023-11-09 16:40:28 +01:00
66e704de47 Extend tests for MD_STD_IN considerably, fix some edge cases 2023-11-08 21:24:23 +01:00
a03f072a69 Add function for validating ZIP codes (somewhat) 2023-11-08 02:18:34 +01:00
d83ed2d0eb Improve indentation in phpunit 2023-11-07 22:52:23 +01:00
2c58e0554b Improve coverage of MD_STD_IN 2023-11-07 22:50:18 +01:00
1f2f63c9af Set beStrictAboutOutputDuringTests=true in phpunit.xml 2023-11-06 23:49:27 +01:00
c9dce8f782 Annotate the available tests as @small 2023-11-06 23:26:21 +01:00
0c0d059dd3 Add testsuite for /tests directory in phpunit.xml 2023-11-06 23:03:35 +01:00
5d80f82040 Remove printer class / use testdox by default in phpunit setup 2023-11-06 23:02:44 +01:00
5c2c1a47cc Fully ensure all URL components are present for rewriting 2023-11-05 23:37:28 +01:00
ae12cfdf0f Add tests for MD_STD_IN::sanitize_url() and ensure it supports rewriting
unencoded cyrillic inputs

Close #7
2023-11-05 23:29:14 +01:00
2176e7312b Remove MD_STD_CACHE open_redis_default() 2023-10-05 16:58:48 +02:00
3ece870f0c Require externally set up redis connection for caching + serving full pages via
redis
2023-10-05 16:45:35 +02:00
b143845aea Fix type-safety issues around curl in new checking HTTP status function 2023-08-18 15:12:45 +02:00
88458df949 Add general abstract classes for tests, starting with test classes for
RSS feeds
2023-08-18 15:09:58 +02:00
24 changed files with 2881 additions and 108 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.phpunit.cache/

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 museum-digital
Copyright (c) 2023-2025 museum-digital
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

500
assets/xsd/Rss2.xsd Normal file
View File

@ -0,0 +1,500 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
XML Schema for RSS v2.0
Copyright (C) 2003-2008 Jorgen Thelin
Microsoft Public License (Ms-PL)
This license governs use of the accompanying software.
If you use the software, you accept this license.
If you do not accept the license, do not use the software.
1. Definitions
The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
A "contribution" is the original software, or any additions or changes to the software.
A "contributor" is any person that distributes its contribution under this license.
"Licensed patents" are a contributor's patent claims that read directly on its contribution.
2. Grant of Rights
(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
3. Conditions and Limitations
(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="unqualified"
version="2.0.2.16">
<xs:annotation>
<xs:documentation>XML Schema for RSS v2.0 feed files.</xs:documentation>
<xs:documentation>Project home: http://www.codeplex.com/rss2schema/ </xs:documentation>
<xs:documentation>Based on the RSS 2.0 specification document at http://cyber.law.harvard.edu/rss/rss.html </xs:documentation>
<xs:documentation>Author: Jorgen Thelin</xs:documentation>
<xs:documentation>Revision: 16</xs:documentation>
<xs:documentation>Date: 01-Nov-2008</xs:documentation>
<xs:documentation>Feedback to: http://www.codeplex.com/rss2schema/WorkItem/List.aspx </xs:documentation>
</xs:annotation>
<xs:element name="rss">
<xs:complexType>
<xs:sequence>
<xs:element name="channel" type="RssChannel"/>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="version" type="xs:decimal" use="required" fixed="2.0"/>
<xs:anyAttribute namespace="##any"/>
</xs:complexType>
</xs:element>
<xs:complexType name="RssItem">
<xs:annotation>
<xs:documentation>An item may represent a "story" -- much like a story in a newspaper or magazine; if so its description is a synopsis of the story, and the link points to the full story. An item may also be complete in itself, if so, the description contains the text (entity-encoded HTML is allowed), and the link and title may be omitted.</xs:documentation>
</xs:annotation>
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="title" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>The title of the item.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="description" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>The item synopsis.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="link" type="xs:anyURI" minOccurs="0">
<xs:annotation>
<xs:documentation>The URL of the item.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="author" type="EmailAddress" minOccurs="0">
<xs:annotation>
<xs:documentation>Email address of the author of the item.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="category" type="Category" minOccurs="0">
<xs:annotation>
<xs:documentation>Includes the item in one or more categories. </xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="comments" type="xs:anyURI" minOccurs="0">
<xs:annotation>
<xs:documentation>URL of a page for comments relating to the item.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="enclosure" type="Enclosure" minOccurs="0">
<xs:annotation>
<xs:documentation>Describes a media object that is attached to the item.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="guid" type="Guid" minOccurs="0">
<xs:annotation>
<xs:documentation>guid or permalink URL for this entry</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="pubDate" type="Rfc822FormatDate" minOccurs="0">
<xs:annotation>
<xs:documentation>Indicates when the item was published.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="source" type="Source" minOccurs="0">
<xs:annotation>
<xs:documentation>The RSS channel that the item came from.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Extensibility element.</xs:documentation>
</xs:annotation>
</xs:any>
</xs:choice>
</xs:sequence>
<xs:anyAttribute namespace="##any"/>
</xs:complexType>
<xs:complexType name="RssChannel">
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="title" type="xs:string">
<xs:annotation>
<xs:documentation>The name of the channel. It's how people refer to your service. If you have an HTML website that contains the same information as your RSS file, the title of your channel should be the same as the title of your website.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="link" type="xs:anyURI">
<xs:annotation>
<xs:documentation>The URL to the HTML website corresponding to the channel.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="description" type="xs:string">
<xs:annotation>
<xs:documentation>Phrase or sentence describing the channel.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="language" type="xs:language" minOccurs="0">
<xs:annotation>
<xs:documentation>The language the channel is written in. This allows aggregators to group all Italian language sites, for example, on a single page. A list of allowable values for this element, as provided by Netscape, is here. You may also use values defined by the W3C.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="copyright" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>Copyright notice for content in the channel.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="managingEditor" type="EmailAddress" minOccurs="0">
<xs:annotation>
<xs:documentation>Email address for person responsible for editorial content.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="webMaster" type="EmailAddress" minOccurs="0">
<xs:annotation>
<xs:documentation>Email address for person responsible for technical issues relating to channel.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="pubDate" type="Rfc822FormatDate" minOccurs="0">
<xs:annotation>
<xs:documentation>The publication date for the content in the channel. All date-times in RSS conform to the Date and Time Specification of RFC 822, with the exception that the year may be expressed with two characters or four characters (four preferred).</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="lastBuildDate" type="Rfc822FormatDate" minOccurs="0">
<xs:annotation>
<xs:documentation>The last time the content of the channel changed.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="category" type="Category" minOccurs="0">
<xs:annotation>
<xs:documentation>Specify one or more categories that the channel belongs to.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="generator" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>A string indicating the program used to generate the channel.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="docs" type="xs:anyURI" minOccurs="0">
<xs:annotation>
<xs:documentation>A URL that points to the documentation for the format used in the RSS file. It's probably a pointer to this page. It's for people who might stumble across an RSS file on a Web server 25 years from now and wonder what it is.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="cloud" type="Cloud" minOccurs="0">
<xs:annotation>
<xs:documentation>Allows processes to register with a cloud to be notified of updates to the channel, implementing a lightweight publish-subscribe protocol for RSS feeds.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="ttl" type="xs:nonNegativeInteger" minOccurs="0">
<xs:annotation>
<xs:documentation>ttl stands for time to live. It's a number of minutes that indicates how long a channel can be cached before refreshing from the source.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="image" type="Image" minOccurs="0">
<xs:annotation>
<xs:documentation>Specifies a GIF, JPEG or PNG image that can be displayed with the channel.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="rating" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>The PICS rating for the channel.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="textInput" type="TextInput" minOccurs="0">
<xs:annotation>
<xs:documentation>Specifies a text input box that can be displayed with the channel.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="skipHours" type="SkipHoursList" minOccurs="0">
<xs:annotation>
<xs:documentation>A hint for aggregators telling them which hours they can skip.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="skipDays" type="SkipDaysList" minOccurs="0">
<xs:annotation>
<xs:documentation>A hint for aggregators telling them which days they can skip.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Extensibility element.</xs:documentation>
</xs:annotation>
</xs:any>
</xs:choice>
<xs:element name="item" type="RssItem" minOccurs="1" maxOccurs="unbounded">
<!--
HACK: According to the RSS 2.0 spec, it should strictly be possible to have zero item elements,
but this makes the schema non-deterministic with regard to extensibility elements
so for the moment we undid bug-fix 10231 and set minOccurs=1 to work around this problem.
-->
</xs:element>
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Extensibility element.</xs:documentation>
</xs:annotation>
</xs:any>
</xs:sequence>
<xs:anyAttribute namespace="##any"/>
</xs:complexType>
<xs:simpleType name="SkipHour">
<xs:annotation>
<xs:documentation>A time in GMT when aggregators should not request the channel data. The hour beginning at midnight is hour zero.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:nonNegativeInteger">
<xs:minInclusive value="0"/>
<xs:maxInclusive value="23"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="SkipHoursList">
<xs:sequence>
<xs:element name="hour" type="SkipHour" minOccurs="0" maxOccurs="24"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="SkipDay">
<xs:annotation>
<xs:documentation>A day when aggregators should not request the channel data.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:enumeration value="Monday"/>
<xs:enumeration value="Tuesday"/>
<xs:enumeration value="Wednesday"/>
<xs:enumeration value="Thursday"/>
<xs:enumeration value="Friday"/>
<xs:enumeration value="Saturday"/>
<xs:enumeration value="Sunday"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="SkipDaysList">
<xs:sequence>
<xs:element name="day" type="SkipDay" minOccurs="0" maxOccurs="7">
<xs:annotation>
<xs:documentation>A time in GMT, when aggregators should not request the channel data. The hour beginning at midnight is hour zero.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Category">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="domain" type="xs:string" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="Image">
<xs:all>
<xs:element name="url" type="xs:anyURI">
<xs:annotation>
<xs:documentation>The URL of the image file.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="title" type="xs:string">
<xs:annotation>
<xs:documentation>Describes the image, it's used in the ALT attribute of the HTML &lt;img&gt; tag when the channel is rendered in HTML.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="link" type="xs:anyURI">
<xs:annotation>
<xs:documentation>The URL of the site, when the channel is rendered, the image is a link to the site. (Note, in practice the image &lt;title&gt; and &lt;link&gt; should have the same value as the channel's &lt;title&gt; and &lt;link&gt;. </xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="width" type="ImageWidth" default="88" minOccurs="0">
<xs:annotation>
<xs:documentation>The width of the image in pixels.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="height" type="ImageHeight" default="31" minOccurs="0">
<xs:annotation>
<xs:documentation>The height of the image in pixels.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="description" type="xs:string" minOccurs="0">
<xs:annotation>
<xs:documentation>Text that is included in the TITLE attribute of the link formed around the image in the HTML rendering.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
</xs:complexType>
<xs:simpleType name="ImageHeight">
<xs:annotation>
<xs:documentation>The height of the image in pixels.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:positiveInteger">
<xs:maxInclusive value="400"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ImageWidth">
<xs:annotation>
<xs:documentation>The width of the image in pixels.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:positiveInteger">
<xs:maxInclusive value="144"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="Cloud">
<xs:annotation>
<xs:documentation>Specifies a web service that supports the rssCloud interface which can be implemented in HTTP-POST, XML-RPC or SOAP 1.1. Its purpose is to allow processes to register with a cloud to be notified of updates to the channel, implementing a lightweight publish-subscribe protocol for RSS feeds.</xs:documentation>
</xs:annotation>
<xs:attribute name="domain" type="xs:string" use="required"/>
<xs:attribute name="port" type="xs:positiveInteger" use="required"/>
<xs:attribute name="path" type="xs:string" use="required"/>
<xs:attribute name="registerProcedure" type="xs:string" use="required"/>
<xs:attribute name="protocol" type="CloudProtocol" use="required"/>
</xs:complexType>
<xs:simpleType name="CloudProtocol">
<xs:restriction base="xs:string">
<xs:enumeration value="xml-rpc"/>
<xs:enumeration value="http-post"/>
<xs:enumeration value="soap"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="TextInput">
<xs:annotation>
<xs:documentation>The purpose of this element is something of a mystery! You can use it to specify a search engine box. Or to allow a reader to provide feedback. Most aggregators ignore it.</xs:documentation>
</xs:annotation>
<xs:all>
<xs:element name="title" type="xs:string">
<xs:annotation>
<xs:documentation>The label of the Submit button in the text input area.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="description" type="xs:string">
<xs:annotation>
<xs:documentation>Explains the text input area.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="name" type="xs:string">
<xs:annotation>
<xs:documentation>The name of the text object in the text input area.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="link" type="xs:anyURI">
<xs:annotation>
<xs:documentation>The URL of the CGI script that processes text input requests.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
</xs:complexType>
<xs:simpleType name="EmailAddress">
<xs:annotation>
<xs:documentation>Using the regexp definiton of E-Mail Address by Lucadean from the .NET RegExp Pattern Repository at http://www.3leaf.com/default/NetRegExpRepository.aspx </xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="([a-zA-Z0-9_\-])([a-zA-Z0-9_\-\.]*)@(\[((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}|((([a-zA-Z0-9\-]+)\.)+))([a-zA-Z]{2,}|(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\])"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="Rfc822FormatDate">
<xs:annotation>
<xs:documentation>A date-time displayed in RFC-822 format.</xs:documentation>
<xs:documentation>Using the regexp definiton of rfc-822 date by Sam Ruby at http://www.intertwingly.net/blog/1360.html </xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:pattern value="(((Mon)|(Tue)|(Wed)|(Thu)|(Fri)|(Sat)|(Sun)), *)?\d\d? +((Jan)|(Feb)|(Mar)|(Apr)|(May)|(Jun)|(Jul)|(Aug)|(Sep)|(Oct)|(Nov)|(Dec)) +\d\d(\d\d)? +\d\d:\d\d(:\d\d)? +(([+\-]?\d\d\d\d)|(UT)|(GMT)|(EST)|(EDT)|(CST)|(CDT)|(MST)|(MDT)|(PST)|(PDT)|\w)"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="Source">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="url" type="xs:anyURI"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="Enclosure">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="url" type="xs:anyURI" use="required">
<xs:annotation>
<xs:documentation>URL where the enclosure is located</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="length" type="xs:nonNegativeInteger" use="required">
<xs:annotation>
<xs:documentation>Size in bytes</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="type" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>MIME media-type of the enclosure</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="Guid">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="isPermaLink" type="xs:boolean" use="optional" default="true"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<!--
TODO:
- Need to add regexp pattern for MIME media-type value of tEnclosure/type
- Need to add regexp pattern for checking contents of guid is a URL when isPermaLink=true"
- Need to add some form of constraint to check on an item that one, or other, or both of title and description are present.
However, I'm not sure it is possible to represent these constraints in XML Schema language alone.
- Need some way to enforce cardinality constraints preventing repeated elements in channels or items
- Unfortunately the bug-fix for issue 10231 made this schema non-deterministic with respect to extensibitity elements.
We can't tell whether an extension element in tRssChannel is within the choice or after the item elements.
Need to reconsider the solution to bug-fix 10231.
-->
<!--
Change Log:
Date Revision Description
31-Mar-2003 1 Initial version released for comment
31-Mar-2003 2 Changes based on feedback from Gudge:
- Remove targetNamespace="" and use elemenfFormDefault="unqualified" instead
- Use namespace="##other" on <any>'s to create a more deterministic data model.
- Added missing xs:documentation inside xs:annotation at the schema level.
- Use xs:language for ISO Language Codes in <language> element.
- Change guid to a single declaration. This loses some of the checking of the
URL when the contents of the guid is a permaLink, so we will need to add
that back in with a regexp pattern.
14-Apr-2003 3 Changes to solve some element ordering problems.
- Use xs:all in place of xs:sequence to support flexible ordering of elements.
Although the ordering constraints for elements is not clear from the
original specification, the custom and practice seems to be that
element ordering is freeform.
- Use elemenfFormDefault="qualified" for explicit intent.
15-Apr-2003 4 Changes to solve some element ordering problems.
- Use xs:choice in place of xs:all as previous usage of <all> was invalid.
This creates the problem that unsufficient constraints can be applied
by the schema - for example, it can't prevent two title elements for an item.
- Use elemenfFormDefault="unqualified" for to get the correct behavious
when importing and combining schemas.
15-Apr-2003 5 Putting the extensibility element inside the repeating choice solves
all problems with element ordering.
15-Apr-2003 6 - skipHours and skipDays should contain a nested list of values,
not just a single value.
- Added version attribute to schema definition.
- Corrected type of the cloud element
25-Apr-2003 7 - Add regexp for RFC-822 date suggested by Sam Ruby
- I had to leave the base type of the tRfc822FormatDate type
as xs:string due to the problems with using
a pattern with xs:dateTime described at
http://www.thearchitect.co.uk/weblog/archives/2003/04/000142.html
19-Jun-2003 8 - Fixed a bug the Oxygen XML Editor spotted in the regexp for RFC-822 dates
23-Jun-2003 9 - Added legal boilerplate license text for LGPL.
- Minor formatting changes.
24-Jun-2003 10 - Missing types for item/title and item/description - Spotted by Andreas Schwotzer.
01-Jan-2008 11 - Copy made available under the Microsoft Public License (MS-PL).
25-May-2008 12 - Bug fix 10231 from Ken Gruven - channel can contain zero or more items.
06-Sep-2008 13 - Fixed tab-space whitespace issues. Now always use spaces.
- Undid the fix for bug-fix 10231 since it made the schema non-deterministic
with respect to extensibility eleemnts in tRssChannel - need to reconsider the fix.
08-Sep-2008 14 - Removed 't' prefixes from type names to improve class names
that get code-generated from the schema.
22-Sep-2008 15 - Move type def for rss element in-line for improved compativility with Java 1.6 tools.
01-Nov-2008 16 - Added the missing rating element from the spec to RssChannel.
-->
</xs:schema>

View File

@ -0,0 +1,18 @@
<?PHP
declare(strict_types = 1);
/**
* Reports a failure to create a new directory.
*/
final class MDFailedToCreateDirectory extends Exception {
/**
* Error message.
*
* @return string
*/
public function errorMessage() {
//error message
return 'Failed to create directory';
}
}

View File

@ -0,0 +1,18 @@
<?PHP
declare(strict_types = 1);
/**
* Reports a failure to create a new directory.
*/
final class MDFailedToDeleteFile extends Exception {
/**
* Error message.
*
* @return string
*/
public function errorMessage() {
//error message
return 'Failed to delete file';
}
}

View File

@ -0,0 +1,18 @@
<?PHP
declare(strict_types = 1);
/**
* Reports a failure due to insufficient permissions in the file system.
*/
final class MDFilePermissionsException extends Exception {
/**
* Error message.
*
* @return string
*/
public function errorMessage() {
//error message
return 'Insufficient file system permissions';
}
}

0
phpstan-baseline.neon Normal file
View File

12
phpstan.neon Normal file
View File

@ -0,0 +1,12 @@
parameters:
level: 8
bootstrapFiles:
- ./tests/bootstrap.php
paths:
- src
- tests
dynamicConstantNames:
- DATABASENAME
- DATABASENAME_NODA
includes:
- phpstan-baseline.neon

14
phpunit.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" backupGlobals="false" beStrictAboutChangesToGlobalState="true" beStrictAboutOutputDuringTests="true" bootstrap="tests/bootstrap.php" cacheResult="false" colors="true" enforceTimeLimit="true" executionOrder="depends,defects" failOnRisky="true" failOnWarning="true" processIsolation="true" stopOnError="true" stopOnFailure="true" stopOnIncomplete="true" stopOnSkipped="true" stopOnRisky="true" testdox="true" timeoutForSmallTests="1" timeoutForMediumTests="10" timeoutForLargeTests="60" cacheDirectory=".phpunit.cache" backupStaticProperties="false" requireCoverageMetadata="false" beStrictAboutCoverageMetadata="false" displayDetailsOnTestsThatTriggerWarnings="true" >
<testsuites>
<testsuite name="tests">
<directory>tests/</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>

View File

@ -13,9 +13,9 @@ declare(strict_types = 1);
*/
final class MD_JAIL {
const STATUS_NONE = 0;
const STATUS_STARTED = 1;
const STATUS_SPECIFIED = 2; // Determines that everything is fine.
public const STATUS_NONE = 0;
public const STATUS_STARTED = 1;
public const STATUS_SPECIFIED = 2; // Determines that everything is fine.
/** @var integer */
private int $_status = self::STATUS_NONE;

View File

@ -33,6 +33,27 @@ final class MD_STD {
}
/**
* Wrapper around file_put_contents, that provides catches errors on it and returns
* with type safety.
*
* @param non-empty-string $filename Filepath of the file to read.
* @param string $data Data to write.
*
* @return integer
*/
public static function file_put_contents(string $filename, string $data):int {
$status = \file_put_contents($filename, $data);
if ($status === false) {
throw new MDFileIsNotWritable("File {$filename} is not writable");
}
return $status;
}
/**
* Returns the real path of a relative file path. Throws an error rather than
* returning the default false.
@ -71,7 +92,7 @@ final class MD_STD {
return;
}
if (\mkdir($pathname, $mode, $recursive) === false) {
throw new Exception("Failed to create directory: $pathname");
throw new MDFailedToCreateDirectory("Failed to create directory: $pathname");
}
}
@ -89,7 +110,15 @@ final class MD_STD {
public static function unlink(string $filename):void {
if (\unlink($filename) === false) {
throw new Exception("Failed to delete: $filename");
if (!\is_file($filename)) {
throw new MDFileDoesNotExist("Failed to delete $filename, it did not exist");
}
else if (!\is_writable(dirname($filename))) {
throw new MDFilePermissionsException("Failed to delete $filename");
}
else {
throw new MDFailedToDeleteFile("Failed to delete $filename");
}
}
}
@ -152,26 +181,6 @@ final class MD_STD {
}
/**
* Function checking if a string starts with another.
* DEPRECATED. Can be replaced by PHP8's str_starts_with.
*
* @param non-empty-string $haystack String to check.
* @param non-empty-string $needle Potential start of $haystack.
*
* @return boolean
*/
public static function startsWith(string $haystack, string $needle):bool {
if (substr($haystack, 0, \strlen($needle)) === $needle) {
return true;
}
else {
return false;
}
}
/**
* Function checking if a string starts with any input from the input array.
*
@ -303,6 +312,27 @@ final class MD_STD {
}
/**
* Wrapper around self::strtotime() that will return MDParseException (an expected
* exception) rather than MDInvalidInputDate (an unexpected exception).
*
* @param string $datetime String to convert.
*
* @return integer
*/
public static function strtotime_for_public_apis(string $datetime):int {
try {
$output = self::strtotime($datetime);
}
catch (MDInvalidInputDate $e) {
throw new MDParseException($e->getMessage());
}
return $output;
}
/**
* Initializes a curl request with the given presets.
*
@ -419,6 +449,44 @@ final class MD_STD {
}
/**
* Check if a URL is reachable (200) using curl.
*
* @param string $url URL to validate.
*
* @return boolean
*/
public static function checkUrlIsReachable(string $url):bool {
if (empty($url = MD_STD_IN::sanitize_url($url))) {
throw new MDInvalidUrl("Input URL cannot be empty");
}
$ch = self::curl_init($url, 5000);
curl_setopt_array($ch, [
CURLOPT_AUTOREFERER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_ENCODING => "",
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 1,
CURLOPT_NOBODY => true,
CURLOPT_TIMEOUT => 5,
// It's very important to let other webmasters know who's probing their servers.
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.2) Gecko/20100101 Firefox/10.0.2',
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
CURLOPT_TCP_FASTOPEN => true,
]);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200) {
return false;
}
return true;
}
/**
* Sets and returns user language based on a session cookie.
*
@ -786,7 +854,11 @@ final class MD_STD {
*/
public static function string_to_color_code(string $str):string {
return \substr(\dechex(\crc32($str)), 0, 6);
$output = \dechex(\crc32($str));
if (\strlen($output) < 6) return '000000';
return \substr($output, 0, 6);
}
@ -828,10 +900,12 @@ final class MD_STD {
$max = count($input);
$offset = 0;
$sizePerEntry = max($size - 1, 1); // Size - 1 is expected, but size below 1 ends in endless loops
while ($offset < $max) {
$cur = array_slice($input, $offset, $size - 1);
$cur = array_slice($input, $offset, $sizePerEntry);
if (!empty($cur)) $output[] = $cur;
$offset += $size - 1;
$offset += $sizePerEntry;
}
return $output;
@ -857,13 +931,106 @@ final class MD_STD {
$max = count($input);
$offset = 0;
$sizePerEntry = max($size - 1, 1); // Size - 1 is expected, but size below 1 ends in endless loops
while ($offset < $max) {
$cur = array_slice($input, $offset, $size - 1);
$cur = array_slice($input, $offset, $sizePerEntry);
if (!empty($cur)) $output[] = $cur;
$offset += $size - 1;
$offset += $sizePerEntry;
}
return $output;
}
/**
* Transfers a date into an integer. 0000-01-01 > 101; 2001-01-01 > 20010101.
* Needed to store negative dates (BC) in MySQL.
*
* @param string $date Date.
*
* @return integer
*/
public static function date_to_int(string $date):int {
$isNegative = substr($date, 0, 1) === '-';
$parts = explode('-', $date);
if ($isNegative === true) return -1 * intval(implode($parts));
else return intval(implode($parts));
}
/**
* Transfers an integer into a date, reversing the effects of date_to_int.
* 0000-01-01 > 101; 2001-01-01 > 20010101.
* Needed to retrieve negative dates (BC) stored in MySQL.
*
* @param integer $date_int Date represented as an integer.
*
* @return string
*/
public static function int_to_date(int $date_int):string {
$dateStr = (string)$date_int;
if (substr($dateStr, 0, 1) === '-') {
$isNegative = true;
$dateStr = substr($dateStr, 1);
}
else $isNegative = false;
if (strlen($dateStr) < 8) {
$dateStr = str_pad($dateStr, 8, "0", STR_PAD_LEFT);
}
$day = substr($dateStr, -2, 2);
$month = substr($dateStr, -4, 2);
$year = substr($dateStr, 0, -4);
return match($isNegative) {
true => '-',
false => '',
} . $year . '-' . $month . '-' . $day;
}
/**
* Finds the next occurence of any of a given set of substrings.
*
* @param string $haystack The string to search in.
* @param non-empty-array<non-empty-string> $needles The strings to search for.
* @param integer $offset If specified, search will
* start this number of
* characters counted from
* the beginning of the string.
* If the offset is negative,
* the search will start this
* number of characters
* counted from the end of
* the string.
*
* @return array{position: integer, needle: string}|array{}
*/
public static function strpos_multi(string $haystack, array $needles, int $offset = 0):array {
$lowest_option = [];
foreach ($needles as $needle) {
if (($pos = strpos($haystack, $needle, $offset)) !== false) {
if (empty($lowest_option)) {
$lowest_option = ['position' => $pos, 'needle' => $needle];
}
else if ($pos < $lowest_option['position']) {
$lowest_option = ['position' => $pos, 'needle' => $needle];
}
}
}
return $lowest_option;
}
}

View File

@ -10,41 +10,20 @@ declare(strict_types = 1);
* Provides caching functions.
*/
final class MD_STD_CACHE {
/** @var string */
public static string $redis_host = '127.0.0.1';
/** @var integer */
public static int $redis_port = 6379;
/**
* Opens a connection to redis.
*
* @return Redis
*/
public static function open_redis_default():Redis {
$redis = new Redis();
$redis->connect(self::$redis_host, self::$redis_port, 1, null, 0, 0, ['auth' => [MD_CONF::$redis_pw]]);
return $redis;
}
/**
* Shutdown function for caching contents of output buffer.
*
* @param Redis $redis Redis connection.
* @param string $redisKey Key to cache by in redis.
* @param integer $expiry Expiration time in seconds.
*
* @return void
*/
public static function shutdown_cache_through_redis(string $redisKey, int $expiry = 3600):void {
public static function shutdown_cache_through_redis(Redis $redis, string $redisKey, int $expiry = 3600):void {
$outputT = trim(MD_STD::minimizeHTMLString(MD_STD::ob_get_clean()));
echo $outputT;
$redis = self::open_redis_default();
$redis->set($redisKey, $outputT);
$redis->expire($redisKey, $expiry);
$redis->close();
@ -55,26 +34,18 @@ final class MD_STD_CACHE {
* Caches and serves a page through redis. Should be called at the start
* of the script generating a page.
*
* @param string $redisKey Key to cache by in redis.
* @param integer $expiry Expiration time in seconds.
* @param Redis|null $redis Redis connection already opened, if one exists.
* If this parameter is not provided, a separate
* redis connection is opened for this function.
* @param Redis $redis Redis connection.
* @param string $redisKey Key to cache by in redis.
* @param integer $expiry Expiration time in seconds.
*
* @return string
*/
public static function serve_page_through_redis_cache(string $redisKey, int $expiry = 3600, ?Redis $redis = null):string {
public static function serve_page_through_redis_cache(Redis $redis, string $redisKey, int $expiry = 3600):string {
if (PHP_SAPI === 'cli') {
return '';
}
if ($redis === null) {
$redis = self::open_redis_default();
$closeRedis = true;
}
else $closeRedis = false;
if ($redis->ping() !== false) {
ob_start();
@ -86,26 +57,22 @@ final class MD_STD_CACHE {
return $redisResult;
}
else {
register_shutdown_function(function(string $redisKey, int $expiry = 3600) :void {
self::shutdown_cache_through_redis($redisKey, $expiry);
}, $redisKey);
register_shutdown_function(function(Redis $redis, string $redisKey, int $expiry) :void {
self::shutdown_cache_through_redis($redis, $redisKey, $expiry);
}, $redis, $redisKey, $expiry);
}
}
else {
register_shutdown_function(function(string $redisKey, int $expiry = 3600) :void {
self::shutdown_cache_through_redis($redisKey, $expiry);
}, $redisKey);
register_shutdown_function(function(Redis $redis, string $redisKey, int $expiry) :void {
self::shutdown_cache_through_redis($redis, $redisKey, $expiry);
}, $redis, $redisKey, $expiry);
}
}
if ($closeRedis === true) {
$redis->close();
}
return '';
}

View File

@ -14,7 +14,7 @@ final class MD_STD_IN {
*
* @param mixed $input Input string.
*
* @return integer
* @return positive-int
*/
public static function sanitize_id(mixed $input):int {
@ -39,11 +39,11 @@ final class MD_STD_IN {
*
* @param mixed $input Input string.
*
* @return integer
* @return 0|positive-int
*/
public static function sanitize_id_or_zero(mixed $input):int {
if ($input === "") {
if ($input === "" || $input === 0) {
return 0;
}
@ -92,14 +92,18 @@ final class MD_STD_IN {
*
* @param mixed $input Input string.
*
* @return string
* @return non-empty-string
*/
public static function sanitize_rgb_color(mixed $input):string {
$output = \filter_var($input, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
if (empty($output = \filter_var($input, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH))) {
throw new MDInvalidColorCode("Invalid color code provided: " . $output);
}
if ($output === false
|| (preg_match('/^[a-zA-Z0-9]{3}$/', $output) === false && preg_match('/^[a-zA-Z0-9]{6}$/', $output) === false)
$output = \strtoupper($output);
if (!in_array(strlen($output), [3, 6], true)
|| (MD_STD::preg_replace_str('/[A-F0-9]/', '', $output) !== '')
) {
throw new MDInvalidColorCode("Invalid color code provided: " . $output);
}
@ -113,7 +117,7 @@ final class MD_STD_IN {
*
* @param array<mixed> $inputs Input array.
*
* @return list<integer>
* @return list<positive-int>
*/
public static function sanitize_id_array(array $inputs):array {
@ -127,6 +131,27 @@ final class MD_STD_IN {
}
/**
* Validates an input array for all entries being strings. Unsets empty values.
* This is especially useful for parsed JSON.
*
* @param array<mixed> $inputs Input array.
*
* @return array<string>
*/
public static function sanitize_text_array(array $inputs):array {
$output = [];
foreach ($inputs as $key => $input) {
if (empty($input)) continue;
$output[$key] = self::sanitize_text($input);
}
return $output;
}
/**
* Retrieves HTTP input texts from GET or POST variables, whatever is provided.
* If neither is given, returns a provided default.
@ -197,13 +222,51 @@ final class MD_STD_IN {
return "";
}
$output = \filter_var($input, FILTER_SANITIZE_URL);
if (($output = \filter_var($output, FILTER_VALIDATE_URL)) === false) {
throw new MDInvalidUrl("Invalid input URL");
try {
if (($output = \filter_var($input, FILTER_VALIDATE_URL)) === false) {
throw new MDInvalidUrl("Invalid input URL");
}
}
catch (MDInvalidUrl $e) {
if (($parsed = parse_url($input)) === false || empty($parsed['scheme']) || empty($parsed['host']) || empty($parsed['path'])) {
throw new MDInvalidUrl("Invalid input URL");
}
$rewritten = $parsed['scheme'] . '://';
if (!empty($parsed['user']) && !empty($parsed['pass'])) {
$rewritten .= $parsed['user'] . ':' . $parsed['pass'] . '@';
}
$rewritten .= $parsed['host'];
if (!empty($parsed['port'])) $rewritten .= ':' . $parsed['port'];
$rewritten .= str_replace('%2F', '/', urlencode($parsed['path']));
if (!empty($parsed['query'])) {
$rewritten .= '?' . str_replace('%3D', '=', urlencode($parsed['query']));
}
if (($output = \filter_var($rewritten, FILTER_VALIDATE_URL)) === false) {
throw new MDInvalidUrl("Invalid input URL" . \urlencode($input));
}
}
if (empty($output)) return '';
// As per the RFC, URLs should not exceed 2048. Enough real-world ones
// do. But they certainly should not exceed 10000 characters.
if (\strlen($output) > 10000) {
throw new MDInvalidUrl("The entered URL seems to be valid otherwise, but is overly long.");
}
// Check for valid schemes
if (MD_STD::startsWithAny($input, ['https://', 'http://', 'ftp://']) === false) {
try {
if (MD_STD::startsWithAny($output, ['https://', 'http://', 'ftp://']) === false) {
throw new MDInvalidUrl("Invalid input URL" . PHP_EOL . $output . PHP_EOL . strtolower($output));
}
}
catch (MDInvalidUrl $e) {
if (MD_STD::startsWithAny(strtolower($output), ['https://', 'http://', 'ftp://']) === true) {
$output = strtolower(substr($output, 0, 8)) . substr($output, 8);
}
else throw $e;
}
if (\str_contains($output, '.') === false) {
throw new MDInvalidUrl("Invalid input URL");
}
@ -224,9 +287,8 @@ final class MD_STD_IN {
return "";
}
$output = \filter_var($input, FILTER_SANITIZE_EMAIL);
if (($output = \filter_var($output, FILTER_VALIDATE_EMAIL)) === false) {
throw new MDInvalidEmail("Invalid input email address");
if (($output = \filter_var($input, FILTER_VALIDATE_EMAIL)) === false) {
throw new MDInvalidEmail("Invalid input email address" . ' ' . $input);
}
return $output;
@ -289,7 +351,7 @@ final class MD_STD_IN {
$output = \str_replace(",", ".", $input);
if (($output = \filter_var($output, FILTER_VALIDATE_FLOAT)) === false) {
throw new MDgenericInvalidInputsException("Input is readable as a floating point value");
throw new MDgenericInvalidInputsException("Input is not readable as a floating point value");
}
return $output;
@ -302,7 +364,7 @@ final class MD_STD_IN {
*
* @return float
*/
public static function validate_longitude(string|int $input):float {
public static function validate_longitude(string|int|float $input):float {
if (is_string($input)) $output = self::sanitize_float($input);
else $output = $input;
@ -322,7 +384,7 @@ final class MD_STD_IN {
*
* @return float
*/
public static function validate_latitude(string|int $input):float {
public static function validate_latitude(string|int|float $input):float {
if (is_string($input)) $output = self::sanitize_float($input);
else $output = $input;
@ -349,7 +411,7 @@ final class MD_STD_IN {
}
// Remove hyphens
$input = trim(strtr($input, ["-" => "", "" => ""]));
$input = trim(strtr($input, ["-" => "", "" => "", '—' => '', " " => ""]));
// ISBN 10
if (\mb_strlen($input) === 10) {
@ -373,6 +435,27 @@ final class MD_STD_IN {
}
/**
* Validates a ZIP code.
*
* @param string $input Input string.
*
* @return string
*/
public static function validate_zip_code(string $input):string {
if (($input = trim($input)) === "") {
return "";
}
if (\mb_strlen($input) > 7) {
throw new MDgenericInvalidInputsException("ZIP code is too long");
}
return $input;
}
/**
* Returns an UTF8 version of a string.
*

View File

@ -9,14 +9,14 @@ declare(strict_types = 1);
*/
final class MD_STD_SEC {
const REFRESH_TIME_GENERAL = 60; // Time until the comp. with the whole service is cleared.
const REFRESH_TIME_USER = 600; // Time until the comp. with the same username service is cleared.
const REFRESH_TIME_IP = 180; // Time until the comp. with the same IP is cleared. This should be lower than the user-level one, as people working together may be using a common IP.
private const REFRESH_TIME_GENERAL = 60; // Time until the comp. with the whole service is cleared.
private const REFRESH_TIME_USER = 600; // Time until the comp. with the same username service is cleared.
private const REFRESH_TIME_IP = 180; // Time until the comp. with the same IP is cleared. This should be lower than the user-level one, as people working together may be using a common IP.
const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds
const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.04;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 2.0;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 1.6;
private const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds
private const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.04;
private const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 2.0;
private const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 1.6;
/**
* Function for retrieving the anti-csrf token or generating it if need be.
@ -25,6 +25,10 @@ final class MD_STD_SEC {
*/
public static function getAntiCsrfToken():string {
if(session_status() !== PHP_SESSION_ACTIVE) {
throw new Exception("Session needs to be started to get csrf token");
}
if (empty($_SESSION['csrf-token'])) {
$_SESSION['csrf-token'] = bin2hex(random_bytes(32));
}
@ -105,7 +109,7 @@ final class MD_STD_SEC {
// Unstable but working way to get the user's IP. If the IP is falsified,
// this can't be found out anyway and security is established by _common.
$ip = \filter_var($_SERVER['REMOTE_ADDR'] ?: ($_SERVER['HTTP_X_FORWARDED_FOR'] ?: $_SERVER['HTTP_CLIENT_IP']), \FILTER_VALIDATE_IP) ?: "Failed to find";
$ip = \filter_var($_SERVER['REMOTE_ADDR'] ?? ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_CLIENT_IP'] ?? ""), \FILTER_VALIDATE_IP) ?: "Failed to find";
// Set name of log file
$logfile_common = \sys_get_temp_dir() . "/logins_{$tool_name}.json";

39
src/MD_STD_STRINGS.php Normal file
View File

@ -0,0 +1,39 @@
<?PHP
/**
* Gathers wrappers for handling strings.
*/
declare(strict_types = 1);
/**
* Encapsulates functions for handling strings.
*/
final class MD_STD_STRINGS {
/**
* Duplicates words ending in a given set of strings (e.g. dots) that serve as bind chars
* to allow indexing to then allow searching for them in either form.
*
* @param string $input Input string.
*
* @return string
*/
public static function duplicate_words_with_dots_for_indexing(string $input):string {
$charsToDuplicateOn = ',.!-';
$wordsToAdd = [];
$words = explode(' ', $input);
foreach ($words as $word) {
$trimmed = trim($word, $charsToDuplicateOn);
if ($trimmed !== $word) {
$wordsToAdd[] = $trimmed;
}
}
if (empty($wordsToAdd)) {
return $input;
}
return $input . ' ' . implode(' ', $wordsToAdd);
}
}

View File

@ -0,0 +1,36 @@
<?PHP
/**
* Test for ensuring that search HTML pages or components are generated correctly.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
/**
* Tests for the manifest.
*/
abstract class MD_STD_HTML_TEST extends TestCase {
/**
* Validates HTML strings.
*
* @param string $input Input string to validate.
*
* @return void
*/
final protected static function validateHtmlString(string $input):void {
self::assertNotEmpty($input);
libxml_use_internal_errors(true);
$xml = simplexml_load_string('<!DOCTYPE HTML><abc>' . trim(strtr($input, [' async' => ' ', ' defer' => ' '])) . '</abc>');
if (!empty($errors = libxml_get_errors())) {
throw new Exception("Invalid HTML code detected: " . var_export($errors, true) . PHP_EOL . PHP_EOL . $input);
}
self::assertNotEquals(null, $xml);
}
}

View File

@ -0,0 +1,60 @@
<?PHP
/**
* Test for ensuring that search RSS feeds are generated correctly.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
/**
* Tests for the manifest.
*/
abstract class MD_STD_RSS_TEST extends TestCase {
protected string $feed;
/**
* Protected function testRssFeedValidity.
*
* @return void
*/
final public function testRssFeedValidity() {
$domDoc = new DomDocument();
self::assertTrue($domDoc->loadXML($this->feed));
self::assertTrue($domDoc->schemaValidate(__DIR__ . "/../../assets/xsd/Rss2.xsd"));
unset($domDoc);
}
/**
* Checks for the availability of RSS feed links and encosures.
*
* @return void
*/
final public function testRssFeedLinksAndEnclosure() {
if (!($xmlData = simplexml_load_string($this->feed))) {
throw new Exception("Failed to load RSS feed string to SimpleXML element");
}
self::assertNotEmpty((string)$xmlData->channel->title);
self::assertTrue(MD_STD::checkUrlIsReachable((string)$xmlData->channel->image->url), "Path " . $xmlData->channel->image->url . " does not appear to be a reachable URL");
self::assertTrue(MD_STD::checkUrlIsReachable((string)$xmlData->channel->image->link), "Path " . $xmlData->channel->image->link . " does not appear to be a reachable URL");
self::assertTrue(MD_STD::checkUrlIsReachable((string)$xmlData->channel->item->link), "Path " . $xmlData->channel->item->link . " does not appear to be a reachable URL");
if (($firstEntryImg = $xmlData->channel->item->enclosure) === null) {
throw new Exception("First item does not seem to have an enclosure");
}
if (($firstEntryImgAttr = $firstEntryImg->attributes()) === null) {
throw new Exception("First enclosure does not seem to have attributes");
}
self::assertTrue(MD_STD::checkUrlIsReachable((string)$firstEntryImgAttr->url), "First enclosure does not appear to be a reachable URL (" . $firstEntryImgAttr->url . ")");
}
}

View File

@ -0,0 +1,145 @@
<?PHP
/**
* Test for ensuring that search RSS feeds are generated correctly.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
/**
* Tests for the manifest.
*/
final class MD_STD_TEST_PROVIDERS {
/**
* Data provider for returning invalid URLs.
*
* @return array<array{0: string}>
*/
public static function invalid_url_provider():array {
return [
'Space in protocol name' => ["h ttps://www.museum-digital.org"],
'Unwanted protocol' => ["telegram://www.museum-digital.org"],
'String without protocol' => ["www.museum-digital.org"],
'Localhost' => ["http://localhost"],
// As per the RFC, URLs should not exceed 2048. Enough real-world ones
// do. But they certainly should not exceed 10000 characters.
'Overly long URL (> 10000 chars)' => ["https://www.museum-digital.org/" . str_repeat('a', 10000)],
];
}
/**
* Data provider for working URLs.
*
* @return array<array{0: string, 1: string}>
*/
public static function valid_url_provider():array {
return [
'Regular URL without path or query' => ['https://www.museum-digital.org', 'https://www.museum-digital.org'],
'URL with uppercase character in scheme' => ['Https://www.museum-digital.org', 'https://www.museum-digital.org'],
'URL with cyrillic characters, HTML-encoded ' => [
'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4',
'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4',
],
'URL with cyrillic characters, not HTML-encoded ' => [
'https://sr.wikipedia.org/wiki/Београд',
'https://sr.wikipedia.org/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4',
],
'URL with: scheme, user, pass, host, path, query' => [
'https://username:password@sr.wikipedia.org:9000/wiki/Београд?test=hi',
'https://username:password@sr.wikipedia.org:9000/wiki/%D0%91%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D0%B4?test=hi',
],
];
}
/**
* Data provider for working mail addresses.
*
* @return array<array{0: string}>
*/
public static function invalid_email_provider():array {
// Invalid addresses as per https://codefool.tumblr.com/post/15288874550/list-of-valid-and-invalid-email-addresses
$invalid = [
'plainaddress',
'#@%^%#$@#$@#.com',
'@example.com',
'Joe Smith <email@example.com>',
'email.example.com',
'email@example@example.com',
'.email@example.com',
'email.@example.com',
'email..email@example.com',
'あいうえお@example.com',
'email@example.com (Joe Smith)',
'email@example',
'email@-example.com',
'email@111.222.333.44444',
'email@example..com',
'Abc..123@example.com',
'“(),:;<>[\]@example.com',
'just"not"right@example.com',
'this\ is"really"not\allowed@example.com',
];
$output = [];
foreach ($invalid as $addr) {
$output[$addr] = [
$addr,
];
}
$output['Mail address is too long'] = [str_repeat("a", 10000) . '@example.com'];
return $output;
}
/**
* Data provider for working mail addresses.
*
* @return array<array{0: string, 1: string}>
*/
public static function valid_email_provider():array {
// Valid addresses as per https://codefool.tumblr.com/post/15288874550/list-of-valid-and-invalid-email-addresses
// Excluding:
//
// 'email@123.123.123.123',
// 'email@[123.123.123.123]',
// '“email”@example.com',
//
// as per PHP's FILTER_VALIDATE_EMAIL
$valid = [
'email@example.com',
'firstname.lastname@example.com',
'email@subdomain.example.com',
'firstname+lastname@example.com',
'1234567890@example.com',
'email@example-one.com',
'_______@example.com',
'email@example.name',
'email@example.museum',
'email@example.co.jp',
'firstname-lastname@example.com',
];
$output = [];
foreach ($valid as $addr) {
$output[$addr] = [
$addr,
$addr,
];
}
return $output;
}
}

107
tests/MDFormatterTest.php Normal file
View File

@ -0,0 +1,107 @@
<?PHP
/**
* Tests for MD_STD_IN.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD_IN.
*/
#[small]
#[CoversClass(\MDFormatter::class)]
final class MDFormatterTest extends TestCase {
/**
* Function for testing formatMarkdownHeadline1().
*
* @return void
*/
public function testFormatMarkdownHeadline1():void {
self::assertEquals("a" . PHP_EOL . "=" . PHP_EOL . PHP_EOL, MDFormatter::formatMarkdownHeadline1("a"));
}
/**
* Function for testing formatMarkdownHeadline2().
*
* @return void
*/
public function testFormatMarkdownHeadline2():void {
self::assertEquals("a" . PHP_EOL . "-" . PHP_EOL . PHP_EOL, MDFormatter::formatMarkdownHeadline2("a"));
}
/**
* Function for testing formatMarkdownHeadline3().
*
* @return void
*/
public function testFormatMarkdownHeadline3():void {
self::assertEquals("### a" . PHP_EOL, MDFormatter::formatMarkdownHeadline3("a"));
}
/**
* Function for testing formatMarkdownHorizontalRule().
*
* @return void
*/
public function testFormatMarkdownHorizontalRule():void {
self::assertEquals("___" . PHP_EOL, MDFormatter::formatMarkdownHorizontalRule());
}
/**
* Function for testing formatMarkdownNamedLink().
*
* @return void
*/
public function testFormatMarkdownNamedLink():void {
self::assertEquals("[a](https://example.com)", MDFormatter::formatMarkdownNamedLink("a", "https://example.com"));
}
/**
* Function for testing formatMarkdownBlockQuote().
*
* @return void
*/
public function testFormatMarkdownBlockQuote():void {
self::assertEquals("> Test" . PHP_EOL . "> test" . PHP_EOL, MDFormatter::formatMarkdownBlockQuote("Test" . PHP_EOL . "test"));
}
/**
* Function for testing formatMarkdownCodeBlock().
*
* @return void
*/
public function testFormatMarkdownCodeBlock():void {
self::assertEquals("```" . PHP_EOL . "Test" . PHP_EOL . "test" . PHP_EOL . "```" . PHP_EOL, MDFormatter::formatMarkdownCodeBlock("Test" . PHP_EOL . "test"));
}
/**
* Function for testing formatMarkdownUnorderedListItem().
*
* @return void
*/
public function testFormatMarkdownUnorderedListItem():void {
self::assertEquals("- a" . PHP_EOL, MDFormatter::formatMarkdownUnorderedListItem("a"));
self::assertEquals("- a" . PHP_EOL . " a" . PHP_EOL, MDFormatter::formatMarkdownUnorderedListItem("a" . PHP_EOL . "a"));
}
}

692
tests/MD_STD_IN_Test.php Normal file
View File

@ -0,0 +1,692 @@
<?PHP
/**
* Tests for MD_STD_IN.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD_IN.
*/
#[small]
#[CoversClass(\MD_STD_IN::class)]
final class MD_STD_IN_Test extends TestCase {
/**
* Data provider for valid IDs.
*
* @return array<array{0: mixed, 1: int}>
*/
public static function valid_id_provider():array {
$values = [
[1, 1],
["1", 1],
["1111111", 1111111],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Data provider for valid longitudes.
*
* @return array<array{0: mixed, 1: float}>
*/
public static function valid_longitude_provider():array {
$values = [
["1", 1.0],
["12", 12.0],
[12, 12.0],
[12.0, 12.0],
[95, 95.0],
[-95, -95.0],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Data provider for invalid longitudes.
*
* @return array<array{0: mixed, 1: string}>
*/
public static function invalid_longitude_provider():array {
$values = [
["test", MDgenericInvalidInputsException::class],
["test1", MDgenericInvalidInputsException::class],
["", MDgenericInvalidInputsException::class],
["1900", MDCoordinateOutOfRange::class],
[1900, MDCoordinateOutOfRange::class],
[-1900, MDCoordinateOutOfRange::class],
[185, MDCoordinateOutOfRange::class],
[-185, MDCoordinateOutOfRange::class],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Data provider for valid latitudes.
*
* @return array<array{0: mixed, 1: float}>
*/
public static function valid_latitude_provider():array {
$values = [
["1", 1.0],
["12", 12.0],
[12, 12.0],
[12.0, 12.0],
[85, 85.0],
[-85, -85.0],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Data provider for invalid latitudes.
*
* @return array<array{0: mixed, 1: class-string<Throwable>}>
*/
public static function invalid_latitude_provider():array {
$values = [
["test", MDgenericInvalidInputsException::class],
["test1", MDgenericInvalidInputsException::class],
["", MDgenericInvalidInputsException::class],
["1900", MDCoordinateOutOfRange::class],
[1900, MDCoordinateOutOfRange::class],
[-1900, MDCoordinateOutOfRange::class],
[95, MDCoordinateOutOfRange::class],
[-95, MDCoordinateOutOfRange::class],
];
$output = [];
foreach ($values as $value) {
$output[gettype($value[0]) . ': ' . var_export($value[0], true)] = $value;
}
return $output;
}
/**
* Function for testing sanitize_id().
*
* @param mixed $to_validate Input to validate.
* @param integer $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_id_provider')]
public function test_sanitize_id_works(mixed $to_validate, int $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_id($to_validate));
}
/**
* Data provider for valid IDs.
*
* @return array<array{0: mixed}>
*/
public static function invalid_id_provider():array {
$output = self::invalid_id_provider_without_zero();
$output['Number 0'] = [0];
return $output;
}
/**
* Function for testing sanitize_id().
*
* @param mixed $to_validate Input to validate.
*
* @return void
*/
#[DataProvider('invalid_id_provider')]
public function test_sanitize_id_fails(mixed $to_validate):void {
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id($to_validate);
}
/**
* Data provider for valid IDs.
*
* @return array<array{0: mixed, 1: int}>
*/
public static function valid_id_or_zero_provider():array {
$output = self::valid_id_provider();
$output['Integer 0'] = [0, 0];
$output['String 0'] = [0, 0];
return $output;
}
/**
* Function for testing sanitize_id_or_zero().
*
* @param mixed $to_validate Input to validate.
* @param integer $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_id_or_zero_provider')]
public function test_sanitize_id_or_zero_works(mixed $to_validate, int $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_id_or_zero($to_validate));
}
/**
* Data provider for valid IDs.
*
* @return array<array{0: mixed}>
*/
public static function invalid_id_provider_without_zero():array {
return [
'Number too high' => [1000000000000000000000000000000000000],
'Number with character in the middle' => ["1a2"],
'Number with suffixed string' => ["12a"],
'String character' => ["a"],
];
}
/**
* Function for testing sanitize_id_or_zero().
*
* @param mixed $to_validate Input to validate.
*
* @return void
*/
#[DataProvider('invalid_id_provider_without_zero')]
public function test_sanitize_id_or_zero_fails(mixed $to_validate):void {
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_or_zero($to_validate);
}
/**
* Data provider for text with its expected cleaned values.
*
* @return array<array{0: mixed, 1: string}>
*/
public static function text_with_expected_return_value_provider():array {
return [
'Empty string' => ['', ''],
'Integer 0' => [0, '0'],
'String 0' => ['0', '0'],
'Regular string' => ['a', 'a'],
'String with double whitespace in between' => ['a a', 'a a'],
'String to be trimmed (spaces)' => ['a ', 'a'],
'String to be trimmed (newline)' => ['a ' . PHP_EOL, 'a'],
'Empty array' => [[], ''],
'Array with content' => [['test' => 'test'], ''],
];
}
/**
* Function for testing sanitize_text().
*
* @param mixed $to_validate Input to validate.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProvider('text_with_expected_return_value_provider')]
public function test_sanitize_text(mixed $to_validate, string $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_text($to_validate));
}
/**
* Data provider for working RGB colors.
*
* @return array<array{0: mixed, 1: string}>
*/
public static function valid_rgb_colors_provider():array {
return [
'Three character version' => ['AAA', 'AAA'],
'Three character version (int)' => ['111', '111'],
'Three character version (mixed)' => ['1a1', '1A1'],
'Six character version' => ['AAAAAA', 'AAAAAA'],
'Six character version (int)' => ['111111', '111111'],
'Six character version (mixed)' => ['1a1AAA', '1A1AAA'],
];
}
/**
* Data provider for strings that are not rgb colors.
*
* @return array<array{0: mixed}>
*/
public static function invalid_rgb_colors_provider():array {
$output = [
'Array' => [[]],
'Three characters, but invalid ones' => ['ZZZ'],
'Six characters, but invalid ones' => ['111ZZZ'],
'Three characters, but with spaces' => ['ZZZ '],
];
for ($i = 0; $i++; $i < 10) {
if ($i === 3 || $i === 6) continue;
$output['Valid characters repeated ' . $i . ' times'] = [$i];
}
return $output;
}
/**
* Function for testing sanitize_rgb_color().
*
* @param mixed $to_validate Input to validate.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_rgb_colors_provider')]
public function test_sanitize_rgb_color_works(mixed $to_validate, string $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_rgb_color($to_validate));
}
/**
* Function for testing sanitize_rgb_color()'s failure modes.
*
* @param mixed $to_validate Input to validate.
*
* @return void
*/
#[DataProvider('invalid_rgb_colors_provider')]
public function test_sanitize_rgb_color_fails(mixed $to_validate):void {
self::expectException(MDInvalidColorCode::class);
MD_STD_IN::sanitize_rgb_color($to_validate);
}
/**
* Function for testing sanitize_text_array().
*
* @return void
*/
public function test_sanitize_text_array():void {
self::assertEquals(["1"], MD_STD_IN::sanitize_text_array([1, '']));
self::assertEquals(["1", "2"], MD_STD_IN::sanitize_text_array(["1", 2]));
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array([[]]);
}
/**
* Function for testing sanitize_id_array().
*
* @return void
*/
public function test_sanitize_id_array():void {
self::assertEquals([1], MD_STD_IN::sanitize_id_array([1]));
self::assertEquals([1, 2], MD_STD_IN::sanitize_id_array(["1", 2]));
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array([0]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array([0, 1]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array([100000000000000000000000000000]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array(["1a2"]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array(["12a"]);
self::expectException(MDpageParameterNotNumericException::class);
MD_STD_IN::sanitize_id_array(["a"]);
}
/**
* Function for testing get_http_input_text().
*
* @return void
*/
public function test_get_http_input_text():void {
$_GET['test'] = "a";
self::assertEquals("a", MD_STD_IN::get_http_input_text("test"));
unset($_GET['test']);
$_POST['test'] = "a";
self::assertEquals("a", MD_STD_IN::get_http_input_text("test"));
unset($_POST['test']);
$_POST['test'] = [];
self::assertEquals("", MD_STD_IN::get_http_input_text("test"));
unset($_POST['test']);
self::assertEquals("", MD_STD_IN::get_http_input_text("test"));
self::expectException(MDpageParameterNotFromListException::class);
MD_STD_IN::get_http_input_text("a", "", ['a']);
}
/**
* Function for testing get_http_post_text().
*
* @return void
*/
public function test_get_http_post_text():void {
$_POST['test'] = "a";
self::assertEquals("a", MD_STD_IN::get_http_post_text("test"));
unset($_POST['test']);
$_POST['test'] = [];
self::assertEquals("", MD_STD_IN::get_http_post_text("test"));
unset($_POST['test']);
self::assertEquals("", MD_STD_IN::get_http_post_text("test"));
self::expectException(MDpageParameterNotFromListException::class);
MD_STD_IN::get_http_post_text("a", "", ['a']);
}
/**
* Function for testing sanitize_url().
*
* @return void
*/
public function test_sanitize_url_with_empty_string():void {
// Ensure empty inputs return empty output
self::assertEquals("", MD_STD_IN::sanitize_url(""));
}
/**
* Function for testing sanitize_url().
*
* @param string $to_validate Input to validate.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'valid_url_provider')]
public function test_sanitize_url_works(string $to_validate, string $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_url($to_validate));
}
/**
* Function for testing sanitize_url().
*
* @param string $to_validate Input to validate.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'invalid_url_provider')]
public function test_sanitize_url_fails(string $to_validate):void {
self::expectException(MDInvalidUrl::class);
MD_STD_IN::sanitize_url($to_validate);
}
/**
* Function for testing sanitize_email().
*
* @return void
*/
public function test_sanitize_email_empty():void {
self::assertEquals("", MD_STD_IN::sanitize_email(""));
}
/**
* Function for testing sanitize_email().
*
* @param string $to_validate Input to validate.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'valid_email_provider')]
public function test_sanitize_email_works(string $to_validate, string $expected):void {
self::assertEquals($expected, MD_STD_IN::sanitize_email($to_validate));
}
/**
* Function for testing sanitize_email() fails when it should.
*
* @param string $to_validate Input to validate.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'invalid_email_provider')]
public function test_sanitize_email_fails(string $to_validate):void {
self::expectException(MDInvalidEmail::class);
MD_STD_IN::sanitize_email($to_validate);
}
/**
* Function for testing validate_password().
*
* @return void
*/
public function test_validate_password():void {
self::assertEquals(['password_too_short', 'password_has_no_number_no_special_char'], MD_STD_IN::validate_password("a"));
self::assertEquals(['password_has_no_number_no_special_char'], MD_STD_IN::validate_password("aaaaaaaaaaaaaaaaaaaa"));
self::assertEquals(['password_too_short'], MD_STD_IN::validate_password("!a323!"));
self::assertEquals([], MD_STD_IN::validate_password("!a324324324123!"));
}
/**
* Function for testing validate_phone_number().
*
* @return void
*/
public function test_validate_phone_number():void {
self::assertEquals("", MD_STD_IN::validate_phone_number(""));
self::assertEquals("+1932-1321123", MD_STD_IN::validate_phone_number("+1932-1321123"));
self::assertEquals("+49 (030) 21321123", MD_STD_IN::validate_phone_number("+49 (030) 21321123"));
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_phone_number("test@example.org");
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_phone_number("+123456789 z");
}
/**
* Function for testing sanitize_float().
*
* @return void
*/
public function test_sanitize_float():void {
self::assertEquals(0, MD_STD_IN::sanitize_float("0"));
self::assertEquals(12, MD_STD_IN::sanitize_float("12"));
self::assertEquals(12.12, MD_STD_IN::sanitize_float("12.12"));
self::assertEquals(12.12, MD_STD_IN::sanitize_float("12,12"));
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::sanitize_float("test");
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::sanitize_float("");
}
/**
* Function for testing validate_longitude().
*
* @param mixed $to_validate Input to validate.
* @param float $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_longitude_provider')]
public function test_validate_longitude_with_valid_entries(mixed $to_validate, float $expected):void {
self::assertEquals($expected, MD_STD_IN::validate_longitude($to_validate));
}
/**
* Function for testing validate_longitude().
*
* @param mixed $to_validate Input to validate.
* @param class-string<Throwable> $exceptionClass Exception class.
*
* @return void
*/
#[DataProvider('invalid_longitude_provider')]
public function test_validate_longitude_with_invalid_entries(mixed $to_validate, string $exceptionClass):void {
self::expectException($exceptionClass);
MD_STD_IN::validate_longitude($to_validate);
}
/**
* Function for testing validate_latitude().
*
* @param mixed $to_validate Input to validate.
* @param float $expected Expected output.
*
* @return void
*/
#[DataProvider('valid_latitude_provider')]
public function test_validate_latitude_with_valid_entries(mixed $to_validate, float $expected):void {
self::assertEquals($expected, MD_STD_IN::validate_latitude($to_validate));
}
/**
* Function for testing validate_latitude().
*
* @param mixed $to_validate Input to validate.
* @param class-string<Throwable> $exceptionClass Exception class.
*
* @return void
*/
#[DataProvider('invalid_latitude_provider')]
public function test_validate_latitude_with_invalid_entries(mixed $to_validate, string $exceptionClass):void {
self::expectException($exceptionClass);
MD_STD_IN::validate_latitude($to_validate);
}
/**
* Function for testing validate_isbn().
*
* @return void
*/
public function test_validate_isbn():void {
self::assertEquals("", MD_STD_IN::validate_isbn(""));
self::assertEquals("0943396042", MD_STD_IN::validate_isbn("0943396042"));
self::assertEquals("0943396042", MD_STD_IN::validate_isbn("0-943396-04-2"));
self::assertEquals("094339604X", MD_STD_IN::validate_isbn("0-943396-04-X"));
self::assertEquals("1230943396042", MD_STD_IN::validate_isbn("1230943396042"));
self::assertEquals("1230943396042", MD_STD_IN::validate_isbn("1230-943396-04-2"));
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_isbn("X094339604");
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_isbn("094339604a");
}
/**
* Function for testing validate_zip_code().
*
* @return void
*/
public function test_validate_zip_code():void {
self::assertEquals("", MD_STD_IN::validate_zip_code(""));
self::assertEquals("1234", MD_STD_IN::validate_zip_code(" 1234"));
self::expectException(MDgenericInvalidInputsException::class);
MD_STD_IN::validate_zip_code("X094339604");
}
/**
* Function for testing ensureStringIsUtf8().
*
* @return void
*/
public function test_ensureStringIsUtf8():void {
if (empty($convToIso8859 = iconv("UTF-8", 'ISO-8859-1//TRANSLIT', "ä"))) {
throw new Exception("Iconv returned empty result");
}
if (empty($convToIso2022 = iconv("UTF-8", 'ISO-2022-JP//TRANSLIT', "ä"))) {
throw new Exception("Iconv returned empty result");
}
self::assertEquals("ä", MD_STD_IN::ensureStringIsUtf8("ä"));
self::assertEquals("ä", MD_STD_IN::ensureStringIsUtf8($convToIso8859));
self::assertEquals("a", MD_STD_IN::ensureStringIsUtf8($convToIso2022));
}
}

View File

@ -7,19 +7,19 @@
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
require __DIR__ . '/../src/MD_STD_SEC.php';
use PHPUnit\Framework\Attributes\Large;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD_SEC.
*/
#[large]
#[CoversClass(\MD_STD_SEC::class)]
final class MD_STD_SECTest extends TestCase {
/**
* Function for testing if the page can be opened using invalid values for objektnum.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
* @group MissingInputs
* @group SafeForProduction
* @small
*
* @return void
*/
@ -34,4 +34,71 @@ final class MD_STD_SECTest extends TestCase {
self::assertLessThan(3 * 1000000, $delay_reduced); // Smaller than 10 seconds
}
/**
* Ensure getAntiCsrfToken does not work without a
* started session.
*
* @return void
*/
public function testGetAntiCsrfTokenFailsWithoutActiveSession():void {
self::expectException(Exception::class);
MD_STD_SEC::getAntiCsrfToken();
}
/**
* Ensure getAntiCsrfToken works.
*
* @return void
*/
public function testGetAntiCsrfTokenWorks():void {
session_start();
self::assertEmpty($_SESSION);
$token = MD_STD_SEC::getAntiCsrfToken();
self::assertNotEmpty($_SESSION['csrf-token']);
self::assertEquals($token, MD_STD_SEC::getAntiCsrfToken());
$_POST = [
'csrf-token' => $token,
];
self::assertTrue(MD_STD_SEC::validateAntiCsrfToken());
}
/**
* Ensure preventBruteForce works.
*
* @return void
*/
public function testPreventBruteForce():void {
self::assertTrue(MD_STD_SEC::preventBruteForce("MD_STD_TEST_SUCCESS", "test_user", 0));
$logFile = \sys_get_temp_dir() . "/logins_MD_STD_TEST_SUCCESS.json";
self::assertFileExists($logFile);
MD_STD::unlink($logFile);
}
/**
* Ensure preventBruteForce returns false on many requests.
*
* @return void
*/
public function testPreventBruteForceReturnsFalseOnManyRequests():void {
for ($i = 0; $i < 10; $i++) {
MD_STD_SEC::preventBruteForce("MD_STD_TEST_FAILURE", "test_user", 3);
}
self::assertFalse(MD_STD_SEC::preventBruteForce("MD_STD_TEST_FAILURE", "test_user", 3));
$logFile = \sys_get_temp_dir() . "/logins_MD_STD_TEST_FAILURE.json";
self::assertFileExists($logFile);
MD_STD::unlink($logFile);
}
}

View File

@ -0,0 +1,53 @@
<?PHP
/**
* Tests for MD_STD_IN.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD_STRINGS.
*/
#[small]
#[CoversClass(\MD_STD_STRINGS::class)]
final class MD_STD_STRINGS_Test extends TestCase {
/**
* Data provider for strings to duplicate words with dots in.
*
* @return array<array{0: string, 1: string}>
*/
public static function duplicated_words_with_dots_provider():array {
$values = [
["hallo test. hallo", "hallo test. hallo test"],
];
$output = [];
foreach ($values as $value) {
$output[$value[0] . ' > ' . $value[1]] = $value;
}
return $output;
}
/**
* Function for testing duplicate_words_with_dots_for_indexing().
*
* @param string $input Input.
* @param string $expected Expected output.
*
* @return void
*/
#[DataProvider('duplicated_words_with_dots_provider')]
public function test_duplicate_words_with_dots_for_indexing(string $input, string $expected):void {
self::assertEquals($expected, MD_STD_STRINGS::duplicate_words_with_dots_for_indexing($input));
}
}

747
tests/MD_STD_Test.php Normal file
View File

@ -0,0 +1,747 @@
<?PHP
/**
* Tests for MD_STD.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\DataProviderExternal;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\CoversClass;
/**
* Tests for MD_STD.
*/
#[small]
#[CoversClass(\MD_STD::class)]
final class MD_STD_Test extends TestCase {
/**
* Data provider for returning a tmp file.
*
* @return array<string, array{0: non-empty-string}>
*/
public static function file_name_in_tmp_provider():array {
$temp_file = \tempnam(\sys_get_temp_dir(), 'test_md_std');
if (!$temp_file) throw new Exception("Failed to get tmp dir");
return [
$temp_file => [$temp_file],
];
}
/**
* Data provider for invalid directories.
*
* @return array<non-empty-string, array{0: non-empty-string}>
*/
public static function invalid_directory_provider():array {
$non_existent = __DIR__ . '/' . uniqid();
return [
"File, not directory" => [__FILE__],
"Non-existent directory" => [$non_existent],
];
}
/**
* Data provider for invalid files for file_get_contents.
*
* @return array<string, array{0: string, 1: string}>
*/
public static function invalid_files_for_file_get_contents():array {
return [
'External path: FTP' => ["ftp://test", MDFileDoesNotExist::class], // Invalid external
'Non-existent file' => ["jklsdjfklasdjfklasdkl.jpg", MDFileDoesNotExist::class], // Non-existent file
];
}
/**
* Data provider for invalid files for file_get_contents.
*
* @return array<string, array{0: string, 1: array<string>, 2: string}>
*/
public static function failure_cases_ensure_file():array {
return [
'Non-existent file' => ["jklsdjfklasdjfklasdkl.jpg", [], MDFileDoesNotExist::class], // Non-existent file
'Invalid mime type' => [__FILE__, ["image/jpeg"], MDWrongFileType::class], // Non-existent file
];
}
/**
* Returns sample dates and their equivalent integer values according
* to MD_STD::date_to_int().
*
* @return array<string, array{0: string, 1: integer}>
*/
public static function date_to_int_provider():array {
$values = [
["2022-01-01", 20220101],
["0022-01-01", 220101],
["-0022-01-01", -220101],
["-2022-01-01", -20220101],
["-0001-01-01", -10101],
["0000-01-01", 101],
["100000-01-01", 1000000101],
];
$output = [];
foreach ($values as $value) {
$output[$value[0]] = $value;
}
return $output;
}
/**
* Checks if a file can be read.
*
* @return void
*/
public function test_file_get_contents_works():void {
self::assertNotEmpty(MD_STD::file_get_contents(__DIR__ . '/../phpunit.xml'));
}
/**
* Checks if a file can be written.
*
* @param non-empty-string $temp_file Tmp file.
*
* @return void
*/
#[DataProvider('file_name_in_tmp_provider')]
public function test_file_put_contents_works(string $temp_file):void {
MD_STD::file_put_contents($temp_file, "Test");
self::assertEquals("Test", MD_STD::file_get_contents($temp_file));
MD_STD::unlink($temp_file);
}
/**
* Check MD_STD::realpath returns absolute file path of an existing file.
*
* @return void
*/
public function test_realpath_works():void {
self::assertEquals(__FILE__, MD_STD::realpath(__FILE__));
}
/**
* Check MD_STD::realpath throws exception on non-existing path.
*
* @param non-empty-string $to_validate Input to validate.
* @param class-string<Throwable> $exceptionClass Exception class.
*
* @return void
*/
#[DataProvider('invalid_files_for_file_get_contents')]
public function test_realpath_fails(string $to_validate, string $exceptionClass):void {
self::expectException($exceptionClass);
MD_STD::realpath($to_validate);
}
/**
* Checks if a file can be read.
*
* @param non-empty-string $to_validate Input to validate.
* @param class-string<Throwable> $exceptionClass Exception class.
*
* @return void
*/
#[DataProvider('invalid_files_for_file_get_contents')]
public function test_file_get_contents_fails_as_expected(string $to_validate, string $exceptionClass):void {
self::expectException($exceptionClass);
MD_STD::file_get_contents($to_validate);
}
/**
* Checks that json_encode works.
*
* @return void
*/
public function test_json_encode_works():void {
self::assertEquals('[0]', MD_STD::json_encode([0]));
}
/**
* Checks that json_encode_object works.
*
* @return void
*/
public function test_json_encode_object_works():void {
self::assertEquals('{"0":0}', MD_STD::json_encode_object((object)[0]));
}
/**
* Checks that mkdir works.
*
* @return void
*/
public function test_mkdir_works():void {
$testDir = sys_get_temp_dir() . '/' . uniqid("_test_");
if (file_exists($testDir)) {
throw new Exception("Test dir already exists");
}
MD_STD::mkdir($testDir);
self::assertTrue(is_dir($testDir));
}
/**
* Checks that nothing happens with mkdir.
*
* @return void
*/
public function test_mkdir_works_with_invalid():void {
MD_STD::mkdir(__DIR__);
self::assertTrue(is_dir(__DIR__));
}
/**
* Checks if nothing happens if a file does not exist.
*
* @param non-empty-string $temp_file Tmp file.
*
* @return void
*/
#[DataProvider('file_name_in_tmp_provider')]
public function test_unlink_if_exists_passes_if_not_exists(string $temp_file):void {
if (file_exists($temp_file)) {
MD_STD::unlink($temp_file);
}
MD_STD::unlink_if_exists($temp_file);
self::assertFalse(is_file($temp_file));
}
/**
* Checks unlink_if_exists works.
*
* @param non-empty-string $temp_file Tmp file.
*
* @return void
*/
#[DataProvider('file_name_in_tmp_provider')]
public function test_unlink_if_exists_works(string $temp_file):void {
touch($temp_file);
MD_STD::unlink_if_exists($temp_file);
self::assertFalse(is_file($temp_file));
}
/**
* Checks scandir works.
*
* @return void
*/
public function test_scandir():void {
if (empty($files = scandir(__DIR__))) {
throw new Exception("Running regular scandir() failed on script directory: " . __DIR__);
}
$filesChecked = MD_STD::scandir(__DIR__);
foreach ($filesChecked as $file) {
self::assertTrue(in_array($file, $files, true));
}
foreach (['.', '..', '.git'] as $excluded) {
self::assertFalse(in_array($excluded, $filesChecked, true));
}
}
/**
* Checks invalid directory.
*
* @param non-empty-string $dir Dir name.
*
* @return void
*/
#[DataProvider('invalid_directory_provider')]
public function test_invalid_directory_throws_error_on_scandir(string $dir):void {
self::expectException(MDFileDoesNotExist::class);
MD_STD::scandir($dir);
}
/**
* Checks that startsWithAny works.
*
* @return void
*/
public function test_startsWithAny():void {
self::assertTrue(MD_STD::startsWithAny("abc", ["a"]));
self::assertFalse(MD_STD::startsWithAny("abc", ["b"]));
}
/**
* Checks that stri_contains works.
*
* @return void
*/
public function test_stri_contains():void {
self::assertTrue(MD_STD::stri_contains("abc", "a"));
self::assertTrue(MD_STD::stri_contains("abc", "b"));
self::assertTrue(MD_STD::stri_contains("abc", "C"));
self::assertFalse(MD_STD::stri_contains("abc", "u"));
}
/**
* Checks that stri_contains_any works.
*
* @return void
*/
public function test_stri_contains_any():void {
self::assertTrue(MD_STD::stri_contains_any("abc", ["a"]));
self::assertTrue(MD_STD::stri_contains_any("abc", ["b"]));
self::assertTrue(MD_STD::stri_contains_any("abc", ["C"]));
self::assertTrue(MD_STD::stri_contains_any("abc", ["C", "u"]));
self::assertFalse(MD_STD::stri_contains_any("abc", ["u"]));
self::assertFalse(MD_STD::stri_contains_any("abc", ["u", "z"]));
}
/**
* Checks that strtotime works.
*
* @return void
*/
public function test_strtotime_works():void {
self::assertEquals(strtotime("2024-01-01"), MD_STD::strtotime("2024-01-01"));
}
/**
* Checks that strtotime works.
*
* @return void
*/
public function test_strtotime_fails_as_expected():void {
self::expectException(MDInvalidInputDate::class);
MD_STD::strtotime("test");
}
/**
* Checks that strtotime_for_public_apis works.
*
* @return void
*/
public function test_strtotime_for_pulic_apis_works():void {
self::assertEquals(strtotime("2024-01-01"), MD_STD::strtotime_for_public_apis("2024-01-01"));
}
/**
* Checks that strtotime_for_public_apis works.
*
* @return void
*/
public function test_strtotime_for_pulic_apis_fails_as_expected():void {
self::expectException(MDParseException::class);
MD_STD::strtotime_for_public_apis("test");
}
/**
* Checks that curl_init works.
*
* @return void
*/
public function test_curl_init():void {
self::expectNotToPerformAssertions();
MD_STD::curl_init("https://example.com", 2000);
}
/**
* Checks that runCurl works.
*
* @return void
*/
public function test_runCurl():void {
self::assertNotEmpty(MD_STD::runCurl("https://example.com", headers: ['X-TEST: Test']));
}
/**
* Checks that runCurl works.
*
* @return void
*/
public function test_runCurlMulti():void {
$output = MD_STD::runCurlMulti(["https://example.com" => "https://example.com"], headers: ['X-TEST: Test']);
self::assertEquals(["https://example.com"], array_keys($output));
}
/**
* Checks that checkUrlIsReachable works.
*
* @return void
*/
public function test_checkUrlIsReachable():void {
self::assertTrue(MD_STD::checkUrlIsReachable("https://example.com"));
self::assertFalse(MD_STD::checkUrlIsReachable("https://example.com/404"));
}
/**
* Checks that checkUrlIsReachable fails as expected.
*
* @param string $invalid_url Input to validate.
*
* @return void
*/
#[DataProviderExternal(\MD_STD_TEST_PROVIDERS::class, 'invalid_url_provider')]
public function test_checkUrlIsReachable_fails_as_expected(string $invalid_url):void {
self::expectException(MDInvalidUrl::class);
self::assertTrue(MD_STD::checkUrlIsReachable($invalid_url));
}
/**
* Checks that checkUrlIsReachable fails as expected.
*
* @return void
*/
public function test_checkUrlIsReachable_fails_as_expected_on_empty_input():void {
self::expectException(MDInvalidUrl::class);
self::assertTrue(MD_STD::checkUrlIsReachable(""));
}
/**
* Checks that filesize works.
*
* @return void
*/
public function test_filesize():void {
self::assertNotEmpty(MD_STD::filesize(__FILE__));
}
/**
* Checks that human_filesize works.
*
* @return void
*/
public function test_human_filesize_works():void {
self::assertEquals("2.00B", MD_STD::human_filesize(2));
self::assertEquals("2.00kB", MD_STD::human_filesize(2048));
}
/**
* Checks that minimizeHTMLString works.
*
* @return void
*/
public function test_minimizeHTMLString():void {
self::assertEquals("hi" . PHP_EOL . "hi" . PHP_EOL, MD_STD::minimizeHTMLString(" hi" . PHP_EOL . " hi"));
}
/**
* Checks that createTextSnippet works.
*
* @return void
*/
public function test_createTextSnippet():void {
self::assertEquals("Hallo...", MD_STD::createTextSnippet("Hallo Hallo, jkfljksdlajkfas", 8));
}
/**
* Checks that ensure_file works.
*
* @return void
*/
public function test_ensure_file_works():void {
self::expectNotToPerformAssertions();
MD_STD::ensure_file(__FILE__);
}
/**
* Checks that minimizeHTMLString works.
*
* @param non-empty-string $filepath File path.
* @param array<string> $mime_types Mime types expected.
* @param class-string<Throwable> $exception Exception class.
*
* @return void
*/
#[DataProvider('failure_cases_ensure_file')]
public function test_ensure_file_fails_as_expected(string $filepath, array $mime_types, string $exception):void {
self::expectException($exception);
MD_STD::ensure_file($filepath, $mime_types);
}
/**
* Checks that levenshtein works.
*
* @return void
*/
public function test_levensthein_works():void {
self::expectNotToPerformAssertions();
MD_STD::levenshtein(str_repeat("hi", 500), str_repeat("ho", 500));
}
/**
* Checks if dates can be translated to int and back.
*
* @param string $date Date to translate.
* @param integer $expectedInt Expected integer value for it.
*
* @return void
*/
#[DataProvider('date_to_int_provider')]
public function test_date_to_int(string $date, int $expectedInt):void {
$toInt = MD_STD::date_to_int($date);
$toStr = MD_STD::int_to_date($toInt);
self::assertEquals($expectedInt, $toInt);
self::assertEquals($date, $toStr);
}
/**
* Checks check_is_writable does not work with non-existent or non-directory paths.
*
* @param non-empty-string $dir Dir name.
*
* @return void
*/
#[DataProvider('invalid_directory_provider')]
public function test_invalid_directory_throws_error_on_check_is_writable(string $dir):void {
self::expectException(MDFileDoesNotExist::class);
MD_STD::check_is_writable($dir);
}
/**
* Checks check_is_writable does not work with non-existent or non-directory paths.
*
* @return void
*/
public function test_is_writable_returns_false_without_permissions():void {
self::expectException(MDFileIsNotWritable::class);
MD_STD::check_is_writable("/etc");
}
/**
* Checks check_is_writable does not work with non-existent or non-directory paths.
*
* @return void
*/
public function test_remote_mime_content_type_works():void {
self::assertEquals("text/html", MD_STD::remote_mime_content_type("https://example.com"));
}
/**
* Checks string_to_color_code works.
*
* @return void
*/
public function test_string_to_color_code():void {
self::assertEquals(6, strlen(MD_STD::string_to_color_code("")));
self::assertEquals(6, strlen(MD_STD::string_to_color_code("a")));
self::assertEquals(6, strlen(MD_STD::string_to_color_code("dsaf")));
}
/**
* Checks split_int_array_into_sized_parts works.
*
* @return void
*/
public function test_split_int_array_into_sized_parts():void {
self::assertEquals([[0 => 1], [0 => 1], [0 => 1], [0 => 1]], MD_STD::split_int_array_into_sized_parts([1, 1, 1, 1], 1));
}
/**
* Checks split_string_array_into_sized_parts works.
*
* @return void
*/
public function test_split_string_array_into_sized_parts():void {
self::assertEquals([[0 => "1"], [0 => "1"], [0 => "1"], [0 => "1"]], MD_STD::split_string_array_into_sized_parts(["1", "1", "1", "1"], 1));
}
/**
* Checks openssl_random_pseudo_bytes works.
*
* @return void
*/
public function test_openssl_random_pseudo_bytes():void {
self::assertNotEmpty(MD_STD::openssl_random_pseudo_bytes(12));
}
/**
* Checks lang_getfrombrowser works by getting value from HTTP header HTTP_ACCEPT_LANGUAGE.
*
* @return void
*/
public function test_lang_getfrombrowser_via_http_header():void {
$_SERVER = [
'HTTP_ACCEPT_LANGUAGE' => 'de',
];
self::assertEquals("de", MD_STD::lang_getfrombrowser(["de", "en"], "en", ""));
$_SERVER = [
'HTTP_ACCEPT_LANGUAGE' => 'es_ES',
];
self::assertEquals("en", MD_STD::lang_getfrombrowser(["de", "en"], "en", ""));
}
/**
* Checks lang_getfrombrowser returns default without any further information being provided.
*
* @return void
*/
public function test_lang_getfrombrowser_get_default():void {
self::assertEquals("en", MD_STD::lang_getfrombrowser(["de", "en"], "en", ""));
}
/**
* Checks get_user_lang_no_cookie works with GET variable set.
*
* @return void
*/
public function test_get_user_lang_no_cookie_works_from_get_var():void {
$_GET = [
'navlang' => 'de',
];
self::assertEquals("de", MD_STD::get_user_lang_no_cookie(["de", "en"], "en"));
}
/**
* Checks get_user_lang_no_cookie works with GET variable set.
*
* @return void
*/
public function test_get_user_lang_no_cookie_works_without_get():void {
self::assertEquals("en", MD_STD::get_user_lang_no_cookie(["de", "en"], "en"));
}
/**
* Data provider for returning a tmp file.
*
* @return array<string, array{0: non-empty-string, 1: non-empty-array<non-empty-string>, 2: int}>
*/
public static function strpos_multi_provider():array {
return [
"Search quotation markes on when='" => ["when='", ['"', '\''], 5],
"Search quotation markes on when=\"" => ["when=\"", ['"', '\''], 5],
"Search quotation markes on when=\"'" => ["when=\"'", ['"', '\''], 5],
"Search quotation markes on when= (non-existent)" => ["when=", ['"', '\''], -1],
];
}
/**
* Checks unlink_if_exists works.
*
* @param non-empty-string $haystack Haystack.
* @param non-empty-array<non-empty-string> $needles Needles.
* @param integer $expected Expected position.
*
* @return void
*/
#[DataProvider('strpos_multi_provider')]
public function test_strpos_multi(string $haystack, array $needles, int $expected):void {
if ($expected === -1) {
self::assertEmpty(MD_STD::strpos_multi($haystack, $needles));
}
else {
self::assertNotEmpty(MD_STD::strpos_multi($haystack, $needles));
self::assertEquals($expected, MD_STD::strpos_multi($haystack, $needles)['position']);
}
}
}

25
tests/bootstrap.php Normal file
View File

@ -0,0 +1,25 @@
<?PHP
declare(strict_types = 1);
ini_set( 'error_log', '/dev/stdout' );
/**
* Autoloader for musdb.
*
* @param string $className Name of the class to load.
*
* @return void
*/
\spl_autoload_register(function(string $className):void {
// Try using class map as defined through /scripts/buildClassMap.php
foreach (array_merge([__DIR__ . '/../tests', __DIR__ . '/../src', __DIR__ . '/../exceptions', __DIR__ . '/../src/testing', __DIR__ . '/../../MDErrorReporter', __DIR__ . '/../../MDErrorReporter/exceptions', __DIR__ . '/../../MDErrorReporter/exceptions/generic', __DIR__ . '/../../MDErrorReporter/exceptions/updates', __DIR__ . '/../../MDErrorReporter/exceptions/page']) as $classDir) {
if (\file_exists("$classDir/$className.php")) {
include "$classDir/$className.php";
return;
}
}
});